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
+36
View File
@@ -681,6 +681,11 @@ impl<'t> Walker<'t> {
// (the isInsideClassLike + methodTypes overlap is Python/Ruby-only)
self.extract_function(node, None);
skip_children = true;
} else if kind == "function_signature" && self.variant.is_ts() && is_ambient_function_signature(node, self.file_path) {
// A bodiless function that no implementation follows: a
// `declare function`, a function in a `declare module` body or a `.d.ts`.
self.extract_function(node, None);
skip_children = true;
} else if is_class_type(self.variant, kind) {
self.extract_class(node);
skip_children = true;
@@ -1162,6 +1167,37 @@ fn is_ambient_export(node: Node) -> bool {
false
}
/// isAmbientFunctionSignature (languages/typescript.ts): a bodiless
/// `function_signature` that is a function's declaration, not an overload of
/// an implementation that follows it — a `declare function`, one in a
/// `declare global` body, a declaration file's (`.d.ts`, `.d.mts`, `.d.cts`)
/// own, and one a declaration file's `declare module 'x'` body declares.
/// Never a namespace's (`declare namespace`, `module Foo {}`), nor one in an
/// ordinary module file's `declare module` body, an augmentation.
fn is_ambient_function_signature(node: Node, file_path: &str) -> bool {
if node.kind() != "function_signature" {
return false;
}
let lower = file_path.to_ascii_lowercase();
let declaration_file = lower.ends_with(".d.ts") || lower.ends_with(".d.mts") || lower.ends_with(".d.cts");
let mut ambient = false;
let mut cur = node.parent();
while let Some(p) = cur {
match p.kind() {
"internal_module" => return false,
// `declare module 'x'`; `module Foo {}` is a namespace.
"module" => {
return declaration_file
&& p.child_by_field_name("name").is_some_and(|n| n.kind() == "string");
}
"ambient_declaration" => ambient = true,
_ => {}
}
cur = p.parent();
}
ambient || declaration_file
}
/// hasExportDeclaration: a statement that exports by name or by assignment
/// (`export {…}`, `export =`, `export default x`), not by declaring.
fn has_export_declaration(block: Node) -> bool {