Repository navigation
fix(js,ts): an import of a package the repository declares itself links to that declaration - #2718
Merged
Merged
Conversation
…ks to that declaration
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>
colbymchenry
added a commit
that referenced
this pull request
Oct 21, 2026
…ts module (#2728) A declaration file's declare module 'x' { ... } declares the members of x, which no other file names by a bare name: an import of x reaches them. The functions of such a block have been held to that since #2718, but its other declarations, the types, classes, constants and enums, still took bare-name matches from any file. On vscode, Node's import { readFileSync } from 'fs' linked to the fs that vscode.d.ts's workspace namespace declares, electron's clipboard and shell to env's, a ProcessEnv to the proposed API's, and an LSP TextDocument re-exported through a project barrel to the API's own. Every block-level declaration now takes a match from another file only when the reference is written in a block declaring the same module (the proposed API files merge into vscode.d.ts), is a type position in a file that imports the module (vscode.TextDocument under a namespace import, which the extractor records by its last name), or names what the file imports from a project file that imports the module and re-exports it (copilot's vscodeTypes.ts). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 21, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
An import of a package the repository declares itself, with
declare module '<name>' { … }in a script.d.ts(a declaration file with no top-levelimportorexport), now links to that declaration. For example, cord-field'stypings/mammoth/index.d.tsand VS Code's ownsrc/vscode-dts/vscode.d.ts. TypeScript's checker looks a specifier up among the ambient modules before it resolves the specifier to a file, soimport { MammothOptions } from 'mammoth'means the repository's declaration, whatever the package ships.Before this change, nothing mapped a package name to a file in the repository:
package.jsondeclares.So these imports linked nothing. Or, where the guard didn't fire (VS Code's extensions don't list
vscodeas a dependency), they linked by bare name to whichever namesake ranked first: copilot's vendored copies of VS Code internals,src/vsimplementation classes, or test fixtures.A function such a file declares without a body (
export function convertToHtml(…): Promise<Result>;, a tree-sitterfunction_signature) produced no node, soconvertToHtmlhad nothing to link to even when the import resolved.Cause
resolveViaImport,resolveModuleImportToFileandjsModuleFileread a bare specifier only as a path to resolve. Members ofdeclare modulebodies have beenisExportedsince fix(dead-code): a type that declare module or declare global merges outward is not dead #2418, but no lookup reached them by module name.function_signature. Skipping it everywhere was safe for overloads (function f(a: number): void;beforefunction f(a) {…}), but it dropped every ambient function.Fix
src/resolution/ambient-modules.ts(new):scanAmbientModulesreads a declaration file'sdeclare moduleblocks with a small token lexer that skips strings, comments and template literals. For each block it records the line range, the default export (export default X,export default class X,export = X), the names its export clause gives (export { Hidden as Shown }) and whether the file is a script..d.tsfiles only.findAmbientModuleExportbinds a named, default or namespace-member import to the declaration at the block's own level, by position. Same-named declarations (overloads, an interface and a value) give the one the reference's kind means.import-resolver.ts:vscodeto a runtime shim throughpaths, but itssrc/extension/vscode-api.d.tsreferencessrc/vscode-dts/vscode.d.ts, and the checker takes the ambient module.isBoundToOutOfRepoImportkeeps theextendsedge of a class whose base the repository declares.Extraction (
languages/typescript.tsisAmbientFunctionSignature, mirrored intsjs/mod.rs). A bodiless function is a function node in these places:declare(declare function f(): T;);declare globalbody;declare modulebody.Two places are left out:
declare namespace semver { function compare… }, napi'sexport declare namespace clipboards { function write… }, VS Code'swindow.showInformationMessage). It is only reached through the namespace, which the index doesn't model. On its own it was only a bare name for unrelated calls to land on: an early version of this PR linked bitwarden'sisAvailable(m.format), a callback parameter, to napi'sbiometrics.isAvailable..tsfile'sdeclare modulebody. That body is an augmentation (below).resolution/index.ts: a function a declaration file'sdeclare moduledeclares is reached from another file only through the import resolver. It stays in the name matcher's candidate crowd, because reachability may reject a guess but never manufacture one (fix(resolution): fuzzy matching skips a nested function the reference cannot reach #1709).name-matcher.ts: a type merged across the blocks that declare one module isn't treated as one declaration's when typing a receiver. VS Code's proposed API files add members tovscode.d.ts's interfaces, so copilot'sthis._progress.thinkingProgress()on aChatResponseStreamlives invscode.proposed.chatParticipantAdditions.d.ts. Applies intsImportedTypeFile,tsNamedType,tsNamedClassand call-result typing.Sync (
module-importers.tsreopenReferencesThroughDeclaredPackages, called fromsync). When a sync adds or edits a declaration file that declares a module, the files importing that package get their failed references, and the edges bound before the sync, re-opened through those imports. They import the package by name, which no import edge into the file records, and an aliased, default or namespace binding waits under a name the declaration never carries.Decisions (made without asking, as asked; open to change)
declare module 'x'in a module file merges into the package's own module (happa'ssrc/@types/grommet.d.ts, MUI theme files), and the import still names the package's declaration. Binding a package import to an in-repo fragment of its type would show the fragment as the definition. Finding augmentations would also mean reading every.tsfile each resolution pass: all 16 of cord-field's are in.ts/.tsxtheme and grid files.isTestFile). Copilot keeps fixture copies of proposed VS Code typings that its tsconfig excludes. Before this rule they took 781 imports from the real ones by being nearer.declare module '*.svg') and shorthands (declare module 'x';) bind nothing, as before.tsOutsideTypeDeclarationis left alone. It is fix(ts): a call on a known class goes through that class's own bases #2656's receiver-typing check, "a file of the project declares this package and names the type", and it asks a looser question on purpose: it also reads Flow libdefs (flow-typed/npm/*.js) and augmentations, which this index doesn't. Converging the two would drop those.Validation
Arms: dist snapshots of main
4286083aand this branch merged onto it (kernel built for each). Each repo gotcodegraph init -yper arm. Edges were diffed on natural keys and each removed edge was triaged at its site(source, kind, line, col). A first run onf5158facgave the same results, apart from 33 more retargeted vscode sites that #1935 changed on main. #1926, which landed afterwards, is Python-only.b3d1e667ce5386b9a321b07b066be79fc96352896f0a4d198b6aee80Byte-identical (nodes, edges and refs): LegendApp/legend-state, metabrainz/bookbrainz-site, hypha-dao/dho-web-client, ghostfolio/ghostfolio and mastodon/mastodon, whose
declare moduleblocks are all augmentations or wildcards (9, 2, 2, 1 and 3 + 5 patterns). The controls without any: taniarascia/takenote, oldboyxx/jira_clone, nextjs/saas-starter.The remaining added edges come from the new functions' own signatures (
referencesto types the same.d.tsdeclares): 30 on vscode and 19 across the rest. Each new node also has itscontainsedge.Triage
Precision. An edge in the "import into the block" column is, by construction, a declaration at the level of a script block whose specifier is exactly the import's. The diff script recomputes that from the target's file, using its own copy of the scanner, for every one of them. The same holds for the binding and module-ref columns: each target file declares that exact specifier.
vscode. Of the 7,736 sites whose edge now points elsewhere, 7,727 land in
src/vscode-dts. 6,594 of those are imported from'vscode'; the rest are member calls on values of those types and interface dispatch from them. Their old targets:src/util/vs/...), and itspathsshimvscodeTypesShim.ts, which itsvscode-api.d.tsoverrides for the type checker;src/vs: implementation classes, andmonaco.d.ts'sUri::joinPathforUri.joinPathfrom'vscode'.import type * as vscode from 'vscode'insrc/vs/workbench/apinow linksvscode.d.tsinstead of a constant namedvscodeinlanguageModelToolsService.ts;The other 9 sites are corrections too:
BufferStream/HtmlNodefrom'EmmetNode'. They bound by name toEmmetFlatNode.d.tsand now bind toEmmetNode.d.ts.testRunner.configurebound to a workbenchNullTestProfileService::configureand now binds to the importedtest/integration/electron/testrunner.d.ts.The 5 sites that lost their edge were all wrong on main:
TelemetrySenderconstructor was dispatched to implementations of VS Code's same-named interface (2 edges);/\w$/.test(…)bound to aCurrentTestRunInfo.testproperty;FileSystemProvider.stat/readDirectoryfanned out to copilot'sNewWorkspacePreviewFileSystemProvider.The 2,241 other removed edges are the same edge with new metadata (
exact-match→import).Onto new functions (vscode 27, frontend 2, bitwarden 2):
testRunner.configurefrom the importedtestrunner.d.ts(16);declare functions:acquireVsCodeApi,requestIdleCallback,__import;.d.ts: frontend's AudioWorkletregisterProcessorand service-workerskipWaiting, bitwarden'sunescape(its apps'globals.d.tseach declare it);clearImmediatein three language servers, which reach the globalclearImmediatedeclared in php-language-features'node.additions.d.ts.None is a module's function reached by bare name: the gate holds. An earlier version without the namespace rule and the gate linked 14 such calls on vscode:
getExtension,createStatusBarItem, semver'scompare,sortandpatch, and averifydeclared in an ordinary.tsfile'sdeclare module. It also linked bitwarden'sisAvailable(m.format), a parameter call, to napi's namespace function.excalidraw. Its
packages/excalidraw/global.d.tsnow has functions (encode,decode,extractof thepng-chunks-*packages). They count in the name matcher's crowd, as everything does (#1709):new TextEncoder().encode(data)→ the project'sencode) decline;Measured on the
f5158facbase, before the merge of #1935 and three Rust PRs:Sync convergence on real repos (
sync-check.sh):src/@types/*.d.tsdeleted and committed, indexed, restored, thencodegraph sync;typings/**/*.d.ts, same procedure. The new step re-opened 46 references there that the name-based retry can't find.Both synced graphs are byte-identical on nodes and edges to a fresh index of the same tree.
Kernel parity (
scripts/kernel-parity.mjs): 214/214 declaration-heavy files at byte parity (vscode'ssrc/vscode-dts, bitwarden's napi, the typings above). codegraph's ownsrc/and outline + happa have 0 diffs, with the same deferral counts as main (3 and 122).Tests
__tests__/ambient-module-imports.test.ts(new, 30 tests):Scanner: block ranges, script vs module (
import x = N.yandimport('y')types are not module markers), default names, export clauses, braces in strings, comments and template literal types, CRLF.Extraction, kernel and wasm, LF and CRLF, plus a parity check: ambient functions under
declareand indeclare global; none in a namespace or in an ordinary module file'sdeclare module; a declaration file's top level anddeclare modulebody; tsx.Resolution on indexed fixtures:
export =imports, export clauses, overloads, aliased imports, namespace members,extends,new;pathsalias mapping the package to its typings folder;encode);pathsshim;Sync: the declaration file added after its importers, and an edit giving it its declarations, each compared with a fresh index.
Red run on main: 24 of the 26 non-scanner tests fail; the scanner tests exercise the new module. The two that pass on main are the kernel/wasm parity check, where neither engine has the nodes, and the merged-declaration case. Main gets that one right only because it never resolves the import; the test guards against this change breaking it (see Teeth).
Teeth: disabling each safeguard in turn (gate, fixture rule, ambient-first, the merged-declaration rule in
tsImportedTypeFileand intsNamedType, the sync step) fails exactly its test. With the sync step disabled, the oracle comparison alone fails (34 vs 39 edges).Full suite on this commit, on main
642c7bb3(--maxWorkers=4, kernel built,CODEGRAPH_KERNEL_EXPECT=1): 8,111 tests, 8,029 passed, 0 failed, 82 skipped.tsc --noEmitis clean.Cost
Interleaved runs of
codegraph init -y, process CPU time including the worker threads:Wall time is unchanged within noise. The index reads only the declaration files that contain
declare module, once per resolver context. The bare-name gate is one set lookup per reference. The per-file and per-node answers are memoized: a first version without the memos cost happa about 5%.Follow-ups (each its own PR, next)
interface X extends Yrecords noextendsreference in either engine, so an interface hierarchy has no edges, even within one file. fix(java,ts): a parameterized supertype names its type, soimplements Store<String>links the interface #2705 covers parameterized supertypes only.declare moduleinterfaces, classes and constants from files that never import the module. On vscode's main this is 1,213 edges, for example Node'sfs→vscode.d.ts'sworkspace.fs. This PR gates only the functions it adds.isAvailable(f)andcompare(a, b)passed in as callbacks. fix(js): a name the calling function binds is its local #2226 settled the same-file case.🤖 Generated with Claude Code