diff --git a/patches/typescript/overlay/src/compiler/tsgoChecker.ts b/patches/typescript/overlay/src/compiler/tsgoChecker.ts index 633bf6f..7177a7a 100644 --- a/patches/typescript/overlay/src/compiler/tsgoChecker.ts +++ b/patches/typescript/overlay/src/compiler/tsgoChecker.ts @@ -4558,6 +4558,26 @@ function refineHostNavigationSymbol(symbol: any, getHostSf: (fileName: string) = refined = ensureExportedSymbolModuleParent(refined, getHostSf); return refined; } + +/** + * Stock's binder always fills ExportSpecifier.symbol. RemoteNode-backed disk + * declaration files do not have binder fields, but stock FAR reads that field + * directly while following a re-export. Materialize the already-resolved RPC + * symbol on the specifier so the stock services walk sees the same contract. + */ +function attachResolvedExportSpecifierSymbol(node: any, symbol: any): any { + const parent = node?.parent; + if ( + symbol && node?.kind === SyntaxKind.Identifier + && parent?.kind === SyntaxKind.ExportSpecifier + && (parent.name === node || parent.propertyName === node) + && parent.symbol === undefined + ) { + parent.symbol = symbol; + } + return symbol; +} + function isCrossFileImportExportName(node: any): boolean { const parent = node?.parent; if (!parent) return false; @@ -9891,7 +9911,10 @@ export function createTsgoChecker(program: any): any { node = node.name; } const lookupNode = node; - const finish = (sym: any) => gateExternalModuleSymbolLookup(lookupNode, sym); + const finish = (sym: any) => attachResolvedExportSpecifierSymbol( + lookupNode, + gateExternalModuleSymbolLookup(lookupNode, sym) + ); // Stock checker.getSymbolAtLocation (switch on node.kind): most // keywords return undefined; a few resolve via the parent // declaration. Never "symbol at this text position" — positional diff --git a/tools/ci-witness-groups.mjs b/tools/ci-witness-groups.mjs index 59b4c1d..531a1ce 100644 --- a/tools/ci-witness-groups.mjs +++ b/tools/ci-witness-groups.mjs @@ -48,7 +48,7 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; import { fileURLToPath } from 'node:url'; -const TOTAL = 95; +const TOTAL = 96; // Witnesses intentionally NOT in the matrix — run on demand (reasons above). const LOCAL_ONLY = [ @@ -117,6 +117,7 @@ const groups = [ 'triage-exit-codes', 'triage-f2hl-typesref', 'triage-f2hl-defaultkw', + 'triage-f2hl-export-star', 'triage-geterr-5823', 'triage-defineprops-refs', 'triage-program-info', diff --git a/tools/triage-f2hl-export-star.mjs b/tools/triage-f2hl-export-star.mjs new file mode 100755 index 0000000..b45a489 --- /dev/null +++ b/tools/triage-f2hl-export-star.mjs @@ -0,0 +1,77 @@ +#!/usr/bin/env node +/** + * Witness f2hl-export-star: documentHighlights on an import reached through + * `export *`. Stock keeps the binder's ExportSpecifier.symbol while walking the + * re-export; TNB's RemoteNode-backed declaration lacked that field, so FAR + * aborted and the IDE saw no highlights. + */ +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; +import { resolveVolarRoot } from './volar-root.mjs'; +import { tnbHarnessEnv, withTsserver } from './tsserver-harness.mjs'; + +const repoRoot = path.resolve(import.meta.dirname, '..'); +const volarRoot = resolveVolarRoot(); +const stockPath = process.env.STOCK_TSSERVER_PATH ?? '/tmp/stock-ts-p3/package/lib/tsserver.js'; +const tnbPath = path.join(repoRoot, 'lib', 'tsserver.js'); + +const fixture = fs.mkdtempSync(path.join(os.tmpdir(), 'tnb-f2hl-export-star-')); +const pkg = path.join(fixture, 'node_modules/tnb-fixture-pkg'); +const shared = path.join(fixture, 'node_modules/tnb-fixture-shared'); +fs.mkdirSync(path.join(pkg, 'dist'), { recursive: true }); +fs.mkdirSync(path.join(shared, 'dist'), { recursive: true }); + +fs.writeFileSync(path.join(pkg, 'package.json'), JSON.stringify({ + name: 'tnb-fixture-pkg', version: '0.0.0', types: './dist/index.d.ts', +})); +fs.writeFileSync(path.join(pkg, 'dist/index.d.ts'), 'export * from "tnb-fixture-shared";\n'); +fs.writeFileSync(path.join(shared, 'package.json'), JSON.stringify({ + name: 'tnb-fixture-shared', version: '0.0.0', types: './dist/index.d.ts', +})); +fs.writeFileSync(path.join(shared, 'dist/index.d.ts'), 'declare const starExportTarget: () => void;\nexport { starExportTarget };\n'); +fs.writeFileSync(path.join(fixture, 'tsconfig.json'), JSON.stringify({ + compilerOptions: { strict: true, module: 'esnext', moduleResolution: 'bundler', noEmit: true }, +})); + +const file = path.join(fixture, 'main.ts'); +const text = 'import { starExportTarget } from "tnb-fixture-pkg";\nstarExportTarget();\n'; +fs.writeFileSync(file, text); + +const positions = [text.indexOf('starExportTarget'), text.indexOf('starExportTarget', 10)]; + +async function run(tsserverPath, env) { + return withTsserver({ + tsserverPath, + args: ['--disableAutomaticTypingAcquisition', '--suppressDiagnosticEvents'], + env, + volarRoot, + deadlineMs: 120_000, + }, async ({ send }) => { + await send('configure', { preferences: {} }); + await send('updateOpen', { + changedFiles: [], closedFiles: [], + openFiles: [{ file, fileContent: text, projectRootPath: fixture }], + }); + return Promise.all(positions.map(async position => { + const highlights = await send('documentHighlights', { + file, line: 1, offset: position + 1, filesToSearch: [file], + }); + return [...new Set((highlights?.body ?? []).flatMap(item => + item.highlightSpans.map(span => + `${span.start.line}:${span.start.offset}:${span.end.offset - span.start.offset}`), + ))].sort(); + })); + }); +} + +console.log('=== WITNESS f2hl-export-star (highlights through `export *`) ==='); +const tnb = await run(tnbPath, tnbHarnessEnv()); +const stock = await run(stockPath, process.env); +const match = JSON.stringify(tnb) === JSON.stringify(stock); +if (!match) { + console.log(`TNB ${JSON.stringify(tnb)}`); + console.log(`STOCK ${JSON.stringify(stock)}`); +} +console.log(match ? 'VERDICT: PASS' : 'VERDICT: FAIL'); +process.exit(match ? 0 : 1);