Просмотр исходного кода

feat(resolution): Django DRF router.register → ViewSet

Django's ORM (_iterable_class, prior work) and URL routing (path/url/as_view→view)
were already covered. The remaining hole: DRF `router.register(r'articles',
ArticleViewSet)` — the core CRUD endpoints — wasn't extracted (only path()/url()),
so a DRF API's main resources connected to nothing (realworld's ArticleViewSet:
0 callers).

Fix (frameworks/python.ts): match `.register(r'prefix', XViewSet)` → route→ViewSet
class. The STRING first arg distinguishes DRF router.register from
`admin.site.register(Model, Admin)` (model class first arg); View/ViewSet suffix
keeps it to viewsets. The ViewSet class resolves via the existing View/ViewSet
pattern.

Validated: realworld VIEWSET /articles → ArticleViewSet (was 0). Narrow in corpus
(realworld 1 router; wagtail=path, saleor=GraphQL) but real for DRF-router APIs.
Agent A/B (wagtail Page flow, medium): with codegraph 4-7 reads / 1-4 grep / 58-81s
vs without 7-9 reads / 6 grep / 82-86s. No regression (wagtail/saleor route counts
unchanged — purely additive). Residuals: signals, DRF inherited viewset actions,
GraphQL resolvers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Colby McHenry 1 месяц назад
Родитель
Сommit
f527ecdfa2

+ 12 - 1
docs/design/dynamic-dispatch-coverage-playbook.md

@@ -180,7 +180,7 @@ Status legend: ✅ done+validated · 🔬 hole identified · ⬜ not started.
 | TypeScript/JS | NestJS | request → @Controller → DI service → repo | R | ✅ already well-covered (realworld S / immich M-L / amplication L): @decorator routes (HTTP/GraphQL/microservice/WS) via resolver + DI `this.svc.method()` controller→service resolves correctly at scale (name + co-location). No dynamic-dispatch hole. 🔬 committed `dist/` build output gets indexed (realworld) — general build-dir-ignore follow-up |
 | TypeScript/JS | RxJS / signals | subscribe → operator → observer | S | ⬜ |
 | Python | Django ORM | QuerySet → SQL compiler | R | ✅ |
-| Python | Django (views/signals) | url → view; signal → receiver | R/S | 🔬 (routes done; signals ⬜) |
+| Python | Django / DRF (views) | url → view → model | R + X | ✅ url→view (`path`/`url`/`as_view`) + **DRF `router.register`→ViewSet** (realworld S / wagtail M / saleor L); ORM QuerySet→SQL (prior work). 🔬 signals (`post_save`→receiver), DRF viewset CRUD actions (inherited), saleor GraphQL resolvers |
 | Python | Flask / FastAPI | request → route → dependency | R | 🔬 (routes done) |
 | Go | Gin / net/http | request → handler chain | ? | ⬜ |
 | Rust | Axum / Cargo workspace | request → handler; trait dispatch | R | 🔬 (workspaces done) |
@@ -280,6 +280,17 @@ Status legend: ✅ done+validated · 🔬 hole identified · ⬜ not started.
   **A first cut regressed mall 292→1** by dropping `@RequestMapping`-on-method — *caught by the cross-repo
   route-count check*; the playbook's regression guard earns its keep. Residuals: halo's custom patterns
   (9/29 resolve); Spring Data JPA derived queries (metaprogramming frontier).
+- **Django / DRF (validated 2026-05-23, realworld S / wagtail M / saleor L) — mostly covered + a DRF-router
+  fix.** The ORM (`_iterable_class`→ModelIterable, the original investigation) and URL routing
+  (`path`/`url`/`as_view`→view) were already done. The one hole: **DRF `router.register(r'articles',
+  ArticleViewSet)`** (the core CRUD endpoints) wasn't extracted — only `path()`/`url()` were. Fix
+  (`frameworks/python.ts`): match `router.register` (the STRING first arg separates it from
+  `admin.register(Model, Admin)`, whose first arg is a model class) → route→ViewSet class. Narrow in this
+  corpus (realworld has 1 router; wagtail uses `path()`, saleor is GraphQL) but real for DRF-router APIs.
+  Agent A/B (wagtail Page flow, medium): codegraph **4–7 reads / 1–4 grep / 58–81s** vs without **7–9 reads
+  / 6 grep / 82–86s** — fewer reads, fewer greps, faster. No regression (wagtail/saleor route counts
+  unchanged — purely additive). Residuals: signals (`post_save`→receiver), DRF viewset CRUD actions
+  (inherited from the base class, not in the user's ViewSet), saleor's GraphQL resolvers.
 - **Difficulty gradient is real:** named-ref dispatch (resolver) is cheap; anonymous
   callback dispatch (synthesizer) is medium; **anonymous-arrow handlers are the hard
   remaining gap** (no identity → need synthesizer link-through-body, not yet built).

+ 28 - 0
src/resolution/frameworks/python.ts

@@ -102,6 +102,34 @@ export const djangoResolver: FrameworkResolver = {
       }
     }
 
+    // DRF router registration: `router.register(r'articles', ArticleViewSet)` →
+    // route → the ViewSet class (the core CRUD endpoints, which path()/url() miss).
+    // The STRING first arg separates this from `admin.site.register(Model, Admin)`
+    // (whose first arg is a model class, not a string); the View/ViewSet suffix on
+    // the 2nd arg keeps it to DRF viewsets.
+    const routerRegex = /\.register\s*\(\s*r?['"]([^'"]+)['"]\s*,\s*([\w.]+)/g;
+    while ((match = routerRegex.exec(safe)) !== null) {
+      const prefix = match[1]!.replace(/^\^|\/?\$$/g, '');
+      const viewset = match[2]!.split('.').pop()!;
+      if (!/View(Set)?$/.test(viewset)) continue;
+      const line = safe.slice(0, match.index).split('\n').length;
+      const routeNode: Node = {
+        id: `route:${filePath}:${line}:VIEWSET:${prefix}`,
+        kind: 'route',
+        name: `VIEWSET /${prefix}`,
+        qualifiedName: `${filePath}::route:${prefix}`,
+        filePath, startLine: line, endLine: line, startColumn: 0, endColumn: match[0].length,
+        language: 'python', updatedAt: now,
+      };
+      nodes.push(routeNode);
+      references.push({
+        fromNodeId: routeNode.id,
+        referenceName: viewset,
+        referenceKind: 'references',
+        line, column: 0, filePath, language: 'python',
+      });
+    }
+
     return { nodes, references };
   },
 };