fix(ts): a supertype import that names a constant reaches the interface beside it (#2657)

vscode's ILogService is both an interface and its createDecorator constant, and the import of it as ITerminalLogService's or IUserDataSyncLogService's supertype resolved to the constant, so the walk up to ILogger stopped there and their this.logService calls linked nowhere.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-19 10:10:03 +00:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 0c22acf36c
commit c38f2678ac
3 changed files with 29 additions and 1 deletions
+1
View File
@@ -172,6 +172,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- In TypeScript and JavaScript, a class whose declaration constrains a type parameter, like `class EditorPane<MementoType extends object = object> extends Composite<MementoType>`, is now read as extending the class after its type parameters. Before, the constraint's type was taken for its base class, so fields, wrapped event emitters and static methods inherited through such a class were missed or found on the wrong class. Class declarations that span several lines or sit below a decorator are read too. Re-index TypeScript and JavaScript projects after upgrading.
- In TypeScript, an abstract method like `abstract send(path: string): Promise<unknown>;` is now indexed as a method of its class. Before, it was missing, so a call through a value typed as an abstract class, as Angular apps register many of their services, linked to whichever class elsewhere had a method of that name, often another app's, and the types written in an abstract method's parameters and return type were read as properties of the class. Such a call now links to the abstract method, which also leads to the classes that implement it. Re-index TypeScript projects after upgrading.
- In TypeScript and JavaScript, a call on a field or variable whose class the file imports now goes through that class and the classes and interfaces it extends, each found the way its own file names it, before any other class of the same name. Before, when the class didn't declare the method itself, the call went to whichever same-named class had it: in vscode, `this._storage.dispose()` reached a test fixture's `Disposable`, and calls on a library's services in bitwarden reached the browser app's copies. A call like `this.logService.trace()`, whose `ILogService` gets `trace` from the `ILogger` it extends, linked nowhere and now links to `ILogger`'s method. A field's call whose class and everything it extends are known but lack the method, as when a package's base class provides it, now links to nothing rather than to an unrelated class. Re-index TypeScript and JavaScript projects after upgrading.
- In TypeScript, a call on a value of an interface that extends one also declared as a constant, like vscode's `ITerminalLogService extends ILogService` where `ILogService` is its dependency-injection decorator too, now links to the method the extended interface declares, such as `ILogger`'s `trace`. Before, the constant was taken for the extended type, so the call linked nowhere. Re-index TypeScript projects after upgrading.
- In JavaScript and TypeScript, an import written from a jsconfig or tsconfig `baseUrl`, like `import MainLayout from 'layout/MainLayout'` under `"baseUrl": "src"`, or through a catch-all `"paths": { "*": ["./src/*"] }`, now links to the file it names, read with the config of the app the importing file belongs to, and so does an import of a single name like `'config'` or `'themes'`. Create React App and Vite admin templates such as Berry, Mantis and MatX write nearly every import this way. Before, each one was taken for an npm package, so it linked nothing, its names were matched by name alone, which could reach a same-named component in another app of the same repository, and a React Router route whose page loads through `lazy(() => import('views/dashboard/Default'))` pointed at that loader instead of the page and its layout. A package the app's `package.json` declares, or a Node built-in, never links to a project module of the same name, only to type declarations the project keeps for it, and only a file the index holds counts, never a folder or something in `node_modules`. `codegraph sync` and the file watcher link such an import too when the file it names is added later or gains what it imports. Re-index JavaScript and TypeScript projects after upgrading.
## [1.6.2] - 2026-10-03
+24
View File
@@ -112,4 +112,28 @@ describe('a call on a value of a class its file names', () => {
'src/app/service.ts: this.logService.trace -> src/platform/log.ts::ILogger::trace',
]);
}, 60_000);
it('reaches it through an interface another file extends, whose import also names a constant', async () => {
// vscode's `ITerminalLogService extends ILogService`, `ILogService` both
// an interface and its `createDecorator` constant.
const calls = await callsOf({
'package.json': JSON.stringify({ name: 'app' }),
'src/platform/log.ts':
'function createDecorator<T>(id: string): T {\n return id as unknown as T;\n}\n' +
'export interface ILogger {\n trace(message: string): void;\n}\n' +
"export const ILogService = createDecorator<ILogService>('logService');\n" +
'export interface ILogService extends ILogger {\n flush(): void;\n}\n',
'src/app/legacy/log.ts': 'export interface ILogger {\n trace(message: string): void;\n}\n',
'src/terminal/terminal.ts':
"import { ILogService } from '../platform/log';\n" +
'export interface ITerminalLogService extends ILogService {\n logTerminal(): void;\n}\n',
'src/terminal/service.ts':
"import { ITerminalLogService } from './terminal';\n" +
'export class TerminalService {\n constructor(private readonly _logService: ITerminalLogService) {}\n' +
" run() {\n this._logService.trace('a');\n }\n}\n",
});
expect(calls.filter((c) => c.includes('this._logService.'))).toEqual([
'src/terminal/service.ts: this._logService.trace -> src/platform/log.ts::ILogger::trace',
]);
}, 60_000);
});
+4 -1
View File
@@ -16023,7 +16023,10 @@ function tsNamedType(written: string, from: Node, at: UnresolvedRef, context: Re
} else if (mapping) {
const imported = context.resolveImport?.({ ...ref, referenceName: written, referenceKind: 'references' });
const node = imported ? context.getNodeById?.(imported.targetNodeId) : undefined;
found = node && TS_MEMBER_OWNER_KINDS.has(node.kind) ? node : null;
// vscode's `ILogService` is also a constant, its dependency-injection
// decorator, which the import may name first: the type is the interface
// of that name beside it.
found = !node ? null : TS_MEMBER_OWNER_KINDS.has(node.kind) ? node : tsTypeDeclaredIn(node.filePath, node.name, context) ?? null;
} else if (!written.includes('.')) {
found = context.getNodesInFile(from.filePath).find((n) => n.id !== from.id && n.name === written && TS_MEMBER_OWNER_KINDS.has(n.kind)) ?? null;
}