From 2a420acee16b1ffdb91dd9cc3dfa7416c8d5acbd Mon Sep 17 00:00:00 2001 From: Neal Date: Wed, 29 Jul 2026 13:02:20 +0800 Subject: [PATCH 1/2] fix: handle accessors in declaration return types --- src/visitor.ts | 27 +++++++++++++++++++++++++++ test/projects/general/core/index.ts | 15 +++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/src/visitor.ts b/src/visitor.ts index bd4ee4b..8d79b1f 100755 --- a/src/visitor.ts +++ b/src/visitor.ts @@ -151,5 +151,32 @@ export function nodeVisitor(this: VisitorContext, node: ts.Node): ts.Node | unde factory.updateModuleDeclaration(node, node.modifiers, p, node.body), ); + /* + * TypeScript suspends the surrounding lexical environment while visiting a + * function's return type. Starting another lexical environment for an + * accessor inside that type currently triggers a compiler assertion + * (microsoft/TypeScript#58020). Visit bodyless accessors manually so import + * types in their annotations are still transformed without entering another + * parameter-list lexical environment. + */ + if (tsInstance.isGetAccessorDeclaration(node) && !node.body) + return factory.updateGetAccessorDeclaration( + node, + node.modifiers, + node.name, + node.parameters, + tsInstance.visitNode(node.type, this.getVisitor(), tsInstance.isTypeNode), + node.body, + ); + + if (tsInstance.isSetAccessorDeclaration(node) && !node.body) + return factory.updateSetAccessorDeclaration( + node, + node.modifiers, + node.name, + tsInstance.visitNodes(node.parameters, this.getVisitor(), tsInstance.isParameter), + node.body, + ); + return tsInstance.visitEachChild(node, this.getVisitor(), transformationContext); } diff --git a/test/projects/general/core/index.ts b/test/projects/general/core/index.ts index 1150ecd..618c518 100644 --- a/test/projects/general/core/index.ts +++ b/test/projects/general/core/index.ts @@ -24,6 +24,21 @@ const n: NoRuntimecodeHere = null as any; subs(2, 3); const a = new A(""); +export function MaySkipHooks() { + class SkippableOnce { + get skipHooks() { + return undefined; + } + } + + return SkippableOnce; +} + +export declare function AccessorTypes(): { + get value(): import("@utils/types-only").NoRuntimecodeHere; + set value(value: import("@utils/types-only").NoRuntimecodeHere); +}; + (async function () { const Logger = await (await import("@dynamic/logger")).Logger; const logger = new Logger(); From 63fc37d3841dce83e19e4f833cdc7515ce11cdc7 Mon Sep 17 00:00:00 2001 From: Neal Date: Wed, 29 Jul 2026 18:43:15 +0800 Subject: [PATCH 2/2] test: validate accessor declaration emit --- test/tests/transformer/general.test.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test/tests/transformer/general.test.ts b/test/tests/transformer/general.test.ts index 52a9f59..12a3582 100755 --- a/test/tests/transformer/general.test.ts +++ b/test/tests/transformer/general.test.ts @@ -31,6 +31,7 @@ const getExpected = (tsInstance: typeof ts, fileName: string, original: string, describe(`Transformer -> General Tests`, () => { const projectRoot = path.join(projectsPaths, "general"); const tsConfigFile = path.join(projectRoot, "tsconfig.json"); + const coreIndexFile = path.join(projectRoot, "core/index.ts"); for (const [s, tsInstance] of tsModules) describe(`TypeScript ${s}`, () => { @@ -63,5 +64,15 @@ describe(`Transformer -> General Tests`, () => { test(`dts matches`, (t) => t.assert.strictEqual(transformed.dts, expected.dts)); }); } + + test(`handles accessors in declaration return types`, (t) => { + const dts = transformedFiles[coreIndexFile].dts; + + t.assert.match(dts, /export declare function MaySkipHooks\(\): \{[\s\S]*?get skipHooks\(\): any;[\s\S]*?\};/); + t.assert.match( + dts, + /export declare function AccessorTypes\(\): \{\s*get value\(\): import\("\.\.\/utils\/types-only"\)\.NoRuntimecodeHere;\s*set value\(value: import\("\.\.\/utils\/types-only"\)\.NoRuntimecodeHere\);\s*\};/, + ); + }); }); });