Procházet zdrojové kódy

fix(impact): link renamed default imports to the module file (Express controllers)

`import articlesController from './article.controller'` where the module does
`export default router` left the controller with no dependent: the renamed local
binds to the default export, so the binding name isn't found as a symbol and only
named imports linked. resolveModuleImportToFile now treats a default import like
a namespace import — the dependency is on the MODULE FILE regardless of the
default export's name. External packages don't resolve (no file), so
`import React from 'react'` still creates no edge.

FAIR coverage: Express realworld 70.4% -> 100% (all route controllers + the
aggregator now resolve; residual main.ts/seed.ts/jest.preset excluded as
entries/config per methodology). NestJS 93.8% -> 96.8%. No regression; FastAPI
98% unchanged. Full suite 1173; 1 regression test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Colby McHenry před 2 týdny
rodič
revize
2a0b6e001d
3 změnil soubory, kde provedl 42 přidání a 2 odebrání
  1. 1 0
      CHANGELOG.md
  2. 33 0
      __tests__/extraction.test.ts
  3. 8 2
      src/resolution/import-resolver.ts

+ 1 - 0
CHANGELOG.md

@@ -41,6 +41,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 - Python `from package import submodule` now links to that submodule's file, resolved through the import's package so it lands on the right one when same-named modules exist in sibling packages (the FastAPI / Django router-aggregator pattern: `from app.api.routes import authentication` with an unrelated `authentication.py` elsewhere). So a route/handler module pulled in only by an aggregator is no longer reported as having no dependents. (Python)
 - Python `from package import submodule` now links to that submodule's file, resolved through the import's package so it lands on the right one when same-named modules exist in sibling packages (the FastAPI / Django router-aggregator pattern: `from app.api.routes import authentication` with an unrelated `authentication.py` elsewhere). So a route/handler module pulled in only by an aggregator is no longer reported as having no dependents. (Python)
 - Django `include('app.urls')` now records a dependency from the project URLconf onto the included app's `urls.py`, so an app's routes module is no longer reported as having no dependents and editing it surfaces the project that mounts it. (Django)
 - Django `include('app.urls')` now records a dependency from the project URLconf onto the included app's `urls.py`, so an app's routes module is no longer reported as having no dependents and editing it surfaces the project that mounts it. (Django)
 - A chained method call (`builder.Services.AddCoreServices(...)`) now resolves to its definition. Previously only a single-segment receiver (`obj.method()`) resolved, so a call through a property/member chain — very common for C# extension methods like ASP.NET dependency-injection registration (`AddCoreServices`/`AddWebServices`) and Guard clauses — found no definition. (C#, and any language with chained calls)
 - A chained method call (`builder.Services.AddCoreServices(...)`) now resolves to its definition. Previously only a single-segment receiver (`obj.method()`) resolved, so a call through a property/member chain — very common for C# extension methods like ASP.NET dependency-injection registration (`AddCoreServices`/`AddWebServices`) and Guard clauses — found no definition. (C#, and any language with chained calls)
+- A renamed default import (`import articlesController from './article.controller'` where the module does `export default router`) now records a dependency on the imported module. Previously only named imports linked, so a module consumed only through a default import — the standard Express/NestJS route-controller pattern — looked like nothing depended on it. External packages (`import React from 'react'`) still create no edge. (TypeScript/JavaScript)
 - The background file watcher no longer exhausts your machine's file-descriptor budget. On macOS it previously kept **one open file handle per watched file**, so on a large project the running MCP server could pile up tens of thousands of handles and blow past the system-wide limit — at which point *unrelated* apps (your shell, editor, Docker, browser) started failing with "too many open files" until the codegraph process was killed. The watcher now uses a single recursive watch on macOS and Windows, and bounded per-directory watches on Linux, so its cost stays flat no matter how large the project is. (#644, #496, #555, #628, #579)
 - The background file watcher no longer exhausts your machine's file-descriptor budget. On macOS it previously kept **one open file handle per watched file**, so on a large project the running MCP server could pile up tens of thousands of handles and blow past the system-wide limit — at which point *unrelated* apps (your shell, editor, Docker, browser) started failing with "too many open files" until the codegraph process was killed. The watcher now uses a single recursive watch on macOS and Windows, and bounded per-directory watches on Linux, so its cost stays flat no matter how large the project is. (#644, #496, #555, #628, #579)
 - Indexing a project with very symbol-dense files (tens of thousands of functions or methods in a single file) no longer runs out of memory. The step that links dynamic call relationships used to load every function and method into memory at once, which could exhaust the heap and abort indexing with "JavaScript heap out of memory" on large or generated codebases; it now streams them, so memory stays flat no matter how many symbols the project has. (#610)
 - Indexing a project with very symbol-dense files (tens of thousands of functions or methods in a single file) no longer runs out of memory. The step that links dynamic call relationships used to load every function and method into memory at once, which could exhaust the heap and abort indexing with "JavaScript heap out of memory" on large or generated codebases; it now streams them, so memory stays flat no matter how many symbols the project has. (#610)
 - Indexing a very large repository no longer aborts during its first sync with a "too many SQL variables" error. (#540)
 - Indexing a very large repository no longer aborts during its first sync with a "too many SQL variables" error. (#540)

+ 33 - 0
__tests__/extraction.test.ts

@@ -3953,6 +3953,39 @@ describe('Python absolute module import resolution', () => {
   });
   });
 });
 });
 
 
+describe('Default import resolution (renamed default export)', () => {
+  let tempDir: string;
+  let cg: CodeGraph;
+
+  beforeEach(() => {
+    tempDir = createTempDir();
+  });
+
+  afterEach(() => {
+    if (cg) cg.close();
+    if (fs.existsSync(tempDir)) fs.rmSync(tempDir, { recursive: true, force: true });
+  });
+
+  it('links a renamed default import to the module file', async () => {
+    // Express route aggregator: `import articlesController from './controller'`
+    // where the module does `export default router`. The renamed local can't be
+    // found as a symbol, so the controller file had no dependent — the dependency
+    // is on the module file regardless of the default export's name.
+    fs.mkdirSync(path.join(tempDir, 'app'), { recursive: true });
+    fs.writeFileSync(path.join(tempDir, 'app/controller.ts'), `const router = { get() {} };\nexport default router;\n`);
+    fs.writeFileSync(path.join(tempDir, 'app/routes.ts'), `import myController from './controller';\nexport const api = myController;\n`);
+
+    cg = CodeGraph.initSync(tempDir);
+    await cg.indexAll();
+    cg.resolveReferences();
+
+    const controller = cg.getNodesByKind('file').find((n) => n.filePath.endsWith('app/controller.ts'));
+    expect(controller, 'controller.ts indexed').toBeDefined();
+    const deps = [...cg.getImpactRadius(controller!.id, 2).nodes.values()].map((n) => n.filePath ?? '');
+    expect(deps.some((p) => p.endsWith('routes.ts')), 'importer depends on the default-exporting module').toBe(true);
+  });
+});
+
 describe('Chained method-call resolution (C# extension methods)', () => {
 describe('Chained method-call resolution (C# extension methods)', () => {
   let tempDir: string;
   let tempDir: string;
   let cg: CodeGraph;
   let cg: CodeGraph;

+ 8 - 2
src/resolution/import-resolver.ts

@@ -1317,8 +1317,14 @@ function resolveModuleImportToFile(
     if (imp.localName !== ref.referenceName) continue;
     if (imp.localName !== ref.referenceName) continue;
 
 
     let modulePath: string;
     let modulePath: string;
-    if (imp.isNamespace) {
-      // `import * as ns from './x'` / `import mod` — the source IS the module.
+    if (imp.isNamespace || imp.isDefault) {
+      // `import * as ns from './x'` (namespace) or `import x from './x'`
+      // (default) — the dependency is on the MODULE FILE. A default import binds
+      // a (possibly renamed) local to whatever the module's default export is
+      // (`import articlesController from './article.controller'` ← `export
+      // default router`), so the binding name can't be found as a symbol — link
+      // the file the import resolves to instead. External modules don't resolve
+      // (no file), so `import React from 'react'` creates no edge.
       modulePath = imp.source;
       modulePath = imp.source;
     } else if (ref.language === 'python') {
     } else if (ref.language === 'python') {
       // `from . import certs` — the imported NAME is a submodule of the source.
       // `from . import certs` — the imported NAME is a submodule of the source.