fix(js,ts): an import of a package the repository declares itself links to that declaration (#2718)

A `declare module 'mammoth' { ... }` in a script .d.ts declares the module
outright, and TypeScript takes it before any file the specifier resolves to.
Nothing mapped a package name to a file in the repository, so an import of
such a package linked nothing, or, where the out-of-repo guard did not fire
(VS Code's extensions list no `vscode` dependency), a project namesake. A
bodiless function in an ambient context produced no node either, so
`convertToHtml` had nothing to link to.

- ambient-modules.ts: a token scanner for `declare module` blocks, a
  per-context index of the project's declaration files, and the lookup the
  import resolver makes: named, default (`export default`, `export =`),
  export-clause and namespace-member imports, by position in the block.
- The import resolver consults a script declaration before a file a path
  alias maps the specifier to (copilot's `paths` shim), and the import's
  module reference and namespace or default binding link the declaring file.
- Extraction, both engines: a function_signature under `declare`, in
  `declare global`, at a declaration file's top level or directly in its
  `declare module` body is a function; never in a namespace, nor in an
  ordinary module file's `declare module`, an augmentation.
- A module's function is reached from another file only through an import.
- A type merged across the files declaring one module is no single
  declaration when a receiver is typed.
- Sync re-opens the importers of a package a changed declaration file
  declares.

Augmentations, wildcard patterns, shorthands and test-fixture declarations
are left alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-20 21:35:02 +00:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 642c7bb3a5
commit a33040f786
12 changed files with 1543 additions and 14 deletions
+41
View File
@@ -94,9 +94,50 @@ function hasExportDeclaration(block: SyntaxNode): boolean {
return false;
}
/** A TypeScript declaration file: `.d.ts`, `.d.mts`, `.d.cts`. */
const DECLARATION_FILE = /\.d\.[cm]?ts$/i;
/**
* Whether a bodiless `function_signature` is a function's declaration rather
* than an overload of an implementation that follows it, and one a name or
* an import reaches.
*
* TypeScript writes a function without a body in two places. Before the
* implementation it overloads (`function f(a: number): void;`, then `function
* f(a) {…}`), and there the implementation is the function's node. In an
* ambient context nothing follows, and the signature is all the declaration
* there is: a `declare function`, a function in a `declare global` body, a
* declaration file's own functions, which are ambient with or without
* `declare`, and a function a declaration file's `declare module 'x'` body
* declares, which an import of `x` reaches (see resolution/ambient-modules).
* Without a node, `import { convertToHtml } from 'mammoth'` had nothing to
* bind to. Two are left out: a namespace's function is only ever reached
* through the namespace (`semver.compare`, `vscode.window.showMessage`), which
* the index doesn't model, so on its own it is only a bare name for unrelated
* calls to land on; and a `declare module` body in an ordinary module file
* augments the package's own module, whose functions the import means.
* Mirrored in the kernel (tsjs/mod.rs `is_ambient_function_signature`).
*/
export function isAmbientFunctionSignature(node: SyntaxNode, filePath: string): boolean {
if (node.type !== 'function_signature') return false;
const declarationFile = DECLARATION_FILE.test(filePath);
let ambient = false;
for (let current = node.parent; current; current = current.parent) {
if (current.type === 'internal_module') return false;
if (current.type === 'module') {
// `declare module 'x'`; `module Foo {}` is a namespace.
if (getChildByField(current, 'name')?.type !== 'string') return false;
return declarationFile;
}
if (current.type === 'ambient_declaration') ambient = true;
}
return ambient || declarationFile;
}
export const typescriptExtractor: LanguageExtractor = {
preParse: blankFlowSyntax,
functionTypes: ['function_declaration', 'generator_function_declaration', 'arrow_function', 'function_expression', 'generator_function'],
isAmbientFunction: isAmbientFunctionSignature,
classTypes: ['class_declaration', 'abstract_class_declaration'],
// `method_signature` is the interface/type-literal form of a method; without it
// an interface's members never enter the graph, so a `.d.ts` platform API has
+8
View File
@@ -101,6 +101,14 @@ export interface LanguageExtractor {
/** Node types that represent functions */
functionTypes: string[];
/**
* A bodiless function that is the function's whole declaration, not one
* overload of an implementation that follows it: TypeScript's
* `function_signature` in an ambient context (`declare function f(): void;`,
* a declaration file's own or its `declare module` body's), never in a
* namespace. Extracted as a function, like a `functionTypes` node.
*/
isAmbientFunction?: (node: SyntaxNode, filePath: string) => boolean;
/** Node types that represent classes */
classTypes: string[];
/** Node types that represent methods */
+6
View File
@@ -1403,6 +1403,12 @@ export class TreeSitterExtractor {
skipChildren = true; // extractFunction visits children via visitFunctionBody
}
}
// A bodiless function that no implementation follows: a TypeScript
// `declare function`, a `.d.ts` file's own or its `declare module` body's.
else if (nodeType === 'function_signature' && this.extractor.isAmbientFunction?.(node, this.filePath)) {
this.extractFunction(node);
skipChildren = true;
}
// Check for class declarations
else if (this.extractor.classTypes.includes(nodeType)) {
// Some languages reuse class_declaration for structs/enums (e.g. Swift)