Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 31 additions & 24 deletions patches/typescript/0001-tsgo-hooks.patch
Original file line number Diff line number Diff line change
Expand Up @@ -609,7 +609,7 @@ index 6034c9dfc3..0c9a6178db 100644
let computedWithoutCacheCount = 0;
const fixes = flatMap(exportInfo, (exportInfo, i) => {
diff --git a/src/services/completions.ts b/src/services/completions.ts
index 28d29136da..e267aa7847 100644
index 28d29136da..48fa363863 100644
--- a/src/services/completions.ts
+++ b/src/services/completions.ts
@@ -175,6 +175,7 @@ import {
Expand Down Expand Up @@ -964,33 +964,40 @@ index 28d29136da..e267aa7847 100644
symbolToOriginInfoMap[symbols.length] = origin;
symbolToSortTextMap[symbolId] = importStatementCompletion ? SortText.LocationPriority : SortText.AutoImportSuggestions;
symbols.push(symbol);
@@ -5383,7 +5511,7 @@ function getAutoImportSymbolFromCompletionEntryData(name: string, data: Completi
@@ -5382,14 +5510,25 @@ function getRelevantTokens(position: number, sourceFile: SourceFile): { contextT
function getAutoImportSymbolFromCompletionEntryData(name: string, data: CompletionEntryData, program: Program, host: LanguageServiceHost): { symbol: Symbol; origin: SymbolOriginInfoExport | SymbolOriginInfoResolvedExport; } | undefined {
const containingProgram = data.isPackageJsonImport ? host.getPackageJsonAutoImportProvider!()! : program;
const checker = containingProgram.getTypeChecker();
const moduleSymbol = data.ambientModuleName ? checker.tryFindAmbientModule(data.ambientModuleName) :
- const moduleSymbol = data.ambientModuleName ? checker.tryFindAmbientModule(data.ambientModuleName) :
- data.fileName ? checker.getMergedSymbol(Debug.checkDefined(containingProgram.getSourceFile(data.fileName)).symbol) :
+ data.fileName ? getModuleSymbolForCompletionEntryData(containingProgram, checker, data.fileName) :
undefined;

- undefined;
-
+ const tsgoProgram = containingProgram as Program & {
+ readonly isTsgoBackedProgram: true;
+ getCompletionEntryDataSymbols(fileName: string | undefined, ambientModuleName: string | undefined, exportName: string): { moduleSymbol: Symbol; symbol: Symbol; } | undefined;
+ };
+ let moduleSymbol: Symbol | undefined;
+ let symbol: Symbol | undefined;
+ if (tsgoProgram.isTsgoBackedProgram === true) {
+ const resolved = tsgoProgram.getCompletionEntryDataSymbols(data.fileName, data.ambientModuleName, data.exportName);
+ if (resolved) ({ moduleSymbol, symbol } = resolved);
+ }
+ else {
+ moduleSymbol = data.ambientModuleName ? checker.tryFindAmbientModule(data.ambientModuleName) :
+ data.fileName ? checker.getMergedSymbol(Debug.checkDefined(containingProgram.getSourceFile(data.fileName)).symbol) :
+ undefined;
+ symbol = moduleSymbol && (data.exportName === InternalSymbolName.ExportEquals
+ ? checker.resolveExternalModuleSymbol(moduleSymbol)
+ : checker.tryGetMemberInModuleExportsAndProperties(data.exportName, moduleSymbol));
+ }
if (!moduleSymbol) return undefined;
@@ -5396,6 +5524,16 @@ function getAutoImportSymbolFromCompletionEntryData(name: string, data: Completi
return { symbol, origin: completionEntryDataToSymbolOriginInfo(data, name, moduleSymbol) };
}

+function getModuleSymbolForCompletionEntryData(program: Program, checker: TypeChecker, fileName: string): Symbol {
+ const sourceFile = Debug.checkDefined(program.getSourceFile(fileName));
+ // tsgo-backed programs hand out a host-binder facade for sf.symbol whose
+ // object identity does not match the registry-proxied symbols inside
+ // export info maps; resolve through the bridged checker when it can answer
+ // (absent on stock checkers — falls back to stock behavior).
+ const forSourceFile = (checker as unknown as { getModuleSymbolForSourceFile?: (sourceFile: SourceFile) => Symbol | undefined; }).getModuleSymbolForSourceFile;
+ return checker.getMergedSymbol(forSourceFile?.(sourceFile) ?? Debug.checkDefined(sourceFile.symbol));
+}
+
interface CompletionEntryDisplayNameForSymbol {
readonly name: string;
readonly needsConvertPropertyAccess: boolean;
@@ -6089,6 +6227,23 @@ function isDeprecated(symbol: Symbol, checker: TypeChecker) {
- let symbol = data.exportName === InternalSymbolName.ExportEquals
- ? checker.resolveExternalModuleSymbol(moduleSymbol)
- : checker.tryGetMemberInModuleExportsAndProperties(data.exportName, moduleSymbol);
if (!symbol) return undefined;
const isDefaultExport = data.exportName === InternalSymbolName.Default;
symbol = isDefaultExport && getLocalSymbolForExportDefault(symbol) || symbol;
@@ -6089,6 +6228,23 @@ function isDeprecated(symbol: Symbol, checker: TypeChecker) {
return !!length(declarations) && every(declarations, isDeprecatedDeclaration);
}

Expand Down
41 changes: 34 additions & 7 deletions patches/typescript/overlay/src/compiler/tsgoChecker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import * as ts from "./_namespaces/ts.js";
import { bindSourceFile } from "./binder.js";
import { createSourceFile } from "./parser.js";
import { SyntaxKind, SymbolFlags, SymbolFormatFlags, NodeFlags, ModifierFlags, JSDocParsingMode, ModuleKind, StructureIsReused, EmitHint, EmitFlags, type Path, type Program, type TypeChecker } from "./types.js";
import { SyntaxKind, SymbolFlags, SymbolFormatFlags, NodeFlags, ModifierFlags, JSDocParsingMode, ModuleKind, StructureIsReused, EmitHint, EmitFlags, InternalSymbolName, type Path, type Program, type TypeChecker } from "./types.js";
import { getBuildInfoText, getTsBuildInfoEmitOutputFilePath, createPrinterWithRemoveComments } from "./emitter.js";
import { usingSingleLineStringWriter, canHaveJSDoc } from "./utilities.js";
import { getParseTreeNode, isFunctionLike } from "./utilitiesPublic.js";
Expand Down Expand Up @@ -8196,6 +8196,18 @@ export function createTsgoProgram(
return result;
},
getTypeChecker: () => checker,
// Stock completion details compare these against the batch export map
// by object identity, so both symbols stay in the registry domain.
getCompletionEntryDataSymbols: (fileName: string | undefined, ambientModuleName: string | undefined, exportName: string) => {
const moduleSymbol = ambientModuleName
? checker.tryFindAmbientModule(ambientModuleName)
: fileName
? checker.getModuleSymbolForSourceFile(thinProgram.getSourceFile(fileName))
: undefined;
if (!moduleSymbol) return undefined;
const symbol = checker.getExportForCompletionEntryData(exportName, moduleSymbol);
return symbol ? { moduleSymbol, symbol } : undefined;
},
getConfigFileParsingDiagnostics: () => configDiags,
getOptionsDiagnostics: () => [],
getSemanticDiagnostics: (sourceFile?: any) => {
Expand Down Expand Up @@ -10867,10 +10879,10 @@ export function createTsgoChecker(program: any): any {
return true;
}

function tryGetMemberInModuleExportsImpl(memberName: any, moduleSymbol: any): any {
function getMemberInModuleExportsRaw(memberName: any, moduleSymbol: any): any {
if (moduleSymbol?.exports) {
const sym = moduleSymbol.exports.get(memberName);
if (sym) return refineNavSymbol(sym);
if (sym) return sym;
// Fall through on miss: the raw host table holds direct members only,
// while stock's getExportsOfModule (checker.ts:5155) also resolves
// `export *` chains — let the Go side answer those. Returning
Expand All @@ -10881,12 +10893,16 @@ export function createTsgoChecker(program: any): any {
}
ensureProject();
try {
return refineNavSymbol(rpc().getMemberInModuleExports(moduleSymbol, memberName));
return rpc().getMemberInModuleExports(moduleSymbol, memberName);
} catch { return undefined; }
}

function tryGetMemberInModuleExportsAndPropertiesImpl(memberName: any, moduleSymbol: any): any {
const symbol = tryGetMemberInModuleExportsImpl(memberName, moduleSymbol);
function tryGetMemberInModuleExportsImpl(memberName: any, moduleSymbol: any): any {
return refineNavSymbol(getMemberInModuleExportsRaw(memberName, moduleSymbol));
}

function getMemberInModuleExportsAndPropertiesRaw(memberName: any, moduleSymbol: any): any {
const symbol = getMemberInModuleExportsRaw(memberName, moduleSymbol);
if (symbol) return symbol;

ensureProject();
Expand All @@ -10897,7 +10913,11 @@ export function createTsgoChecker(program: any): any {
const exportEqualsType = getTypeOfSymbolForExportEquals(exportEquals);
if (!exportEqualsType || !shouldTreatPropertiesOfExternalModuleAsExports(exportEqualsType)) return undefined;

return refineNavSymbol(resolvePropertyOfType(exportEqualsType, memberName));
return resolvePropertyOfType(exportEqualsType, memberName);
}

function tryGetMemberInModuleExportsAndPropertiesImpl(memberName: any, moduleSymbol: any): any {
return refineNavSymbol(getMemberInModuleExportsAndPropertiesRaw(memberName, moduleSymbol));
}

function getSymbolFlagsImpl(symbol: any, excludeLocalMeanings?: boolean): number {
Expand Down Expand Up @@ -13080,6 +13100,13 @@ export function createTsgoChecker(program: any): any {
tryGetMemberInModuleExportsAndProperties(memberName: any, moduleSymbol: any): any {
return tryGetMemberInModuleExportsAndPropertiesImpl(memberName, moduleSymbol);
},
// Completion details match this result against the batch export map by
// object identity; navigation remapping would replace the registry symbol.
getExportForCompletionEntryData(exportName: any, moduleSymbol: any): any {
return exportName === InternalSymbolName.ExportEquals
? resolveExternalModuleSymbolImpl(moduleSymbol)
: getMemberInModuleExportsAndPropertiesRaw(exportName, moduleSymbol);
},
resolveExternalModuleSymbol(moduleSymbol: any): any {
return refineNavSymbol(resolveExternalModuleSymbolImpl(moduleSymbol));
},
Expand Down
3 changes: 2 additions & 1 deletion tools/ci-witness-groups.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ import * as fs from 'node:fs';
import * as path from 'node:path';
import { fileURLToPath } from 'node:url';

const TOTAL = 70;
const TOTAL = 71;

// Witnesses intentionally NOT in the matrix — run on demand (reasons above).
const LOCAL_ONLY = [
Expand Down Expand Up @@ -137,6 +137,7 @@ const groups = [
'check-readme-ledger',
'triage-overlay-delta-sync',
'triage-completion-details-array',
'triage-local-autoimport-details',
'triage-alias-self-loop',
'triage-alias-nil',
'triage-empty-literal',
Expand Down
71 changes: 71 additions & 0 deletions tools/triage-local-autoimport-details.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#!/usr/bin/env node
/**
* Issue #58: a project-local auto-import completion must resolve to an import
* edit when its entry data is sent back through completionEntryDetails.
*/
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { tnbHarnessEnv, withTsserver } from './tsserver-harness.mjs';

const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const tsserverPath = path.join(repoRoot, 'lib', 'tsserver.js');
const fixture = fs.mkdtempSync(path.join(os.tmpdir(), 'tnb-local-autoimport-'));
const main = path.join(fixture, 'src', 'main.ts');
const model = path.join(fixture, 'src', 'model.ts');

fs.mkdirSync(path.dirname(main), { recursive: true });
fs.writeFileSync(path.join(fixture, 'tsconfig.json'), JSON.stringify({ include: ['./src/*.ts'] }));
fs.writeFileSync(main, 'ReadRecordModel;\n');
fs.writeFileSync(model, 'export const ReadRecordModel = {};\n');

try {
const result = await withTsserver({
tsserverPath,
args: ['--disableAutomaticTypingAcquisition', '--suppressDiagnosticEvents'],
env: tnbHarnessEnv(),
}, async ({ send }) => {
await send('configure', {
preferences: {
includeCompletionsForModuleExports: true,
includeCompletionsWithInsertText: true,
},
});
await send('updateOpen', {
changedFiles: [],
closedFiles: [],
openFiles: [{ file: main, fileContent: 'ReadRecordModel;\n', projectRootPath: fixture }],
});

const completion = await send('completionInfo', {
file: main,
line: 1,
offset: 16,
includeExternalModuleExports: true,
includeInsertTextCompletions: true,
});
const entry = completion.body?.entries?.find(candidate => candidate.name === 'ReadRecordModel' && candidate.source === './model');
if (!entry) throw new Error('completionInfo did not return ReadRecordModel from ./model');

const details = await send('completionEntryDetails', {
file: main,
line: 1,
offset: 16,
entryNames: [{ name: entry.name, source: entry.source, data: entry.data }],
});
if (!details.success) throw new Error(details.message || 'completionEntryDetails failed');
const changes = details.body?.[0]?.codeActions?.flatMap(action => action.changes ?? []) ?? [];
const importEdit = changes
.filter(change => change.fileName === main)
.flatMap(change => change.textChanges ?? [])
.find(change => change.newText.includes('ReadRecordModel') && change.newText.includes('./model'));
if (!importEdit) throw new Error('completionEntryDetails returned no ./model import edit');
return { data: entry.data, edit: importEdit.newText };
});
console.log(`ok project-local auto-import details: ${JSON.stringify(result)}`);
}
finally {
fs.rmSync(fixture, { recursive: true, force: true });
}
process.exit(0);
Loading