소스 검색

fix(impact): Django include('app.urls') links project urlconf → app urls

`url(r'^api/', include('conduit.apps.articles.urls'))` emitted an `imports` ref
to the dotted module path, but resolveOne's pre-filter dropped it (no symbol /
import / framework claim named `conduit.apps.articles.urls`) before resolution
could map it to the file. The Django resolver now claims `*.urls` names so the
ref survives the pre-filter; resolvePythonAbsoluteModule then maps the dotted
module to its `urls.py`. So an app's routes module records a dependency on the
project urlconf that mounts it.

FAIR coverage: django-realworld 63.0% -> 74.1% (all three app urls.py now
resolve). Residual zeros are framework entries (root urlconf + AppConfig
__init__.py, loaded via settings strings) and dynamic frontiers (signals via
in-body `ready(): import myapp.signals`; DRF/string-config exception classes) —
documented, not in scope here. Full suite 1172; 1 regression test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Colby McHenry 2 주 전
부모
커밋
58dc463330
3개의 변경된 파일35개의 추가작업 그리고 3개의 파일을 삭제
  1. 1 0
      CHANGELOG.md
  2. 27 0
      __tests__/extraction.test.ts
  3. 7 3
      src/resolution/frameworks/python.ts

+ 1 - 0
CHANGELOG.md

@@ -39,6 +39,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 - Python now follows whole-module imports — `from . import certs` then `certs.where()`, or `from pkg import sub` then `sub.run()`. Calls and attribute access through an imported submodule now resolve to that submodule, and importing a module is recorded as a dependency on it even when the member you use is itself re-exported from a third-party package. This also fixed Python relative-import path resolution generally (`from .sub.mod import x`), so `codegraph affected` and impact see the real module graph of a package.
 - Python now also links a whole-module **absolute** import (`import conduit.apps.signals`) to that module's file, not just `from x import y`. A module imported by its dotted path — common in package setup and side-effect imports — is no longer reported as having no dependents. Standard-library imports (`import os`) correctly create no edge. (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)
 - 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 very large repository no longer aborts during its first sync with a "too many SQL variables" error. (#540)

+ 27 - 0
__tests__/extraction.test.ts

@@ -3896,6 +3896,33 @@ describe('Python absolute module import resolution', () => {
     expect(osNode, 'no stdlib os.py node').toBeUndefined();
   });
 
+  it('Django include() links the root URLconf to the included app urls module', async () => {
+    // `url(r'^api/', include('app.urls'))` should record a dependency from the
+    // root urlconf onto the included app's `urls.py` — so editing an app's routes
+    // surfaces the project urlconf that mounts them.
+    fs.mkdirSync(path.join(tempDir, 'app'), { recursive: true });
+    fs.writeFileSync(path.join(tempDir, 'requirements.txt'), `django==4.0\n`);
+    fs.writeFileSync(path.join(tempDir, 'app/__init__.py'), '');
+    fs.writeFileSync(path.join(tempDir, 'app/views.py'), `def home(request):\n    return None\n`);
+    fs.writeFileSync(
+      path.join(tempDir, 'app/urls.py'),
+      `from django.conf.urls import url\nfrom . import views\nurlpatterns = [url(r'^$', views.home)]\n`
+    );
+    fs.writeFileSync(
+      path.join(tempDir, 'urls.py'),
+      `from django.conf.urls import include, url\nurlpatterns = [url(r'^app/', include('app.urls'))]\n`
+    );
+
+    cg = CodeGraph.initSync(tempDir);
+    await cg.indexAll();
+    cg.resolveReferences();
+
+    const appUrls = cg.getNodesByKind('file').find((n) => n.filePath.endsWith('app/urls.py'));
+    expect(appUrls, 'app/urls.py indexed').toBeDefined();
+    const deps = [...cg.getImpactRadius(appUrls!.id, 2).nodes.values()].map((n) => n.filePath ?? '');
+    expect(deps.some((p) => p.endsWith('urls.py') && !p.endsWith('app/urls.py')), 'root urlconf depends on the included app urls').toBe(true);
+  });
+
   it('resolves `from pkg import submodule` to the submodule under that package, not a same-named one', async () => {
     // FastAPI router-aggregator pattern: `from app.api.routes import authentication`
     // with same-named modules in sibling packages must resolve via the import's

+ 7 - 3
src/resolution/frameworks/python.ts

@@ -48,10 +48,14 @@ export const djangoResolver: FrameworkResolver = {
     return null;
   },
 
-  // Let the ORM dynamic-dispatch ref reach resolve() despite no symbol being
-  // named `_iterable_class` (it's a QuerySet attribute, not a declared method).
+  // Let two ref shapes past resolveOne's "no possible match" pre-filter so they
+  // reach resolution: the ORM dynamic-dispatch `_iterable_class` (a QuerySet
+  // attribute, not a declared symbol), and a Django `include('app.urls')` module
+  // path — a dotted module name with no symbol/import to match, which resolution
+  // (resolvePythonAbsoluteModule) then maps to its `urls.py` file so the included
+  // URLconf records a dependency on the root urlconf.
   claimsReference(name) {
-    return name === '_iterable_class';
+    return name === '_iterable_class' || name.endsWith('.urls');
   },
 
   extract(filePath, content) {