import { describe, it, expect, beforeAll, afterEach } from 'vitest'; import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; import { CodeGraph } from '../src'; import { initGrammars, loadAllGrammars } from '../src/extraction/grammars'; beforeAll(async () => { await initGrammars(); await loadAllGrammars(); }); describe('Django end-to-end framework extraction', () => { let tmpDir: string | undefined; afterEach(() => { if (tmpDir) fs.rmSync(tmpDir, { recursive: true, force: true }); tmpDir = undefined; }); it('creates a route->view edge from urls.py to view class', async () => { tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-django-')); fs.writeFileSync(path.join(tmpDir, 'manage.py'), '# marker\n'); fs.writeFileSync(path.join(tmpDir, 'requirements.txt'), 'django==4.2\n'); fs.mkdirSync(path.join(tmpDir, 'users')); fs.writeFileSync(path.join(tmpDir, 'users/__init__.py'), ''); fs.writeFileSync( path.join(tmpDir, 'users/views.py'), 'class UserListView:\n def get(self, request): pass\n' ); fs.writeFileSync( path.join(tmpDir, 'users/urls.py'), 'from django.urls import path\n' + 'from users.views import UserListView\n' + 'urlpatterns = [path("users/", UserListView.as_view(), name="user-list")]\n' ); const cg = CodeGraph.initSync(tmpDir); await cg.indexAll(); // Route node exists const routes = cg.getNodesByKind('route'); expect(routes.length).toBeGreaterThan(0); const route = routes.find((n) => n.name === 'users/'); expect(route).toBeDefined(); // View class exists const classNodes = cg.getNodesByKind('class'); const view = classNodes.find((n) => n.name === 'UserListView'); expect(view).toBeDefined(); // Edge route -> view exists const edges = cg.getOutgoingEdges(route!.id); const toView = edges.find((e) => e.target === view!.id); expect(toView).toBeDefined(); expect(toView!.kind).toBe('references'); cg.close(); }); }); describe('Flask end-to-end framework extraction', () => { let tmpDir: string | undefined; afterEach(() => { if (tmpDir) fs.rmSync(tmpDir, { recursive: true, force: true }); tmpDir = undefined; }); it('resolves stacked routes across @login_required to a view named after a builtin (index)', async () => { tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-flask-')); fs.writeFileSync(path.join(tmpDir, 'requirements.txt'), 'flask==3.0\n'); fs.writeFileSync( path.join(tmpDir, 'app.py'), 'from flask import Blueprint, render_template\n' + 'from flask_login import login_required\n' + 'bp = Blueprint("main", __name__)\n' + '\n' + '@bp.route("/", methods=["GET", "POST"])\n' + '@bp.route("/index", methods=["GET", "POST"])\n' + '@login_required\n' + 'def index():\n' + ' return render_template("index.html")\n' ); const cg = CodeGraph.initSync(tmpDir); await cg.indexAll(); // Both stacked @bp.route decorators are extracted (the second was previously // dropped because @login_required broke the "def must follow" assumption). const routes = cg.getNodesByKind('route'); expect(routes.map((r) => r.name).sort()).toEqual(['GET /', 'GET /index']); // The view function exists even though its name is a Python builtin method. const fn = cg.getNodesByKind('function').find((n) => n.name === 'index'); expect(fn).toBeDefined(); // Both routes resolve to it — exercises the bare-name builtin guard, which // previously filtered the `index` reference as a builtin method. for (const route of routes) { const edges = cg.getOutgoingEdges(route.id); const toView = edges.find((e) => e.target === fn!.id && e.kind === 'references'); expect(toView, `route ${route.name} should resolve to index()`).toBeDefined(); } cg.close(); }); }); describe('Flutter end-to-end — setState→build synthesis', () => { let tmpDir: string | undefined; afterEach(() => { if (tmpDir) fs.rmSync(tmpDir, { recursive: true, force: true }); tmpDir = undefined; }); it('synthesizes a handler→build edge when a State method calls setState', async () => { tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-flutter-')); fs.writeFileSync( path.join(tmpDir, 'main.dart'), 'import "package:flutter/material.dart";\n' + 'class CounterPage extends StatefulWidget {\n' + ' @override\n' + ' State createState() => _CounterPageState();\n' + '}\n' + 'class _CounterPageState extends State {\n' + ' int _count = 0;\n' + ' void _increment() {\n' + ' setState(() {\n' + ' _count++;\n' + ' });\n' + ' }\n' + ' @override\n' + ' Widget build(BuildContext context) {\n' + ' return Text("$_count");\n' + ' }\n' + '}\n' ); const cg = CodeGraph.initSync(tmpDir); await cg.indexAll(); const methods = cg.getNodesByKind('method'); const increment = methods.find((n) => n.name === '_increment'); const build = methods.find((n) => n.name === 'build'); expect(increment).toBeDefined(); expect(build).toBeDefined(); // setState re-runs build (Flutter-internal, no static edge). The synthesizer // bridges the handler → build so the "tap → setState → rebuilt UI" flow connects. const edges = cg.getOutgoingEdges(increment!.id); const toBuild = edges.find((e) => e.target === build!.id && e.kind === 'calls'); expect(toBuild, '_increment should reach build via setState synthesis').toBeDefined(); cg.close(); }); }); describe('C++ end-to-end — virtual override synthesis', () => { let tmpDir: string | undefined; afterEach(() => { if (tmpDir) fs.rmSync(tmpDir, { recursive: true, force: true }); tmpDir = undefined; }); it('resolves callers through typed object pointers', async () => { tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-cpp-')); let cg: CodeGraph | undefined; try { fs.writeFileSync( path.join(tmpDir, 'detect.hpp'), 'class CDetect {\n' + ' public:\n' + ' int Processing();\n' + '};\n' + 'class CDetector {\n' + ' private:\n' + ' CDetect* m_cpAlg = nullptr;\n' + ' public:\n' + ' int Run();\n' + ' int Flush();\n' + '};\n' ); fs.writeFileSync( path.join(tmpDir, 'detect.cpp'), '#include "detect.hpp"\n' + 'int CDetector::Run() { return m_cpAlg->Processing(); }\n' + 'int CDetector::Flush() { return m_cpAlg->Processing(); }\n' + 'int CDetect::Processing() { return 0; }\n' ); cg = CodeGraph.initSync(tmpDir); await cg.indexAll(); const processing = cg .getNodesByKind('method') .find((n) => n.qualifiedName.endsWith('CDetect::Processing')); expect(processing).toBeDefined(); const callers = cg.getCallers(processing!.id).map((c) => c.node.qualifiedName); expect(callers).toContain('CDetector::Run'); expect(callers).toContain('CDetector::Flush'); const runMethod = cg .getNodesByKind('method') .find((n) => n.qualifiedName.endsWith('CDetector::Run')); expect(runMethod).toBeDefined(); const callees = cg.getCallees(runMethod!.id).map((c) => c.node.qualifiedName); expect(callees).toContain('CDetect::Processing'); } finally { cg?.close(); } }); it('resolves typed pointer callers when the method name is ambiguous and the call sits inside a return/declaration', async () => { // Regression: an earlier version of the C++ receiver-type inference matched // the call line itself (`return m_cpAlg->Processing()`) and treated `return` // as the type, OR grabbed `int r =` as a type from the prefix. With Strategy // 3's "unique method name" fallback, the original issue example resolved // anyway — but as soon as two classes share a method name (very common in // real C++), both calls go unresolved. tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-cpp-')); let cg: CodeGraph | undefined; try { fs.writeFileSync( path.join(tmpDir, 'detect.hpp'), 'class CDetect { public: int Processing(); };\n' + 'class CWidget { public: int Processing(); };\n' + 'class CDetector {\n' + ' private:\n' + ' CDetect* m_cpAlg = nullptr;\n' + ' public:\n' + ' int RunReturn();\n' + ' int RunAssign();\n' + '};\n' ); fs.writeFileSync( path.join(tmpDir, 'detect.cpp'), '#include "detect.hpp"\n' + 'int CDetector::RunReturn() { return m_cpAlg->Processing(); }\n' + 'int CDetector::RunAssign() { int r = m_cpAlg->Processing(); return r; }\n' + 'int CDetect::Processing() { return 0; }\n' + 'int CWidget::Processing() { return 0; }\n' ); cg = CodeGraph.initSync(tmpDir); await cg.indexAll(); const detectProc = cg .getNodesByKind('method') .find((n) => n.qualifiedName === 'CDetect::Processing'); const widgetProc = cg .getNodesByKind('method') .find((n) => n.qualifiedName === 'CWidget::Processing'); expect(detectProc).toBeDefined(); expect(widgetProc).toBeDefined(); const detectCallers = cg.getCallers(detectProc!.id).map((c) => c.node.qualifiedName); expect(detectCallers).toContain('CDetector::RunReturn'); expect(detectCallers).toContain('CDetector::RunAssign'); // CWidget::Processing is never called — calls must NOT misroute here. const widgetCallers = cg.getCallers(widgetProc!.id).map((c) => c.node.qualifiedName); expect(widgetCallers).not.toContain('CDetector::RunReturn'); expect(widgetCallers).not.toContain('CDetector::RunAssign'); } finally { cg?.close(); } }); it('bridges a base virtual method to the subclass override', async () => { tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-cpp-')); fs.writeFileSync( path.join(tmpDir, 'iter.cpp'), 'class Iterator {\n' + ' public:\n' + ' virtual void Next() { }\n' + '};\n' + 'class DBIter : public Iterator {\n' + ' public:\n' + ' void Next() override { advance(); }\n' + ' void advance() { }\n' + '};\n' ); const cg = CodeGraph.initSync(tmpDir); await cg.indexAll(); // Two methods named Next: the base virtual (lower line) and the override. const nexts = cg .getNodesByKind('method') .filter((n) => n.name === 'Next') .sort((a, b) => a.startLine - b.startLine); expect(nexts.length).toBe(2); const [baseNext, overrideNext] = nexts; // A vtable call to Iterator::Next dispatches to DBIter::Next — bridge it so // trace/callees from the interface method reaches the implementation. const edge = cg .getOutgoingEdges(baseNext!.id) .find((e) => e.target === overrideNext!.id && e.kind === 'calls'); expect(edge, 'Iterator::Next should reach DBIter::Next via override synthesis').toBeDefined(); cg.close(); }); });