Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- In Rust, a `for` loop's variable now has the type of an item of what the loop goes over, like `for item in &self.items` over a `Vec<Item>`, `for (key, value) in &map` over a `HashMap`, or `for sc in cmd.get_subcommands()` over the `impl Iterator` a method returns, and each name in a tuple pattern like `let (tx, rx) = mpsc::channel(8);` has the type of its part of the tuple, so calls on them link to that type's methods. Before, such calls were linked by name: tokio's `tx.send(…)` calls on channel senders went to an internal type's `send`. Re-index Rust projects after upgrading.
- In C++, every name a declaration lists now has the declaration's type, like `strings_found` in google/breakpad's `ByteBuffer symbols_found, strings_found;` or `data_` in a class's `Table *index_, *data_;`, so calls on it link to that class's methods. Before, only the first name had its type read, and a call on another name linked to nothing or to the class of an earlier variable with the same name.
- In C++, a call on a parameter, a local lambda or functor, a lambda's parameter or a range-`for` variable no longer links to a function or method elsewhere that only shares its name, like protocolbuffers/protobuf's code generators calling `format(…)` on their `Formatter& format` parameter, which linked to the conformance suite's `TestResult::format`, or facebook/rocksdb's tests calling a local `auto key = [](int i) {…};`. C++ finds the local first, so that is what the call calls.
- In TypeScript and JavaScript, an import of a package your project declares itself with `declare module '…'` in a `.d.ts` file, like a `typings/mammoth/index.d.ts` written for an untyped package or VS Code's own `vscode.d.ts`, now links to that declaration, so `codegraph callers`, impact and `codegraph affected` show the code that uses your typings instead of nothing or a project symbol that only shares a name. Named, default and namespace imports, `export =` and renaming export lists are read the way TypeScript reads them, a function such a file declares without a body (`export function convertToHtml(…): Promise<Result>;`) is now indexed so calls to it have something to link to, and only an import of its package reaches it. A `declare module` that augments a package's own types, a wildcard like `declare module '*.svg'`, and copies of typings kept in test fixtures are left alone. Re-index TypeScript projects after upgrading.

## [1.6.2] - 2026-10-03

Expand Down
676 changes: 676 additions & 0 deletions __tests__/ambient-module-imports.test.ts

Large diffs are not rendered by default.

36 changes: 36 additions & 0 deletions codegraph-kernel/src/tsjs/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down
41 changes: 41 additions & 0 deletions src/extraction/languages/typescript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions src/extraction/tree-sitter-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down
6 changes: 6 additions & 0 deletions src/extraction/tree-sitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
10 changes: 9 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ import {
ResolutionResult,
} from './resolution';
import { hasSynthesisPattern, isJsxChildCandidate } from './resolution/callback-synthesizer';
import { createImportReader, reopenReferencesThroughAddedFiles, reopenReferencesThroughBarrels } from './resolution/module-importers';
import { createImportReader, reopenReferencesThroughAddedFiles, reopenReferencesThroughBarrels, reopenReferencesThroughDeclaredPackages } from './resolution/module-importers';
import { GraphTraverser, GraphQueryManager } from './graph';
import { findNamedCopybooks, type NamedCopybook } from './graph/cobol-copybooks';
import { ContextBuilder, createContextBuilder } from './context';
Expand Down Expand Up @@ -1177,6 +1177,14 @@ export class CodeGraph {
this.queries, importReader, [...seeds].map(([path, language]) => ({ path, language })), edgesBeforeResolution
);
if (process.env.CODEGRAPH_SYNTH_TIMINGS) console.error(`[phase-timing] sync-barrel-modules: ${Date.now() - tBarrels}ms (${throughBarrels} refs re-opened)`);

// A declaration file that declares a package itself (`declare
// module 'mammoth' { … }`): the files importing the package bound
// nothing through it while it was missing or lacked a name.
const declared = reopenReferencesThroughDeclaredPackages(
this.queries, importReader, this.projectRoot, result.changedFilePaths, edgesBeforeResolution
);
if (process.env.CODEGRAPH_SYNTH_TIMINGS && declared > 0) console.error(`[phase-timing] sync-declared-packages: ${declared} refs re-opened`);
} else {
// No git info — use batched resolution to avoid OOM
const unresolvedCount = this.queries.getUnresolvedReferencesCount();
Expand Down
Loading