From a624d0d0819672b19fe88d4e2da275bab762b2e1 Mon Sep 17 00:00:00 2001 From: Cold Fry Date: Sat, 2 May 2026 21:50:08 +0000 Subject: [PATCH 1/2] respect @noself on interface containing call signature --- src/transformation/utils/function-context.ts | 13 ++++++ .../noSelfAnnotation.spec.ts.snap | 18 +++++++++ test/unit/functions/noSelfAnnotation.spec.ts | 40 +++++++++++++++++++ 3 files changed, 71 insertions(+) diff --git a/src/transformation/utils/function-context.ts b/src/transformation/utils/function-context.ts index e28950e2e..4fce80f49 100644 --- a/src/transformation/utils/function-context.ts +++ b/src/transformation/utils/function-context.ts @@ -143,6 +143,19 @@ function computeDeclarationContextType(context: TransformationContext, signature return ContextType.NonVoid; } + // Call signature inside a class or interface respects @noSelf on the enclosing class/interface + if (ts.isCallSignatureDeclaration(signatureDeclaration)) { + const scopeDeclaration = findFirstNodeAbove( + signatureDeclaration, + (n): n is ts.ClassLikeDeclaration | ts.InterfaceDeclaration => + ts.isClassDeclaration(n) || ts.isClassExpression(n) || ts.isInterfaceDeclaration(n) + ); + + if (scopeDeclaration !== undefined && getNodeAnnotations(scopeDeclaration).has(AnnotationKind.NoSelf)) { + return ContextType.Void; + } + } + // When using --noImplicitSelf and the signature is defined in a file targeted by the program apply the @noSelf rule. const program = context.program; const options = program.getCompilerOptions() as CompilerOptions; diff --git a/test/unit/functions/__snapshots__/noSelfAnnotation.spec.ts.snap b/test/unit/functions/__snapshots__/noSelfAnnotation.spec.ts.snap index 64eb65457..0bd6aded5 100644 --- a/test/unit/functions/__snapshots__/noSelfAnnotation.spec.ts.snap +++ b/test/unit/functions/__snapshots__/noSelfAnnotation.spec.ts.snap @@ -2,6 +2,12 @@ exports[`@noSelf on declared function removes context argument 1`] = `"myFunction()"`; +exports[`@noSelf on interface with call signature removes context argument 1`] = ` +"func = function() +end +func()" +`; + exports[`@noSelf on method inside class declaration removes context argument 1`] = `"holder.myMethod()"`; exports[`@noSelf on method inside interface declaration removes context argument 1`] = `"holder.myMethod()"`; @@ -10,6 +16,18 @@ exports[`@noSelf on method inside namespace declaration removes context argument exports[`@noSelf on parent class declaration removes context argument 1`] = `"holder.myMethod()"`; +exports[`@noSelf on parent interface applies to property with interface call-signature type 1`] = ` +"demo = {func = function() +end} +demo.func()" +`; + +exports[`@noSelf on parent interface applies to property with type-literal call signature 1`] = ` +"demo = {func = function() +end} +demo.func()" +`; + exports[`@noSelf on parent interface declaration removes context argument 1`] = `"holder.myMethod()"`; exports[`@noSelf on parent namespace declaration removes context argument 1`] = `"MyNamespace.myMethod()"`; diff --git a/test/unit/functions/noSelfAnnotation.spec.ts b/test/unit/functions/noSelfAnnotation.spec.ts index f8418fabd..9dc943f5e 100644 --- a/test/unit/functions/noSelfAnnotation.spec.ts +++ b/test/unit/functions/noSelfAnnotation.spec.ts @@ -65,6 +65,46 @@ test("@noSelf on static class methods with string key access", () => { `.expectLuaToMatchSnapshot(); }); +// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1661 +test("@noSelf on interface with call signature removes context argument", () => { + util.testModule` + /** @noSelf */ + interface CallSignature { + (): void; + } + const func: CallSignature = () => {}; + func(); + `.expectLuaToMatchSnapshot(); +}); + +// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1661 +test("@noSelf on parent interface applies to property with interface call-signature type", () => { + util.testModule` + /** @noSelf */ + interface CallSignature { + (): void; + } + /** @noSelf */ + interface DemoType { + func: CallSignature; + } + const demo: DemoType = { func: () => {} }; + demo.func(); + `.expectLuaToMatchSnapshot(); +}); + +// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1661 +test("@noSelf on parent interface applies to property with type-literal call signature", () => { + util.testModule` + /** @noSelf */ + interface DemoType { + func: { (): void }; + } + const demo: DemoType = { func: () => {} }; + demo.func(); + `.expectLuaToMatchSnapshot(); +}); + // additional coverage for https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1292 test("explicit this parameter respected over @noSelf", () => { util.testModule` From 223b8148dcd0c5f85a416c3e6d6133ba7e24a8ce Mon Sep 17 00:00:00 2001 From: Cold Fry Date: Sun, 3 May 2026 13:30:49 +0000 Subject: [PATCH 2/2] replace @noSelf call-signature snapshot tests with runtime argc probes --- .../noSelfAnnotation.spec.ts.snap | 18 ------ test/unit/functions/noSelfAnnotation.spec.ts | 56 ++++++++++--------- 2 files changed, 30 insertions(+), 44 deletions(-) diff --git a/test/unit/functions/__snapshots__/noSelfAnnotation.spec.ts.snap b/test/unit/functions/__snapshots__/noSelfAnnotation.spec.ts.snap index 0bd6aded5..64eb65457 100644 --- a/test/unit/functions/__snapshots__/noSelfAnnotation.spec.ts.snap +++ b/test/unit/functions/__snapshots__/noSelfAnnotation.spec.ts.snap @@ -2,12 +2,6 @@ exports[`@noSelf on declared function removes context argument 1`] = `"myFunction()"`; -exports[`@noSelf on interface with call signature removes context argument 1`] = ` -"func = function() -end -func()" -`; - exports[`@noSelf on method inside class declaration removes context argument 1`] = `"holder.myMethod()"`; exports[`@noSelf on method inside interface declaration removes context argument 1`] = `"holder.myMethod()"`; @@ -16,18 +10,6 @@ exports[`@noSelf on method inside namespace declaration removes context argument exports[`@noSelf on parent class declaration removes context argument 1`] = `"holder.myMethod()"`; -exports[`@noSelf on parent interface applies to property with interface call-signature type 1`] = ` -"demo = {func = function() -end} -demo.func()" -`; - -exports[`@noSelf on parent interface applies to property with type-literal call signature 1`] = ` -"demo = {func = function() -end} -demo.func()" -`; - exports[`@noSelf on parent interface declaration removes context argument 1`] = `"holder.myMethod()"`; exports[`@noSelf on parent namespace declaration removes context argument 1`] = `"MyNamespace.myMethod()"`; diff --git a/test/unit/functions/noSelfAnnotation.spec.ts b/test/unit/functions/noSelfAnnotation.spec.ts index 9dc943f5e..e9fbb853d 100644 --- a/test/unit/functions/noSelfAnnotation.spec.ts +++ b/test/unit/functions/noSelfAnnotation.spec.ts @@ -66,43 +66,47 @@ test("@noSelf on static class methods with string key access", () => { }); // https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1661 -test("@noSelf on interface with call signature removes context argument", () => { +// A Lua-side function observes the actual argc, so a missing @noSelf would +// surface as a phantom leading nil (argc 2 instead of 1). +const argcProbeHeader = ` + function probe(...) + return select("#", ...) + end +`; + +test("@noSelf on interface call signature: Lua probe sees correct argc", () => { util.testModule` /** @noSelf */ - interface CallSignature { - (): void; - } - const func: CallSignature = () => {}; - func(); - `.expectLuaToMatchSnapshot(); + interface Probe { (a: string): number; } + declare const probe: Probe; + export const result = probe("hi"); + ` + .setLuaHeader(argcProbeHeader) + .expectToEqual({ result: 1 }); }); -// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1661 -test("@noSelf on parent interface applies to property with interface call-signature type", () => { +test("@noSelf parent interface, property typed by call-signature interface: Lua probe sees correct argc", () => { util.testModule` /** @noSelf */ - interface CallSignature { - (): void; - } + interface CallSignature { (a: string): number; } /** @noSelf */ - interface DemoType { - func: CallSignature; - } - const demo: DemoType = { func: () => {} }; - demo.func(); - `.expectLuaToMatchSnapshot(); + interface Holder { fn: CallSignature; } + declare const holder: Holder; + export const result = holder.fn("hi"); + ` + .setLuaHeader(`${argcProbeHeader}\nholder = { fn = probe }`) + .expectToEqual({ result: 1 }); }); -// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1661 -test("@noSelf on parent interface applies to property with type-literal call signature", () => { +test("@noSelf parent interface, property typed by type-literal call signature: Lua probe sees correct argc", () => { util.testModule` /** @noSelf */ - interface DemoType { - func: { (): void }; - } - const demo: DemoType = { func: () => {} }; - demo.func(); - `.expectLuaToMatchSnapshot(); + interface Holder { fn: { (a: string): number }; } + declare const holder: Holder; + export const result = holder.fn("hi"); + ` + .setLuaHeader(`${argcProbeHeader}\nholder = { fn = probe }`) + .expectToEqual({ result: 1 }); }); // additional coverage for https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1292