/** * Extraction Tests * * Tests for the tree-sitter extraction system. */ import { describe, it, expect, beforeAll, beforeEach, afterEach } from 'vitest'; import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; import { CodeGraph } from '../src'; import { extractFromSource, scanDirectory, buildDefaultIgnore, discoverEmbeddedRepoRoots, buildScopeIgnore } from '../src/extraction'; import { detectLanguage, isLanguageSupported, getSupportedLanguages, initGrammars, loadAllGrammars, isSourceFile } from '../src/extraction/grammars'; import { stripCppTemplateArgs, blankCppExportMacros, blankCppInlineMacros, blankMetalAttributes, blankCudaConstructs, blankCppAnnotationMacroCalls, blankCppApiPrefixMacros, blankCppInlineAnnotationMacros, recoverMangledCppName } from '../src/extraction/languages/c-cpp'; import { normalizePath } from '../src/utils'; beforeAll(async () => { await initGrammars(); await loadAllGrammars(); }); // Create a temporary directory for each test function createTempDir(): string { return fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-test-')); } // Clean up temporary directory function cleanupTempDir(dir: string): void { if (fs.existsSync(dir)) { fs.rmSync(dir, { recursive: true, force: true }); } } describe('Language Detection', () => { it('should detect TypeScript files', () => { expect(detectLanguage('src/index.ts')).toBe('typescript'); expect(detectLanguage('components/Button.tsx')).toBe('tsx'); }); it('should detect JavaScript files', () => { expect(detectLanguage('index.js')).toBe('javascript'); expect(detectLanguage('App.jsx')).toBe('jsx'); expect(detectLanguage('config.mjs')).toBe('javascript'); }); it('should detect Python files', () => { expect(detectLanguage('main.py')).toBe('python'); }); it('should detect Go files', () => { expect(detectLanguage('main.go')).toBe('go'); }); it('should detect Rust files', () => { expect(detectLanguage('lib.rs')).toBe('rust'); }); it('should detect Java files', () => { expect(detectLanguage('Main.java')).toBe('java'); }); it('should detect C files', () => { expect(detectLanguage('main.c')).toBe('c'); expect(detectLanguage('utils.h')).toBe('c'); }); it('should detect C++ files', () => { expect(detectLanguage('main.cpp')).toBe('cpp'); expect(detectLanguage('class.hpp')).toBe('cpp'); }); it('should detect C# files', () => { expect(detectLanguage('Program.cs')).toBe('csharp'); }); it('should detect PHP files', () => { expect(detectLanguage('index.php')).toBe('php'); }); it('should detect Ruby files', () => { expect(detectLanguage('app.rb')).toBe('ruby'); }); it('should detect Swift files', () => { expect(detectLanguage('ViewController.swift')).toBe('swift'); }); it('should detect Kotlin files', () => { expect(detectLanguage('MainActivity.kt')).toBe('kotlin'); expect(detectLanguage('build.gradle.kts')).toBe('kotlin'); }); it('should detect Dart files', () => { expect(detectLanguage('main.dart')).toBe('dart'); }); it('should detect Objective-C files', () => { expect(detectLanguage('AppDelegate.m')).toBe('objc'); expect(detectLanguage('ViewController.mm')).toBe('objc'); const objcHeader = '@interface Foo : NSObject\n@end\n'; expect(detectLanguage('Foo.h', objcHeader)).toBe('objc'); expect(detectLanguage('stdio.h', '#ifndef STDIO_H\nvoid printf();\n#endif\n')).toBe('c'); }); it('should detect Metal shader files as C++ (#1121)', () => { expect(detectLanguage('Shaders.metal')).toBe('cpp'); expect(isSourceFile('Renderer/Shaders.metal')).toBe(true); }); it('should detect CUDA files as C++ (#387)', () => { expect(detectLanguage('kernels/scan.cu')).toBe('cpp'); expect(detectLanguage('include/reduce.cuh')).toBe('cpp'); expect(isSourceFile('csrc/flash_attn/softmax.cu')).toBe(true); expect(isSourceFile('include/block_reduce.cuh')).toBe(true); }); it('should detect Erlang files', () => { expect(detectLanguage('src/my_server.erl')).toBe('erlang'); expect(detectLanguage('include/records.hrl')).toBe('erlang'); expect(detectLanguage('bin/release_tool.escript')).toBe('erlang'); // OTP app resource files route by full suffix — `.src` alone is too generic. expect(detectLanguage('src/myapp.app.src')).toBe('erlang'); expect(detectLanguage('ebin/myapp.app')).toBe('erlang'); expect(detectLanguage('legacy/module.src')).toBe('unknown'); expect(isSourceFile('src/myapp.app.src')).toBe(true); expect(isSourceFile('ebin/myapp.app')).toBe(true); expect(isSourceFile('legacy/module.src')).toBe(false); }); it('should detect Solidity files', () => { expect(detectLanguage('contracts/Vault.sol')).toBe('solidity'); }); it('should detect Terraform files', () => { expect(detectLanguage('main.tf')).toBe('terraform'); expect(detectLanguage('variables.tf')).toBe('terraform'); expect(detectLanguage('terraform.tfvars')).toBe('terraform'); expect(detectLanguage('versions.tofu')).toBe('terraform'); }); it('should detect ArkTS files', () => { expect(detectLanguage('entry/src/main/ets/pages/Index.ets')).toBe('arkts'); // Plain `.ts` in a HarmonyOS project is still TypeScript. expect(detectLanguage('entry/src/main/ets/common/utils.ts')).toBe('typescript'); }); it('should detect Nix files', () => { expect(detectLanguage('default.nix')).toBe('nix'); expect(detectLanguage('pkgs/development/tools/misc/codegraph/default.nix')).toBe('nix'); expect(isSourceFile('default.nix')).toBe(true); }); it('should detect a .h whose only C++ signal is an export-macro class as cpp', () => { // Lean Unreal-Engine style header: the class is annotated with an export // macro and carries no explicit `public:`/`virtual`/`namespace`/`template`, // so the macro-blind `class\s+\w+\s*[:{]` branch alone can't see it. It must // still detect as C++ — otherwise the C extractor (classTypes: []) drops the // class definition entirely. (#1093 follow-up) const macroClassHeader = `#pragma once #include "CoreMinimal.h" UCLASS() class ENGINE_API UNetConnectionRepControl : public UObject { \tGENERATED_BODY() \tbool IsRepControlEnable() const; }; `; expect(detectLanguage('NetConnectionRepControl.h', macroClassHeader)).toBe('cpp'); // Macro class with no base clause, brace on the next line, still C++. expect(detectLanguage('Foo.h', 'MYMODULE_API_DECL\nclass MYMODULE_API FFoo\n{\n\tint X;\n};\n')).toBe('cpp'); // Export-macro struct with inheritance is likewise C++-only. expect(detectLanguage('Bar.h', 'struct ENGINE_API FBar : public FBase {};\n')).toBe('cpp'); // Guard: a genuine C header must NOT be dragged to C++ by the new branch. expect(detectLanguage('cfoo.h', '#ifndef CFOO_H\nstruct Point { int x; int y; };\nvoid f(struct Point p);\n#endif\n')).toBe('c'); }); it('should return unknown for unsupported extensions', () => { expect(detectLanguage('styles.css')).toBe('unknown'); expect(detectLanguage('data.json')).toBe('unknown'); }); }); describe('Language Support', () => { it('should report supported languages', () => { expect(isLanguageSupported('typescript')).toBe(true); expect(isLanguageSupported('python')).toBe(true); expect(isLanguageSupported('go')).toBe(true); expect(isLanguageSupported('unknown')).toBe(false); }); it('should list all supported languages', () => { const languages = getSupportedLanguages(); expect(languages).toContain('typescript'); expect(languages).toContain('javascript'); expect(languages).toContain('python'); expect(languages).toContain('go'); expect(languages).toContain('rust'); expect(languages).toContain('java'); expect(languages).toContain('csharp'); expect(languages).toContain('php'); expect(languages).toContain('ruby'); expect(languages).toContain('swift'); expect(languages).toContain('kotlin'); expect(languages).toContain('dart'); expect(languages).toContain('solidity'); expect(languages).toContain('nix'); }); }); describe('Nix Extraction', () => { it('should distinguish Nix variable and function bindings', () => { const code = ` let plainValue = 10; simpleFn = arg: arg + 1; destructuredFn = { lib, stdenv }: lib.getName stdenv; curriedFn = a: b: builtins.toString (a + b); in { exportedValue = plainValue; exportedFn = curriedFn; } `; const result = extractFromSource('default.nix', code); expect(result.nodes.find((n) => n.kind === 'variable' && n.name === 'plainValue')).toBeDefined(); expect(result.nodes.find((n) => n.kind === 'variable' && n.name === 'exportedValue')).toBeDefined(); const simpleFn = result.nodes.find((n) => n.kind === 'function' && n.name === 'simpleFn'); const destructuredFn = result.nodes.find((n) => n.kind === 'function' && n.name === 'destructuredFn'); const curriedFn = result.nodes.find((n) => n.kind === 'function' && n.name === 'curriedFn'); expect(simpleFn?.signature).toBe('(arg)'); expect(destructuredFn?.signature).toBe('{ lib, stdenv }'); expect(curriedFn?.signature).toBe('a : b'); const calls = result.unresolvedReferences.filter((r) => r.referenceKind === 'calls').map((r) => r.referenceName); expect(calls).toContain('lib.getName'); expect(calls.filter((name) => name === 'builtins.toString')).toHaveLength(1); }); it('should extract inherited Nix attributes as variables', () => { const code = ` let inherit lib; inherit (pkgs) stdenv writeShellScriptBin; in stdenv.mkDerivation {} `; const result = extractFromSource('default.nix', code); const variables = result.nodes.filter((n) => n.kind === 'variable').map((n) => n.name); expect(variables).toContain('lib'); expect(variables).toContain('stdenv'); expect(variables).toContain('writeShellScriptBin'); }); it('should emit only static project path imports for Nix import calls', () => { const code = ` let local = import ./x.nix; defaultFile = builtins.import ./dir; packageSet = import {}; fromSources = import sources.nixpkgs {}; dynamic = import selectedPath; in local `; const result = extractFromSource('default.nix', code); const imports = result.nodes.filter((n) => n.kind === 'import').map((n) => n.name); const importRefs = result.unresolvedReferences.filter((r) => r.referenceKind === 'imports').map((r) => r.referenceName); expect(imports).toEqual(['./x.nix', './dir']); expect(importRefs).toEqual(['./x.nix', './dir']); }); it('should emit file imports for NixOS module imports/modules lists (literal paths only)', () => { const code = ` { config, lib, ... }: { imports = [ ./hardware.nix ../common inputs.foo.nixosModules.bar ]; home-manager.users.demo.imports = [ ./home.nix ]; flake.modules = [ ./configuration.nix ]; notAModuleList = [ ./ignored.nix ]; } `; const result = extractFromSource('configuration.nix', code); const importRefs = result.unresolvedReferences.filter((r) => r.referenceKind === 'imports').map((r) => r.referenceName); expect(importRefs).toEqual(['./hardware.nix', '../common', './home.nix', './configuration.nix']); // The dynamic entry (inputs.foo.nixosModules.bar) must not create a ref. expect(importRefs).not.toContain('inputs.foo.nixosModules.bar'); }); it('should emit file imports for callPackage with a literal path and skip dynamic ones', () => { const code = ` { pkgs, newScope }: let hello = pkgs.callPackage ./pkgs/hello { }; tools = pkgs.callPackages ../tools/all.nix { }; dynamic = pkgs.callPackage pkgPath { }; in { inherit hello tools dynamic; } `; const result = extractFromSource('overlay.nix', code); const importRefs = result.unresolvedReferences.filter((r) => r.referenceKind === 'imports').map((r) => r.referenceName); const calls = result.unresolvedReferences.filter((r) => r.referenceKind === 'calls').map((r) => r.referenceName); expect(importRefs).toEqual(['./pkgs/hello', '../tools/all.nix']); // The call edge to callPackage itself is still recorded. expect(calls).toContain('pkgs.callPackage'); }); it('should mark returned top-level Nix attrset members exported and keep let or nested attrs private', () => { const code = ` { lib, stdenv }: let localValue = 10; in { exported = localValue; package = { name }: stdenv.mkDerivation { inherit name; }; nested = { privateNested = true; }; inherit (lib) licenses; } `; const result = extractFromSource('default.nix', code); const node = (name: string) => result.nodes.find((n) => n.name === name); expect(node('localValue')?.isExported).toBe(false); expect(node('exported')?.isExported).toBe(true); expect(node('package')?.kind).toBe('function'); expect(node('package')?.isExported).toBe(true); expect(node('privateNested')?.isExported).toBe(false); expect(node('licenses')?.isExported).toBe(true); }); }); describe('TypeScript Extraction', () => { it('should extract function declarations', () => { const code = ` export function processPayment(amount: number): Promise { return stripe.charge(amount); } `; const result = extractFromSource('payment.ts', code); // File node + function node const fileNode = result.nodes.find((n) => n.kind === 'file'); expect(fileNode).toBeDefined(); expect(fileNode?.name).toBe('payment.ts'); const funcNode = result.nodes.find((n) => n.kind === 'function'); expect(funcNode).toMatchObject({ kind: 'function', name: 'processPayment', language: 'typescript', isExported: true, }); expect(funcNode?.signature).toContain('amount: number'); }); it('should extract class declarations', () => { const code = ` export class PaymentService { private stripe: StripeClient; constructor(apiKey: string) { this.stripe = new StripeClient(apiKey); } async charge(amount: number): Promise { return this.stripe.charge(amount); } } `; const result = extractFromSource('service.ts', code); const classNode = result.nodes.find((n) => n.kind === 'class'); const methodNodes = result.nodes.filter((n) => n.kind === 'method'); expect(classNode).toBeDefined(); expect(classNode?.name).toBe('PaymentService'); expect(classNode?.isExported).toBe(true); expect(methodNodes.length).toBeGreaterThanOrEqual(1); const chargeMethod = methodNodes.find((m) => m.name === 'charge'); expect(chargeMethod).toBeDefined(); }); it('captures docstrings for export- and const-wrapped declarations (#780)', () => { const code = ` // plain class control class Ledger {} // exported class export class Invoice {} // export default export default function settle() { return true; } // exported arrow const export const refund = (amount: number) => amount; // non-export arrow const const audit = (amount: number) => amount; `; const byName = new Map(extractFromSource('doc.ts', code).nodes.map((n) => [n.name, n])); expect(byName.get('Ledger')?.docstring).toBe('plain class control'); // control still works expect(byName.get('Invoice')?.docstring).toBe('exported class'); expect(byName.get('settle')?.docstring).toBe('export default'); expect(byName.get('refund')?.docstring).toBe('exported arrow const'); expect(byName.get('audit')?.docstring).toBe('non-export arrow const'); }); it('does not mis-attribute a class comment to an uncommented member (#780)', () => { const code = ` // Comment for Box export class Box { noComment() {} // own comment withComment() {} } `; const byName = new Map(extractFromSource('box.ts', code).nodes.map((n) => [n.name, n])); expect(byName.get('Box')?.docstring).toBe('Comment for Box'); expect(byName.get('noComment')?.docstring ?? null).toBeNull(); // no over-walk expect(byName.get('withComment')?.docstring).toBe('own comment'); }); it('captures docstrings for decorated Python declarations, stripping `#` (#780)', () => { const code = [ '# decorated function', '@app.route("/x")', 'def py_handler():', ' return 1', '', '', '# plain function control', 'def py_plain():', ' return 1', '', '', '# decorated class', '@dataclass', 'class PyModel:', ' pass', '', ].join('\n'); const byName = new Map(extractFromSource('mod.py', code).nodes.map((n) => [n.name, n])); expect(byName.get('py_handler')?.docstring).toBe('decorated function'); expect(byName.get('py_plain')?.docstring).toBe('plain function control'); // `#` stripped expect(byName.get('PyModel')?.docstring).toBe('decorated class'); }); it('cleans comment markers across language styles (#780)', () => { const doc = (file: string, code: string, name: string) => new Map(extractFromSource(file, code).nodes.map((n) => [n.name, n])).get(name)?.docstring; // Rust doc lines (`///`, `//!`) — the trailing slash used to leak through. expect(doc('m.rs', '/// rust doc line\nfn rs_fn() {}', 'rs_fn')).toBe('rust doc line'); // Lua line + long-bracket comments. expect(doc('m.lua', '-- lua line\nfunction lua_fn() end', 'lua_fn')).toBe('lua line'); expect(doc('b.lua', '--[[ lua block ]]\nfunction lua_b() end', 'lua_b')).toBe('lua block'); // Pascal brace and paren-star comments. const pasUnit = (c: string) => `unit U;\ninterface\n${c}\nprocedure P;\nimplementation\nprocedure P;\nbegin\nend;\nend.\n`; expect(doc('a.pas', pasUnit('{ pascal brace }'), 'P')).toBe('pascal brace'); expect(doc('c.pas', pasUnit('(* pascal paren *)'), 'P')).toBe('pascal paren'); // C block comment still clean (no regression). expect(doc('m.c', '/* c block */\nvoid c_fn(void) {}', 'c_fn')).toBe('c block'); }); it('should extract interfaces', () => { const code = ` export interface User { id: string; name: string; email: string; } `; const result = extractFromSource('types.ts', code); const fileNode = result.nodes.find((n) => n.kind === 'file'); expect(fileNode).toBeDefined(); const ifaceNode = result.nodes.find((n) => n.kind === 'interface'); expect(ifaceNode).toMatchObject({ kind: 'interface', name: 'User', isExported: true, }); }); it('should extract type references from interface property signatures', () => { const code = ` import type { IPage } from '../PromoterList'; import type { IOrderField } from '../types'; interface Hprops { value?: Partial & Partial; } `; const result = extractFromSource('HeaderFilter.ts', code); const refs = result.unresolvedReferences.filter((r) => r.referenceKind === 'references'); expect(refs.some((r) => r.referenceName === 'IPage')).toBe(true); expect(refs.some((r) => r.referenceName === 'IOrderField')).toBe(true); }); it('should extract type references from interface method signatures', () => { const code = ` import type { IPage } from '../PromoterList'; import type { IOrderField } from '../types'; interface MethodForm { fetchPage(arg: IPage): IOrderField; } `; const result = extractFromSource('MethodForm.ts', code); const refs = result.unresolvedReferences.filter((r) => r.referenceKind === 'references'); expect(refs.some((r) => r.referenceName === 'IPage')).toBe(true); expect(refs.some((r) => r.referenceName === 'IOrderField')).toBe(true); }); it('extracts type references from in-body local variable annotations', () => { // A function that uses a type ONLY in its body — `const items: Foo[] = []` — // still depends on Foo. The body walker used to capture calls but never type // annotations, so impact / `affected` missed the dependency. Must cover // function, class-method, and object-literal-method bodies — and must NOT // turn the locals themselves into graph nodes (that would explode the graph). const code = ` import { Foo } from './types'; export function build(): void { const items: Foo[] = []; void items; } export class K { run(): void { const a: Foo = { x: 1 }; void a; } } export const handler = { handle(): void { const b: Foo = { x: 1 }; void b; }, }; `; const result = extractFromSource('inbody.ts', code); const fooRefs = result.unresolvedReferences.filter( (r) => r.referenceKind === 'references' && r.referenceName === 'Foo' ); // One per body scope: build(), K.run(), handler.handle(). expect(fooRefs.length).toBeGreaterThanOrEqual(3); // Each reference is attributed to its enclosing function/method node — never // to a local-variable node, because locals are intentionally not extracted. const byId = new Map(result.nodes.map((n) => [n.id, n])); for (const ref of fooRefs) { const owner = byId.get(ref.fromNodeId); expect(owner).toBeDefined(); expect(['function', 'method']).toContain(owner!.kind); } // The locals (items/a/b) must not leak in as symbols. expect(result.nodes.some((n) => ['items', 'a', 'b'].includes(n.name))).toBe(false); }); it('should track function calls', () => { const code = ` function main() { const result = processData(); console.log(result); } `; const result = extractFromSource('main.ts', code); expect(result.unresolvedReferences.length).toBeGreaterThan(0); const calls = result.unresolvedReferences.filter((r) => r.referenceKind === 'calls'); expect(calls.some((c) => c.referenceName === 'processData')).toBe(true); }); }); describe('Arrow Function Export Extraction', () => { it('should extract exported arrow functions assigned to const', () => { const code = ` export const useAuth = (): AuthContextValue => { return useContext(AuthContext); }; `; const result = extractFromSource('hooks.ts', code); const funcNode = result.nodes.find((n) => n.kind === 'function' && n.name === 'useAuth'); expect(funcNode).toBeDefined(); expect(funcNode).toMatchObject({ kind: 'function', name: 'useAuth', isExported: true, }); }); it('should extract exported function expressions assigned to const', () => { const code = ` export const processData = function(input: string): string { return input.trim(); }; `; const result = extractFromSource('utils.ts', code); const funcNode = result.nodes.find((n) => n.kind === 'function' && n.name === 'processData'); expect(funcNode).toBeDefined(); expect(funcNode).toMatchObject({ kind: 'function', name: 'processData', isExported: true, }); }); it('should not extract non-exported arrow functions as exported', () => { const code = ` const internalHelper = () => { return 42; }; `; const result = extractFromSource('internal.ts', code); const helperNode = result.nodes.find((n) => n.name === 'internalHelper'); expect(helperNode).toBeDefined(); expect(helperNode?.isExported).toBeFalsy(); }); it('should still skip truly anonymous arrow functions', () => { const code = ` const items = [1, 2, 3].map((x) => x * 2); `; const result = extractFromSource('anon.ts', code); // The inline arrow function passed to .map() has no variable_declarator parent // and should remain anonymous (skipped) const anonFunctions = result.nodes.filter( (n) => n.kind === 'function' && n.name === '' ); expect(anonFunctions).toHaveLength(0); }); it('should extract multiple exported arrow functions from the same file', () => { const code = ` export const add = (a: number, b: number): number => a + b; export const subtract = (a: number, b: number): number => a - b; const internal = () => 'not exported'; `; const result = extractFromSource('math.ts', code); const exported = result.nodes.filter((n) => n.kind === 'function' && n.isExported); expect(exported).toHaveLength(2); expect(exported.map((n) => n.name).sort()).toEqual(['add', 'subtract']); const internalNode = result.nodes.find((n) => n.name === 'internal'); expect(internalNode).toBeDefined(); expect(internalNode?.isExported).toBeFalsy(); }); it('should extract arrow functions in JavaScript files', () => { const code = ` export const fetchData = async () => { const response = await fetch('/api/data'); return response.json(); }; `; const result = extractFromSource('api.js', code); const funcNode = result.nodes.find((n) => n.kind === 'function' && n.name === 'fetchData'); expect(funcNode).toBeDefined(); expect(funcNode).toMatchObject({ kind: 'function', name: 'fetchData', isExported: true, }); }); }); describe('Type Alias Extraction', () => { it('should extract exported type aliases in TypeScript', () => { const code = ` export type AuthContextValue = { user: User | null; login: () => void; logout: () => void; }; `; const result = extractFromSource('types.ts', code); const typeNode = result.nodes.find((n) => n.kind === 'type_alias'); expect(typeNode).toMatchObject({ kind: 'type_alias', name: 'AuthContextValue', isExported: true, }); }); it('should extract non-exported type aliases', () => { const code = ` type InternalState = { loading: boolean; error: string | null; }; `; const result = extractFromSource('internal.ts', code); const typeNode = result.nodes.find((n) => n.kind === 'type_alias'); expect(typeNode).toMatchObject({ kind: 'type_alias', name: 'InternalState', isExported: false, }); }); it('should extract multiple type aliases from the same file', () => { const code = ` export type UnitSystem = 'metric' | 'imperial'; export type DateFormat = 'ISO' | 'US' | 'EU'; type Internal = string; `; const result = extractFromSource('config.ts', code); const typeAliases = result.nodes.filter((n) => n.kind === 'type_alias'); expect(typeAliases).toHaveLength(3); const exported = typeAliases.filter((n) => n.isExported); expect(exported).toHaveLength(2); expect(exported.map((n) => n.name).sort()).toEqual(['DateFormat', 'UnitSystem']); }); // A service/contract registry written as a tuple of generic instantiations — // the names are string-literal type arguments, not declarations, so static // extraction otherwise never indexes them (issue #634). it('extracts string-literal contract names from a generic tuple type alias (#634)', () => { const code = ` interface Service { name: Name; } export type MyServiceList = [ Service<'query_apply_record', { pageNo: number }, { ok: boolean }>, Service<'apply_confirm', { code: string }, { ok: boolean }> ]; `; const result = extractFromSource('services/api.ts', code); const names = result.nodes.filter( (n) => n.kind === 'method' && n.qualifiedName.startsWith('MyServiceList::') ); expect(names.map((n) => n.name).sort()).toEqual(['apply_confirm', 'query_apply_record']); const queryNode = names.find((n) => n.name === 'query_apply_record'); expect(queryNode?.qualifiedName).toBe('MyServiceList::query_apply_record'); // Signature carries the full contract entry so search results show context. expect(queryNode?.signature).toContain("Service<'query_apply_record'"); // The string-literal name is contained by the type alias. const alias = result.nodes.find((n) => n.kind === 'type_alias' && n.name === 'MyServiceList'); const containsEdge = result.edges.find( (e) => e.kind === 'contains' && e.source === alias?.id && e.target === queryNode?.id ); expect(containsEdge).toBeDefined(); }); it('does not extract string literals from utility types or nested generics (#634)', () => { const code = ` interface User { id: string; name: string; } interface Service { name: Name; } export type Picked = Pick; export type Rec = Record<'foo' | 'bar', number>; // Tuple entry, but the name is a non-identifier route path; the nested Pick's // 'id' must also stay out (only DIRECT literal args of a tuple's generic count). export type Routes = [Service<'/api/users', Pick, {}>]; // Bare string-literal tuple — not generic type arguments. export type Names = ['alpha', 'beta']; `; const result = extractFromSource('noise.ts', code); const leaked = result.nodes.filter( (n) => (n.kind === 'method' || n.kind === 'property') && ['id', 'name', 'foo', 'bar', 'alpha', 'beta'].includes(n.name) ); expect(leaked).toEqual([]); }); }); describe('Exported Variable Extraction', () => { it('should extract exported const with call expression (Zustand store)', () => { const code = ` export const useUIStore = create((set) => ({ isOpen: false, toggle: () => set((s) => ({ isOpen: !s.isOpen })), })); `; const result = extractFromSource('store.ts', code); const varNode = result.nodes.find((n) => n.kind === 'constant' && n.name === 'useUIStore'); expect(varNode).toBeDefined(); expect(varNode?.isExported).toBe(true); }); it('should extract exported const with object literal', () => { const code = ` export const config = { apiUrl: 'https://api.example.com', timeout: 5000, }; `; const result = extractFromSource('config.ts', code); const varNode = result.nodes.find((n) => n.kind === 'constant' && n.name === 'config'); expect(varNode).toBeDefined(); expect(varNode?.isExported).toBe(true); }); it('should extract exported const with array literal', () => { const code = ` export const SCREEN_NAMES = ['home', 'settings', 'profile'] as const; `; const result = extractFromSource('constants.ts', code); const varNode = result.nodes.find((n) => n.kind === 'constant' && n.name === 'SCREEN_NAMES'); expect(varNode).toBeDefined(); expect(varNode?.isExported).toBe(true); }); it('should extract exported const with primitive value', () => { const code = ` export const MAX_RETRIES = 3; export const API_VERSION = "v2"; `; const result = extractFromSource('constants.ts', code); const variables = result.nodes.filter((n) => n.kind === 'constant'); expect(variables).toHaveLength(2); expect(variables.map((n) => n.name).sort()).toEqual(['API_VERSION', 'MAX_RETRIES']); }); it('should NOT duplicate arrow functions as both function and variable', () => { const code = ` export const useAuth = () => { return useContext(AuthContext); }; `; const result = extractFromSource('hooks.ts', code); // Should be extracted as function (from arrow function handler), NOT as variable const funcNodes = result.nodes.filter((n) => n.kind === 'function' && n.name === 'useAuth'); const varNodes = result.nodes.filter((n) => n.kind === 'variable' && n.name === 'useAuth'); expect(funcNodes).toHaveLength(1); expect(varNodes).toHaveLength(0); }); it('should extract non-exported const as non-exported variable', () => { const code = ` const internalConfig = { debug: true, }; `; const result = extractFromSource('internal.ts', code); // Non-exported const at file level should be extracted as a constant (not exported) const varNodes = result.nodes.filter((n) => (n.kind === 'variable' || n.kind === 'constant') && n.name === 'internalConfig'); expect(varNodes).toHaveLength(1); expect(varNodes[0]?.isExported).toBeFalsy(); }); it('should extract Zod schema exports', () => { const code = ` export const userSchema = z.object({ id: z.string(), name: z.string(), email: z.string().email(), }); `; const result = extractFromSource('schemas.ts', code); const varNode = result.nodes.find((n) => n.kind === 'constant' && n.name === 'userSchema'); expect(varNode).toBeDefined(); expect(varNode?.isExported).toBe(true); }); it('should extract XState machine exports', () => { const code = ` export const authMachine = createMachine({ id: "auth", initial: "idle", states: { idle: {}, authenticated: {}, }, }); `; const result = extractFromSource('machine.ts', code); const varNode = result.nodes.find((n) => n.kind === 'constant' && n.name === 'authMachine'); expect(varNode).toBeDefined(); expect(varNode?.isExported).toBe(true); }); it('should extract calls from a top-level variable initializer (issue #425)', () => { const code = ` import { getTokenMp } from './api/upload'; const token = getTokenMp(); `; const result = extractFromSource('app.ts', code); const call = result.unresolvedReferences.find( (ref) => ref.referenceKind === 'calls' && ref.referenceName === 'getTokenMp' ); expect(call).toBeDefined(); }); }); describe('File Node Extraction', () => { it('should create a file-kind node for each parsed file', () => { const code = ` export function greet(name: string): string { return "Hello " + name; } `; const result = extractFromSource('greeter.ts', code); const fileNode = result.nodes.find((n) => n.kind === 'file'); expect(fileNode).toBeDefined(); expect(fileNode?.name).toBe('greeter.ts'); expect(fileNode?.filePath).toBe('greeter.ts'); expect(fileNode?.language).toBe('typescript'); expect(fileNode?.startLine).toBe(1); }); it('should create file nodes for Python files', () => { const code = ` def main(): pass `; const result = extractFromSource('main.py', code); const fileNode = result.nodes.find((n) => n.kind === 'file'); expect(fileNode).toBeDefined(); expect(fileNode?.name).toBe('main.py'); expect(fileNode?.language).toBe('python'); }); it('should create containment edges from file node to top-level declarations', () => { const code = ` export function foo() {} export function bar() {} `; const result = extractFromSource('fns.ts', code); const fileNode = result.nodes.find((n) => n.kind === 'file'); expect(fileNode).toBeDefined(); // There should be contains edges from the file node to each function const containsEdges = result.edges.filter( (e) => e.source === fileNode?.id && e.kind === 'contains' ); expect(containsEdges.length).toBeGreaterThanOrEqual(2); }); }); describe('Python Extraction', () => { it('should extract function definitions', () => { const code = ` def calculate_total(items: list, tax_rate: float) -> float: """Calculate total with tax.""" subtotal = sum(item.price for item in items) return subtotal * (1 + tax_rate) `; const result = extractFromSource('calc.py', code); const fileNode = result.nodes.find((n) => n.kind === 'file'); expect(fileNode).toBeDefined(); const funcNode = result.nodes.find((n) => n.kind === 'function'); expect(funcNode).toMatchObject({ kind: 'function', name: 'calculate_total', language: 'python', }); }); it('should extract class definitions', () => { const code = ` class UserService: """Service for managing users.""" def __init__(self, db): self.db = db def get_user(self, user_id: str) -> User: return self.db.find_user(user_id) `; const result = extractFromSource('service.py', code); const classNode = result.nodes.find((n) => n.kind === 'class'); expect(classNode).toBeDefined(); expect(classNode?.name).toBe('UserService'); }); }); describe('Go Extraction', () => { it('should extract function declarations', () => { const code = ` package main func ProcessOrder(order Order) (Receipt, error) { // Process the order return Receipt{}, nil } `; const result = extractFromSource('main.go', code); const funcNode = result.nodes.find((n) => n.kind === 'function'); expect(funcNode).toBeDefined(); expect(funcNode?.name).toBe('ProcessOrder'); }); it('should extract method declarations', () => { const code = ` package main type Service struct { db *Database } func (s *Service) GetUser(id string) (*User, error) { return s.db.FindUser(id) } `; const result = extractFromSource('service.go', code); const methodNode = result.nodes.find((n) => n.kind === 'method'); expect(methodNode).toBeDefined(); expect(methodNode?.name).toBe('GetUser'); }); }); describe('Rust Extraction', () => { it('should extract function declarations', () => { const code = ` pub fn process_data(input: &str) -> Result { // Process data Ok(Output::new()) } `; const result = extractFromSource('lib.rs', code); const funcNode = result.nodes.find((n) => n.kind === 'function'); expect(funcNode).toBeDefined(); expect(funcNode?.name).toBe('process_data'); expect(funcNode?.visibility).toBe('public'); }); it('should extract struct declarations', () => { const code = ` pub struct User { pub id: String, pub name: String, email: String, } `; const result = extractFromSource('models.rs', code); const structNode = result.nodes.find((n) => n.kind === 'struct'); expect(structNode).toBeDefined(); expect(structNode?.name).toBe('User'); }); it('should extract trait declarations', () => { const code = ` pub trait Repository { fn find(&self, id: &str) -> Option; fn save(&mut self, entity: Entity) -> Result<(), Error>; } `; const result = extractFromSource('traits.rs', code); const traitNode = result.nodes.find((n) => n.kind === 'trait'); expect(traitNode).toBeDefined(); expect(traitNode?.name).toBe('Repository'); }); it('should extract impl Trait for Type as implements edges', () => { const code = ` pub struct MyCache {} pub trait Cache { fn get(&self, key: &str) -> Option; } impl Cache for MyCache { fn get(&self, key: &str) -> Option { None } } `; const result = extractFromSource('cache.rs', code); // Should have an unresolved reference for implements const implRef = result.unresolvedReferences.find( (r) => r.referenceKind === 'implements' && r.referenceName === 'Cache' ); expect(implRef).toBeDefined(); // The struct MyCache should be the source const myCacheNode = result.nodes.find((n) => n.name === 'MyCache' && n.kind === 'struct'); expect(myCacheNode).toBeDefined(); expect(implRef?.fromNodeId).toBe(myCacheNode?.id); }); it('should extract trait supertraits as extends references', () => { const code = ` pub trait Display {} pub trait Error: Display { fn description(&self) -> &str; } `; const result = extractFromSource('error.rs', code); const extendsRef = result.unresolvedReferences.find( (r) => r.referenceKind === 'extends' && r.referenceName === 'Display' ); expect(extendsRef).toBeDefined(); const errorTrait = result.nodes.find((n) => n.name === 'Error' && n.kind === 'trait'); expect(errorTrait).toBeDefined(); expect(extendsRef?.fromNodeId).toBe(errorTrait?.id); }); it('should not create implements edges for plain impl blocks', () => { const code = ` pub struct Counter { count: u32, } impl Counter { pub fn new() -> Counter { Counter { count: 0 } } pub fn increment(&mut self) { self.count += 1; } } `; const result = extractFromSource('counter.rs', code); // Should have no implements references (no trait involved) const implRefs = result.unresolvedReferences.filter( (r) => r.referenceKind === 'implements' ); expect(implRefs).toHaveLength(0); }); }); describe('Java Extraction', () => { it('should extract class declarations', () => { const code = ` public class UserService { private final UserRepository repository; public UserService(UserRepository repository) { this.repository = repository; } public User getUser(String id) { return repository.findById(id); } } `; const result = extractFromSource('UserService.java', code); const classNode = result.nodes.find((n) => n.kind === 'class'); expect(classNode).toBeDefined(); expect(classNode?.name).toBe('UserService'); expect(classNode?.visibility).toBe('public'); }); it('should extract method declarations', () => { const code = ` public class Calculator { public static int add(int a, int b) { return a + b; } } `; const result = extractFromSource('Calculator.java', code); const methodNode = result.nodes.find((n) => n.kind === 'method' && n.name === 'add'); expect(methodNode).toBeDefined(); expect(methodNode?.isStatic).toBe(true); }); it('wraps top-level declarations in a namespace from package_declaration', () => { const code = ` package com.example.foo; public class Bar { public String greet() { return "hi"; } } `; const result = extractFromSource('Bar.java', code); const ns = result.nodes.find((n) => n.kind === 'namespace'); expect(ns?.name).toBe('com.example.foo'); const cls = result.nodes.find((n) => n.kind === 'class' && n.name === 'Bar'); expect(cls?.qualifiedName).toBe('com.example.foo::Bar'); const greet = result.nodes.find((n) => n.kind === 'method' && n.name === 'greet'); expect(greet?.qualifiedName).toBe('com.example.foo::Bar::greet'); }); it('does not wrap when no package is declared', () => { const code = ` public class Bar { public String greet() { return "hi"; } } `; const result = extractFromSource('Bar.java', code); expect(result.nodes.find((n) => n.kind === 'namespace')).toBeUndefined(); const cls = result.nodes.find((n) => n.kind === 'class' && n.name === 'Bar'); expect(cls?.qualifiedName).toBe('Bar'); }); it('extracts anonymous-class overrides from `new T() { ... }`', () => { // The pattern that breaks the trace through `strategy.foo()` in // libraries like guava's Splitter: the lambda-returned anonymous // class overrides abstract methods on the base, but without // extracting those overrides the interface→impl synthesizer has // nothing to bridge. const code = ` package com.example; abstract class Base { abstract int compute(int x); } public class Factory { public Base make() { return new Base() { @Override int compute(int x) { return x + 1; } }; } } `; const result = extractFromSource('Factory.java', code); const anon = result.nodes.find((n) => n.kind === 'class' && /Base\$anon@/.test(n.name)); expect(anon, 'anonymous Base subclass should be extracted as a class').toBeDefined(); const compute = result.nodes.find( (n) => n.kind === 'method' && n.name === 'compute' && n.qualifiedName.includes('$anon@') ); expect(compute, 'override method should be a method on the anon class').toBeDefined(); expect(compute!.qualifiedName).toContain('Factory::make:: r.referenceKind === 'extends' && r.referenceName === 'Base' && r.fromNodeId === anon!.id ); expect(extendsRef, 'anon class should carry an `extends Base` reference').toBeDefined(); // The enclosing `make` method still emits an instantiates edge to Base — // anon extraction must not swallow that signal. const instantiatesRef = result.unresolvedReferences.find( (r) => r.referenceKind === 'instantiates' && r.referenceName === 'Base' ); expect(instantiatesRef, 'enclosing method should still instantiate Base').toBeDefined(); }); it('extracts anonymous-class overrides inside a lambda body', () => { // The exact guava pattern: a lambda is passed to a constructor, and the // lambda body returns `new T() { @Override ... }`. The anon class must // still surface even though it sits inside a lambda_expression node. const code = ` package com.example; interface Strategy { java.util.Iterator iterator(String s); } abstract class BaseIter implements java.util.Iterator { abstract int separatorStart(int start); } public class Splitter { private final Strategy strategy; public Splitter(Strategy s) { this.strategy = s; } public static Splitter on(char c) { return new Splitter((seq) -> new BaseIter() { @Override int separatorStart(int start) { return start + 1; } @Override public boolean hasNext() { return false; } @Override public String next() { return null; } }); } } `; const result = extractFromSource('Splitter.java', code); const anon = result.nodes.find((n) => n.kind === 'class' && /BaseIter\$anon@/.test(n.name)); expect(anon, 'anon BaseIter inside the lambda body should be extracted').toBeDefined(); const sepStart = result.nodes.find( (n) => n.kind === 'method' && n.name === 'separatorStart' && n.qualifiedName.includes('$anon@') ); expect(sepStart, 'override inside the lambda-returned anon class should be a method node').toBeDefined(); }); }); describe('C# Extraction', () => { it('should extract class declarations', () => { const code = ` public class OrderService { private readonly IOrderRepository _repository; public OrderService(IOrderRepository repository) { _repository = repository; } public async Task GetOrderAsync(string id) { return await _repository.FindByIdAsync(id); } } `; const result = extractFromSource('OrderService.cs', code); const classNode = result.nodes.find((n) => n.kind === 'class'); expect(classNode).toBeDefined(); expect(classNode?.name).toBe('OrderService'); expect(classNode?.visibility).toBe('public'); }); it('indexes every record form with the right kind (#831)', () => { // The grammar parses ALL record forms as record_declaration — there is no // record_struct_declaration node — so the value-type forms are told apart // by their `struct` keyword child. Positional one-liners have no body // block and must still index (the no-body gate is for C/C++ forward // declarations, not records). const code = ` namespace Fixture; public record SimplePositional(int A); public record WithBody(int A) { public int DoubleIt() => A * 2; } public record class ExplicitClassRec(string Name); public record struct ValueRec(int X); public readonly record struct ReadonlyRec(int X, int Y); public record DerivedRec(int A, string B) : SimplePositional(A); public record GenericRec(T Value); public partial record PartialRec(int A); `; const result = extractFromSource('Records.cs', code); const kindOf = (name: string) => result.nodes.find((n) => n.name === name)?.kind; expect(kindOf('SimplePositional')).toBe('class'); expect(kindOf('WithBody')).toBe('class'); expect(kindOf('ExplicitClassRec')).toBe('class'); expect(kindOf('DerivedRec')).toBe('class'); expect(kindOf('GenericRec')).toBe('class'); expect(kindOf('PartialRec')).toBe('class'); // Value-type records are structs, not classes. expect(kindOf('ValueRec')).toBe('struct'); expect(kindOf('ReadonlyRec')).toBe('struct'); // Members of a bodied record still extract. expect(kindOf('DoubleIt')).toBe('method'); }); it('indexes primary-constructor classes, including keyed-DI attribute params (#237)', () => { // C# 12 primary constructors (`class Foo(IDep dep) { … }`) are parsed // natively by the vendored tree-sitter-c-sharp 0.23.x grammar. The worst // shape under the previous (older) grammar — an attribute-with-args on a // ctor param (`[FromKeyedServices("primary")] …`, the ASP.NET keyed-DI // pattern) — used to parse as an ERROR that swallowed the whole class, so // the class and all its methods vanished. They now index in every case. const code = ` public class DataService(IMemoryCache cache) { public void Warm() { } } public class InstanceService(InstanceManager m, ProfileManager p) { public void DeployAndLaunchAsync() { } public void Deploy() { } } public partial class UpdateService(int x) : ILifetimeService { public void Run() { } } public class K1KeyedDi([FromKeyedServices("primary")] IMemoryCache cache) { public void Warm() { } } public record CatalogBrand(int Id, string Name); `; const result = extractFromSource('Services.cs', code); const classNames = result.nodes.filter((n) => n.kind === 'class').map((n) => n.name); expect(classNames).toContain('DataService'); expect(classNames).toContain('InstanceService'); expect(classNames).toContain('UpdateService'); // partial + base list expect(classNames).toContain('K1KeyedDi'); // attribute-arg ctor param — used to vanish entirely expect(classNames).toContain('CatalogBrand'); // record const methods = result.nodes.filter((n) => n.kind === 'method').map((n) => n.name); expect(methods).toContain('DeployAndLaunchAsync'); expect(methods).toContain('Deploy'); expect(methods).toContain('Run'); }); it('keeps a class indexable when a nested enum has #if-guarded members (#237)', () => { // A `#if` directive inside an enum member list (the multi-targeting pattern // in libraries like Newtonsoft.Json) makes the grammar emit an ERROR that, // for a nested enum, detaches the enclosing class's member list — dropping // most of the class's methods. A pre-parse pass blanks the directive lines // (keeping both branches), so the class and all its methods still index. const code = ` public class Reader { private enum ReadType { #if HAVE_DATE_TIME_OFFSET ReadAsDateTimeOffset, #endif ReadAsDouble, ReadAsString, } public void Open() { } public void Close() { } public int ReadInt() { return 0; } } `; const result = extractFromSource('Reader.cs', code); const methods = result.nodes.filter((n) => n.kind === 'method').map((n) => n.name); // All three methods after the #if-bearing enum must survive. expect(methods).toContain('Open'); expect(methods).toContain('Close'); expect(methods).toContain('ReadInt'); // Both enum branches are kept. const enumMembers = result.nodes.filter((n) => n.kind === 'enum_member').map((n) => n.name); expect(enumMembers).toContain('ReadAsDateTimeOffset'); expect(enumMembers).toContain('ReadAsDouble'); }); }); describe('PHP Extraction', () => { it('should extract class declarations', () => { const code = `userService = $userService; } public function show(string $id): User { return $this->userService->find($id); } } `; const result = extractFromSource('UserController.php', code); const classNode = result.nodes.find((n) => n.kind === 'class'); expect(classNode).toBeDefined(); expect(classNode?.name).toBe('UserController'); }); it('should extract class inheritance (extends) and interface implementation', () => { const code = ` n.kind === 'class'); expect(classNode).toBeDefined(); expect(classNode?.name).toBe('ChildController'); const extendsRef = result.unresolvedReferences.find( (r) => r.referenceKind === 'extends' ); expect(extendsRef).toBeDefined(); expect(extendsRef?.referenceName).toBe('BaseController'); const implementsRefs = result.unresolvedReferences.filter( (r) => r.referenceKind === 'implements' ); expect(implementsRefs.length).toBe(2); expect(implementsRefs.map((r) => r.referenceName)).toContain('Serializable'); expect(implementsRefs.map((r) => r.referenceName)).toContain('JsonSerializable'); }); }); describe('Swift Extraction', () => { it('should extract class declarations', () => { const code = ` public class NetworkManager { private let session: URLSession public init(session: URLSession = .shared) { self.session = session } public func fetchData(from url: URL) async throws -> Data { let (data, _) = try await session.data(from: url) return data } } `; const result = extractFromSource('NetworkManager.swift', code); const classNode = result.nodes.find((n) => n.kind === 'class'); expect(classNode).toBeDefined(); expect(classNode?.name).toBe('NetworkManager'); }); it('should extract function declarations', () => { const code = ` func calculateSum(_ numbers: [Int]) -> Int { return numbers.reduce(0, +) } public func formatCurrency(amount: Double) -> String { return String(format: "$%.2f", amount) } `; const result = extractFromSource('utils.swift', code); const functions = result.nodes.filter((n) => n.kind === 'function'); expect(functions.length).toBeGreaterThanOrEqual(1); }); it('should extract struct declarations', () => { const code = ` public struct User { let id: UUID var name: String var email: String func displayName() -> String { return name } } `; const result = extractFromSource('User.swift', code); const structNode = result.nodes.find((n) => n.kind === 'struct'); expect(structNode).toBeDefined(); expect(structNode?.name).toBe('User'); }); it('should extract protocol declarations', () => { const code = ` public protocol Repository { associatedtype Entity func find(id: String) async throws -> Entity? func save(_ entity: Entity) async throws } `; const result = extractFromSource('Repository.swift', code); const protocolNode = result.nodes.find((n) => n.kind === 'interface'); expect(protocolNode).toBeDefined(); expect(protocolNode?.name).toBe('Repository'); }); it('should extract class inheritance and protocol conformance', () => { const code = ` class DataRequest: Request { func validate() {} } class UploadRequest: DataRequest, Sendable { func upload() {} } enum AFError: Error { case invalidURL } struct HTTPMethod: RawRepresentable { let rawValue: String } protocol UploadConvertible: URLRequestConvertible { func asURLRequest() throws -> URLRequest } `; const result = extractFromSource('Inheritance.swift', code); const extendsRefs = result.unresolvedReferences.filter( (r) => r.referenceKind === 'extends' ); // DataRequest extends Request expect(extendsRefs.find((r) => r.referenceName === 'Request')).toBeDefined(); // UploadRequest extends DataRequest and Sendable expect(extendsRefs.find((r) => r.referenceName === 'DataRequest')).toBeDefined(); expect(extendsRefs.find((r) => r.referenceName === 'Sendable')).toBeDefined(); // AFError extends Error expect(extendsRefs.find((r) => r.referenceName === 'Error')).toBeDefined(); // HTTPMethod extends RawRepresentable expect(extendsRefs.find((r) => r.referenceName === 'RawRepresentable')).toBeDefined(); // UploadConvertible extends URLRequestConvertible expect(extendsRefs.find((r) => r.referenceName === 'URLRequestConvertible')).toBeDefined(); }); it('indexes Swift properties so they are findable: computed → property, stored → field, static → constant/variable (#1020)', () => { const code = ` struct ReproConfig { let reproStoredValue: Int var reproComputedFlag: Bool { reproStoredValue > 0 } static let sharedLimit = 10 static var sharedCount = 0 func reproControlMethod() -> Bool { reproComputedFlag } } final class ReproService { private let reproClassStored: String = "x" var reproClassComputed: Int { reproClassStored.count } } `; const result = extractFromSource('Repro.swift', code); const byName = (name: string) => result.nodes.find((n) => n.name === name); // Computed properties are the regression this fix targets: before #1020 they // were dropped entirely, so search/explore returned nothing for them. expect(byName('reproComputedFlag')?.kind).toBe('property'); expect(byName('reproClassComputed')?.kind).toBe('property'); // Stored instance properties stay `field` (fixed earlier in #708 — guard it). expect(byName('reproStoredValue')?.kind).toBe('field'); expect(byName('reproClassStored')?.kind).toBe('field'); // `static let`/`static var` members remain shared constant/variable nodes. expect(byName('sharedLimit')?.kind).toBe('constant'); expect(byName('sharedCount')?.kind).toBe('variable'); // The control method is unaffected. expect(byName('reproControlMethod')?.kind).toBe('method'); }); it("attributes a computed property's getter calls to the property, not the type (SwiftUI body flow) (#1020)", () => { const code = ` struct GreetingView { let name: String var body: some View { let prefix = "Hi" return VStack { Text(greeting(prefix)) } } func greeting(_ p: String) -> String { p } } `; const result = extractFromSource('View.swift', code); const body = result.nodes.find((n) => n.kind === 'property' && n.name === 'body'); expect(body).toBeDefined(); // The getter's call to greeting() must originate from `body` (so a SwiftUI // view's render flow is reachable through the property), not flatten onto the // enclosing struct. const callsFromBody = result.unresolvedReferences.filter( (r) => r.fromNodeId === body!.id && r.referenceKind === 'calls' ); expect(callsFromBody.some((r) => r.referenceName === 'greeting')).toBe(true); // The getter is walked as a body, so a local declared inside it is NOT // node-ified (locals are the data-flow frontier we leave uncovered). Before // this fix the generic walker treated such a local as a struct `field`. expect(result.nodes.find((n) => n.name === 'prefix')).toBeUndefined(); }); it('indexes a Swift protocol property requirement as a findable property (#1020)', () => { const code = ` protocol Themable { var accentColor: Color { get } var title: String { get set } } `; const result = extractFromSource('Themable.swift', code); expect(result.nodes.find((n) => n.name === 'accentColor')?.kind).toBe('property'); expect(result.nodes.find((n) => n.name === 'title')?.kind).toBe('property'); }); }); describe('Kotlin Extraction', () => { it('should extract class declarations', () => { const code = ` class UserRepository(private val database: Database) { fun findById(id: String): User? { return database.query("SELECT * FROM users WHERE id = ?", id) } suspend fun save(user: User) { database.insert(user) } } `; const result = extractFromSource('UserRepository.kt', code); const classNode = result.nodes.find((n) => n.kind === 'class'); expect(classNode).toBeDefined(); expect(classNode?.name).toBe('UserRepository'); }); it('should extract function declarations', () => { const code = ` fun calculateTotal(items: List): Double { return items.sumOf { it.price } } suspend fun fetchUserData(userId: String): User { return api.getUser(userId) } `; const result = extractFromSource('utils.kt', code); const functions = result.nodes.filter((n) => n.kind === 'function'); expect(functions.length).toBeGreaterThanOrEqual(1); }); it('should detect suspend functions as async', () => { const code = ` suspend fun loadData(): List { delay(1000) return listOf("a", "b", "c") } `; const result = extractFromSource('loader.kt', code); const funcNode = result.nodes.find((n) => n.kind === 'function'); expect(funcNode).toBeDefined(); expect(funcNode?.isAsync).toBe(true); }); it('should extract fun interface declarations', () => { const code = ` fun interface OnObjectRetainedListener { fun onObjectRetained() } `; const result = extractFromSource('listener.kt', code); const ifaceNode = result.nodes.find((n) => n.kind === 'interface'); expect(ifaceNode).toBeDefined(); expect(ifaceNode?.name).toBe('OnObjectRetainedListener'); const methodNode = result.nodes.find((n) => n.kind === 'method'); expect(methodNode).toBeDefined(); expect(methodNode?.name).toBe('onObjectRetained'); expect(methodNode?.qualifiedName).toBe('OnObjectRetainedListener::onObjectRetained'); }); it('should extract complex fun interface with nested classes', () => { const code = ` fun interface EventListener { fun onEvent(event: Event) sealed class Event { class DumpingHeap : Event() } } `; const result = extractFromSource('events.kt', code); const ifaceNode = result.nodes.find((n) => n.kind === 'interface'); expect(ifaceNode).toBeDefined(); expect(ifaceNode?.name).toBe('EventListener'); // Nested sealed class should still be extracted (as sibling due to grammar limitations) const eventClass = result.nodes.find((n) => n.kind === 'class' && n.name === 'Event'); expect(eventClass).toBeDefined(); const dumpingHeap = result.nodes.find((n) => n.kind === 'class' && n.name === 'DumpingHeap'); expect(dumpingHeap).toBeDefined(); }); it('should not affect regular function declarations', () => { const code = ` fun interface MyCallback { fun invoke(value: Int) } fun regularFunction(): String { return "hello" } `; const result = extractFromSource('mixed.kt', code); const ifaceNode = result.nodes.find((n) => n.kind === 'interface'); expect(ifaceNode).toBeDefined(); expect(ifaceNode?.name).toBe('MyCallback'); const funcNode = result.nodes.find((n) => n.kind === 'function'); expect(funcNode).toBeDefined(); expect(funcNode?.name).toBe('regularFunction'); }); it('should extract fun interface with annotation on method (Pattern 2b)', () => { // When the SAM method has annotations like @Throws, tree-sitter produces a different // misparse: function_declaration > ERROR("interface Name {") instead of // function_declaration > user_type("interface"). This is the OkHttp Interceptor pattern. const code = ` import java.io.IOException fun interface Interceptor { @Throws(IOException::class) fun intercept(chain: Chain): Response } `; const result = extractFromSource('interceptor.kt', code); const ifaceNode = result.nodes.find((n) => n.kind === 'interface'); expect(ifaceNode).toBeDefined(); expect(ifaceNode?.name).toBe('Interceptor'); }); it('should extract methods from interface with nested fun interface', () => { // When an interface contains a nested `fun interface`, tree-sitter misparsed // the parent body as ERROR. Methods inside should still be extracted. const code = ` interface WebSocket { fun request(): Request fun send(text: String): Boolean fun cancel() fun interface Factory { fun newWebSocket(request: Request): WebSocket } } `; const result = extractFromSource('websocket.kt', code); const wsIface = result.nodes.find((n) => n.kind === 'interface' && n.name === 'WebSocket'); expect(wsIface).toBeDefined(); const methods = result.nodes.filter((n) => n.kind === 'method' && n.qualifiedName?.startsWith('WebSocket::')); const methodNames = methods.map((m) => m.name); expect(methodNames).toContain('request'); expect(methodNames).toContain('send'); expect(methodNames).toContain('cancel'); }); it('wraps top-level declarations in a namespace from package_header', () => { const code = ` package com.example.foo class Bar { fun greet(): String = "hi" } fun util(): Int = 42 `; const result = extractFromSource('Bar.kt', code); const ns = result.nodes.find((n) => n.kind === 'namespace'); expect(ns?.name).toBe('com.example.foo'); const cls = result.nodes.find((n) => n.kind === 'class' && n.name === 'Bar'); expect(cls?.qualifiedName).toBe('com.example.foo::Bar'); const greet = result.nodes.find((n) => n.kind === 'method' && n.name === 'greet'); expect(greet?.qualifiedName).toBe('com.example.foo::Bar::greet'); const util = result.nodes.find((n) => n.kind === 'function' && n.name === 'util'); expect(util?.qualifiedName).toBe('com.example.foo::util'); }); it('handles a single-segment package', () => { const code = ` package foo class Bar `; const result = extractFromSource('Bar.kt', code); const cls = result.nodes.find((n) => n.kind === 'class' && n.name === 'Bar'); expect(cls?.qualifiedName).toBe('foo::Bar'); }); it('does not wrap when no package is declared', () => { const code = ` class Bar { fun greet() = "hi" } `; const result = extractFromSource('Bar.kt', code); expect(result.nodes.find((n) => n.kind === 'namespace')).toBeUndefined(); const cls = result.nodes.find((n) => n.kind === 'class' && n.name === 'Bar'); expect(cls?.qualifiedName).toBe('Bar'); }); }); describe('Dart Extraction', () => { it('should extract class declarations', () => { const code = ` class UserService { final Database _db; Future findById(String id) async { return await _db.query(id); } void _privateMethod() {} } `; const result = extractFromSource('service.dart', code); const classNode = result.nodes.find((n) => n.kind === 'class'); expect(classNode).toBeDefined(); expect(classNode?.name).toBe('UserService'); expect(classNode?.visibility).toBe('public'); const methodNodes = result.nodes.filter((n) => n.kind === 'method'); expect(methodNodes.length).toBeGreaterThanOrEqual(2); const findById = methodNodes.find((m) => m.name === 'findById'); expect(findById).toBeDefined(); expect(findById?.isAsync).toBe(true); const privateMethod = methodNodes.find((m) => m.name === '_privateMethod'); expect(privateMethod).toBeDefined(); expect(privateMethod?.visibility).toBe('private'); // Dart models a method body as a SIBLING of the signature, so the method // node must be extended to span its body (not just the signature line) — // required for body-level analysis (callees, the callback synthesizer). expect(findById!.endLine).toBeGreaterThan(findById!.startLine); }); it('should extract top-level function declarations', () => { const code = ` void topLevelFunction(String name) { print(name); } `; const result = extractFromSource('utils.dart', code); const funcNode = result.nodes.find((n) => n.kind === 'function'); expect(funcNode).toBeDefined(); expect(funcNode?.name).toBe('topLevelFunction'); expect(funcNode?.language).toBe('dart'); }); it('should extract enum declarations', () => { const code = ` enum Status { active, inactive, pending } `; const result = extractFromSource('models.dart', code); const enumNode = result.nodes.find((n) => n.kind === 'enum'); expect(enumNode).toBeDefined(); expect(enumNode?.name).toBe('Status'); }); it('should extract mixin declarations', () => { const code = ` mixin LoggerMixin { void log(String message) {} } `; const result = extractFromSource('mixins.dart', code); const classNode = result.nodes.find((n) => n.kind === 'class'); expect(classNode).toBeDefined(); expect(classNode?.name).toBe('LoggerMixin'); const methodNode = result.nodes.find((n) => n.kind === 'method'); expect(methodNode).toBeDefined(); expect(methodNode?.name).toBe('log'); }); it('should extract extension declarations', () => { const code = ` extension StringExt on String { bool get isBlank => trim().isEmpty; } `; const result = extractFromSource('extensions.dart', code); const classNode = result.nodes.find((n) => n.kind === 'class'); expect(classNode).toBeDefined(); expect(classNode?.name).toBe('StringExt'); }); it('should detect static methods', () => { const code = ` class Utils { static void doWork() {} } `; const result = extractFromSource('utils.dart', code); const methodNode = result.nodes.find((n) => n.kind === 'method'); expect(methodNode).toBeDefined(); expect(methodNode?.name).toBe('doWork'); expect(methodNode?.isStatic).toBe(true); }); it('should detect async functions', () => { const code = ` Future fetchData() async { return await http.get('/data'); } `; const result = extractFromSource('api.dart', code); const funcNode = result.nodes.find((n) => n.kind === 'function'); expect(funcNode).toBeDefined(); expect(funcNode?.name).toBe('fetchData'); expect(funcNode?.isAsync).toBe(true); }); it('should detect private visibility via underscore convention', () => { const code = ` void _privateHelper() {} void publicFunction() {} `; const result = extractFromSource('helpers.dart', code); const functions = result.nodes.filter((n) => n.kind === 'function'); const privateFunc = functions.find((f) => f.name === '_privateHelper'); const publicFunc = functions.find((f) => f.name === 'publicFunction'); expect(privateFunc?.visibility).toBe('private'); expect(publicFunc?.visibility).toBe('public'); }); }); describe('Import Extraction', () => { describe('TypeScript/JavaScript imports', () => { it('should extract default imports', () => { const code = `import React from 'react';`; const result = extractFromSource('app.tsx', code); const importNode = result.nodes.find((n) => n.kind === 'import'); expect(importNode).toBeDefined(); expect(importNode?.name).toBe('react'); expect(importNode?.signature).toBe("import React from 'react';"); }); it('should extract named imports', () => { const code = `import { Bug, Database } from '@phosphor-icons/react';`; const result = extractFromSource('icons.tsx', code); const importNode = result.nodes.find((n) => n.kind === 'import'); expect(importNode).toBeDefined(); expect(importNode?.name).toBe('@phosphor-icons/react'); expect(importNode?.signature).toContain('Bug'); expect(importNode?.signature).toContain('Database'); }); it('should extract namespace imports', () => { const code = `import * as Icons from '@phosphor-icons/react';`; const result = extractFromSource('icons.tsx', code); const importNode = result.nodes.find((n) => n.kind === 'import'); expect(importNode).toBeDefined(); expect(importNode?.name).toBe('@phosphor-icons/react'); expect(importNode?.signature).toContain('* as Icons'); }); it('should extract side-effect imports', () => { const code = `import './styles.css';`; const result = extractFromSource('app.tsx', code); const importNode = result.nodes.find((n) => n.kind === 'import'); expect(importNode).toBeDefined(); expect(importNode?.name).toBe('./styles.css'); }); it('should extract mixed imports (default + named)', () => { const code = `import React, { useState, useEffect } from 'react';`; const result = extractFromSource('app.tsx', code); const importNode = result.nodes.find((n) => n.kind === 'import'); expect(importNode).toBeDefined(); expect(importNode?.name).toBe('react'); expect(importNode?.signature).toContain('React'); expect(importNode?.signature).toContain('useState'); expect(importNode?.signature).toContain('useEffect'); }); it('should extract multiple import statements', () => { const code = ` import React from 'react'; import { Button } from './components'; import './styles.css'; `; const result = extractFromSource('app.tsx', code); const importNodes = result.nodes.filter((n) => n.kind === 'import'); expect(importNodes.length).toBe(3); const names = importNodes.map((n) => n.name); expect(names).toContain('react'); expect(names).toContain('./components'); expect(names).toContain('./styles.css'); }); it('should extract type imports', () => { const code = `import type { FC, ReactNode } from 'react';`; const result = extractFromSource('types.ts', code); const importNode = result.nodes.find((n) => n.kind === 'import'); expect(importNode).toBeDefined(); expect(importNode?.name).toBe('react'); expect(importNode?.signature).toContain('type'); expect(importNode?.signature).toContain('FC'); }); it('should extract aliased named imports', () => { const code = `import { useState as useStateAlias } from 'react';`; const result = extractFromSource('hooks.ts', code); const importNode = result.nodes.find((n) => n.kind === 'import'); expect(importNode).toBeDefined(); expect(importNode?.name).toBe('react'); expect(importNode?.signature).toContain('useState'); expect(importNode?.signature).toContain('useStateAlias'); }); it('should extract relative path imports', () => { const code = `import { helper } from '../utils/helper';`; const result = extractFromSource('components/Button.tsx', code); const importNode = result.nodes.find((n) => n.kind === 'import'); expect(importNode).toBeDefined(); expect(importNode?.name).toBe('../utils/helper'); expect(importNode?.signature).toContain('helper'); }); }); describe('Python imports', () => { it('should extract simple import statement', () => { const code = `import json`; const result = extractFromSource('utils.py', code); const importNode = result.nodes.find((n) => n.kind === 'import'); expect(importNode).toBeDefined(); expect(importNode?.name).toBe('json'); }); it('should extract from import statement', () => { const code = `from os import path`; const result = extractFromSource('utils.py', code); const importNode = result.nodes.find((n) => n.kind === 'import'); expect(importNode).toBeDefined(); expect(importNode?.name).toBe('os'); expect(importNode?.signature).toContain('path'); }); it('should extract multiple imports from same module', () => { const code = `from typing import List, Dict, Optional`; const result = extractFromSource('types.py', code); const importNode = result.nodes.find((n) => n.kind === 'import'); expect(importNode).toBeDefined(); expect(importNode?.name).toBe('typing'); expect(importNode?.signature).toContain('List'); expect(importNode?.signature).toContain('Dict'); }); it('should extract multiple import statements', () => { const code = ` import os import sys `; const result = extractFromSource('main.py', code); const importNodes = result.nodes.filter((n) => n.kind === 'import'); expect(importNodes.length).toBe(2); const names = importNodes.map((n) => n.name); expect(names).toContain('os'); expect(names).toContain('sys'); }); it('should extract aliased import', () => { const code = `import numpy as np`; const result = extractFromSource('data.py', code); const importNode = result.nodes.find((n) => n.kind === 'import'); expect(importNode).toBeDefined(); expect(importNode?.name).toBe('numpy'); expect(importNode?.signature).toContain('as np'); }); it('should extract relative import', () => { const code = `from .utils import helper`; const result = extractFromSource('module.py', code); const importNode = result.nodes.find((n) => n.kind === 'import'); expect(importNode).toBeDefined(); expect(importNode?.name).toBe('.utils'); expect(importNode?.signature).toContain('helper'); }); it('should extract wildcard import', () => { const code = `from typing import *`; const result = extractFromSource('types.py', code); const importNode = result.nodes.find((n) => n.kind === 'import'); expect(importNode).toBeDefined(); expect(importNode?.name).toBe('typing'); expect(importNode?.signature).toContain('*'); }); }); describe('Rust imports', () => { it('should extract simple use declaration', () => { const code = `use std::io;`; const result = extractFromSource('main.rs', code); const importNode = result.nodes.find((n) => n.kind === 'import'); expect(importNode).toBeDefined(); expect(importNode?.name).toBe('std'); expect(importNode?.signature).toBe('use std::io;'); }); it('should extract scoped use list', () => { const code = `use std::{ffi::OsStr, io, path::Path};`; const result = extractFromSource('main.rs', code); const importNode = result.nodes.find((n) => n.kind === 'import'); expect(importNode).toBeDefined(); expect(importNode?.name).toBe('std'); expect(importNode?.signature).toContain('ffi::OsStr'); expect(importNode?.signature).toContain('path::Path'); }); it('should extract crate imports', () => { const code = `use crate::error::Error;`; const result = extractFromSource('lib.rs', code); const importNode = result.nodes.find((n) => n.kind === 'import'); expect(importNode).toBeDefined(); expect(importNode?.name).toBe('crate'); }); it('should extract super imports', () => { const code = `use super::utils;`; const result = extractFromSource('submod.rs', code); const importNode = result.nodes.find((n) => n.kind === 'import'); expect(importNode).toBeDefined(); expect(importNode?.name).toBe('super'); }); it('should extract external crate imports', () => { const code = `use serde::{Serialize, Deserialize};`; const result = extractFromSource('types.rs', code); const importNode = result.nodes.find((n) => n.kind === 'import'); expect(importNode).toBeDefined(); expect(importNode?.name).toBe('serde'); expect(importNode?.signature).toContain('Serialize'); expect(importNode?.signature).toContain('Deserialize'); }); }); describe('Go imports', () => { it('should extract single import', () => { const code = ` package main import "fmt" `; const result = extractFromSource('main.go', code); const importNode = result.nodes.find((n) => n.kind === 'import'); expect(importNode).toBeDefined(); expect(importNode?.name).toBe('fmt'); }); it('should extract grouped imports', () => { const code = ` package main import ( "fmt" "os" "encoding/json" ) `; const result = extractFromSource('main.go', code); const importNodes = result.nodes.filter((n) => n.kind === 'import'); expect(importNodes.length).toBe(3); const names = importNodes.map((n) => n.name); expect(names).toContain('fmt'); expect(names).toContain('os'); expect(names).toContain('encoding/json'); }); it('should extract aliased import', () => { const code = ` package main import f "fmt" `; const result = extractFromSource('main.go', code); const importNode = result.nodes.find((n) => n.kind === 'import'); expect(importNode).toBeDefined(); expect(importNode?.name).toBe('fmt'); expect(importNode?.signature).toContain('f'); }); it('should extract dot import', () => { const code = ` package main import . "math" `; const result = extractFromSource('main.go', code); const importNode = result.nodes.find((n) => n.kind === 'import'); expect(importNode).toBeDefined(); expect(importNode?.name).toBe('math'); expect(importNode?.signature).toContain('.'); }); it('should extract blank import', () => { const code = ` package main import _ "github.com/go-sql-driver/mysql" `; const result = extractFromSource('main.go', code); const importNode = result.nodes.find((n) => n.kind === 'import'); expect(importNode).toBeDefined(); expect(importNode?.name).toBe('github.com/go-sql-driver/mysql'); expect(importNode?.signature).toContain('_'); }); }); describe('Swift imports', () => { it('should extract simple import', () => { const code = `import Foundation`; const result = extractFromSource('main.swift', code); const importNode = result.nodes.find((n) => n.kind === 'import'); expect(importNode).toBeDefined(); expect(importNode?.name).toBe('Foundation'); expect(importNode?.signature).toBe('import Foundation'); }); it('should extract @testable import', () => { const code = `@testable import Alamofire`; const result = extractFromSource('Tests.swift', code); const importNode = result.nodes.find((n) => n.kind === 'import'); expect(importNode).toBeDefined(); expect(importNode?.name).toBe('Alamofire'); expect(importNode?.signature).toContain('@testable'); }); it('should extract @preconcurrency import', () => { const code = `@preconcurrency import Security`; const result = extractFromSource('Auth.swift', code); const importNode = result.nodes.find((n) => n.kind === 'import'); expect(importNode).toBeDefined(); expect(importNode?.name).toBe('Security'); }); it('should extract multiple imports', () => { const code = ` import Foundation import UIKit import Alamofire `; const result = extractFromSource('App.swift', code); const importNodes = result.nodes.filter((n) => n.kind === 'import'); expect(importNodes.length).toBe(3); const names = importNodes.map((n) => n.name); expect(names).toContain('Foundation'); expect(names).toContain('UIKit'); expect(names).toContain('Alamofire'); }); }); describe('Kotlin imports', () => { it('should extract simple import', () => { const code = `import java.io.IOException`; const result = extractFromSource('Main.kt', code); const importNode = result.nodes.find((n) => n.kind === 'import'); expect(importNode).toBeDefined(); expect(importNode?.name).toBe('java.io.IOException'); expect(importNode?.signature).toBe('import java.io.IOException'); }); it('should extract aliased import', () => { const code = `import okhttp3.Request.Builder as RequestBuilder`; const result = extractFromSource('Utils.kt', code); const importNode = result.nodes.find((n) => n.kind === 'import'); expect(importNode).toBeDefined(); expect(importNode?.name).toBe('okhttp3.Request.Builder'); expect(importNode?.signature).toContain('as RequestBuilder'); }); it('should extract wildcard import', () => { const code = `import java.util.concurrent.TimeUnit.*`; const result = extractFromSource('Time.kt', code); const importNode = result.nodes.find((n) => n.kind === 'import'); expect(importNode).toBeDefined(); expect(importNode?.name).toBe('java.util.concurrent.TimeUnit'); expect(importNode?.signature).toContain('.*'); }); it('should extract multiple imports', () => { const code = ` import java.io.IOException import kotlin.test.assertFailsWith import okhttp3.OkHttpClient `; const result = extractFromSource('Test.kt', code); const importNodes = result.nodes.filter((n) => n.kind === 'import'); expect(importNodes.length).toBe(3); const names = importNodes.map((n) => n.name); expect(names).toContain('java.io.IOException'); expect(names).toContain('kotlin.test.assertFailsWith'); expect(names).toContain('okhttp3.OkHttpClient'); }); }); describe('Java imports', () => { it('should extract simple import', () => { const code = `import java.util.List;`; const result = extractFromSource('Main.java', code); const importNode = result.nodes.find((n) => n.kind === 'import'); expect(importNode).toBeDefined(); expect(importNode?.name).toBe('java.util.List'); expect(importNode?.signature).toBe('import java.util.List;'); }); it('should extract static import', () => { const code = `import static java.util.Collections.emptyList;`; const result = extractFromSource('Utils.java', code); const importNode = result.nodes.find((n) => n.kind === 'import'); expect(importNode).toBeDefined(); expect(importNode?.name).toBe('java.util.Collections.emptyList'); expect(importNode?.signature).toContain('static'); }); it('should extract wildcard import', () => { const code = `import java.util.*;`; const result = extractFromSource('App.java', code); const importNode = result.nodes.find((n) => n.kind === 'import'); expect(importNode).toBeDefined(); expect(importNode?.name).toBe('java.util'); expect(importNode?.signature).toContain('.*'); }); it('should extract nested class import', () => { const code = `import java.util.Map.Entry;`; const result = extractFromSource('MapUtil.java', code); const importNode = result.nodes.find((n) => n.kind === 'import'); expect(importNode).toBeDefined(); expect(importNode?.name).toBe('java.util.Map.Entry'); }); it('should extract multiple imports', () => { const code = ` import java.util.List; import java.util.Map; import java.io.IOException; `; const result = extractFromSource('Service.java', code); const importNodes = result.nodes.filter((n) => n.kind === 'import'); expect(importNodes.length).toBe(3); const names = importNodes.map((n) => n.name); expect(names).toContain('java.util.List'); expect(names).toContain('java.util.Map'); expect(names).toContain('java.io.IOException'); }); }); describe('C# imports', () => { it('should extract simple using', () => { const code = `using System;`; const result = extractFromSource('Program.cs', code); const importNode = result.nodes.find((n) => n.kind === 'import'); expect(importNode).toBeDefined(); expect(importNode?.name).toBe('System'); expect(importNode?.signature).toBe('using System;'); }); it('should extract qualified using', () => { const code = `using System.Collections.Generic;`; const result = extractFromSource('Utils.cs', code); const importNode = result.nodes.find((n) => n.kind === 'import'); expect(importNode).toBeDefined(); expect(importNode?.name).toBe('System.Collections.Generic'); }); it('should extract static using', () => { const code = `using static System.Console;`; const result = extractFromSource('App.cs', code); const importNode = result.nodes.find((n) => n.kind === 'import'); expect(importNode).toBeDefined(); expect(importNode?.name).toBe('System.Console'); expect(importNode?.signature).toContain('static'); }); it('should extract alias using', () => { const code = `using MyList = System.Collections.Generic.List;`; const result = extractFromSource('Types.cs', code); const importNode = result.nodes.find((n) => n.kind === 'import'); expect(importNode).toBeDefined(); expect(importNode?.name).toBe('System.Collections.Generic.List'); expect(importNode?.signature).toContain('MyList ='); }); it('should extract multiple usings', () => { const code = ` using System; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; `; const result = extractFromSource('Service.cs', code); const importNodes = result.nodes.filter((n) => n.kind === 'import'); expect(importNodes.length).toBe(3); const names = importNodes.map((n) => n.name); expect(names).toContain('System'); expect(names).toContain('System.Threading.Tasks'); expect(names).toContain('Microsoft.Extensions.DependencyInjection'); }); }); describe('PHP imports', () => { it('should extract simple use', () => { const code = ` n.kind === 'import'); expect(importNode).toBeDefined(); expect(importNode?.name).toBe('PHPUnit\\Framework\\TestCase'); }); it('should extract aliased use', () => { const code = ` n.kind === 'import'); expect(importNode).toBeDefined(); expect(importNode?.name).toBe('Mockery'); expect(importNode?.signature).toContain('as m'); }); it('should extract function use', () => { const code = ` n.kind === 'import'); expect(importNode).toBeDefined(); expect(importNode?.name).toBe('Illuminate\\Support\\env'); expect(importNode?.signature).toContain('function'); }); it('should extract grouped use', () => { const code = ` n.kind === 'import'); expect(importNodes.length).toBe(2); const names = importNodes.map((n) => n.name); expect(names).toContain('Illuminate\\Database\\Model'); expect(names).toContain('Illuminate\\Database\\Builder'); }); it('should extract multiple uses', () => { const code = ` n.kind === 'import'); expect(importNodes.length).toBe(3); const names = importNodes.map((n) => n.name); expect(names).toContain('Illuminate\\Support\\Collection'); expect(names).toContain('Illuminate\\Support\\Str'); expect(names).toContain('Closure'); }); it('should extract include/require (+_once) static paths as imports (#660)', () => { const code = ` n.kind === 'import').map((n) => n.name); expect(names).toContain('lib.php'); expect(names).toContain('other.php'); expect(names).toContain('r.php'); expect(names).toContain('io.php'); }); it('should skip dynamic include/require with no static path (#660)', () => { const code = ` n.kind === 'import'); expect(imports).toHaveLength(0); }); it('should extract include alongside namespace use without interference (#660)', () => { const code = ` n.kind === 'import').map((n) => n.name); expect(names).toContain('App\\Service\\Mailer'); expect(names).toContain('bootstrap.php'); }); }); describe('Ruby imports', () => { it('should extract require', () => { const code = `require 'json'`; const result = extractFromSource('app.rb', code); const importNode = result.nodes.find((n) => n.kind === 'import'); expect(importNode).toBeDefined(); expect(importNode?.name).toBe('json'); expect(importNode?.signature).toBe("require 'json'"); }); it('should extract require with path', () => { const code = `require 'active_support/core_ext/string'`; const result = extractFromSource('config.rb', code); const importNode = result.nodes.find((n) => n.kind === 'import'); expect(importNode).toBeDefined(); expect(importNode?.name).toBe('active_support/core_ext/string'); }); it('should extract require_relative', () => { const code = `require_relative '../test_helper'`; const result = extractFromSource('test/my_test.rb', code); const importNode = result.nodes.find((n) => n.kind === 'import'); expect(importNode).toBeDefined(); expect(importNode?.name).toBe('../test_helper'); expect(importNode?.signature).toContain('require_relative'); }); it('should not extract non-require calls', () => { const code = `puts 'hello'`; const result = extractFromSource('app.rb', code); const importNode = result.nodes.find((n) => n.kind === 'import'); expect(importNode).toBeUndefined(); }); it('should extract multiple requires', () => { const code = ` require 'json' require 'yaml' require_relative 'helper' `; const result = extractFromSource('lib.rb', code); const importNodes = result.nodes.filter((n) => n.kind === 'import'); expect(importNodes.length).toBe(3); const names = importNodes.map((n) => n.name); expect(names).toContain('json'); expect(names).toContain('yaml'); expect(names).toContain('helper'); }); }); describe('Ruby modules', () => { it('should extract module as module node with containment', () => { const code = ` module CachedCounting def self.disable @enabled = false end def perform_increment!(key, count) write_cache!(key, count) end end `; const result = extractFromSource('concerns/cached_counting.rb', code); const moduleNode = result.nodes.find((n) => n.kind === 'module' && n.name === 'CachedCounting'); expect(moduleNode).toBeDefined(); expect(moduleNode?.qualifiedName).toBe('CachedCounting'); // Methods inside module should have module-qualified names const disableMethod = result.nodes.find((n) => n.name === 'disable' && n.kind === 'method'); expect(disableMethod).toBeDefined(); expect(disableMethod?.qualifiedName).toBe('CachedCounting::disable'); const incrementMethod = result.nodes.find((n) => n.name === 'perform_increment!' && n.kind === 'method'); expect(incrementMethod).toBeDefined(); expect(incrementMethod?.qualifiedName).toBe('CachedCounting::perform_increment!'); // Containment edge from module to methods const containsEdges = result.edges.filter((e) => e.source === moduleNode?.id && e.kind === 'contains'); expect(containsEdges.length).toBeGreaterThanOrEqual(2); }); it('should handle nested modules with classes', () => { const code = ` module Discourse module Auth class AuthProvider def authenticate(params) validate(params) end end end end `; const result = extractFromSource('lib/auth.rb', code); const discourseModule = result.nodes.find((n) => n.kind === 'module' && n.name === 'Discourse'); expect(discourseModule).toBeDefined(); const authModule = result.nodes.find((n) => n.kind === 'module' && n.name === 'Auth'); expect(authModule).toBeDefined(); expect(authModule?.qualifiedName).toBe('Discourse::Auth'); const authProvider = result.nodes.find((n) => n.kind === 'class' && n.name === 'AuthProvider'); expect(authProvider).toBeDefined(); expect(authProvider?.qualifiedName).toBe('Discourse::Auth::AuthProvider'); const authMethod = result.nodes.find((n) => n.name === 'authenticate'); expect(authMethod).toBeDefined(); expect(authMethod?.qualifiedName).toBe('Discourse::Auth::AuthProvider::authenticate'); }); }); describe('PHP return type capture (#608)', () => { it('captures self/static factory returns as the `self` marker; primitives as undefined', () => { const code = ` n.name === 'for' && n.kind === 'method')?.returnType).toBe('self'); expect(result.nodes.find((n) => n.name === 'make' && n.kind === 'method')?.returnType).toBe('self'); // `array` is not a class to chain on → no return type recorded. expect(result.nodes.find((n) => n.name === 'send' && n.kind === 'method')?.returnType).toBeUndefined(); }); it('captures a concrete return type as its short class name', () => { const code = ` n.name === 'make' && n.kind === 'method')?.returnType).toBe('Widget'); }); }); describe('C/C++ return type capture (#645)', () => { it('captures the normalized return type of a C++ method/function', () => { const code = ` struct Widget { void draw(); }; class Factory { public: static Widget create(); }; Widget Factory::create() { return Widget(); } void doNothing() {} `; const result = extractFromSource('f.cpp', code); const create = result.nodes.find( (n) => n.name === 'create' && (n.kind === 'method' || n.kind === 'function') ); expect(create?.returnType).toBe('Widget'); // A `void` return records no type, so resolution never tries to resolve a // method on it. const doNothing = result.nodes.find((n) => n.name === 'doNothing'); expect(doNothing).toBeDefined(); expect(doNothing?.returnType).toBeUndefined(); }); it('unwraps a smart-pointer return type to its pointee', () => { const code = ` #include struct Widget {}; std::unique_ptr makeWidget() { return nullptr; } `; const result = extractFromSource('f.cpp', code); const make = result.nodes.find((n) => n.name === 'makeWidget'); expect(make?.returnType).toBe('Widget'); }); }); describe('C++ macro-prefixed class/struct misparse (#946 → recovered in #1061)', () => { // An export/visibility macro before the class name (`class MACRO Name : // public Base { … }`) makes tree-sitter read `class MACRO` as an elaborated // type and the whole declaration as a function_definition named after the // class — a phantom `function` that polluted callers/impact/blast-radius. // #946 dropped that phantom; #1061's preParse (`blankCppExportMacros`) now // blanks the ALL-CAPS macro before parsing, so the class parses normally and // is *recovered* — node, members, and base edge all present — not just // de-phantomed. The #946 drop survives as the fallback for any residual // misparse the blanking doesn't catch. it('recovers a macro-annotated class that inherits (no phantom, real class + base edge)', () => { const code = `#pragma once #define MAPCORE_EXPORT __attribute__((visibility("default"))) class DataProvider { public: virtual bool Request(void* param) = 0; }; class MAPCORE_EXPORT LocalDataProvider : public DataProvider { public: LocalDataProvider(int dataType); virtual bool Request(void* param) override; }; `; // A header rich in C++ (class / public: / virtual) detects as C++ — the // issue's exact scenario (a `.h` file). Guard it so a detection regression // can't make this test pass for the wrong reason. expect(detectLanguage('provider.h', code)).toBe('cpp'); const result = extractFromSource('provider.h', code); // The misparse used to surface as `function | LocalDataProvider` spanning // the whole class body — a false caller in the graph. It's gone. expect( result.nodes.find((n) => n.name === 'LocalDataProvider' && n.kind === 'function') ).toBeUndefined(); // …and the class is now recovered (was dropped under #946), with its // `extends DataProvider` edge — the whole point of #1061. expect(result.nodes.find((n) => n.name === 'LocalDataProvider')?.kind).toBe('class'); expect( result.unresolvedReferences.find( (r) => r.referenceKind === 'extends' && r.referenceName === 'DataProvider' ) ).toBeTruthy(); // The sibling class without the macro is unaffected — still a class. expect(result.nodes.find((n) => n.name === 'DataProvider')?.kind).toBe('class'); }); it('recovers the struct variant too, without disturbing a genuine class', () => { const code = ` #define API __declspec(dllexport) struct API Widget : public Base { int x; }; class Plain : public Base { public: int y; }; `; const result = extractFromSource('widget.cpp', code); // `struct MACRO Name : Base { … }` misparses the same way — no phantom // function, and the struct is recovered with its base edge. expect( result.nodes.find((n) => n.name === 'Widget' && n.kind === 'function') ).toBeUndefined(); expect(result.nodes.find((n) => n.name === 'Widget')?.kind).toBe('struct'); // A normal class with a base clause and no macro is untouched. expect(result.nodes.find((n) => n.name === 'Plain')?.kind).toBe('class'); const exts = result.unresolvedReferences .filter((r) => r.referenceKind === 'extends') .map((r) => r.referenceName); expect(exts.filter((n) => n === 'Base').length).toBe(2); // Widget + Plain both extend Base }); }); describe('C++ export-macro class recovery (#1061)', () => { // Unreal-Engine style: `class MYGAME_API UMyComponent : public UActorComponent`. // The leading `*_API` macro alone (base clause or not) triggers the #946 // misparse and dropped the class — breaking subclass / type-hierarchy / // inheritance-impact queries for effectively every gameplay class in a UE // project. blankCppExportMacros recovers them. it('recovers UE *_API classes and the inheritance edge (the issue repro)', () => { const code = `class ENGINE_API UActorComponent { }; class MYGAME_API UMyComponent : public UActorComponent { }; `; const result = extractFromSource('ue.cpp', code); const classes = result.nodes.filter((n) => n.kind === 'class').map((n) => n.name); expect(classes).toContain('UActorComponent'); // macro, no base — also was dropped expect(classes).toContain('UMyComponent'); expect(result.nodes.find((n) => n.kind === 'function')).toBeUndefined(); // no phantom expect( result.unresolvedReferences.find( (r) => r.referenceKind === 'extends' && r.referenceName === 'UActorComponent' ) ).toBeTruthy(); }); it('blankCppExportMacros blanks only the header macro, offset-preserving', () => { // Blanking replaces the macro with equal-length spaces, so the output is // byte-for-byte the same length and identical *except* the macro is gone — // every downstream line/column stays exact. const check = (inp: string, macro: string, rest: string) => { const out = blankCppExportMacros(inp); expect(out.length).toBe(inp.length); // every byte offset preserved expect(out).not.toContain(macro); // the macro token is blanked expect(out.replace(/ +/g, ' ')).toBe(rest); // nothing else changed }; // Generalizes across the export-macro space: UE _API, Qt/Boost _EXPORT, // LLVM _ABI, bare API. check( 'class MYGAME_API UMyComponent : public UActorComponent { };', 'MYGAME_API', 'class UMyComponent : public UActorComponent { };' ); check('struct MAPCORE_EXPORT W : B {}', 'MAPCORE_EXPORT', 'struct W : B {}'); check('class LLVM_ABI Foo {}', 'LLVM_ABI', 'class Foo {}'); }); it('does NOT blank an all-caps class NAME or an elaborated-type var decl', () => { // The name itself being ALL-CAPS (with or without a base) must survive — // the macro is only the token *before* the name, gated on a `: { ` def. for (const c of [ 'class FOO { int x; };', 'class FOO : public Base { int x; };', 'struct BAR : public Base { int y; };', 'enum class COLOR { Red, Green };', // elaborated-type variable declarations end in ; = [ — never : { 'struct FOO bar;', 'class FOO obj = make();', 'struct FOO arr[10];', // a *_API macro used as an ordinary value elsewhere 'int x = SOME_API; void f() { use(MYMODULE_API); }', ]) { expect(blankCppExportMacros(c)).toBe(c); } // And the all-caps-named class keeps its base edge through real extraction. const result = extractFromSource('ctrl.cpp', 'class FOO : public Base { int x; };'); expect(result.nodes.find((n) => n.name === 'FOO')?.kind).toBe('class'); expect( result.unresolvedReferences.find( (r) => r.referenceKind === 'extends' && r.referenceName === 'Base' ) ).toBeTruthy(); }); }); describe('Metal shader extraction (#1121)', () => { // Metal Shading Language (≈ C++14) parses with the C++ grammar. MSL puts // `[[attribute]]` annotations AFTER the declarator — a position // tree-sitter-cpp misparses: a struct field with a trailing attribute // emitted a spurious `extends` ref from the struct to the field's own type. // blankMetalAttributes (preParse, `.metal`-gated) blanks them so extraction // matches plain C++. const METAL = `#include using namespace metal; struct VertexIn { float3 position [[attribute(0)]]; float2 texCoord [[attribute(1)]]; }; struct VertexOut { float4 position [[position]]; float2 texCoord; }; struct Uniforms { float4x4 modelViewProjection; }; static float4 applyGamma(float4 color) { return pow(color, float4(1.0 / 2.2)); } vertex VertexOut vertexShader(VertexIn in [[stage_in]], constant Uniforms &uniforms [[buffer(0)]]) { VertexOut out; out.position = uniforms.modelViewProjection * float4(in.position, 1.0); out.texCoord = in.texCoord; return out; } fragment float4 fragmentShader(VertexOut in [[stage_in]], texture2d colorTexture [[texture(0)]], sampler textureSampler [[sampler(0)]]) { float4 color = colorTexture.sample(textureSampler, in.texCoord); return applyGamma(color); } kernel void computeBlur(texture2d inTexture [[texture(0)]], texture2d outTexture [[texture(1)]], uint2 gid [[thread_position_in_grid]]) { float4 color = inTexture.read(gid); outTexture.write(color, gid); } `; it('extracts vertex/fragment/kernel functions, structs, and calls from a .metal file', () => { const result = extractFromSource('Shaders.metal', METAL); expect(result.errors).toHaveLength(0); const functions = result.nodes.filter((n) => n.kind === 'function').map((n) => n.name); expect(functions).toEqual( expect.arrayContaining(['applyGamma', 'vertexShader', 'fragmentShader', 'computeBlur']) ); const structs = result.nodes.filter((n) => n.kind === 'struct').map((n) => n.name); expect(structs).toEqual(expect.arrayContaining(['VertexIn', 'VertexOut', 'Uniforms'])); expect(result.nodes.find((n) => n.kind === 'import')?.name).toBe('metal_stdlib'); // Attribute blanking is offset-preserving, so positions stay exact. const vertexFn = result.nodes.find((n) => n.name === 'vertexShader')!; expect(vertexFn.startLine).toBe(22); // The shader call graph connects: fragmentShader → applyGamma. expect( result.unresolvedReferences.find( (r) => r.referenceKind === 'calls' && r.referenceName === 'applyGamma' ) ).toBeTruthy(); // The regression the blanking fixes: field attributes (`float3 position // [[attribute(0)]];`) misparsed into `extends` refs from the struct to the // field's type — a wrong inheritance edge whenever the repo defines that // type itself (simd typedefs in a shared ShaderTypes.h are common). expect(result.unresolvedReferences.filter((r) => r.referenceKind === 'extends')).toHaveLength(0); }); it('blankMetalAttributes blanks every attribute form, offset-preserving', () => { const inp = [ 'float4 position [[position]];', 'constant Uniforms &u [[buffer(0)]]', 'float2 uv [[user(locn0)]];', 'device float *out [[buffer(0), raster_order_group(0)]]', ].join('\n'); const out = blankMetalAttributes(inp); expect(out.length).toBe(inp.length); // every byte offset preserved expect(out).not.toContain('[['); // Nothing but the attributes changed: collapsing the blank runs gives the // plain declarations back, newlines untouched. expect(out.split('\n').map((l) => l.replace(/ +/g, ' ').trimEnd())).toEqual([ 'float4 position ;', 'constant Uniforms &u', 'float2 uv ;', 'device float *out', ]); }); it('blankMetalAttributes never touches non-attribute [[ sequences', () => { for (const c of [ 'auto x = arr[[]{ return 0; }()];', // lambda in subscript — the only other [[ in C++-family code 'int y = a[b[i]];', // nested subscript 'int z = 1;', // no [[ at all — early-return path ]) { expect(blankMetalAttributes(c)).toBe(c); } }); }); describe('C++ in-body reflection-macro annotations do not collapse the class (UE)', () => { // Unreal reflection markup — `UPROPERTY(...)`, `UFUNCTION(...)`, // `GENERATED_BODY()`, `UE_DEPRECATED_*(...)`, `DECLARE_DELEGATE_*(...)` — are // no-semicolon macro CALLS decorating members. tree-sitter doesn't know they // are macros, so each drops into error recovery; in a heavily-reflected class // the errors accumulate until the enclosing class_specifier can't close and // the whole class (its base clause and members) collapses into an ERROR node // and disappears from the graph. blankCppAnnotationMacroCalls strips them, // offset-preserving, so the class parses normally. it('recovers a heavily-reflected class with multiple inheritance + members', () => { const code = `UCLASS(MinimalAPI) class UMyMovement : public UPawnMovementComponent, public IRVOAvoidanceInterface, public INetworkPredictionInterface { \tGENERATED_BODY() public: \tUE_DEPRECATED_FORGAME(5.0, "Deprecated; note the commas, and (parens) inside the string") \tUPROPERTY(Category="Move", EditAnywhere, BlueprintReadWrite, meta=(ClampMin="0", UIMin="0")) \tfloat MaxWalkSpeed; \tUFUNCTION(BlueprintCallable, Category="Move") \tfloat ComputeSpeed() const { return MaxWalkSpeed * 2.0f; } }; `; const result = extractFromSource('movement.cpp', code); const cls = result.nodes.find((n) => n.kind === 'class' && n.name === 'UMyMovement'); expect(cls).toBeTruthy(); // The class body parses, so its inline method definition is extracted too — // proof the class_specifier closed instead of collapsing into an ERROR node. expect(result.nodes.some((n) => n.name === 'ComputeSpeed')).toBe(true); // The base clause survives (inheritance queries keep working). expect( result.unresolvedReferences.find( (r) => r.referenceKind === 'extends' && r.referenceName === 'UPawnMovementComponent' ) ).toBeTruthy(); }); it('strips line-leading no-semicolon ALL-CAPS calls, offset-preserving', () => { const inp = `\tUPROPERTY(EditAnywhere, meta=(ClampMin="0"))\n\tfloat X;\n`; const out = blankCppAnnotationMacroCalls(inp); expect(out.length).toBe(inp.length); // every byte offset preserved expect(out).not.toContain('UPROPERTY'); expect(out).toContain('float X;'); // A macro whose args carry commas/parens inside a string still balances. const inp2 = `UE_DEPRECATED_FORGAME(5.0, "a, b (c)")\nUPROPERTY(Foo)\nfloat Y;\n`; const out2 = blankCppAnnotationMacroCalls(inp2); expect(out2.length).toBe(inp2.length); expect(out2).not.toContain('UE_DEPRECATED_FORGAME'); expect(out2).not.toContain('UPROPERTY'); expect(out2).toContain('float Y;'); }); it('does NOT blank expression / condition / statement / init-list macro uses', () => { for (const c of [ 'void f() {\n\tif (CHECK_FLAG(x)) { g(); }\n}', // condition — not line-leading 'void f() {\n\tLOG_MESSAGE("hi");\n}', // statement call — trailing ; 'C::C()\n\t: MEMBER_A(1)\n\t, MEMBER_B(2)\n{}', // init-list — comma / not line-leading 'C::C() :\n\tMEMBER_A(1),\n\tMEMBER_B(2)\n{}', // init-list wrapped — trailing , / { 'auto y =\n\tMAKE_THING(a) + 1;', // line-leading but an expression fragment ]) { expect(blankCppAnnotationMacroCalls(c)).toBe(c); } }); }); describe('C++ member/method-level export macros do not orphan declarations (UE)', () => { // The `*_API` visibility macro doesn't only prefix the class header — it // prefixes almost every exported member/method of a big UE class // (`ENGINE_API virtual void Tick(…)`, `static ENGINE_API void Foo(…)`). // blankCppExportMacros only recovers the class-HEADER form; without blanking // the member form, tree-sitter reads `MACRO (` as an extra type // token and each declaration drops into error recovery. it('recovers a class + base + members when members are *_API-prefixed', () => { const code = `class ENGINE_API AActor : public UObject { \tGENERATED_BODY() public: \tENGINE_API virtual void Tick(float DeltaSeconds); \tstatic ENGINE_API void AddReferencedObjects(int32 Count); \tENGINE_API float GetLifeSpan() const { return LifeSpan; } }; `; const result = extractFromSource('actor.cpp', code); expect(result.nodes.some((n) => n.kind === 'class' && n.name === 'AActor')).toBe(true); // The inline definition (its body prefixed by ENGINE_API) is extracted — // proof the class_specifier closed instead of collapsing into an ERROR. expect(result.nodes.some((n) => n.name === 'GetLifeSpan')).toBe(true); // The base clause survives (inheritance queries keep working). expect( result.unresolvedReferences.find( (r) => r.referenceKind === 'extends' && r.referenceName === 'UObject' ) ).toBeTruthy(); }); it('blanks only the suffix macro before a declaration, offset-preserving', () => { const inp = `ENGINE_API void Tick();\nstatic MYMOD_EXPORT int32 X;\nLLVM_ABI bool Y();\n`; const out = blankCppApiPrefixMacros(inp); expect(out.length).toBe(inp.length); // every byte offset preserved expect(out).not.toContain('ENGINE_API'); expect(out).not.toContain('MYMOD_EXPORT'); expect(out).not.toContain('LLVM_ABI'); expect(out).toContain('void Tick();'); expect(out).toContain('int32 X;'); expect(out).toContain('bool Y();'); expect(out).toMatch(/static\s+int32 X;/); // `static` kept, only the macro blanked }); it('does NOT blank an *_API token used as a value or in non-declaration position', () => { for (const c of [ 'int x = SOME_API;', // rvalue — trailing ; 'if (mode == FOO_API) { g(); }', // comparison — trailing ) 'return DEFAULT_API, other;', // comma operand 'auto v = NS_API::Make();', // qualified name — trailing :: 'x = A_API + B_API;', // operands of + / trailing ; ]) { expect(blankCppApiPrefixMacros(c)).toBe(c); } }); it('leaves a genuine _API-suffixed word alone when it is itself the name', () => { // A longer word merely CONTAINING _API (not ending in it) must not match. const inp = 'FOO_APIENTRY handler;'; expect(blankCppApiPrefixMacros(inp)).toBe(inp); }); }); describe('C++ mid-line UE annotation macros do not collapse the enum/class (UE)', () => { // UMETA / UPARAM / UE_DEPRECATED can sit MID-LINE (not line-leading), where // blankCppAnnotationMacroCalls structurally can't reach them: an enum value's // `UMETA(...)`, or a deprecation tag wedged into a class-scope `using` // (`using X UE_DEPRECATED(5.5, "…") = …;`) — which alone collapsed UWorld in // World.h. blankCppInlineAnnotationMacros strips them, offset-preserving. it('recovers a class whose in-body using-alias carries a mid-line UE_DEPRECATED', () => { const code = `class ENGINE_API UWorld : public UObject { \tGENERATED_BODY() public: \tusing FOnNetTickEvent UE_DEPRECATED(5.5, "use TMulticastDelegate") = TMulticastDelegate; \tENGINE_API float GetTimeSeconds() const { return TimeSeconds; } }; `; const result = extractFromSource('world.cpp', code); expect(result.nodes.some((n) => n.kind === 'class' && n.name === 'UWorld')).toBe(true); // The member after the poison using-alias is reached — the class closed. expect(result.nodes.some((n) => n.name === 'GetTimeSeconds')).toBe(true); expect( result.unresolvedReferences.find( (r) => r.referenceKind === 'extends' && r.referenceName === 'UObject' ) ).toBeTruthy(); }); it('blanks mid-line UMETA/UPARAM/UE_DEPRECATED with balanced parens, offset-preserving', () => { const inp = `enum class EMode : uint8 {\n\tWalk UMETA(DisplayName="Walk (fast), safe"),\n\tRun\n};\n`; const out = blankCppInlineAnnotationMacros(inp); expect(out.length).toBe(inp.length); expect(out).not.toContain('UMETA'); expect(out).toContain('Walk'); expect(out).toContain('Run'); const inp2 = `void F(UPARAM(ref) int& x) {}\n`; const out2 = blankCppInlineAnnotationMacros(inp2); expect(out2.length).toBe(inp2.length); expect(out2).not.toContain('UPARAM'); expect(out2).toContain('int& x'); }); it('does NOT touch source without those UE-only macro names', () => { const c = 'enum class E { A, B };\nvoid metadata(int meta) { return; }\n'; expect(blankCppInlineAnnotationMacros(c)).toBe(c); }); }); describe('C++ dense Unreal-Engine header regression (#1160/#1158)', () => { // Regression guard for the three UE blank passes together, on a HEAVILY // reflected class in the shape that broke real engine headers // (`CharacterMovementComponent.h` carries ~240 in-body reflection macros). // On the real headers the accumulated tree-sitter errors collapse the whole // `class_specifier` into an ERROR node and the class itself vanishes; that // full collapse is emergent from real-header content we can't ship here // (Unreal's source is EULA-licensed), so this reproduces the *recoverable* // signal it leaves: with the blank passes reverted, tree-sitter drops every // one of these decorated members and the `UMETA` enum into error recovery, // so the assertions below flip from pass to fail. Verified against the // pre-fix build: `Compute0`, the last member, and `EDenseMode` are all // absent before the fix and present after — reverting any of // blankCppAnnotationMacroCalls / blankCppApiPrefixMacros / // blankCppInlineAnnotationMacros regresses at least one of them. const N = 120; // 120 UPROPERTY + 120 UFUNCTION = ~240 in-body macros function denseReflectedHeader(): string { let members = ''; for (let i = 0; i < N; i++) { // line-leading UPROPERTY with nested meta=(...) (blankCppAnnotationMacroCalls) members += `\tUPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Move", meta=(ClampMin="0.0", EditCondition="bOn${i}", AllowedClasses="/Script/Engine.Texture"))\n\tTSubclassOf Prop${i};\n`; // line-leading UFUNCTION + member-level ENGINE_API + UPARAM(ref) param // (all three passes) on an inline definition (has a body → is a node) members += `\tUFUNCTION(BlueprintCallable, Category="Move", meta=(DisplayName="Compute ${i}"))\n\tENGINE_API float Compute${i}(UPARAM(ref) float& In) const { return In * ${i}.0f; }\n`; } return `UCLASS(MinimalAPI, Blueprintable) class ENGINE_API UDenseMovement : public UPawnMovementComponent, public IRVOAvoidanceInterface, public INetworkPredictionInterface { \tGENERATED_BODY() public: \tDECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnMoved, float, Speed, FVector, Loc); \tusing FLegacyTick UE_DEPRECATED(5.5, "use TDelegate") = TMulticastDelegate; ${members}}; UENUM(BlueprintType) enum class EDenseMode : uint8 { \tWalking UMETA(DisplayName="Walk (fast), safe"), \tFlying UMETA(DisplayName="Fly"), \tCustom UMETA(Hidden), }; `; } it('recovers a ~240-macro reflected class, its base clause, and every decorated member', () => { const result = extractFromSource('DenseMovement.h', denseReflectedHeader()); // The class and its whole multiple-inheritance base clause survive. expect(result.nodes.some((n) => n.kind === 'class' && n.name === 'UDenseMovement')).toBe(true); for (const base of ['UPawnMovementComponent', 'IRVOAvoidanceInterface', 'INetworkPredictionInterface']) { expect( result.unresolvedReferences.find((r) => r.referenceKind === 'extends' && r.referenceName === base) ).toBeTruthy(); } // The real guard: the decorated inline members parse instead of being lost // to error recovery — the first, a middle, and the LAST (proof the whole // dense body closed, not just the head). expect(result.nodes.some((n) => n.name === 'Compute0')).toBe(true); expect(result.nodes.some((n) => n.name === 'Compute60')).toBe(true); expect(result.nodes.some((n) => n.name === `Compute${N - 1}`)).toBe(true); }); it('recovers a UENUM whose values carry mid-line UMETA', () => { const result = extractFromSource('DenseMovement.h', denseReflectedHeader()); // A mid-line UMETA drops the enum into error recovery pre-fix; // blankCppInlineAnnotationMacros restores it. expect(result.nodes.some((n) => n.kind === 'enum' && n.name === 'EDenseMode')).toBe(true); }); }); describe('CUDA extraction (#387)', () => { // CUDA parses with the C++ grammar. Three CUDA-only shapes misparse: // execution-space specifiers (`__global__ void f(…)`) shunt the real return // type into an ERROR node, `__shared__ float tile[256]` mangles the declared // name to `float`, and — the critical one — `k<<>>(args)` lexes // as shift operators around an empty-named template so NO call_expression // (and therefore no host→kernel call edge) exists. blankCudaConstructs // (preParse, `.cu`/`.cuh`-gated) blanks all three so extraction matches // plain C++. const CUDA = `#include #include "kernels.cuh" __constant__ float d_scale[16]; __device__ __forceinline__ float warp_reduce_sum(float val) { for (int offset = 16; offset > 0; offset /= 2) { val += __shfl_down_sync(0xffffffff, val, offset); } return val; } __global__ void scale_kernel(float* out, const float* __restrict__ in, int n) { int i = blockIdx.x * blockDim.x + threadIdx.x; __shared__ float tile[256]; if (i < n) { tile[threadIdx.x] = in[i]; __syncthreads(); out[i] = warp_reduce_sum(tile[threadIdx.x]) * d_scale[0]; } } __global__ void __launch_bounds__(256, 4) bounded_kernel(float* data, int n) { if (blockIdx.x * blockDim.x + threadIdx.x < n) data[0] *= 2.0f; } template __global__ void templated_kernel(T* data, int n) { if (blockIdx.x * BLOCK + threadIdx.x < n) data[0] += T(1); } class GpuBuffer { public: explicit GpuBuffer(size_t n) { cudaMalloc(&ptr_, n * sizeof(float)); } ~GpuBuffer() { cudaFree(ptr_); } private: float* ptr_ = nullptr; }; void launch_scale(float* out, const float* in, int n, cudaStream_t stream) { dim3 block(256); dim3 grid((n + block.x - 1) / block.x); scale_kernel<<>>(out, in, n); bounded_kernel<<>>(out, n); templated_kernel<<>>(out, n); } `; it('extracts kernels, device functions, and host→kernel launch calls from a .cu file', () => { const result = extractFromSource('kernels/scan.cu', CUDA); expect(result.errors).toHaveLength(0); const functions = result.nodes.filter((n) => n.kind === 'function').map((n) => n.name); expect(functions).toEqual( expect.arrayContaining([ 'warp_reduce_sum', 'scale_kernel', 'bounded_kernel', 'templated_kernel', 'launch_scale', ]) ); expect(result.nodes.filter((n) => n.kind === 'class').map((n) => n.name)).toContain('GpuBuffer'); expect(result.nodes.find((n) => n.kind === 'import')?.name).toBe('cuda_runtime.h'); // No misparse artifacts: pre-blank, `__shared__ float tile[256]` parsed // with `float` as the declared name. (Top-level C++ variables aren't // extracted as nodes — matching plain-C++ behavior is the target.) expect(result.nodes.map((n) => n.name)).not.toContain('float'); // Blanking is offset-preserving, so positions stay exact. expect(result.nodes.find((n) => n.name === 'scale_kernel')!.startLine).toBe(13); // THE point of CUDA support: every `<<<…>>>` launch form — plain, // launch-bounds, multi-line config, and templated — emits a `calls` // reference, so the host→kernel edge exists in the graph. Pre-blank, // the chevrons lexed as shifts and none of these existed. const calls = result.unresolvedReferences .filter((r) => r.referenceKind === 'calls') .map((r) => r.referenceName); // The templated launch is normalized to the bare kernel name (template // args stripped at extraction, like base-class extends refs — #1043), so // it resolves to the kernel the template was defined as. expect(calls).toEqual( expect.arrayContaining([ 'scale_kernel', 'bounded_kernel', 'templated_kernel', 'warp_reduce_sum', ]) ); }); it('blankCudaConstructs blanks every CUDA form, offset- and newline-preserving', () => { const inp = [ '__global__ void __launch_bounds__(256, 4) step(float* p) {', ' __shared__ float tile[32];', '}', '__host__ __device__ int both() { return 0; }', 'void run(float* p, int n) {', ' step<<>>(p);', '}', ].join('\n'); const out = blankCudaConstructs(inp); expect(out.length).toBe(inp.length); // every byte offset preserved expect(out.split('\n').length).toBe(inp.split('\n').length); // newlines survive the multi-line launch config expect(out).not.toMatch(/__global__|__launch_bounds__|__shared__|__host__|__device__|<<<|>>>/); // Collapsing blank runs gives plain C++ back. expect(out.split('\n').map((l) => l.replace(/ +/g, ' ').trimEnd())).toEqual([ ' void step(float* p) {', ' float tile[32];', '}', ' int both() { return 0; }', 'void run(float* p, int n) {', ' step', ' (p);', '}', ]); }); it('blankCudaConstructs never touches non-CUDA chevrons or identifiers', () => { for (const c of [ 'std::cout << "a" << b << c;', // shift chains — never three consecutive < 'auto x = f(a >> 3, b >> 3);', // right shifts 'std::vector>> deep;', // template >>> closer with no <<< opener 'printf("<<>> anywhere 'int __restrict__like = 1;', // dunder-ish identifier not in the specifier list 'int z = 1;', // nothing CUDA at all — early-return path ]) { expect(blankCudaConstructs(c)).toBe(c); } // A stray `<<<` (committed merge-conflict marker) must not blank the code // between markers. Two independent guards: statements between markers // carry `;` (excluded from the span)… const conflict = [ '<<<<<<< HEAD', 'int a = compute(1);', '=======', 'int a = compute(2);', '>>>>>>> feature-branch', ].join('\n'); expect(blankCudaConstructs(conflict)).toBe(conflict); // …and a `;`-free region still fails the brace-balance check (the `{`s // opened between the markers never close before the `>>>`). const semicolonFree = [ '<<<<<<< HEAD', 'void foo() {', '=======', 'void bar() {', '>>>>>>> feature-branch', ].join('\n'); expect(blankCudaConstructs(semicolonFree)).toBe(semicolonFree); }); it('blanks brace-initialized launch configs (`dim3{…}`), balanced-only', () => { const inp = 'run_it<<>>(data, n);'; const out = blankCudaConstructs(inp); expect(out.length).toBe(inp.length); expect(out.replace(/ +/g, ' ')).toBe('run_it (data, n);'); }); it('recovers the real kernel name from a macro-definition idiom, gtest/pybind untouched', () => { const code = `#define DEFINE_MY_FWD_KERNEL(kernelName, ...) \\ template \\ __global__ void kernelName(const Params params) DEFINE_MY_FWD_KERNEL(fwd_kernel, bool Is_causal, int kBlockM) { do_work(params); } TEST_F(MyFixture, HandlesEmptyInput) { check(1); } `; const result = extractFromSource('kernels/impl.cu', code); const functions = result.nodes.filter((n) => n.kind === 'function').map((n) => n.name); // The macro invocation's first argument is the defined name. expect(functions).toContain('fwd_kernel'); expect(functions).not.toContain('DEFINE_MY_FWD_KERNEL'); // gtest's TEST_F(Fixture, Name) has TWO lone identifiers — ambiguous, so // it keeps the macro name rather than guessing. expect(functions).toContain('TEST_F'); expect(functions).not.toContain('MyFixture'); }); it('links launches through a local function pointer to the real kernel(s)', () => { // The flash-attention launch-template shape end-to-end: a macro-defined // kernel + `auto kernel = &fn<…>` + branch reassignment + launch through // the local. The call refs must name the real kernels, not `kernel`. const code = `template __global__ void fwd_kernel(T* data, int n) { if (blockIdx.x * blockDim.x + threadIdx.x < n) data[0] += T(1); } template __global__ void fwd_splitkv_kernel(T* data, int n) { if (blockIdx.x * blockDim.x + threadIdx.x < n) data[0] += T(2); } template void run_fwd(T* data, int n, cudaStream_t stream) { auto kernel = &fwd_kernel; if (n % 2 == 0) { kernel = &fwd_kernel; } else if (n % 3 == 0) { kernel = &fwd_splitkv_kernel; } kernel<<<(n + 255) / 256, 256, 0, stream>>>(data, n); } `; const result = extractFromSource('kernels/launch.cu', code); expect(result.errors).toHaveLength(0); const calls = result.unresolvedReferences .filter((r) => r.referenceKind === 'calls') .map((r) => r.referenceName); // Every DISTINCT branch target recorded once — the two fwd_kernel<…> // instantiations strip to one target, the splitkv branch adds a second. // The local's name never leaks as a callee. expect(calls.filter((c) => c === 'fwd_kernel')).toHaveLength(1); expect(calls.filter((c) => c === 'fwd_splitkv_kernel')).toHaveLength(1); expect(calls).not.toContain('kernel'); }); it('CUDA blanking is gated by extension or content — plain C++ shift/template chevrons are untouched', () => { const cpp = `#include int shift_it(int a, int b) { return a << b << 1; } std::vector>> matrix() { return {}; } `; const result = extractFromSource('math.cpp', cpp); expect(result.errors).toHaveLength(0); const functions = result.nodes.filter((n) => n.kind === 'function').map((n) => n.name); expect(functions).toEqual(expect.arrayContaining(['shift_it', 'matrix'])); }); it('CUDA in extension-less headers is caught by content: launch templates in .h connect host→kernel', () => { // Much real CUDA lives in .h: cutlass launches most of its kernels from // headers, flash-attention's launch templates are .h, llm.c keeps device // helpers in C-detected .h. `looksLikeCudaSource` content-gates the same // blank there — no CUDA marker is valid C++ anywhere, so this can't // affect a genuinely-plain C++ header. const header = `#pragma once #include template __global__ void fill_kernel(T* out, T value, int n) { int i = blockIdx.x * blockDim.x + threadIdx.x; if (i < n) out[i] = value; } template void launch_fill(T* out, T value, int n, cudaStream_t stream) { fill_kernel<<<(n + 255) / 256, 256, 0, stream>>>(out, value, n); } `; const result = extractFromSource('include/fill_launch_template.h', header); expect(result.errors).toHaveLength(0); const functions = result.nodes.filter((n) => n.kind === 'function').map((n) => n.name); expect(functions).toEqual(expect.arrayContaining(['fill_kernel', 'launch_fill'])); const calls = result.unresolvedReferences .filter((r) => r.referenceKind === 'calls') .map((r) => r.referenceName); expect(calls).toContain('fill_kernel'); }); }); describe('C++ namespace qualifiedName prefixing', () => { // C++ namespaces previously left no trace in qualifiedNames, so a // namespace-qualified call (`flash::compute(...)`) could never match its // definition — every `ns::fn()` call site was a permanently dead edge. // The namespace name now prefixes contained symbols' qualifiedNames // (prefix-only: no namespace node is minted — `namespace cutlass {` opens // in thousands of files and a node per block would crowd search, #1093). it('prefixes contained symbols and handles nesting; anonymous stays bare', () => { const code = `namespace flash { namespace detail { void helper() {} } void compute_attn(int x) { detail::helper(); } class Softmax { public: void rescale() {} }; } namespace { void file_local() {} } void global_fn() { flash::compute_attn(1); } `; const result = extractFromSource('dispatch.cpp', code); expect(result.errors).toHaveLength(0); const byName = new Map(result.nodes.map((n) => [n.name, n])); expect(byName.get('compute_attn')?.qualifiedName).toBe('flash::compute_attn'); expect(byName.get('helper')?.qualifiedName).toBe('flash::detail::helper'); expect(byName.get('Softmax')?.qualifiedName).toBe('flash::Softmax'); // Class scope still stacks under the namespace prefix. expect(byName.get('rescale')?.qualifiedName).toBe('flash::Softmax::rescale'); // Anonymous namespace contents and true globals stay bare. expect(byName.get('file_local')?.qualifiedName).toBe('file_local'); expect(byName.get('global_fn')?.qualifiedName).toBe('global_fn'); // The qualified call refs are emitted as spelled. const calls = result.unresolvedReferences .filter((r) => r.referenceKind === 'calls') .map((r) => r.referenceName); expect(calls).toEqual(expect.arrayContaining(['flash::compute_attn', 'detail::helper'])); }); it('C++17 nested namespace form prefixes as written', () => { const code = `namespace a::b { int f() { return 1; } } `; const result = extractFromSource('nested.cpp', code); expect(result.nodes.find((n) => n.name === 'f')?.qualifiedName).toBe('a::b::f'); }); }); describe('C++ forward declarations do not mint phantom class nodes (#1093)', () => { // `class Foo;` parses as a bodiless class_specifier. Repeated across headers, // each forward decl minted a phantom bodiless `class` node that crowded out — // and could be picked as the blast-radius representative over — the single // real definition. Bodiless struct/enum specifiers were already skipped; // classes now are too, but ONLY for C/C++ (opt-in flag), never for languages // where a bodiless class is a complete definition. it('keeps only the real definition, dropping repeated forward decls', () => { const code = ` class APXCharacter; // forward decl (header 1) class APXCharacter; // forward decl (header 2) friend class APXCharacter; // elaborated / friend forward reference class APXCharacter { // the one real definition int hp; void takeDamage(int amount) { hp -= amount; } }; `; const result = extractFromSource('character.cpp', code); const classes = result.nodes.filter( (n) => n.kind === 'class' && n.name === 'APXCharacter' ); // Exactly one class node, and it's the definition (carries the member). expect(classes).toHaveLength(1); expect(classes[0].startLine).toBe(6); expect( result.nodes.some((n) => n.kind === 'method' && n.name === 'takeDamage') ).toBe(true); }); it('elaborated type references in declarations create no phantom class', () => { // `class Foo obj;` is a variable declaration using an elaborated type, not // a class definition — it must not mint a `Foo` class node. const result = extractFromSource('use.cpp', 'class Foo;\nvoid f() { class Foo *p = nullptr; (void)p; }\n'); expect(result.nodes.filter((n) => n.kind === 'class' && n.name === 'Foo')).toHaveLength(0); }); it('does NOT affect languages where a bodiless class is complete', () => { // Kotlin `class Empty` and Scala `trait`/`case object`/`class` with no body // are complete definitions — the C/C++-only skip must leave them indexed. const kt = extractFromSource('Empty.kt', 'class Empty\nclass Full { val x = 1 }\n'); const ktClasses = kt.nodes.filter((n) => n.kind === 'class').map((n) => n.name); expect(ktClasses).toContain('Empty'); expect(ktClasses).toContain('Full'); const scala = extractFromSource('M.scala', 'trait Marker\ncase object Red\nclass Foo\n'); const scalaNames = scala.nodes .filter((n) => ['class', 'trait', 'interface'].includes(n.kind)) .map((n) => n.name); expect(scalaNames).toEqual(expect.arrayContaining(['Marker', 'Red', 'Foo'])); }); }); describe('C++ reference-return method/function names (#1093 follow-up)', () => { // An inline method/function returning a reference parses with a // `reference_declarator` wrapping the `function_declarator`. That wrapper // wasn't unwrapped (only `pointer_declarator` was), so the name captured the // whole declarator — `const int& getRef() const {…}` became the method named // "& getRef() const" instead of "getRef", polluting search and callers. Very // common in Unreal Engine headers (`const FGameplayTagContainer& GetActiveTags() const`). const namesOf = (code: string) => extractFromSource('r.cpp', code).nodes .filter((n) => n.kind === 'method' || n.kind === 'function') .map((n) => n.name); it('names an inline reference-returning method by its identifier, not the declarator', () => { const names = namesOf('class C {\npublic:\n const int& getRef() const { return x; }\n int& mutRef() { return x; }\n int x;\n};'); expect(names).toContain('getRef'); expect(names).toContain('mutRef'); // No name leaks the reference sigil or the parameter/qualifier tail. expect(names.some((n) => /[&()]/.test(n))).toBe(false); }); it('handles rvalue-reference returns and reference-returning free functions', () => { expect(namesOf('class C { int&& take() { return 1; } };')).toContain('take'); expect(namesOf('const int& globalRef() { static int x; return x; }')).toContain('globalRef'); }); it('leaves pointer, value, and out-of-line reference returns unchanged (controls)', () => { expect(namesOf('class C { int* getPtr() { return &x; } int x; };')).toContain('getPtr'); expect(namesOf('class C { int getVal() const { return x; } int x; };')).toContain('getVal'); // Out-of-line `T& C::f()` already resolves via the qualified-name hook. expect(namesOf('const int& C::getRef() const { return x; }')).toContain('getRef'); }); }); describe('C++ user-defined conversion operator names (#1093 follow-up)', () => { // A conversion operator's declarator is an `operator_cast` (target type + // `() const` tail). It was named with the whole declarator — // `operator EALSMovementState() const` — so it didn't match the symbolic- // overload style (`operator+`) and carried parameter noise. It's now named // `operator `. Common in Unreal Engine enum-wrapper structs. const namesOf = (code: string) => extractFromSource('o.cpp', code).nodes .filter((n) => n.kind === 'method' || n.kind === 'function') .map((n) => n.name); it('names a conversion operator as "operator ", not the full declarator', () => { const names = namesOf('struct S {\n operator int() const { return 1; }\n operator bool() { return true; }\n int x;\n};'); expect(names).toContain('operator int'); expect(names).toContain('operator bool'); expect(names.some((n) => n.includes('(') || n.includes('const'))).toBe(false); }); it('handles a user-type conversion operator', () => { expect( namesOf('struct FALSMovementState {\n operator EALSMovementState() const { return State; }\n EALSMovementState State;\n};') ).toContain('operator EALSMovementState'); }); it('leaves symbolic operator overloads unchanged (control)', () => { const names = namesOf('struct S {\n S operator+(const S& o) const { return o; }\n int& operator[](int i) { return x; }\n int x;\n};'); expect(names).toContain('operator+'); expect(names).toContain('operator[]'); // reference-returning subscript, name still clean }); }); describe('C++ explicit operator-call refs (#1247)', () => { // tree-sitter-cpp can't parse an operator_name in field position: // `a.operator+(b)` yields `call_expression(function: identifier «a», // ERROR(operator_name), argument_list)` instead of a field_expression // callee, so the emitted ref was just the receiver (`a`) and the call never // resolved. The extractor recovers the operator_name from the ERROR child // and emits `.operator+` like any other member call. const HEADER = 'struct V {\n V operator+(const V& o) const;\n V operator[](int i) const;\n V operator()(int i) const;\n bool operator==(const V& o) const;\n int get() const;\n};\n'; const callRefsOf = (body: string) => extractFromSource('op.cpp', HEADER + body) .unresolvedReferences.filter((r) => r.referenceKind === 'calls') .map((r) => r.referenceName); it('recovers receiver.operator+ from the explicit call form', () => { expect(callRefsOf('V f(const V& a, const V& b) { return a.operator+(b); }\n')).toContain('a.operator+'); }); it('recovers pointer receivers (p->operator+ → p.operator+)', () => { expect(callRefsOf('V f(const V* p, const V& b) { return p->operator+(b); }\n')).toContain('p.operator+'); }); it('recovers subscript, call, and comparison operator forms', () => { const refs = callRefsOf( 'V f1(const V& a) { return a.operator[](3); }\n' + 'V f2(V& a) { return a.operator()(1); }\n' + 'bool f3(const V& a, const V& b) { return a.operator==(b); }\n' ); expect(refs).toContain('a.operator[]'); expect(refs).toContain('a.operator()'); expect(refs).toContain('a.operator=='); }); it('normalizes spaced call-site operator names to the compact definition form', () => { // nlohmann/json calls `it.operator * ()` / `other.operator < (*this)` // while defining `operator*` / `operator<` compact. const refs = callRefsOf( 'bool f(const V& a, const V& b) { return a.operator == (b); }\n' + 'V g(const V& a) { return a.operator [] (3); }\n' ); expect(refs).toContain('a.operator=='); expect(refs).toContain('a.operator[]'); }); it('drops the ref for a complex receiver instead of guessing (no wrong edge)', () => { // `object->operator[](val)` through a member chain ending in a call — // the receiver type isn't inferable and a bare `operator[]` ref would // let exact-name matching guess among unrelated operators. const refs = callRefsOf( 'struct W { V* obj(); };\n' + 'V f(W& w, const V& b) { return w.obj()->operator+(b); }\n' ); expect(refs.some((r) => r.includes('operator+'))).toBe(false); expect(refs).toContain('w.obj'); // the inner call itself still refs normally }); it('emits the bare operator name for a this-> receiver', () => { const refs = extractFromSource( 'op.cpp', 'struct V {\n V operator+(const V& o) const;\n V twice() const { return this->operator+(*this); }\n};\n' ).unresolvedReferences.filter((r) => r.referenceKind === 'calls').map((r) => r.referenceName); expect(refs).toContain('operator+'); expect(refs.some((r) => r.includes('this'))).toBe(false); }); it('leaves plain member calls unchanged (control)', () => { expect(callRefsOf('int f(const V& a) { return a.get(); }\n')).toContain('a.get'); }); }); describe('C++ macro-prefixed function names (#1093 follow-up)', () => { // An unknown inline-specifier macro before the return type // (`FORCEINLINE FString GetName(…)`) threw tree-sitter into error recovery: // the macro became the return type and — for a non-primitive return — the // return type was glued onto the name (`"FString GetName"`), so the function // was unfindable by name and its callers didn't link. `blankCppInlineMacros` // blanks the known UE inline macros before parsing (offset-preserving), the // same recover-don't-drop approach as the macro-annotated-class fix. Pervasive // in Unreal Engine (`FORCEINLINE`). const infoOf = (code: string) => extractFromSource('m.cpp', code).nodes .filter((n) => n.kind === 'method' || n.kind === 'function') .map((n) => ({ name: n.name, ret: n.returnType })); it('recovers the real name AND return type of a FORCEINLINE function', () => { expect(infoOf('static FORCEINLINE FString GetName(int V) { return H(V); }')).toEqual([ { name: 'GetName', ret: 'FString' }, ]); }); it('handles the templated UE helper shape (GetEnumerationToString)', () => { const names = infoOf( 'template static FORCEINLINE FString GetEnumerationToString(const E V) { return H(V); }' ).map((x) => x.name); expect(names).toContain('GetEnumerationToString'); }); it('handles FORCENOINLINE / FORCEINLINE_DEBUGGABLE, methods, void, and reference returns', () => { expect(infoOf('FORCENOINLINE FString A(int V){return H(V);}').map((x) => x.name)).toContain('A'); expect(infoOf('FORCEINLINE_DEBUGGABLE FString B(int V){return H(V);}').map((x) => x.name)).toContain('B'); expect(infoOf('struct S { FORCEINLINE FString GetName(int V) { return H(V); } };').map((x) => x.name)).toContain('GetName'); expect(infoOf('static FORCEINLINE void DoThing(int V) { H(V); }').map((x) => x.name)).toContain('DoThing'); expect(infoOf('static FORCEINLINE const FString& GetRef(int V) { return H(V); }').map((x) => x.name)).toContain('GetRef'); }); it('handles common third-party inline macros (pugixml, Godot, Boost, generic)', () => { // pugixml: PUGI__FN before the return type; PUGIXML_FUNCTION (linkage) // between the return type and the name — both recovered. expect(infoOf('PUGI__FN void* default_allocate(size_t n) { return H(n); }').map((x) => x.name)).toContain('default_allocate'); expect(infoOf('PUGI__FN_NO_INLINE bool strequal(const char_t* a) { return H(a); }').map((x) => x.name)).toContain('strequal'); expect(infoOf('std::string PUGIXML_FUNCTION as_utf8(const wchar_t* s) { return H(s); }').map((x) => x.name)).toContain('as_utf8'); // Godot / Boost / generic inline hints expect(infoOf('_FORCE_INLINE_ String get_name() const { return H(); }').map((x) => x.name)).toContain('get_name'); expect(infoOf('_ALWAYS_INLINE_ Vector2 get_pos() { return H(); }').map((x) => x.name)).toContain('get_pos'); expect(infoOf('BOOST_FORCEINLINE result_type call() { return H(); }').map((x) => x.name)).toContain('call'); expect(infoOf('ALWAYS_INLINE MyType compute() { return H(); }').map((x) => x.name)).toContain('compute'); }); it('leaves ordinary functions and real all-caps return types untouched (controls)', () => { expect(infoOf('FString GetName(int V) { return H(V); }')).toEqual([{ name: 'GetName', ret: 'FString' }]); // A real all-caps type that is NOT a listed inline macro stays the return type. expect(infoOf('HRESULT DoIt(int V) { return H(V); }')).toEqual([{ name: 'DoIt', ret: 'HRESULT' }]); }); it('blankCppInlineMacros preserves offsets and only touches specifier-position macros', () => { // Blanked with equal-length spaces (byte offsets preserved). expect(blankCppInlineMacros('FORCEINLINE FString F()')).toBe(' FString F()'); expect(blankCppInlineMacros('FORCEINLINE FString F()')).toHaveLength('FORCEINLINE FString F()'.length); // Not in specifier position → untouched: string literals, expressions, // longer word (`FORCEINLINE_COUNT`), and the fast path. expect(blankCppInlineMacros('const char* s = "FORCEINLINE";')).toBe('const char* s = "FORCEINLINE";'); expect(blankCppInlineMacros('x = FORCEINLINE + 1;')).toBe('x = FORCEINLINE + 1;'); expect(blankCppInlineMacros('int FORCEINLINE_COUNT = 3;')).toBe('int FORCEINLINE_COUNT = 3;'); expect(blankCppInlineMacros('no macros here')).toBe('no macros here'); }); }); describe('C++ universal macro-mangled name recovery', () => { // Curated pre-parse blanking can't list every library's inline macro, so a // post-parse salvage recovers the real function name from ANY leftover // `MACRO Ret name(…)` mangle — no list needed. It only ever touches an // already-mangled name, so it can't corrupt a clean one. const namesOf = (code: string, file = 's.cpp') => extractFromSource(file, code).nodes .filter((n) => n.kind === 'method' || n.kind === 'function') .map((n) => n.name); it('recovers the name from a completely unknown macro (no list entry)', () => { expect(namesOf('WEBKIT_EXPORT WTFString computeThing(int x) { return H(x); }')).toContain('computeThing'); expect(namesOf('SOMELIB_INLINE MyResult doWork(int x) { return H(x); }')).toContain('doWork'); expect(namesOf('MZ_FORCEINLINE char_t* to_str(double v) { return H(v); }')).toContain('to_str'); }); it('recoverMangledCppName only touches already-mangled names, with guards', () => { // Recovered: expect(recoverMangledCppName('WTFString computeThing')).toBe('computeThing'); expect(recoverMangledCppName('char_t* to_str(double v)')).toBe('to_str'); expect(recoverMangledCppName('unspecified_bool_type() const')).toBe('unspecified_bool_type'); // Left unchanged — clean names, operators, destructors, the `Ret (name)` // idiom, and non-identifier tails: expect(recoverMangledCppName('computeThing')).toBe('computeThing'); expect(recoverMangledCppName('operator EALSMovementState')).toBe('operator EALSMovementState'); expect(recoverMangledCppName('~Widget')).toBe('~Widget'); expect(recoverMangledCppName('bool (likely)')).toBe('bool (likely)'); expect(recoverMangledCppName('void (free)')).toBe('void (free)'); expect(recoverMangledCppName('QDockWidget *')).toBe('QDockWidget *'); }); it('does not disturb clean C++ names or non-C++ (Kotlin backtick) names', () => { expect(namesOf('int foo(int x) { return x; }')).toEqual(['foo']); // Kotlin backtick identifiers legitimately contain spaces; the salvage is // C/C++-only, so they are untouched. const kt = extractFromSource('T.kt', 'class T {\n fun `decode simple cert`() { }\n}').nodes .filter((n) => n.kind === 'method' || n.kind === 'function') .map((n) => n.name); expect(kt).toContain('`decode simple cert`'); }); it('curated list now also covers Qt / Folly / Abseil / LLVM / V8 / Eigen / rapidjson (full recovery)', () => { const info = (c: string) => extractFromSource('x.cpp', c).nodes .filter((n) => n.kind === 'method' || n.kind === 'function') .map((n) => ({ name: n.name, ret: n.returnType })); expect(info('FOLLY_ALWAYS_INLINE Str f(int x) { return H(x); }')).toEqual([{ name: 'f', ret: 'Str' }]); expect(namesOf('Q_INVOKABLE void onClicked() { H(); }')).toContain('onClicked'); expect(namesOf('ABSL_ATTRIBUTE_ALWAYS_INLINE int hash(int x) { return H(x); }')).toContain('hash'); expect(namesOf('EIGEN_STRONG_INLINE Scalar dot(const V& v) { return H(v); }')).toContain('dot'); expect(namesOf('V8_INLINE MaybeLocal Get(int i) { return H(i); }')).toContain('Get'); expect(namesOf('RAPIDJSON_FORCEINLINE bool Parse(const char* s) { return H(s); }')).toContain('Parse'); }); it('curated list spans the broader ecosystem (Mozilla, GLM, Bullet, OpenCV, Skia, EASTL, protobuf, fmt, Windows conventions)', () => { const info = (c: string) => extractFromSource('x.cpp', c).nodes .filter((n) => n.kind === 'method' || n.kind === 'function') .map((n) => ({ name: n.name, ret: n.returnType })); expect(info('MOZ_ALWAYS_INLINE Value get(int i) { return H(i); }')).toEqual([{ name: 'get', ret: 'Value' }]); expect(info('GLM_FUNC_QUALIFIER vec3 cross(const vec3& a) { return H(a); }')).toEqual([{ name: 'cross', ret: 'vec3' }]); expect(info('SIMD_FORCE_INLINE btScalar dot(const btVector3& v) const { return H(v); }')).toEqual([{ name: 'dot', ret: 'btScalar' }]); expect(info('CV_INLINE Mat clone() const { return H(); }')).toEqual([{ name: 'clone', ret: 'Mat' }]); expect(namesOf('PROTOBUF_ALWAYS_INLINE int size() const { return H(); }')).toContain('size'); expect(namesOf('FMT_CONSTEXPR auto parse(int x) { return H(x); }')).toContain('parse'); expect(namesOf('SK_ALWAYS_INLINE SkScalar width() const { return H(); }')).toContain('width'); expect(namesOf('EA_FORCE_INLINE size_type size() const { return H(); }')).toContain('size'); // Windows calling-convention macros sit between return type and name; the // macro is blanked so the real return type survives. expect(info('HRESULT WINAPI CreateThing(int x) { return H(x); }')).toEqual([{ name: 'CreateThing', ret: 'HRESULT' }]); expect(info('ULONG STDMETHODCALLTYPE AddRef() { return H(); }')).toEqual([{ name: 'AddRef', ret: 'ULONG' }]); }); }); describe('C++ templated base-class inheritance (#1043)', () => { // Inheriting from a template (`class D : public Base`) recorded the base // ref as the full instantiation `Base`, which never name-matched the // template indexed as the bare node `Base`. The `<…>` args are stripped so the // `extends` reference matches. it('strips template args from a templated base so the extends ref is the bare name', () => { const code = ` template class Base {}; template class CRTPBase {}; namespace ns { template class Tpl {}; } class Plain {}; class Widget : public Base {}; class App : public CRTPBase {}; class Q : public ns::Tpl {}; class Both : public Base, public Plain {}; `; const extendsRefs = extractFromSource('f.cpp', code).unresolvedReferences.filter( (r) => r.referenceKind === 'extends' ); const names = extendsRefs.map((r) => r.referenceName); // Templated bases carry the bare name, NOT the `<…>` instantiation. expect(names).toContain('Base'); // from Base / Base expect(names).toContain('CRTPBase'); // from CRTPBase (CRTP) expect(names).toContain('ns::Tpl'); // qualified head preserved, args dropped expect(names).toContain('Plain'); // non-templated base unchanged // No reference still carries angle brackets. expect(names.find((n) => n.includes('<'))).toBeUndefined(); }); it('stripCppTemplateArgs removes balanced <…> at any depth and is a no-op without them', () => { expect(stripCppTemplateArgs('Base')).toBe('Base'); expect(stripCppTemplateArgs('ns::Tpl')).toBe('ns::Tpl'); expect(stripCppTemplateArgs('ns::Tpl>')).toBe('ns::Tpl'); // nested expect(stripCppTemplateArgs('Outer::Inner')).toBe('Outer::Inner'); // mid-name expect(stripCppTemplateArgs('Base')).toBe('Base'); // no-op expect(stripCppTemplateArgs('ns::Plain')).toBe('ns::Plain'); // no-op qualified }); }); describe('C++ stack-allocation construction (#1035)', () => { // `Calculator calc(0)` (direct-init) and `Widget w{1, 2}` (brace-init) carry // the constructor args directly on the declarator — no call/new node — so // they emitted no constructor reference, unlike heap `new Calculator(0)`. An // `instantiates` ref to the constructed type is now emitted for both. const instNames = (code: string) => extractFromSource('f.cpp', `void run() {\n${code}\n}`) .unresolvedReferences.filter((r) => r.referenceKind === 'instantiates') .map((r) => r.referenceName); it('emits an instantiates ref for direct-init and brace-init', () => { expect(instNames('Calculator calc(0);')).toEqual(['Calculator']); expect(instNames('Widget w{1, 2};')).toEqual(['Widget']); }); it('strips template args and namespace to the bare class name', () => { // `std::vector v(10)` → `vector`; `ns::Widget w(0)` → `Widget`. expect(instNames('std::vector v(10);')).toEqual(['vector']); expect(instNames('ns::Widget w(0);')).toEqual(['Widget']); }); it('does not emit for primitives, default construction, or the most-vexing parse', () => { expect(instNames('int x(5);')).toEqual([]); // primitive direct-init expect(instNames('int y{6};')).toEqual([]); // primitive brace-init expect(instNames('auto z = make();')).toEqual([]); // auto + call (handled elsewhere) expect(instNames('Calculator deferred;')).toEqual([]); // default construction, no args expect(instNames('Calculator calc();')).toEqual([]); // function declaration (most-vexing parse) }); it('emits a single instantiates ref for a multi-declarator statement', () => { // `Calculator a(1), b(2);` shares one `type` field; both construct a // Calculator, so one ref suffices (it dedups to one edge regardless). expect(instNames('Calculator a(1), b(2);')).toEqual(['Calculator']); }); }); describe('C/C++ imports', () => { it('should extract system include', () => { const code = `#include `; const result = extractFromSource('main.cpp', code); const importNode = result.nodes.find((n) => n.kind === 'import'); expect(importNode).toBeDefined(); expect(importNode?.name).toBe('iostream'); expect(importNode?.signature).toBe('#include '); }); it('should extract system include with path', () => { const code = `#include `; const result = extractFromSource('app.cpp', code); const importNode = result.nodes.find((n) => n.kind === 'import'); expect(importNode).toBeDefined(); expect(importNode?.name).toBe('nlohmann/json.hpp'); }); it('should extract local include', () => { const code = `#include "myheader.h"`; const result = extractFromSource('main.cpp', code); const importNode = result.nodes.find((n) => n.kind === 'import'); expect(importNode).toBeDefined(); expect(importNode?.name).toBe('myheader.h'); }); it('should extract C header', () => { const code = `#include `; const result = extractFromSource('main.c', code); const importNode = result.nodes.find((n) => n.kind === 'import'); expect(importNode).toBeDefined(); expect(importNode?.name).toBe('stdio.h'); }); it('should extract multiple includes', () => { const code = ` #include #include #include "config.h" `; const result = extractFromSource('app.cpp', code); const importNodes = result.nodes.filter((n) => n.kind === 'import'); expect(importNodes.length).toBe(3); const names = importNodes.map((n) => n.name); expect(names).toContain('iostream'); expect(names).toContain('vector'); expect(names).toContain('config.h'); }); it('should create unresolved references for local includes', () => { const code = `#include "myheader.h"`; const result = extractFromSource('main.cpp', code); const importRef = result.unresolvedReferences.find( (r) => r.referenceKind === 'imports' && r.referenceName === 'myheader.h' ); expect(importRef).toBeDefined(); expect(importRef?.line).toBe(1); }); it('should create unresolved references for system includes', () => { const code = `#include `; const result = extractFromSource('main.cpp', code); const importRef = result.unresolvedReferences.find( (r) => r.referenceKind === 'imports' && r.referenceName === 'iostream' ); expect(importRef).toBeDefined(); }); }); describe('Dart imports', () => { it('should extract dart: import', () => { const code = `import 'dart:async';`; const result = extractFromSource('main.dart', code); const importNode = result.nodes.find((n) => n.kind === 'import'); expect(importNode).toBeDefined(); expect(importNode?.name).toBe('dart:async'); expect(importNode?.signature).toBe("import 'dart:async';"); }); it('should extract package import', () => { const code = `import 'package:flutter/material.dart';`; const result = extractFromSource('app.dart', code); const importNode = result.nodes.find((n) => n.kind === 'import'); expect(importNode).toBeDefined(); expect(importNode?.name).toBe('package:flutter/material.dart'); }); it('should extract aliased import', () => { const code = `import 'package:http/http.dart' as http;`; const result = extractFromSource('api.dart', code); const importNode = result.nodes.find((n) => n.kind === 'import'); expect(importNode).toBeDefined(); expect(importNode?.name).toBe('package:http/http.dart'); expect(importNode?.signature).toContain('as http'); }); it('should extract multiple imports', () => { const code = ` import 'dart:async'; import 'dart:convert'; import 'package:flutter/material.dart'; `; const result = extractFromSource('main.dart', code); const importNodes = result.nodes.filter((n) => n.kind === 'import'); expect(importNodes.length).toBe(3); const names = importNodes.map((n) => n.name); expect(names).toContain('dart:async'); expect(names).toContain('dart:convert'); expect(names).toContain('package:flutter/material.dart'); }); it('should extract relative import', () => { const code = `import '../utils/helpers.dart';`; const result = extractFromSource('lib/main.dart', code); const importNode = result.nodes.find((n) => n.kind === 'import'); expect(importNode).toBeDefined(); expect(importNode?.name).toBe('../utils/helpers.dart'); }); }); describe('Liquid imports', () => { it('should extract render tag', () => { const code = `{% render 'loading-spinner' %}`; const result = extractFromSource('template.liquid', code); const importNode = result.nodes.find((n) => n.kind === 'import'); expect(importNode).toBeDefined(); expect(importNode?.name).toBe('loading-spinner'); expect(importNode?.signature).toContain('render'); }); it('should extract section tag', () => { const code = `{% section 'header' %}`; const result = extractFromSource('layout/theme.liquid', code); const importNode = result.nodes.find((n) => n.kind === 'import'); expect(importNode).toBeDefined(); expect(importNode?.name).toBe('header'); expect(importNode?.signature).toContain('section'); }); it('should extract include tag', () => { const code = `{% include 'icon-cart' %}`; const result = extractFromSource('snippets/header.liquid', code); const importNode = result.nodes.find((n) => n.kind === 'import'); expect(importNode).toBeDefined(); expect(importNode?.name).toBe('icon-cart'); expect(importNode?.signature).toContain('include'); }); it('should extract render with whitespace control', () => { const code = `{%- render 'price' -%}`; const result = extractFromSource('snippets/product.liquid', code); const importNode = result.nodes.find((n) => n.kind === 'import'); expect(importNode).toBeDefined(); expect(importNode?.name).toBe('price'); }); it('should extract multiple imports', () => { const code = ` {% section 'header' %} {% render 'loading-spinner' %} {% render 'cart-drawer' %} `; const result = extractFromSource('layout/theme.liquid', code); const importNodes = result.nodes.filter((n) => n.kind === 'import'); expect(importNodes.length).toBe(3); const names = importNodes.map((n) => n.name); expect(names).toContain('header'); expect(names).toContain('loading-spinner'); expect(names).toContain('cart-drawer'); }); }); }); // ============================================================================= // Pascal / Delphi Extraction // ============================================================================= describe('Pascal / Delphi Extraction', () => { describe('Language detection', () => { it('should detect Pascal files', () => { expect(detectLanguage('UAuth.pas')).toBe('pascal'); expect(detectLanguage('App.dpr')).toBe('pascal'); expect(detectLanguage('Package.dpk')).toBe('pascal'); expect(detectLanguage('App.lpr')).toBe('pascal'); expect(detectLanguage('MainForm.dfm')).toBe('pascal'); expect(detectLanguage('MainForm.fmx')).toBe('pascal'); }); it('should report Pascal as supported', () => { expect(isLanguageSupported('pascal')).toBe(true); expect(getSupportedLanguages()).toContain('pascal'); }); }); describe('Unit extraction', () => { it('should extract unit as module', () => { const code = `unit MyUnit;\ninterface\nimplementation\nend.`; const result = extractFromSource('MyUnit.pas', code); const moduleNode = result.nodes.find((n) => n.kind === 'module'); expect(moduleNode).toBeDefined(); expect(moduleNode?.name).toBe('MyUnit'); expect(moduleNode?.language).toBe('pascal'); }); it('should extract program as module', () => { const code = `program MyApp;\nbegin\nend.`; const result = extractFromSource('MyApp.dpr', code); const moduleNode = result.nodes.find((n) => n.kind === 'module'); expect(moduleNode).toBeDefined(); expect(moduleNode?.name).toBe('MyApp'); }); it('should fallback to filename when module name is empty', () => { // Some .dpr templates use "program;" without a name const code = `program;\nuses SysUtils;\nbegin\nend.`; const result = extractFromSource('Console.dpr', code); const moduleNode = result.nodes.find((n) => n.kind === 'module'); expect(moduleNode).toBeDefined(); expect(moduleNode?.name).toBe('Console'); }); }); describe('Uses clause (imports)', () => { it('should extract uses as individual imports', () => { const code = `unit Test;\ninterface\nuses\n System.SysUtils,\n System.Classes;\nimplementation\nend.`; const result = extractFromSource('Test.pas', code); const imports = result.nodes.filter((n) => n.kind === 'import'); expect(imports.length).toBe(2); expect(imports.map((n) => n.name)).toContain('System.SysUtils'); expect(imports.map((n) => n.name)).toContain('System.Classes'); }); it('should create unresolved references for imports', () => { const code = `unit Test;\ninterface\nuses\n UAuth;\nimplementation\nend.`; const result = extractFromSource('Test.pas', code); const importRef = result.unresolvedReferences.find( (r) => r.referenceKind === 'imports' ); expect(importRef).toBeDefined(); expect(importRef?.referenceName).toBe('UAuth'); }); }); describe('Class extraction', () => { it('should extract class declarations', () => { const code = `unit Test;\ninterface\ntype\n TMyClass = class\n public\n procedure DoSomething;\n end;\nimplementation\nend.`; const result = extractFromSource('Test.pas', code); const classNode = result.nodes.find((n) => n.kind === 'class'); expect(classNode).toBeDefined(); expect(classNode?.name).toBe('TMyClass'); }); it('should extract class with inheritance', () => { const code = `unit Test;\ninterface\ntype\n TChild = class(TParent)\n end;\nimplementation\nend.`; const result = extractFromSource('Test.pas', code); const extendsRef = result.unresolvedReferences.find( (r) => r.referenceKind === 'extends' ); expect(extendsRef).toBeDefined(); expect(extendsRef?.referenceName).toBe('TParent'); }); it('should extract class with interface implementation', () => { const code = `unit Test;\ninterface\ntype\n TService = class(TInterfacedObject, ILogger)\n end;\nimplementation\nend.`; const result = extractFromSource('Test.pas', code); const extendsRef = result.unresolvedReferences.find( (r) => r.referenceKind === 'extends' ); const implementsRef = result.unresolvedReferences.find( (r) => r.referenceKind === 'implements' ); expect(extendsRef?.referenceName).toBe('TInterfacedObject'); expect(implementsRef?.referenceName).toBe('ILogger'); }); }); describe('Record extraction', () => { it('should extract records as class nodes', () => { const code = `unit Test;\ninterface\ntype\n TPoint = record\n X: Double;\n Y: Double;\n end;\nimplementation\nend.`; const result = extractFromSource('Test.pas', code); const classNode = result.nodes.find((n) => n.kind === 'class'); expect(classNode).toBeDefined(); expect(classNode?.name).toBe('TPoint'); const fields = result.nodes.filter((n) => n.kind === 'field'); expect(fields.length).toBe(2); expect(fields.map((f) => f.name)).toContain('X'); expect(fields.map((f) => f.name)).toContain('Y'); }); }); describe('Interface extraction', () => { it('should extract interface declarations', () => { const code = `unit Test;\ninterface\ntype\n ILogger = interface\n procedure Log(const AMsg: string);\n end;\nimplementation\nend.`; const result = extractFromSource('Test.pas', code); const ifaceNode = result.nodes.find((n) => n.kind === 'interface'); expect(ifaceNode).toBeDefined(); expect(ifaceNode?.name).toBe('ILogger'); }); }); describe('Method extraction', () => { it('should extract methods with visibility', () => { const code = `unit Test;\ninterface\ntype\n TMyClass = class\n private\n FValue: Integer;\n public\n constructor Create;\n function GetValue: Integer;\n end;\nimplementation\nend.`; const result = extractFromSource('Test.pas', code); const methods = result.nodes.filter((n) => n.kind === 'method'); expect(methods.length).toBe(2); const createMethod = methods.find((m) => m.name === 'Create'); expect(createMethod?.visibility).toBe('public'); const getValue = methods.find((m) => m.name === 'GetValue'); expect(getValue?.visibility).toBe('public'); const fields = result.nodes.filter((n) => n.kind === 'field'); const fValue = fields.find((f) => f.name === 'FValue'); expect(fValue?.visibility).toBe('private'); }); it('should detect static methods (class methods)', () => { const code = `unit Test;\ninterface\ntype\n THelper = class\n public\n class function Create: THelper; static;\n end;\nimplementation\nend.`; const result = extractFromSource('Test.pas', code); const methods = result.nodes.filter((n) => n.kind === 'method'); const staticMethod = methods.find((m) => m.name === 'Create'); expect(staticMethod?.isStatic).toBe(true); }); }); describe('Enum extraction', () => { it('should extract enums with members', () => { const code = `unit Test;\ninterface\ntype\n TColor = (clRed, clGreen, clBlue);\nimplementation\nend.`; const result = extractFromSource('Test.pas', code); const enumNode = result.nodes.find((n) => n.kind === 'enum'); expect(enumNode).toBeDefined(); expect(enumNode?.name).toBe('TColor'); const members = result.nodes.filter((n) => n.kind === 'enum_member'); expect(members.length).toBe(3); expect(members.map((m) => m.name)).toEqual(['clRed', 'clGreen', 'clBlue']); }); }); describe('Property extraction', () => { it('should extract properties', () => { const code = `unit Test;\ninterface\ntype\n TObj = class\n public\n property Name: string read FName write FName;\n end;\nimplementation\nend.`; const result = extractFromSource('Test.pas', code); const propNode = result.nodes.find((n) => n.kind === 'property'); expect(propNode).toBeDefined(); expect(propNode?.name).toBe('Name'); expect(propNode?.visibility).toBe('public'); }); }); describe('Constant extraction', () => { it('should extract constants', () => { const code = `unit Test;\ninterface\nconst\n MAX_RETRIES = 3;\n APP_NAME = 'MyApp';\nimplementation\nend.`; const result = extractFromSource('Test.pas', code); const constants = result.nodes.filter((n) => n.kind === 'constant'); expect(constants.length).toBe(2); expect(constants.map((c) => c.name)).toContain('MAX_RETRIES'); expect(constants.map((c) => c.name)).toContain('APP_NAME'); }); }); describe('Type alias extraction', () => { it('should extract type aliases', () => { const code = `unit Test;\ninterface\ntype\n TUserName = string;\nimplementation\nend.`; const result = extractFromSource('Test.pas', code); const aliasNode = result.nodes.find((n) => n.kind === 'type_alias'); expect(aliasNode).toBeDefined(); expect(aliasNode?.name).toBe('TUserName'); }); }); describe('Call extraction', () => { it('should extract calls from implementation bodies', () => { const code = `unit Test;\ninterface\ntype\n TObj = class\n public\n procedure DoWork;\n end;\nimplementation\nprocedure TObj.DoWork;\nbegin\n WriteLn('hello');\nend;\nend.`; const result = extractFromSource('Test.pas', code); const callRef = result.unresolvedReferences.find( (r) => r.referenceKind === 'calls' ); expect(callRef).toBeDefined(); expect(callRef?.referenceName).toBe('WriteLn'); }); }); describe('Containment edges', () => { it('should create contains edges for class members', () => { const code = `unit Test;\ninterface\ntype\n TObj = class\n public\n procedure Foo;\n end;\nimplementation\nend.`; const result = extractFromSource('Test.pas', code); const classNode = result.nodes.find((n) => n.kind === 'class'); const methodNode = result.nodes.find((n) => n.kind === 'method'); expect(classNode).toBeDefined(); expect(methodNode).toBeDefined(); const containsEdge = result.edges.find( (e) => e.source === classNode?.id && e.target === methodNode?.id && e.kind === 'contains' ); expect(containsEdge).toBeDefined(); }); }); describe('Full fixture: UAuth.pas', () => { const code = `unit UAuth; interface uses System.SysUtils, System.Classes; type ITokenValidator = interface ['{11111111-1111-1111-1111-111111111111}'] function Validate(const AToken: string): Boolean; end; TAuthService = class(TInterfacedObject, ITokenValidator) private FToken: string; FLoginCount: Integer; procedure IncLoginCount; protected function GetToken: string; public constructor Create; destructor Destroy; override; function Validate(const AToken: string): Boolean; function Login(const AUser, APass: string): string; property Token: string read GetToken; property LoginCount: Integer read FLoginCount; end; implementation constructor TAuthService.Create; begin inherited Create; FToken := ''; FLoginCount := 0; end; destructor TAuthService.Destroy; begin FToken := ''; inherited Destroy; end; procedure TAuthService.IncLoginCount; begin Inc(FLoginCount); end; function TAuthService.GetToken: string; begin Result := FToken; end; function TAuthService.Validate(const AToken: string): Boolean; begin Result := AToken <> ''; end; function TAuthService.Login(const AUser, APass: string): string; begin IncLoginCount; if Validate(AUser + ':' + APass) then begin FToken := AUser; Result := 'ok'; end else Result := ''; end; end.`; it('should extract all expected nodes', () => { const result = extractFromSource('UAuth.pas', code); expect(result.errors).toHaveLength(0); // Module const moduleNode = result.nodes.find((n) => n.kind === 'module'); expect(moduleNode?.name).toBe('UAuth'); // Imports const imports = result.nodes.filter((n) => n.kind === 'import'); expect(imports.length).toBe(2); // Interface const ifaceNode = result.nodes.find((n) => n.kind === 'interface'); expect(ifaceNode?.name).toBe('ITokenValidator'); // Class const classNode = result.nodes.find((n) => n.kind === 'class'); expect(classNode?.name).toBe('TAuthService'); // Methods const methods = result.nodes.filter((n) => n.kind === 'method'); expect(methods.length).toBeGreaterThanOrEqual(6); expect(methods.map((m) => m.name)).toContain('Create'); expect(methods.map((m) => m.name)).toContain('Destroy'); expect(methods.map((m) => m.name)).toContain('Login'); // Fields const fields = result.nodes.filter((n) => n.kind === 'field'); expect(fields.length).toBe(2); expect(fields.every((f) => f.visibility === 'private')).toBe(true); // Properties const props = result.nodes.filter((n) => n.kind === 'property'); expect(props.length).toBe(2); expect(props.map((p) => p.name)).toContain('Token'); expect(props.map((p) => p.name)).toContain('LoginCount'); }); it('should extract inheritance and interface implementation', () => { const result = extractFromSource('UAuth.pas', code); const extendsRef = result.unresolvedReferences.find( (r) => r.referenceKind === 'extends' ); expect(extendsRef?.referenceName).toBe('TInterfacedObject'); const implementsRef = result.unresolvedReferences.find( (r) => r.referenceKind === 'implements' ); expect(implementsRef?.referenceName).toBe('ITokenValidator'); }); it('should extract calls from implementation', () => { const result = extractFromSource('UAuth.pas', code); const callRefs = result.unresolvedReferences.filter( (r) => r.referenceKind === 'calls' ); expect(callRefs.map((r) => r.referenceName)).toContain('Inc'); expect(callRefs.map((r) => r.referenceName)).toContain('Validate'); }); }); describe('Full fixture: UTypes.pas', () => { const code = `unit UTypes; interface uses System.SysUtils; const C_MAX_RETRIES = 3; C_DEFAULT_NAME = 'Guest'; type TUserRole = (urAdmin, urEditor, urViewer); TPoint2D = record X: Double; Y: Double; end; TUserName = string; TUserInfo = class public type TAddress = record Street: string; City: string; Zip: string; end; private FName: TUserName; FRole: TUserRole; FAddress: TAddress; public constructor Create(const AName: TUserName; ARole: TUserRole); function GetDisplayName: string; class function CreateAdmin(const AName: TUserName): TUserInfo; static; property Name: TUserName read FName write FName; property Role: TUserRole read FRole; property Address: TAddress read FAddress write FAddress; end; implementation constructor TUserInfo.Create(const AName: TUserName; ARole: TUserRole); begin FName := AName; FRole := ARole; end; function TUserInfo.GetDisplayName: string; begin if FRole = urAdmin then Result := '[Admin] ' + FName else Result := FName; end; class function TUserInfo.CreateAdmin(const AName: TUserName): TUserInfo; begin Result := TUserInfo.Create(AName, urAdmin); end; end.`; it('should extract enums with members', () => { const result = extractFromSource('UTypes.pas', code); const enumNode = result.nodes.find((n) => n.kind === 'enum'); expect(enumNode?.name).toBe('TUserRole'); const members = result.nodes.filter((n) => n.kind === 'enum_member'); expect(members.length).toBe(3); expect(members.map((m) => m.name)).toEqual(['urAdmin', 'urEditor', 'urViewer']); }); it('should extract constants', () => { const result = extractFromSource('UTypes.pas', code); const constants = result.nodes.filter((n) => n.kind === 'constant'); expect(constants.length).toBe(2); expect(constants.map((c) => c.name)).toContain('C_MAX_RETRIES'); expect(constants.map((c) => c.name)).toContain('C_DEFAULT_NAME'); }); it('should extract type aliases', () => { const result = extractFromSource('UTypes.pas', code); const aliases = result.nodes.filter((n) => n.kind === 'type_alias'); expect(aliases.map((a) => a.name)).toContain('TUserName'); }); it('should extract records as classes with fields', () => { const result = extractFromSource('UTypes.pas', code); const classes = result.nodes.filter((n) => n.kind === 'class'); expect(classes.map((c) => c.name)).toContain('TPoint2D'); // TPoint2D fields const fields = result.nodes.filter((n) => n.kind === 'field'); expect(fields.map((f) => f.name)).toContain('X'); expect(fields.map((f) => f.name)).toContain('Y'); }); it('should extract static class methods', () => { const result = extractFromSource('UTypes.pas', code); const methods = result.nodes.filter((n) => n.kind === 'method'); const staticMethod = methods.find((m) => m.name === 'CreateAdmin'); expect(staticMethod).toBeDefined(); expect(staticMethod?.isStatic).toBe(true); }); it('should extract nested types', () => { const result = extractFromSource('UTypes.pas', code); const classes = result.nodes.filter((n) => n.kind === 'class'); expect(classes.map((c) => c.name)).toContain('TAddress'); }); }); }); // ============================================================================= // DFM/FMX Extraction // ============================================================================= describe('DFM/FMX Extraction', () => { it('should extract components from DFM', () => { const code = `object Form1: TForm1 Left = 0 Top = 0 Caption = 'My Form' object Button1: TButton Left = 10 Top = 10 Caption = 'Click Me' end end`; const result = extractFromSource('Form1.dfm', code); const components = result.nodes.filter((n) => n.kind === 'component'); expect(components.length).toBe(2); expect(components.map((c) => c.name)).toContain('Form1'); expect(components.map((c) => c.name)).toContain('Button1'); const button = components.find((c) => c.name === 'Button1'); expect(button?.signature).toBe('TButton'); }); it('should extract nested component hierarchy', () => { const code = `object Form1: TForm1 object Panel1: TPanel object Label1: TLabel Caption = 'Hello' end end end`; const result = extractFromSource('Form1.dfm', code); const components = result.nodes.filter((n) => n.kind === 'component'); expect(components.length).toBe(3); // Check nesting: Panel1 contains Label1 const panel = components.find((c) => c.name === 'Panel1'); const label = components.find((c) => c.name === 'Label1'); const containsEdge = result.edges.find( (e) => e.source === panel?.id && e.target === label?.id && e.kind === 'contains' ); expect(containsEdge).toBeDefined(); }); it('should extract event handler references', () => { const code = `object Form1: TForm1 OnCreate = FormCreate OnDestroy = FormDestroy object Button1: TButton OnClick = Button1Click end end`; const result = extractFromSource('Form1.dfm', code); const refs = result.unresolvedReferences; expect(refs.length).toBe(3); expect(refs.map((r) => r.referenceName)).toContain('FormCreate'); expect(refs.map((r) => r.referenceName)).toContain('FormDestroy'); expect(refs.map((r) => r.referenceName)).toContain('Button1Click'); expect(refs.every((r) => r.referenceKind === 'references')).toBe(true); }); it('should handle multi-line properties', () => { const code = `object Form1: TForm1 SQL.Strings = ( 'SELECT * FROM users' 'WHERE active = 1') object Button1: TButton OnClick = Button1Click end end`; const result = extractFromSource('Form1.dfm', code); const components = result.nodes.filter((n) => n.kind === 'component'); expect(components.length).toBe(2); const refs = result.unresolvedReferences; expect(refs.length).toBe(1); expect(refs[0]?.referenceName).toBe('Button1Click'); }); it('should handle inherited keyword', () => { const code = `inherited Form1: TForm1 Caption = 'Inherited Form' object Button1: TButton OnClick = Button1Click end end`; const result = extractFromSource('Form1.dfm', code); const components = result.nodes.filter((n) => n.kind === 'component'); expect(components.length).toBe(2); expect(components.map((c) => c.name)).toContain('Form1'); }); it('should handle item collection properties', () => { const code = `object Form1: TForm1 object StatusBar1: TStatusBar Panels = < item Width = 200 end item Width = 200 end> end end`; const result = extractFromSource('Form1.dfm', code); const components = result.nodes.filter((n) => n.kind === 'component'); expect(components.length).toBe(2); }); describe('Full fixture: MainForm.dfm', () => { const code = `object frmMain: TfrmMain Left = 0 Top = 0 Caption = 'CodeGraph DFM Fixture' ClientHeight = 480 ClientWidth = 640 OnCreate = FormCreate OnDestroy = FormDestroy object pnlTop: TPanel Left = 0 Top = 0 Width = 640 Height = 50 object lblTitle: TLabel Left = 16 Top = 16 Caption = 'Authentication Service' end object btnLogin: TButton Left = 540 Top = 12 OnClick = btnLoginClick end end object pnlContent: TPanel Left = 0 Top = 50 object edtUsername: TEdit Left = 16 Top = 16 OnChange = edtUsernameChange end object edtPassword: TEdit Left = 16 Top = 48 OnKeyPress = edtPasswordKeyPress end object mmoLog: TMemo Left = 16 Top = 88 end end object pnlStatus: TStatusBar Left = 0 Top = 440 Panels = < item Width = 200 end item Width = 200 end> end end`; it('should extract all components', () => { const result = extractFromSource('MainForm.dfm', code); const components = result.nodes.filter((n) => n.kind === 'component'); expect(components.length).toBe(9); expect(components.map((c) => c.name)).toEqual( expect.arrayContaining([ 'frmMain', 'pnlTop', 'lblTitle', 'btnLogin', 'pnlContent', 'edtUsername', 'edtPassword', 'mmoLog', 'pnlStatus', ]) ); }); it('should extract all event handlers', () => { const result = extractFromSource('MainForm.dfm', code); const refs = result.unresolvedReferences; expect(refs.length).toBe(5); expect(refs.map((r) => r.referenceName)).toEqual( expect.arrayContaining([ 'FormCreate', 'FormDestroy', 'btnLoginClick', 'edtUsernameChange', 'edtPasswordKeyPress', ]) ); }); }); }); describe('Kotlin Multiplatform expect/actual', () => { 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 expect declarations to platform actual implementations and surfaces them in impact', async () => { const common = path.join(tempDir, 'src', 'commonMain'); const jvm = path.join(tempDir, 'src', 'jvmMain'); fs.mkdirSync(common, { recursive: true }); fs.mkdirSync(jvm, { recursive: true }); // common source set: expect declarations + a caller that uses them fs.writeFileSync( path.join(common, 'SystemProps.kt'), `package demo.internal expect fun systemProp(name: String): String? expect class Platform { fun describe(): String } ` ); fs.writeFileSync( path.join(common, 'Caller.kt'), `package demo import demo.internal.systemProp import demo.internal.Platform fun useIt(): String { val v = systemProp("os.name") return Platform().describe() + v } ` ); // jvm source set: actual implementations fs.writeFileSync( path.join(jvm, 'SystemProps.kt'), `package demo.internal actual fun systemProp(name: String): String? = System.getProperty(name) actual class Platform { actual fun describe(): String = "JVM" } ` ); cg = CodeGraph.initSync(tempDir); await cg.indexAll(); cg.resolveReferences(); // The expect/actual markers are captured onto the node's decorators. const fns = cg.getNodesByKind('function'); const actualFn = fns.find( (n) => n.name === 'systemProp' && n.decorators?.includes('actual') ); const expectFn = fns.find( (n) => n.name === 'systemProp' && n.decorators?.includes('expect') ); expect(actualFn).toBeDefined(); expect(expectFn).toBeDefined(); expect(actualFn!.filePath).not.toBe(expectFn!.filePath); // Editing the JVM actual must surface the common expect AND its caller — // before the expect/actual bridge the actual had zero dependents. const impact = cg.getImpactRadius(actualFn!.id, 3); const impacted = [...impact.nodes.values()].map((n) => n.name); expect(impacted).toContain('systemProp'); // the common expect expect(impacted).toContain('useIt'); // the caller, reached transitively // The bridging edge is a heuristic `calls` edge tagged by the synthesizer. const bridge = impact.edges.find( (e) => e.target === actualFn!.id && e.provenance === 'heuristic' && (e.metadata as { synthesizedBy?: string } | undefined)?.synthesizedBy === 'kotlin-expect-actual' ); expect(bridge).toBeDefined(); expect(bridge!.source).toBe(expectFn!.id); }); it('links an expect class to an actual typealias (different node kinds)', async () => { const common = path.join(tempDir, 'src', 'commonMain'); const jvm = path.join(tempDir, 'src', 'jvmMain'); fs.mkdirSync(common, { recursive: true }); fs.mkdirSync(jvm, { recursive: true }); fs.writeFileSync( path.join(common, 'Lock.kt'), `package demo expect class Lock { fun acquire() } ` ); fs.writeFileSync( path.join(jvm, 'Lock.kt'), `package demo actual typealias Lock = java.util.concurrent.locks.ReentrantLock ` ); cg = CodeGraph.initSync(tempDir); await cg.indexAll(); cg.resolveReferences(); const aliasNode = cg .getNodesByKind('type_alias') .find((n) => n.name === 'Lock' && n.decorators?.includes('actual')); expect(aliasNode).toBeDefined(); // The actual typealias is now a cross-file dependency target (linked from // the expect class), so it participates in impact rather than being orphaned. const impact = cg.getImpactRadius(aliasNode!.id, 3); const bridge = impact.edges.find( (e) => e.target === aliasNode!.id && (e.metadata as { synthesizedBy?: string } | undefined)?.synthesizedBy === 'kotlin-expect-actual' ); expect(bridge).toBeDefined(); }); }); describe('Scala cross-file dependencies', () => { 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 parameterized supertypes, type annotations, and implicit params across files', async () => { const src = path.join(tempDir, 'src', 'main', 'scala', 'demo'); fs.mkdirSync(src, { recursive: true }); fs.writeFileSync( path.join(src, 'Semigroup.scala'), `package demo trait Semigroup[A] { def combine(x: A, y: A): A } ` ); fs.writeFileSync( path.join(src, 'Monoid.scala'), `package demo trait Monoid[A] extends Semigroup[A] { def empty: A } ` ); fs.writeFileSync( path.join(src, 'Instances.scala'), `package demo object Instances { implicit val intMonoid: Monoid[Int] = new Monoid[Int] { def empty: Int = 0 def combine(x: Int, y: Int): Int = x + y } } ` ); fs.writeFileSync( path.join(src, 'Folding.scala'), `package demo object Folding { def fold[A](xs: List[A])(implicit M: Monoid[A]): A = xs.foldLeft(M.empty)(M.combine) } ` ); cg = CodeGraph.initSync(tempDir); await cg.indexAll(); cg.resolveReferences(); const monoid = cg.getNodesByKind('trait').find((n) => n.name === 'Monoid'); const semigroup = cg.getNodesByKind('trait').find((n) => n.name === 'Semigroup'); expect(monoid).toBeDefined(); expect(semigroup).toBeDefined(); expect(monoid!.filePath).not.toBe(semigroup!.filePath); // Parameterized supertype `extends Semigroup[A]` must create an extends edge — // the whole point of the fix (the `[A]` used to defeat name matching). const semaImpact = cg.getImpactRadius(semigroup!.id, 3); expect([...semaImpact.nodes.values()].map((n) => n.name)).toContain('Monoid'); // Editing Monoid surfaces the cross-file users: the instance val typed // `Monoid[Int]` and the method taking it as an implicit (curried) param. const impacted = [...cg.getImpactRadius(monoid!.id, 3).nodes.values()].map((n) => n.name); expect(impacted).toContain('intMonoid'); // field type annotation expect(impacted).toContain('fold'); // trailing implicit parameter list }); }); describe('PHP namespace + import resolution', () => { 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('resolves `use` imports to the namespace-qualified definition and type-hints across files', async () => { const src = path.join(tempDir, 'src'); // Two interfaces with the SAME simple name in different namespaces — the // exact ambiguity (Laravel has 7+ `Factory`) that bare-name matching can't // resolve. The namespace qualifies them; the `use` import disambiguates. fs.mkdirSync(path.join(src, 'Cache'), { recursive: true }); fs.mkdirSync(path.join(src, 'Mail'), { recursive: true }); fs.mkdirSync(path.join(src, 'App'), { recursive: true }); fs.writeFileSync( path.join(src, 'Cache', 'Factory.php'), ` n.qualifiedName === 'Contracts\\Cache::Factory'); const mailFactory = cg .getNodesByKind('interface') .find((n) => n.qualifiedName === 'Contracts\\Mail::Factory'); expect(cacheFactory).toBeDefined(); expect(mailFactory).toBeDefined(); // Service `use`s Contracts\Cache\Factory, so editing THAT interface reaches // Service.php — and editing the same-named Contracts\Mail\Factory must NOT // (the import resolved to the right namespace, not an arbitrary `Factory`). const serviceFile = 'src/App/Service.php'; const cacheReaches = [...cg.getImpactRadius(cacheFactory!.id, 3).nodes.values()].some( (n) => (n.filePath ?? '').endsWith(serviceFile) ); const mailReaches = [...cg.getImpactRadius(mailFactory!.id, 3).nodes.values()].some( (n) => (n.filePath ?? '').endsWith(serviceFile) ); expect(cacheReaches).toBe(true); expect(mailReaches).toBe(false); }); }); describe('Ruby mixins (include/extend/prepend)', () => { 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 include/extend/prepend to the mixed-in module across files', async () => { const lib = path.join(tempDir, 'lib'); fs.mkdirSync(lib, { recursive: true }); fs.writeFileSync( path.join(lib, 'concerns.rb'), `module Trackable def track; end end module Cacheable def cache; end end module Loggable def log; end end ` ); fs.writeFileSync( path.join(lib, 'model.rb'), `class Model include Trackable prepend Cacheable extend Loggable end ` ); cg = CodeGraph.initSync(tempDir); await cg.indexAll(); cg.resolveReferences(); const model = cg.getNodesByKind('class').find((n) => n.name === 'Model'); expect(model).toBeDefined(); // All three mixin forms create an `implements` edge Model → module, so // editing a concern surfaces every class that mixes it in (across files). for (const moduleName of ['Trackable', 'Cacheable', 'Loggable']) { const mod = cg.getNodesByKind('module').find((n) => n.name === moduleName); expect(mod, moduleName).toBeDefined(); const impacted = [...cg.getImpactRadius(mod!.id, 3).nodes.values()].map((n) => n.name); expect(impacted, `${moduleName} should be depended on by Model`).toContain('Model'); } }); it('resolves require / require_relative to the required file', async () => { const lib = path.join(tempDir, 'lib'); fs.mkdirSync(path.join(lib, 'app'), { recursive: true }); // A leaf file whose class is referenced only dynamically — so without // require resolution it would look like nothing depends on it. fs.writeFileSync( path.join(lib, 'app', 'fetcher.rb'), `module App class Fetcher def fetch; end end end ` ); // Pulled in by a load-path `require` … fs.writeFileSync( path.join(lib, 'app', 'worker.rb'), `require "app/fetcher" module App class Worker; end end ` ); // … and a sibling pulled in by `require_relative`. fs.writeFileSync( path.join(lib, 'app', 'boot.rb'), `require_relative "fetcher" ` ); cg = CodeGraph.initSync(tempDir); await cg.indexAll(); cg.resolveReferences(); // The require edges target fetcher.rb's FILE node. Editing it should reach // BOTH the load-path requirer (worker.rb) and the require_relative one // (boot.rb) — without require resolution its file would have no dependents. const fetcher = cg.getNodesByKind('file').find((n) => n.filePath.endsWith('app/fetcher.rb')); expect(fetcher, 'fetcher.rb indexed').toBeDefined(); const reached = [...cg.getImpactRadius(fetcher!.id, 2).nodes.values()].map((n) => n.filePath ?? ''); expect(reached.some((p) => p.endsWith('app/worker.rb'))).toBe(true); expect(reached.some((p) => p.endsWith('app/boot.rb'))).toBe(true); }); }); describe('C++ free-function name extraction', () => { 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('names a free function correctly when it has qualified-type params or a trailing return type', async () => { const src = path.join(tempDir, 'src'); fs.mkdirSync(src, { recursive: true }); // TableFileName has a `const std::string&` parameter; BuildName uses an // `auto … -> std::string` trailing return type. Both used to be named // `string` (picked up from the parameter / return type), so callers never // resolved and the defining file looked like nothing depended on it. fs.writeFileSync( path.join(src, 'names.cc'), `#include std::string TableFileName(const std::string& dbname, int number) { return dbname; } auto BuildName(const std::string& a) -> std::string { return a; } ` ); fs.writeFileSync( path.join(src, 'user.cc'), `#include std::string use() { return TableFileName("db", 1) + BuildName("x"); } ` ); cg = CodeGraph.initSync(tempDir); await cg.indexAll(); cg.resolveReferences(); // The functions are extracted under their real names, not `string`. const fns = cg.getNodesByKind('function'); const tableFn = fns.find((n) => n.name === 'TableFileName'); const buildFn = fns.find((n) => n.name === 'BuildName'); expect(tableFn, 'TableFileName extracted (not "string")').toBeDefined(); expect(buildFn, 'BuildName extracted (not "string")').toBeDefined(); // And the cross-file calls resolve to them, so editing names.cc surfaces user.cc. for (const fn of [tableFn!, buildFn!]) { const reached = [...cg.getImpactRadius(fn.id, 3).nodes.values()].map((n) => n.filePath ?? ''); expect(reached.some((p) => p.endsWith('user.cc')), `${fn.name} should be called from user.cc`).toBe(true); } }); }); describe('Dart mixins and type references', () => { 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 `with` mixins and method parameter/return types across files', async () => { const lib = path.join(tempDir, 'lib'); fs.mkdirSync(lib, { recursive: true }); fs.writeFileSync( path.join(lib, 'models.dart'), `class User { final String name; User(this.name); } mixin Loggable { void log() {} } abstract class Repository { User find(int id); } ` ); fs.writeFileSync( path.join(lib, 'service.dart'), `import 'models.dart'; class UserService extends Repository with Loggable { @override User find(int id) => User('x'); List all() => []; } ` ); cg = CodeGraph.initSync(tempDir); await cg.indexAll(); cg.resolveReferences(); const inModels = (name: string) => cg.getNodesByKind('class').concat(cg.getNodesByKind('module')) .find((n) => n.name === name && n.filePath.endsWith('models.dart')); // The `with Loggable` mixin records a dependency — editing the mixin surfaces // the class that mixes it in (across files). Loggable is a `mixin`, indexed // as a class-like node. const loggable = cg.getNodesByKind('class').find((n) => n.name === 'Loggable') ?? cg.getNodesByKind('module').find((n) => n.name === 'Loggable'); expect(loggable, 'Loggable mixin indexed').toBeDefined(); const mixinUsers = [...cg.getImpactRadius(loggable!.id, 3).nodes.values()].map((n) => n.name); expect(mixinUsers).toContain('UserService'); // `User` is used only as a method parameter/return type in service.dart — // editing it must still surface service.dart via the type references. const user = inModels('User') ?? cg.getNodesByKind('class').find((n) => n.name === 'User'); expect(user, 'User indexed').toBeDefined(); const userDeps = [...cg.getImpactRadius(user!.id, 3).nodes.values()].map((n) => n.filePath ?? ''); expect(userDeps.some((p) => p.endsWith('service.dart'))).toBe(true); }); }); describe('Static-member / value-read references', () => { 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 type referenced only via a static field / enum value (and ignores lowercase receivers)', async () => { fs.writeFileSync( path.join(tempDir, 'JsonScope.java'), `class JsonScope { static final int EMPTY_DOCUMENT = 1; } ` ); fs.writeFileSync( path.join(tempDir, 'Reader.java'), `class Reader { private int helper; int peek() { return JsonScope.EMPTY_DOCUMENT; } int noop() { return this.helper; } } ` ); cg = CodeGraph.initSync(tempDir); await cg.indexAll(); cg.resolveReferences(); // JsonScope is used ONLY as `JsonScope.EMPTY_DOCUMENT` (a static-field value // read — never constructed or called), so before the static-member pass it // had no dependents. Editing it now surfaces Reader.java. const scope = cg.getNodesByKind('class').find((n) => n.name === 'JsonScope'); expect(scope, 'JsonScope indexed').toBeDefined(); const reached = [...cg.getImpactRadius(scope!.id, 3).nodes.values()].map((n) => n.filePath ?? ''); expect(reached.some((p) => p.endsWith('Reader.java'))).toBe(true); // A lowercase receiver (`this.helper`) must NOT be emitted as a type ref — // only Capitalized receivers (types) are. No node named `this`/`helper` // should appear as a reference target from peek/noop beyond JsonScope. const refTargets = cg .getNodesByKind('class') .filter((n) => n.name === 'this' || n.name === 'helper'); expect(refTargets.length).toBe(0); }); it('does not link a static-member read across language families (coincidental name)', async () => { // A native (Kotlin) `Build.VERSION` reads the Android system class — it must // NOT link to a coincidentally same-named TS class (the cross-language false // positive that name-matching produces; `references` edges are language-local). fs.writeFileSync( path.join(tempDir, 'Build.ts'), `export class Build {\n static version = 1;\n}\n` ); fs.writeFileSync( path.join(tempDir, 'Device.kt'), `package app\nclass Device {\n fun sdk(): Int = Build.VERSION\n}\n` ); cg = CodeGraph.initSync(tempDir); await cg.indexAll(); cg.resolveReferences(); const tsBuild = cg.getNodesByKind('class').find((n) => n.name === 'Build' && n.filePath.endsWith('Build.ts')); expect(tsBuild).toBeDefined(); // The Kotlin file is `app/Device.kt`; the TS Build must have NO dependent there. const deps = [...cg.getImpactRadius(tsBuild!.id, 2).nodes.values()].map((n) => n.filePath ?? ''); expect(deps.some((p) => p.endsWith('Device.kt'))).toBe(false); }); }); describe('Cross-language type/import gate (RN name collisions)', () => { 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('a TS PascalCase type ref lands on the TS type, never a same-named native class', async () => { // react-native-async-storage's example app has a TS `type TestRunner` AND a // Kotlin `class TestRunner`. The React PascalCase resolver name-matched the // Kotlin `class` (its COMPONENT_KINDS includes `class`) with no language // check at confidence 0.8, outranking the (cross-language-penalized 0.5) // TS name-match — so a TS ref to `TestRunner` crossed web→jvm. The ref here // is intentionally NOT imported: a clean relative import would mask the bug // by resolving via the import map before the framework strategy can win. fs.writeFileSync( path.join(tempDir, 'package.json'), JSON.stringify({ dependencies: { 'react-native': '*' } }) ); fs.writeFileSync( path.join(tempDir, 'useTests.ts'), `export type TestRunner = { run: () => void };\n` ); fs.writeFileSync( path.join(tempDir, 'basic.tsx'), `export function useBasicTest(r: TestRunner): TestRunner {\n return r;\n}\n` ); fs.writeFileSync( path.join(tempDir, 'TestUtils.kt'), `package app\nclass TestRunner {\n fun run() {}\n}\n` ); cg = CodeGraph.initSync(tempDir); await cg.indexAll(); cg.resolveReferences(); const ktRunner = cg .getNodesByKind('class') .find((n) => n.name === 'TestRunner' && n.filePath.endsWith('TestUtils.kt')); expect(ktRunner, 'Kotlin TestRunner class').toBeDefined(); const ktDeps = [...cg.getImpactRadius(ktRunner!.id, 2).nodes.values()].map((n) => n.filePath ?? ''); expect(ktDeps.some((p) => p.endsWith('basic.tsx')), 'Kotlin class has NO TS dependent').toBe(false); const tsRunner = cg.getNodesByKind('type_alias').find((n) => n.name === 'TestRunner'); expect(tsRunner, 'TS TestRunner type_alias').toBeDefined(); const tsDeps = [...cg.getImpactRadius(tsRunner!.id, 2).nodes.values()].map((n) => n.filePath ?? ''); expect(tsDeps.some((p) => p.endsWith('basic.tsx')), 'TS type captured the ref (re-pointed)').toBe(true); }); it('gates a cross-family import name collision but keeps same-family imports', async () => { // A TS `import { Widget }` that only matches a Swift `class Widget` must not // create a web→apple dependency — but a sibling TS module imported by // another TS file (same family) must still resolve (no over-gating). fs.writeFileSync(path.join(tempDir, 'Widget.swift'), `class Widget {\n func render() {}\n}\n`); fs.writeFileSync( path.join(tempDir, 'widget.ts'), `import { Widget } from './native';\nexport function mount(w: Widget) {}\n` ); fs.writeFileSync(path.join(tempDir, 'util.ts'), `export class Helper {}\n`); fs.writeFileSync( path.join(tempDir, 'app.ts'), `import { Helper } from './util';\nexport const h = new Helper();\n` ); cg = CodeGraph.initSync(tempDir); await cg.indexAll(); cg.resolveReferences(); const swiftWidget = cg .getNodesByKind('class') .find((n) => n.name === 'Widget' && n.filePath.endsWith('.swift')); expect(swiftWidget, 'Swift Widget class').toBeDefined(); const wDeps = [...cg.getImpactRadius(swiftWidget!.id, 2).nodes.values()].map((n) => n.filePath ?? ''); expect(wDeps.some((p) => p.endsWith('widget.ts')), 'Swift class has NO TS dependent').toBe(false); // Same-family control — the TS Helper must still see its TS dependent. const helper = cg.getNodesByKind('class').find((n) => n.name === 'Helper'); expect(helper, 'TS Helper class').toBeDefined(); const hDeps = [...cg.getImpactRadius(helper!.id, 2).nodes.values()].map((n) => n.filePath ?? ''); expect(hDeps.some((p) => p.endsWith('app.ts')), 'same-family TS import preserved').toBe(true); }); }); describe('Python absolute module import resolution', () => { 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 bare `import pkg.module` of an internal module to its file', async () => { // `import conduit.apps.signals` (a Django-style side-effect import, and any // dotted absolute module import) had no edge to the module file — only // `from x import y` was linked — so a module imported by its dotted path // looked like nothing depended on it. fs.mkdirSync(path.join(tempDir, 'conduit/apps'), { recursive: true }); fs.writeFileSync(path.join(tempDir, 'conduit/__init__.py'), ''); fs.writeFileSync(path.join(tempDir, 'conduit/apps/__init__.py'), ''); fs.writeFileSync(path.join(tempDir, 'conduit/apps/signals.py'), `def handler():\n pass\n`); fs.writeFileSync( path.join(tempDir, 'conduit/apps/app.py'), `import conduit.apps.signals\nimport os\n\nVALUE = 1\n` ); cg = CodeGraph.initSync(tempDir); await cg.indexAll(); cg.resolveReferences(); const signals = cg.getNodesByKind('file').find((n) => n.filePath.endsWith('conduit/apps/signals.py')); expect(signals, 'signals.py indexed').toBeDefined(); const deps = [...cg.getImpactRadius(signals!.id, 2).nodes.values()].map((n) => n.filePath ?? ''); expect(deps.some((p) => p.endsWith('app.py')), 'importer depends on the module').toBe(true); // `import os` (stdlib) must NOT fabricate an edge — no os.py file in the repo. const osNode = cg.getNodesByKind('file').find((n) => n.filePath.endsWith('/os.py')); 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 // SOURCE (the package), not a coincidental same-basename file elsewhere. fs.mkdirSync(path.join(tempDir, 'app/api/routes'), { recursive: true }); fs.mkdirSync(path.join(tempDir, 'app/api/dependencies'), { recursive: true }); for (const p of ['app/__init__.py', 'app/api/__init__.py', 'app/api/routes/__init__.py', 'app/api/dependencies/__init__.py']) { fs.writeFileSync(path.join(tempDir, p), ''); } fs.writeFileSync(path.join(tempDir, 'app/api/routes/authentication.py'), `def login():\n pass\n`); fs.writeFileSync(path.join(tempDir, 'app/api/dependencies/authentication.py'), `def get_user():\n pass\n`); fs.writeFileSync( path.join(tempDir, 'app/api/routes/api.py'), `from app.api.routes import authentication\n\nROUTER = authentication\n` ); cg = CodeGraph.initSync(tempDir); await cg.indexAll(); cg.resolveReferences(); const routesAuth = cg.getNodesByKind('file').find((n) => n.filePath.endsWith('routes/authentication.py')); const depsAuth = cg.getNodesByKind('file').find((n) => n.filePath.endsWith('dependencies/authentication.py')); expect(routesAuth && depsAuth).toBeTruthy(); const routesDeps = [...cg.getImpactRadius(routesAuth!.id, 2).nodes.values()].map((n) => n.filePath ?? ''); const depsDeps = [...cg.getImpactRadius(depsAuth!.id, 2).nodes.values()].map((n) => n.filePath ?? ''); expect(routesDeps.some((p) => p.endsWith('routes/api.py')), 'submodule under the imported package is the dependent').toBe(true); expect(depsDeps.some((p) => p.endsWith('routes/api.py')), 'same-named module in a sibling package is NOT').toBe(false); }); }); describe('Razor / Blazor markup extraction', () => { 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 @model and Blazor component tags to their C# types; ignores HTML elements', async () => { fs.mkdirSync(path.join(tempDir, 'Views'), { recursive: true }); fs.writeFileSync( path.join(tempDir, 'LoginViewModel.cs'), `namespace App { public class LoginViewModel { public string Email { get; set; } } }` ); fs.writeFileSync( path.join(tempDir, 'ToastComponent.cs'), `namespace App { public class ToastComponent { } }` ); fs.writeFileSync( path.join(tempDir, 'Views/Login.cshtml'), `@model LoginViewModel\n
\n \n
\n` ); fs.writeFileSync( path.join(tempDir, 'Index.razor'), `
\n \n
\n` ); cg = CodeGraph.initSync(tempDir); await cg.indexAll(); cg.resolveReferences(); // `@model LoginViewModel` → the view-model class. const vm = cg.getNodesByKind('class').find((n) => n.name === 'LoginViewModel'); expect(vm, 'LoginViewModel class').toBeDefined(); const vmDeps = [...cg.getImpactRadius(vm!.id, 2).nodes.values()].map((n) => n.filePath ?? ''); expect(vmDeps.some((p) => p.endsWith('Login.cshtml')), '@model links the view').toBe(true); // `` → the component class. const toast = cg.getNodesByKind('class').find((n) => n.name === 'ToastComponent'); expect(toast, 'ToastComponent class').toBeDefined(); const toastDeps = [...cg.getImpactRadius(toast!.id, 2).nodes.values()].map((n) => n.filePath ?? ''); expect(toastDeps.some((p) => p.endsWith('Index.razor')), 'Blazor tag links the component').toBe(true); // HTML elements (`
`, ``) must NOT become component references. const htmlNodes = cg.getNodesByKind('class').filter((n) => n.name === 'div' || n.name === 'input'); expect(htmlNodes.length, 'no node for HTML elements').toBe(0); }); it('C# namespaces qualify type names so same-named types are distinct', async () => { fs.writeFileSync(path.join(tempDir, 'entity.cs'), `namespace App.Entities { public class CatalogBrand { } }`); fs.writeFileSync(path.join(tempDir, 'dto.cs'), `namespace App.Models { public class CatalogBrand { } }`); cg = CodeGraph.initSync(tempDir); await cg.indexAll(); const brands = cg.getNodesByKind('class').filter((n) => n.name === 'CatalogBrand'); expect(brands.length, 'both CatalogBrand classes indexed').toBe(2); const qns = brands.map((b) => b.qualifiedName).sort(); expect(qns[0]).not.toBe(qns[1]); // distinct qualified names (namespace-scoped) expect(qns.some((q) => q.includes('Entities') && q.endsWith('CatalogBrand'))).toBe(true); expect(qns.some((q) => q.includes('Models') && q.endsWith('CatalogBrand'))).toBe(true); }); it('disambiguates a Razor type ref via @using (incl. folder _Imports.razor)', async () => { // `CatalogBrand` exists as both a domain entity and a DTO; the component // `@using`s the DTO's namespace (here via the folder _Imports.razor), so the // ref must resolve to the DTO, not the same-named entity. fs.mkdirSync(path.join(tempDir, 'Models'), { recursive: true }); fs.mkdirSync(path.join(tempDir, 'Entities'), { recursive: true }); fs.mkdirSync(path.join(tempDir, 'Pages'), { recursive: true }); fs.writeFileSync(path.join(tempDir, 'Models/CatalogBrand.cs'), `namespace App.Models { public class CatalogBrand { public int Id { get; set; } } }`); fs.writeFileSync(path.join(tempDir, 'Entities/CatalogBrand.cs'), `namespace App.Entities { public class CatalogBrand { public int Id { get; set; } } }`); fs.writeFileSync(path.join(tempDir, 'Pages/_Imports.razor'), `@using App.Models\n`); fs.writeFileSync( path.join(tempDir, 'Pages/List.razor'), `

List

\n@code {\n private CatalogBrand _b = new CatalogBrand();\n}\n` ); cg = CodeGraph.initSync(tempDir); await cg.indexAll(); cg.resolveReferences(); const dto = cg.getNodesByKind('class').find((n) => n.qualifiedName === 'App.Models::CatalogBrand'); const entity = cg.getNodesByKind('class').find((n) => n.qualifiedName === 'App.Entities::CatalogBrand'); expect(dto && entity, 'both CatalogBrand classes').toBeTruthy(); const dtoDeps = [...cg.getImpactRadius(dto!.id, 2).nodes.values()].map((n) => n.filePath ?? ''); const entityDeps = [...cg.getImpactRadius(entity!.id, 2).nodes.values()].map((n) => n.filePath ?? ''); expect(dtoDeps.some((p) => p.endsWith('List.razor')), 'resolves to the @using\'d DTO').toBe(true); expect(entityDeps.some((p) => p.endsWith('List.razor')), 'NOT the same-named entity').toBe(false); }); it('delegates Blazor @code block C# to cover types used in component logic', async () => { fs.writeFileSync( path.join(tempDir, 'CatalogService.cs'), `namespace App { public class CatalogService { public void Load() { } } }` ); fs.writeFileSync( path.join(tempDir, 'List.razor'), `

Catalog

\n\n@code {\n private CatalogService _svc = new CatalogService();\n void Refresh() { _svc.Load(); }\n}\n` ); cg = CodeGraph.initSync(tempDir); await cg.indexAll(); cg.resolveReferences(); const svc = cg.getNodesByKind('class').find((n) => n.name === 'CatalogService'); expect(svc, 'CatalogService class').toBeDefined(); const deps = [...cg.getImpactRadius(svc!.id, 2).nodes.values()].map((n) => n.filePath ?? ''); expect(deps.some((p) => p.endsWith('List.razor')), '@code usage links the component to the service').toBe(true); }); }); 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)', () => { 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('resolves a chained extension-method call (a.b.Method()) to its definition', async () => { // ASP.NET DI registration: `builder.Services.AddCoreServices(...)` calls a // static extension method elsewhere. A multi-dot receiver chain matched no // method-call pattern before, so the extension method had no caller. fs.mkdirSync(path.join(tempDir, 'cfg'), { recursive: true }); fs.writeFileSync( path.join(tempDir, 'cfg/Ext.cs'), `namespace App {\n public static class Ext {\n public static object AddCoreServices(this object services, int x) { return services; }\n }\n}\n` ); fs.writeFileSync( path.join(tempDir, 'Program.cs'), `namespace App {\n public class Program {\n public void Run(object builder) {\n builder.Services.AddCoreServices(1);\n }\n }\n}\n` ); cg = CodeGraph.initSync(tempDir); await cg.indexAll(); cg.resolveReferences(); const ext = cg .getNodesByKind('method') .find((n) => n.name === 'AddCoreServices') ?? cg.getNodesByKind('function').find((n) => n.name === 'AddCoreServices'); expect(ext, 'AddCoreServices defined').toBeDefined(); const callers = [...cg.getImpactRadius(ext!.id, 2).nodes.values()].map((n) => n.filePath ?? ''); expect(callers.some((p) => p.endsWith('Program.cs')), 'chained extension call resolves to its definition').toBe(true); }); }); describe('Same-directory include + KMP import resolution', () => { 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('a C/C++ #include resolves to the same-directory header, not a same-named one elsewhere', async () => { // A multi-platform native module has a header of the same basename per // platform. `windows/Provider.cpp`'s `#include "Storage.h"` means its OWN // sibling header — not `apple/Storage.h` (which sorts first and so was // picked arbitrarily before, leaving the real local header with 0 deps). fs.mkdirSync(path.join(tempDir, 'apple'), { recursive: true }); fs.mkdirSync(path.join(tempDir, 'windows'), { recursive: true }); fs.writeFileSync(path.join(tempDir, 'apple', 'Storage.h'), `#pragma once\nstruct Storage { int n; };\n`); fs.writeFileSync(path.join(tempDir, 'windows', 'Storage.h'), `#pragma once\nstruct Storage { int n; };\n`); fs.writeFileSync( path.join(tempDir, 'windows', 'Provider.cpp'), `#include "Storage.h"\nint use() { Storage s; return s.n; }\n` ); cg = CodeGraph.initSync(tempDir); await cg.indexAll(); cg.resolveReferences(); const winHeader = cg.getNodesByKind('file').find((n) => n.filePath.endsWith('windows/Storage.h')); const appleHeader = cg.getNodesByKind('file').find((n) => n.filePath.endsWith('apple/Storage.h')); expect(winHeader, 'windows/Storage.h indexed').toBeDefined(); expect(appleHeader, 'apple/Storage.h indexed').toBeDefined(); const winDeps = [...cg.getImpactRadius(winHeader!.id, 2).nodes.values()].map((n) => n.filePath ?? ''); const appleDeps = [...cg.getImpactRadius(appleHeader!.id, 2).nodes.values()].map((n) => n.filePath ?? ''); expect(winDeps.some((p) => p.endsWith('Provider.cpp')), 'same-dir header gets the includer').toBe(true); expect(appleDeps.some((p) => p.endsWith('Provider.cpp')), 'other-platform header does NOT').toBe(false); }); it('a Kotlin Multiplatform commonMain import resolves to the expect, not a platform actual', async () => { const common = path.join(tempDir, 'src/commonMain/kotlin/app'); const android = path.join(tempDir, 'src/androidMain/kotlin/app'); fs.mkdirSync(common, { recursive: true }); fs.mkdirSync(android, { recursive: true }); fs.writeFileSync(path.join(common, 'Platform.kt'), `package app\nexpect class PlatformContext\n`); fs.writeFileSync(path.join(android, 'Platform.android.kt'), `package app\nactual class PlatformContext\n`); fs.writeFileSync( path.join(common, 'Db.kt'), `package app\nimport app.PlatformContext\nclass Db {\n fun open(ctx: PlatformContext) {}\n}\n` ); cg = CodeGraph.initSync(tempDir); await cg.indexAll(); cg.resolveReferences(); const expectCtx = cg .getNodesByKind('class') .find((n) => n.name === 'PlatformContext' && n.filePath.endsWith('commonMain/kotlin/app/Platform.kt')); expect(expectCtx, 'commonMain expect PlatformContext').toBeDefined(); const deps = [...cg.getImpactRadius(expectCtx!.id, 2).nodes.values()].map((n) => n.filePath ?? ''); expect(deps.some((p) => p.endsWith('Db.kt')), 'commonMain import lands on the expect, not the actual').toBe(true); }); }); describe('Delphi form code-behind pairing', () => { 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 `.dfm` form to its sibling `.pas` code-behind unit', async () => { // A Delphi form unit owns its visual form definition via `{$R *.dfm}`, not a // `uses` clause — so a `.dfm` used only as a form definition looked orphaned. fs.writeFileSync(path.join(tempDir, 'UFRMAbout.dfm'), `object FRMAbout: TFRMAbout\n Caption = 'About'\nend\n`); fs.writeFileSync(path.join(tempDir, 'UFRMAbout.pas'), `unit UFRMAbout;\ninterface\nuses Forms;\ntype\n TFRMAbout = class(TForm)\n end;\nimplementation\n{$R *.dfm}\nend.\n`); cg = CodeGraph.initSync(tempDir); await cg.indexAll(); cg.resolveReferences(); const dfm = cg.getNodesByKind('file').find((n) => n.filePath.endsWith('UFRMAbout.dfm')); expect(dfm, 'UFRMAbout.dfm file node').toBeDefined(); const deps = cg.getFileDependents(dfm!.filePath); expect(deps.some((p) => p.endsWith('UFRMAbout.pas')), 'the .pas unit links its .dfm form').toBe(true); }); }); describe('Liquid Shopify JSON template section resolution', () => { 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 Shopify JSON template section `type` to its sections/.liquid', async () => { // Shopify OS 2.0 templates are JSON, referencing sections by `type` — not // a `{% section %}` Liquid tag — so a section used only from a JSON template // looked unused. The JSON is now indexed and its `type`s linked. fs.mkdirSync(path.join(tempDir, 'sections'), { recursive: true }); fs.mkdirSync(path.join(tempDir, 'templates/customers'), { recursive: true }); fs.writeFileSync(path.join(tempDir, 'sections/main-product.liquid'), `
{{ product.title }}
\n`); fs.writeFileSync(path.join(tempDir, 'sections/main-login.liquid'), `
{{ 'customer.login' | t }}
\n`); fs.writeFileSync(path.join(tempDir, 'templates/product.json'), JSON.stringify({ sections: { main: { type: 'main-product' } }, order: ['main'] })); // Nested template dir (templates/customers/login.json) must resolve too. fs.writeFileSync(path.join(tempDir, 'templates/customers/login.json'), JSON.stringify({ sections: { main: { type: 'main-login' } }, order: ['main'] })); cg = CodeGraph.initSync(tempDir); await cg.indexAll(); cg.resolveReferences(); const product = cg.getNodesByKind('file').find((n) => n.filePath.endsWith('sections/main-product.liquid')); const login = cg.getNodesByKind('file').find((n) => n.filePath.endsWith('sections/main-login.liquid')); expect(product, 'main-product section').toBeDefined(); expect(login, 'main-login section').toBeDefined(); expect(cg.getFileDependents(product!.filePath).some((p) => p.endsWith('templates/product.json')), 'top-level JSON template links its section').toBe(true); expect(cg.getFileDependents(login!.filePath).some((p) => p.endsWith('customers/login.json')), 'nested JSON template links its section').toBe(true); }); }); describe('Lua/Luau require resolution', () => { 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('resolves a dotted Lua require and an instance-path Luau require to their module files', async () => { // The require is the ONLY link (no method call), so coverage here proves the // require resolver specifically, not method-call name-matching. // Lua dotted module path: require("myapp.config") → lua/myapp/config.lua. fs.mkdirSync(path.join(tempDir, 'lua/myapp'), { recursive: true }); fs.writeFileSync(path.join(tempDir, 'lua/myapp/config.lua'), `local M = {}\nfunction M.setup() end\nreturn M\n`); fs.writeFileSync(path.join(tempDir, 'lua/myapp/init.lua'), `local config = require("myapp.config")\nreturn config\n`); // Luau Roblox instance-path require (only the leaf survives extraction): // require(script.Util.helper) → src/Util/helper.luau. fs.mkdirSync(path.join(tempDir, 'src/Util'), { recursive: true }); fs.writeFileSync(path.join(tempDir, 'src/Util/helper.luau'), `local H = {}\nfunction H.go() end\nreturn H\n`); fs.writeFileSync(path.join(tempDir, 'src/init.luau'), `local helper = require(script.Util.helper)\nreturn helper\n`); cg = CodeGraph.initSync(tempDir); await cg.indexAll(); cg.resolveReferences(); const config = cg.getNodesByKind('file').find((n) => n.filePath.endsWith('myapp/config.lua')); const helper = cg.getNodesByKind('file').find((n) => n.filePath.endsWith('Util/helper.luau')); expect(config, 'config.lua file node').toBeDefined(); expect(helper, 'helper.luau file node').toBeDefined(); const cfgDeps = cg.getFileDependents(config!.filePath); const helpDeps = cg.getFileDependents(helper!.filePath); expect(cfgDeps.some((p) => p.endsWith('myapp/init.lua')), 'dotted Lua require resolves to the module').toBe(true); expect(helpDeps.some((p) => p.endsWith('src/init.luau')), 'instance-path Luau require resolves to the module').toBe(true); }); }); describe('Rust module-path call resolution', () => { 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('a bare submodule call (`users::router()`) resolves self-relative to the submodule fn', async () => { // The canonical Axum router-assembly pattern: a parent module calls each // submodule's `router()`. `users::` / `profiles::` are SELF-relative // submodule prefixes (2018 edition) — `mod users;` makes `users` a child of // the CURRENT module, NOT `crate::users`. Before the fix the bare prefix was // resolved crate-relative only (looking for `src/users.rs`), so it found // nothing and the handler modules looked dependent-less. const http = path.join(tempDir, 'src/http'); fs.mkdirSync(http, { recursive: true }); fs.writeFileSync(path.join(tempDir, 'src/lib.rs'), `pub mod http;\n`); fs.writeFileSync( path.join(http, 'mod.rs'), `mod users;\nmod profiles;\npub fn api_router() {\n users::router();\n profiles::router();\n}\n` ); fs.writeFileSync(path.join(http, 'users.rs'), `pub fn router() -> i32 { 1 }\n`); fs.writeFileSync(path.join(http, 'profiles.rs'), `pub fn router() -> i32 { 2 }\n`); cg = CodeGraph.initSync(tempDir); await cg.indexAll(); cg.resolveReferences(); // Each submodule's same-named `router` fn must get mod.rs as a dependent — // proving the bare prefix resolved self-relative AND disambiguated the // colliding `router` name to the correct file (not an arbitrary one). const routers = cg.getNodesByKind('function').filter((n) => n.name === 'router'); const usersRouter = routers.find((n) => n.filePath.endsWith('http/users.rs')); const profilesRouter = routers.find((n) => n.filePath.endsWith('http/profiles.rs')); expect(usersRouter, 'users.rs router fn').toBeDefined(); expect(profilesRouter, 'profiles.rs router fn').toBeDefined(); const usersDeps = [...cg.getImpactRadius(usersRouter!.id, 2).nodes.values()].map((n) => n.filePath ?? ''); const profilesDeps = [...cg.getImpactRadius(profilesRouter!.id, 2).nodes.values()].map((n) => n.filePath ?? ''); expect(usersDeps.some((p) => p.endsWith('http/mod.rs')), 'users::router() lands on users.rs').toBe(true); expect(profilesDeps.some((p) => p.endsWith('http/mod.rs')), 'profiles::router() lands on profiles.rs').toBe(true); }); it('a 3-segment module-path call (`database::profiles::find()`) resolves to the leaf fn', async () => { // A 2-level module path — the common `db.run(move |c| database::profiles::find(c))` // / `crate::a::b::func()` shape. The reference-resolver pre-filter used to drop any // `a::b::c` whose leaf it never checked (it tested only the first segment and the // `b::c` remainder, neither of which names a symbol), so the call never reached the // Rust path resolver and the leaf module looked dependent-less. const routes = path.join(tempDir, 'src/routes'); const database = path.join(tempDir, 'src/database'); fs.mkdirSync(routes, { recursive: true }); fs.mkdirSync(database, { recursive: true }); fs.writeFileSync(path.join(tempDir, 'src/lib.rs'), `pub mod routes;\npub mod database;\n`); fs.writeFileSync(path.join(database, 'mod.rs'), `pub mod profiles;\n`); fs.writeFileSync(path.join(database, 'profiles.rs'), `pub fn find(id: i32) -> i32 { id }\n`); fs.writeFileSync( path.join(routes, 'mod.rs'), `use crate::database;\npub fn get_profile(id: i32) -> i32 {\n database::profiles::find(id)\n}\n` ); cg = CodeGraph.initSync(tempDir); await cg.indexAll(); cg.resolveReferences(); const find = cg .getNodesByKind('function') .find((n) => n.name === 'find' && n.filePath.endsWith('database/profiles.rs')); expect(find, 'database/profiles.rs find fn').toBeDefined(); const deps = [...cg.getImpactRadius(find!.id, 2).nodes.values()].map((n) => n.filePath ?? ''); expect(deps.some((p) => p.endsWith('routes/mod.rs')), 'database::profiles::find() resolves to the leaf fn').toBe(true); }); it('Rocket `routes![…]` / `catchers![…]` macros link the mount to the handler fns', async () => { // Tree-sitter leaves the macro body as a raw token tree, so the handler // paths inside `routes![a::b::handler, …]` are invisible to the call walker // and the handlers — mounted by Rocket at runtime, not called in-repo — look // like they have no caller. The route-macro extractor reconstructs each path // and emits a reference, which the Rust path resolver links to the handler. const routes = path.join(tempDir, 'src/routes'); fs.mkdirSync(routes, { recursive: true }); fs.writeFileSync(path.join(tempDir, 'src/lib.rs'), `mod routes;\nfn not_found() {}\npub fn rocket() {\n` + ` rocket::build()\n` + ` .mount("/api", routes![routes::users::post_users, routes::users::get_user])\n` + ` .register("/", catchers![not_found]);\n}\n`); fs.writeFileSync(path.join(routes, 'mod.rs'), `pub mod users;\n`); fs.writeFileSync(path.join(routes, 'users.rs'), `pub fn post_users() {}\npub fn get_user() {}\n`); cg = CodeGraph.initSync(tempDir); await cg.indexAll(); cg.resolveReferences(); const handlers = cg.getNodesByKind('function').filter((n) => n.filePath.endsWith('routes/users.rs')); expect(handlers.length, 'both handler fns indexed').toBe(2); for (const h of handlers) { const deps = [...cg.getImpactRadius(h.id, 2).nodes.values()].map((n) => n.filePath ?? ''); expect(deps.some((p) => p.endsWith('lib.rs')), `routes![] links ${h.name} to its mount in lib.rs`).toBe(true); } }); }); describe('SvelteKit load → page synthesizer', () => { 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 +page.svelte to its OWN directory\'s +page.server.js load, not another route\'s', async () => { // SvelteKit wires +page.server.js's `load` to +page.svelte's `data` BY FILE // PATH — there is no static import — so editing a loader showed no impact on // the page it feeds. The synthesizer links each page component to the `load` // in its OWN directory (path-deterministic, so it never crosses routes). const login = path.join(tempDir, 'src/routes/login'); const register = path.join(tempDir, 'src/routes/register'); fs.mkdirSync(login, { recursive: true }); fs.mkdirSync(register, { recursive: true }); fs.writeFileSync(path.join(login, '+page.svelte'), `\n

Login {data.x}

\n`); fs.writeFileSync(path.join(login, '+page.server.js'), `export function load() { return { x: 1 }; }\n`); fs.writeFileSync(path.join(register, '+page.svelte'), `\n

Register

\n`); fs.writeFileSync(path.join(register, '+page.server.js'), `export function load() { return { y: 2 }; }\n`); cg = CodeGraph.initSync(tempDir); await cg.indexAll(); cg.resolveReferences(); const loginLoad = cg .getNodesByKind('function') .find((n) => n.name === 'load' && n.filePath.endsWith('login/+page.server.js')); expect(loginLoad, 'login load fn').toBeDefined(); const impacted = [...cg.getImpactRadius(loginLoad!.id, 3).nodes.values()].map((n) => n.filePath ?? ''); // editing login's load surfaces login's page (the framework-wired data flow)… expect(impacted.some((p) => p.endsWith('login/+page.svelte')), 'load links to its own page').toBe(true); // …but never register's page (same-directory only). expect(impacted.some((p) => p.endsWith('register/+page.svelte')), 'does not cross routes').toBe(false); }); }); describe('Nuxt nested auto-imported component resolution', () => { 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 `` usage to components/media/Card.vue (Nuxt dir-prefixed auto-import)', async () => { // Nuxt auto-imports a nested component by a DIRECTORY-PREFIXED name — // components/media/Card.vue is used as , not — but the // component node is named by basename (`Card`), so the PascalCase usage // didn't resolve and the nested component looked unused. const media = path.join(tempDir, 'components/media'); fs.mkdirSync(media, { recursive: true }); fs.writeFileSync(path.join(media, 'Card.vue'), `\n\n`); fs.writeFileSync( path.join(tempDir, 'components/Grid.vue'), `\n\n` ); cg = CodeGraph.initSync(tempDir); await cg.indexAll(); cg.resolveReferences(); const card = cg.getNodesByKind('component').find((n) => n.filePath.endsWith('media/Card.vue')); expect(card, 'media/Card.vue component').toBeDefined(); const deps = [...cg.getImpactRadius(card!.id, 2).nodes.values()].map((n) => n.filePath ?? ''); expect(deps.some((p) => p.endsWith('components/Grid.vue')), ' links Grid to media/Card.vue').toBe(true); }); }); describe('Swift property-wrapper attribute type references', () => { 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('a Fluent `@Siblings(through: Pivot.self)` links the model to the pivot type', async () => { // A many-to-many pivot/join model is referenced ONLY through the relationship // property wrapper's metatype argument (`Pivot.self`), never by a controller // query. The wrapper type was captured but the argument expression wasn't // walked, so the pivot model looked like nothing depended on it. fs.writeFileSync(path.join(tempDir, 'Pivot.swift'), `import Fluent\nfinal class AcronymCategoryPivot: Model {\n static let schema = "acronym-category"\n}\n`); fs.writeFileSync(path.join(tempDir, 'Acronym.swift'), `import Fluent\nfinal class Acronym: Model {\n` + ` @Siblings(through: AcronymCategoryPivot.self, from: \\.$acronym, to: \\.$category)\n` + ` var categories: [Category]\n}\n`); cg = CodeGraph.initSync(tempDir); await cg.indexAll(); cg.resolveReferences(); const pivot = cg.getNodesByKind('class').find((n) => n.name === 'AcronymCategoryPivot'); expect(pivot, 'pivot model class').toBeDefined(); const deps = [...cg.getImpactRadius(pivot!.id, 2).nodes.values()].map((n) => n.filePath ?? ''); expect(deps.some((p) => p.endsWith('Acronym.swift')), '@Siblings metatype arg links Acronym to the pivot').toBe(true); }); }); describe('Objective-C messages, class receivers, and #import', () => { 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('resolves single-arg selectors, class-message receivers, and #import headers', async () => { fs.writeFileSync( path.join(tempDir, 'SDImageCache.h'), `#import @interface SDImageCache : NSObject + (instancetype)sharedCache; + (void)storeImage:(NSString *)key; @end ` ); fs.writeFileSync( path.join(tempDir, 'SDImageCache.m'), `#import "SDImageCache.h" @implementation SDImageCache + (instancetype)sharedCache { return nil; } + (void)storeImage:(NSString *)key { } @end ` ); fs.writeFileSync( path.join(tempDir, 'SDManager.m'), `#import "SDImageCache.h" @interface SDManager : NSObject @end @implementation SDManager - (void)run { [SDImageCache sharedCache]; [SDImageCache storeImage:@"k"]; } @end ` ); cg = CodeGraph.initSync(tempDir); await cg.indexAll(); cg.resolveReferences(); // 1. The single-argument selector `[SDImageCache storeImage:@"k"]` resolves // to the `storeImage:` method — named WITH its colon both at the call site // and the definition (before the fix the call site dropped the colon). const storeImage = cg.getNodesByKind('method').find((n) => n.name === 'storeImage:'); expect(storeImage, 'storeImage: method').toBeDefined(); const storeCallers = [...cg.getImpactRadius(storeImage!.id, 2).nodes.values()].map((n) => n.filePath ?? ''); expect(storeCallers.some((p) => p.endsWith('SDManager.m'))).toBe(true); // 2. The class-message receiver `[SDImageCache sharedCache]` references the // SDImageCache class (whose @interface lives in the header). const cache = cg.getNodesByKind('class').find((n) => n.name === 'SDImageCache'); expect(cache, 'SDImageCache class').toBeDefined(); const classDeps = [...cg.getImpactRadius(cache!.id, 2).nodes.values()].map((n) => n.filePath ?? ''); expect(classDeps.some((p) => p.endsWith('SDManager.m'))).toBe(true); // 3. `#import "SDImageCache.h"` resolves to the header FILE — editing it // surfaces both importers. const header = cg.getNodesByKind('file').find((n) => n.filePath.endsWith('SDImageCache.h')); expect(header, 'SDImageCache.h indexed').toBeDefined(); const importers = [...cg.getImpactRadius(header!.id, 2).nodes.values()].map((n) => n.filePath ?? ''); expect(importers.some((p) => p.endsWith('SDManager.m'))).toBe(true); }); }); describe('Full Indexing', () => { let tempDir: string; beforeEach(() => { tempDir = createTempDir(); }); afterEach(() => { cleanupTempDir(tempDir); }); it('should index a TypeScript file', async () => { // Create test file const srcDir = path.join(tempDir, 'src'); fs.mkdirSync(srcDir); fs.writeFileSync( path.join(srcDir, 'utils.ts'), ` export function add(a: number, b: number): number { return a + b; } export function multiply(a: number, b: number): number { return a * b; } ` ); // Initialize and index const cg = CodeGraph.initSync(tempDir); const result = await cg.indexAll(); expect(result.success).toBe(true); expect(result.filesIndexed).toBe(1); expect(result.nodesCreated).toBeGreaterThanOrEqual(2); // Check nodes were stored const nodes = cg.getNodesInFile('src/utils.ts'); expect(nodes.length).toBeGreaterThanOrEqual(2); const addFunc = nodes.find((n) => n.name === 'add'); expect(addFunc).toBeDefined(); expect(addFunc?.kind).toBe('function'); cg.close(); }); it('should index multiple files', async () => { // Create test files const srcDir = path.join(tempDir, 'src'); fs.mkdirSync(srcDir); fs.writeFileSync( path.join(srcDir, 'math.ts'), `export function add(a: number, b: number) { return a + b; }` ); fs.writeFileSync( path.join(srcDir, 'string.ts'), `export function capitalize(s: string) { return s.toUpperCase(); }` ); // Initialize and index const cg = CodeGraph.initSync(tempDir); const result = await cg.indexAll(); expect(result.success).toBe(true); expect(result.filesIndexed).toBe(2); const files = cg.getFiles(); expect(files.length).toBe(2); cg.close(); }); it('should track file hashes for incremental updates', async () => { // Create initial file const srcDir = path.join(tempDir, 'src'); fs.mkdirSync(srcDir); fs.writeFileSync(path.join(srcDir, 'main.ts'), `export const x = 1;`); // Initialize and index const cg = CodeGraph.initSync(tempDir); await cg.indexAll(); // Check file is tracked const file = cg.getFile('src/main.ts'); expect(file).toBeDefined(); expect(file?.contentHash).toBeDefined(); // Modify file fs.writeFileSync(path.join(srcDir, 'main.ts'), `export const x = 2;`); // Check for changes const changes = cg.getChangedFiles(); expect(changes.modified).toContain('src/main.ts'); cg.close(); }); it('should sync and detect changes', async () => { // Create initial file const srcDir = path.join(tempDir, 'src'); fs.mkdirSync(srcDir); fs.writeFileSync( path.join(srcDir, 'main.ts'), `export function original() { return 1; }` ); // Initialize and index const cg = CodeGraph.initSync(tempDir); await cg.indexAll(); const initialNodes = cg.getNodesInFile('src/main.ts'); expect(initialNodes.some((n) => n.name === 'original')).toBe(true); // Modify file fs.writeFileSync( path.join(srcDir, 'main.ts'), `export function updated() { return 2; }` ); // Sync const syncResult = await cg.sync(); expect(syncResult.filesModified).toBe(1); // Check nodes were updated const updatedNodes = cg.getNodesInFile('src/main.ts'); expect(updatedNodes.some((n) => n.name === 'updated')).toBe(true); expect(updatedNodes.some((n) => n.name === 'original')).toBe(false); cg.close(); }); it('should count file-level tracked YAML files as indexed', async () => { fs.writeFileSync(path.join(tempDir, 'app.yaml'), 'name: test\n'); fs.writeFileSync(path.join(tempDir, 'routes.yml'), 'route: value\n'); const cg = CodeGraph.initSync(tempDir); const result = await cg.indexAll(); expect(result.success).toBe(true); expect(result.filesIndexed).toBe(2); expect(result.filesSkipped).toBe(0); expect(cg.getFiles().map((f) => f.path).sort()).toEqual(['app.yaml', 'routes.yml']); cg.close(); }); it('should count file-level tracked YAML/Twig files as indexed in indexFiles()', async () => { fs.writeFileSync(path.join(tempDir, 'app.yaml'), 'name: test\n'); fs.writeFileSync(path.join(tempDir, 'view.twig'), '{{ title }}\n'); const cg = CodeGraph.initSync(tempDir); const result = await cg.indexFiles(['app.yaml', 'view.twig']); expect(result.success).toBe(true); expect(result.filesIndexed).toBe(2); expect(result.filesSkipped).toBe(0); const tracked = cg.getFiles().map((f) => `${f.path}:${f.language}`).sort(); expect(tracked).toEqual(['app.yaml:yaml', 'view.twig:twig']); cg.close(); }); it('should count file-level tracked .properties files as indexed', async () => { fs.writeFileSync(path.join(tempDir, 'application.properties'), 'server.port=8080\n'); fs.writeFileSync(path.join(tempDir, 'log.properties'), 'log.level=INFO\n'); const cg = CodeGraph.initSync(tempDir); const result = await cg.indexAll(); expect(result.success).toBe(true); expect(result.filesIndexed).toBe(2); expect(result.filesSkipped).toBe(0); cg.close(); }); it('should count the full file-level tracked class (yaml/twig/properties) in indexFiles()', async () => { fs.writeFileSync(path.join(tempDir, 'app.yaml'), 'name: test\n'); fs.writeFileSync(path.join(tempDir, 'view.twig'), '{{ title }}\n'); fs.writeFileSync(path.join(tempDir, 'application.properties'), 'server.port=8080\n'); const cg = CodeGraph.initSync(tempDir); const result = await cg.indexFiles(['app.yaml', 'view.twig', 'application.properties']); expect(result.success).toBe(true); expect(result.filesIndexed).toBe(3); expect(result.filesSkipped).toBe(0); const tracked = cg.getFiles().map((f) => `${f.path}:${f.language}`).sort(); expect(tracked).toEqual(['app.yaml:yaml', 'application.properties:properties', 'view.twig:twig']); cg.close(); }); }); describe('Path Normalization', () => { it('should convert backslashes to forward slashes', () => { expect(normalizePath('gui\\node_modules\\foo')).toBe('gui/node_modules/foo'); expect(normalizePath('src\\components\\Button.tsx')).toBe('src/components/Button.tsx'); }); it('should leave forward-slash paths unchanged', () => { expect(normalizePath('src/components/Button.tsx')).toBe('src/components/Button.tsx'); }); it('should handle empty string', () => { expect(normalizePath('')).toBe(''); }); }); describe('Directory Exclusion', () => { let tempDir: string; beforeEach(() => { tempDir = createTempDir(); }); afterEach(() => { cleanupTempDir(tempDir); }); it('should exclude directories listed in .gitignore', () => { // Create structure: src/index.ts + node_modules/pkg/index.js, gitignore node_modules const srcDir = path.join(tempDir, 'src'); const nmDir = path.join(tempDir, 'node_modules', 'pkg'); fs.mkdirSync(srcDir, { recursive: true }); fs.mkdirSync(nmDir, { recursive: true }); fs.writeFileSync(path.join(srcDir, 'index.ts'), 'export const x = 1;'); fs.writeFileSync(path.join(nmDir, 'index.js'), 'module.exports = {};'); fs.writeFileSync(path.join(tempDir, '.gitignore'), 'node_modules/\n'); const files = scanDirectory(tempDir); expect(files).toContain('src/index.ts'); expect(files.every((f) => !f.includes('node_modules'))).toBe(true); }); it('should exclude nested node_modules via a root .gitignore', () => { // A trailing-slash pattern with no leading slash matches at any depth. const srcDir = path.join(tempDir, 'packages', 'app', 'src'); const nmDir = path.join(tempDir, 'packages', 'app', 'node_modules', 'pkg'); fs.mkdirSync(srcDir, { recursive: true }); fs.mkdirSync(nmDir, { recursive: true }); fs.writeFileSync(path.join(srcDir, 'index.ts'), 'export const x = 1;'); fs.writeFileSync(path.join(nmDir, 'index.js'), 'module.exports = {};'); fs.writeFileSync(path.join(tempDir, '.gitignore'), 'node_modules/\n'); const files = scanDirectory(tempDir); expect(files).toContain('packages/app/src/index.ts'); expect(files.every((f) => !f.includes('node_modules'))).toBe(true); }); it('should apply a nested .gitignore only to its own subtree', () => { const appSrc = path.join(tempDir, 'app', 'src'); fs.mkdirSync(appSrc, { recursive: true }); fs.writeFileSync(path.join(appSrc, 'keep.ts'), 'export const a = 1;'); fs.writeFileSync(path.join(appSrc, 'skip.ts'), 'export const b = 2;'); fs.writeFileSync(path.join(tempDir, 'app', '.gitignore'), 'src/skip.ts\n'); // A sibling with the same name outside app/ must NOT be ignored. const otherDir = path.join(tempDir, 'other', 'src'); fs.mkdirSync(otherDir, { recursive: true }); fs.writeFileSync(path.join(otherDir, 'skip.ts'), 'export const c = 3;'); const files = scanDirectory(tempDir); expect(files).toContain('app/src/keep.ts'); expect(files).not.toContain('app/src/skip.ts'); expect(files).toContain('other/src/skip.ts'); }); it('should always skip .git directories', () => { const srcDir = path.join(tempDir, 'src'); const gitDir = path.join(tempDir, '.git', 'objects'); fs.mkdirSync(srcDir, { recursive: true }); fs.mkdirSync(gitDir, { recursive: true }); fs.writeFileSync(path.join(srcDir, 'index.ts'), 'export const x = 1;'); fs.writeFileSync(path.join(gitDir, 'pack.ts'), 'export const y = 2;'); const files = scanDirectory(tempDir); expect(files).toContain('src/index.ts'); expect(files.every((f) => !f.includes('.git'))).toBe(true); }); it('should return forward-slash paths on all platforms', () => { const srcDir = path.join(tempDir, 'src', 'components'); fs.mkdirSync(srcDir, { recursive: true }); fs.writeFileSync(path.join(srcDir, 'Button.tsx'), 'export function Button() {}'); const files = scanDirectory(tempDir); expect(files.length).toBe(1); expect(files[0]).toBe('src/components/Button.tsx'); expect(files[0]).not.toContain('\\'); }); }); describe('Git Submodules', () => { let tempDir: string; beforeEach(() => { tempDir = createTempDir(); }); afterEach(() => { cleanupTempDir(tempDir); }); it('should index files inside git submodules (issue #147)', async () => { const { execFileSync } = await import('child_process'); const git = (cwd: string, ...args: string[]) => execFileSync('git', args, { cwd, stdio: 'pipe' }); // Build a separate "library" repo to use as a submodule source. const libDir = path.join(tempDir, '_lib'); fs.mkdirSync(libDir, { recursive: true }); git(libDir, 'init', '-q'); git(libDir, 'config', 'user.email', 'test@test.com'); git(libDir, 'config', 'user.name', 'Test'); fs.writeFileSync(path.join(libDir, 'lib.ts'), 'export const fromSubmodule = 1;'); git(libDir, 'add', '-A'); git(libDir, 'commit', '-q', '-m', 'lib init'); // Build the main repo and add the lib repo as a submodule. const mainDir = path.join(tempDir, 'main'); fs.mkdirSync(mainDir, { recursive: true }); git(mainDir, 'init', '-q'); git(mainDir, 'config', 'user.email', 'test@test.com'); git(mainDir, 'config', 'user.name', 'Test'); fs.writeFileSync(path.join(mainDir, 'app.ts'), 'export const app = 1;'); git(mainDir, 'add', '-A'); git(mainDir, 'commit', '-q', '-m', 'app init'); // protocol.file.allow=always is required to add a local-path submodule on // recent git versions (CVE-2022-39253 mitigation). execFileSync( 'git', ['-c', 'protocol.file.allow=always', 'submodule', 'add', '-q', libDir, 'libs/lib'], { cwd: mainDir, stdio: 'pipe' } ); git(mainDir, 'commit', '-q', '-m', 'add submodule'); const files = scanDirectory(mainDir); expect(files).toContain('app.ts'); expect(files).toContain('libs/lib/lib.ts'); }); }); describe('Nested gitlink repos (#1031, #1033)', () => { let tempDir: string; // Helper: make a self-contained git repo at `dir` with one committed TS file. const makeRepo = async (dir: string, base: string) => { const { execFileSync } = await import('child_process'); const git = (...args: string[]) => execFileSync('git', args, { cwd: dir, stdio: 'pipe' }); fs.mkdirSync(dir, { recursive: true }); git('init', '-q'); git('config', 'user.email', 'test@test.com'); git('config', 'user.name', 'Test'); fs.writeFileSync(path.join(dir, `${base}.ts`), `export const ${base} = 1;`); git('add', '-A'); git('commit', '-q', '-m', `${base} init`); }; beforeEach(() => { tempDir = createTempDir(); }); afterEach(() => { cleanupTempDir(tempDir); }); // The #1031 case: a nested repo `git add`ed inside the super-repo becomes a // gitlink (mode 160000) with NO `.gitmodules`. It is tracked (so it never shows // in the untracked `-o` listing) yet not an active submodule (so // `--recurse-submodules` won't expand it) — it used to fall through both passes // and only the super-repo's own files got indexed. it('indexes a bare gitlink (git add\'ed embedded repo, no .gitmodules), recursively', async () => { const { execFileSync } = await import('child_process'); const git = (cwd: string, ...args: string[]) => execFileSync('git', args, { cwd, stdio: 'pipe' }); const root = path.join(tempDir, 'root'); await makeRepo(root, 'app'); // An embedded clone, itself holding a further nested clone (untracked inside it). await makeRepo(path.join(root, 'embedded'), 'inner'); await makeRepo(path.join(root, 'embedded', 'deep'), 'deep'); // `git add embedded` records it as a 160000 gitlink (no fetch, no .gitmodules). git(root, 'add', 'embedded'); git(root, 'commit', '-q', '-m', 'add embedded as gitlink'); expect(fs.existsSync(path.join(root, '.gitmodules'))).toBe(false); const files = scanDirectory(root); expect(files).toContain('app.ts'); expect(files).toContain('embedded/inner.ts'); // the gitlink's own source expect(files).toContain('embedded/deep/deep.ts'); // recursion continues into its nested repo }); // The -c → -s switch must not regress active submodules (#147): a repo can hold // BOTH an active submodule (expanded by --recurse-submodules) and a bare gitlink // (handled by the new pass), and the mixed 160000/100644 modes must parse right. it('indexes a gitlink alongside an active submodule', async () => { const { execFileSync } = await import('child_process'); const git = (cwd: string, ...args: string[]) => execFileSync('git', args, { cwd, stdio: 'pipe' }); const lib = path.join(tempDir, '_lib'); await makeRepo(lib, 'lib'); const root = path.join(tempDir, 'root'); await makeRepo(root, 'app'); // A proper, active submodule. execFileSync('git', ['-c', 'protocol.file.allow=always', 'submodule', 'add', '-q', lib, 'libs/lib'], { cwd: root, stdio: 'pipe' }); git(root, 'commit', '-q', '-m', 'add submodule'); // A bare gitlink in the same repo (under a non-ignored dir name). await makeRepo(path.join(root, 'external', 'tool'), 'tool'); git(root, 'add', 'external/tool'); git(root, 'commit', '-q', '-m', 'add gitlink'); const files = scanDirectory(root); expect(files).toContain('app.ts'); expect(files).toContain('libs/lib/lib.ts'); // active submodule still expands (#147) expect(files).toContain('external/tool/tool.ts'); // bare gitlink now indexed }); // A gitlink under a built-in default-ignored directory (vendor/, node_modules/, // …) stays excluded — a committed dependency doesn't become project code just // because it's a nested repo. Mirrors how the untracked-embedded path treats // the same dirs (#407), so the two passes agree. it('does not index a gitlink under a default-ignored directory (e.g. vendor/)', async () => { const { execFileSync } = await import('child_process'); const git = (cwd: string, ...args: string[]) => execFileSync('git', args, { cwd, stdio: 'pipe' }); const root = path.join(tempDir, 'root'); await makeRepo(root, 'app'); await makeRepo(path.join(root, 'vendor', 'pkg'), 'dep'); git(root, 'add', 'vendor/pkg'); git(root, 'commit', '-q', '-m', 'add vendored gitlink'); const files = scanDirectory(root); expect(files).toContain('app.ts'); expect(files).not.toContain('vendor/pkg/dep.ts'); }); // A gitlink with NO working tree on disk (the common "cloned without // --recurse-submodules" state) has nothing to index — we must leave it alone, // not fabricate entries, and must not break the rest of the scan. it('leaves an uninitialized submodule (no checkout on disk) alone', async () => { const { execFileSync } = await import('child_process'); const lib = path.join(tempDir, '_lib'); await makeRepo(lib, 'lib'); const sup = path.join(tempDir, 'super'); await makeRepo(sup, 'app'); execFileSync('git', ['-c', 'protocol.file.allow=always', 'submodule', 'add', '-q', lib, 'libs/lib'], { cwd: sup, stdio: 'pipe' }); execFileSync('git', ['commit', '-q', '-m', 'add submodule'], { cwd: sup, stdio: 'pipe' }); // Clone the super-repo WITHOUT --recurse-submodules → libs/lib is an empty // gitlink dir (mode 160000, no `.git` inside, no files). const clone = path.join(tempDir, 'clone'); execFileSync('git', ['clone', '-q', sup, clone], { stdio: 'pipe' }); expect(fs.readdirSync(path.join(clone, 'libs', 'lib'))).toHaveLength(0); const files = scanDirectory(clone); expect(files).toContain('app.ts'); expect(files).not.toContain('libs/lib/lib.ts'); // not on disk → correctly absent }); // #1065: a gitlink under a path the super-repo's OWN `.gitignore` covers is the // tracked-gitlink twin of the untracked-ignored embedded repo (#514, #970). The // gitlink-discovery pass must honor that `.gitignore` the same way — otherwise a // gitignored reference/benchmark corpus full of `git add`ed clones gets pulled // into the index (the 138k-file blow-up the reporter hit). Respect it by default; // re-include only via `codegraph.json` `includeIgnored`. it('does not index a gitlink under a gitignored directory by default (#1065)', async () => { const { execFileSync } = await import('child_process'); const git = (cwd: string, ...args: string[]) => execFileSync('git', args, { cwd, stdio: 'pipe' }); const root = path.join(tempDir, 'root'); await makeRepo(root, 'app'); // An embedded clone under a path the super-repo gitignores (a benchmark corpus). await makeRepo(path.join(root, 'benchmark', 'repos', 'ref'), 'ref'); git(root, 'add', 'benchmark/repos/ref'); // tracked as a 160000 gitlink fs.writeFileSync(path.join(root, '.gitignore'), 'benchmark/repos/\n'); git(root, 'add', '.gitignore'); git(root, 'commit', '-q', '-m', 'add gitignored gitlink + ignore rule'); const files = scanDirectory(root); expect(files).toContain('app.ts'); expect(files).not.toContain('benchmark/repos/ref/ref.ts'); // gitignored → excluded // The watcher path agrees: the ignored root is never discovered, and the dir is // pruned (the reporter's exact clue — `ignores('benchmark/repos/')` was false). expect(discoverEmbeddedRepoRoots(root)).toEqual([]); expect(buildScopeIgnore(root).ignores('benchmark/repos/')).toBe(true); expect(buildScopeIgnore(root).ignores('benchmark/repos/ref/ref.ts')).toBe(true); }); it('re-includes a gitignored gitlink when codegraph.json includeIgnored opts in (#1065)', async () => { const { execFileSync } = await import('child_process'); const git = (cwd: string, ...args: string[]) => execFileSync('git', args, { cwd, stdio: 'pipe' }); const root = path.join(tempDir, 'root'); await makeRepo(root, 'app'); await makeRepo(path.join(root, 'benchmark', 'repos', 'ref'), 'ref'); git(root, 'add', 'benchmark/repos/ref'); fs.writeFileSync(path.join(root, '.gitignore'), 'benchmark/repos/\n'); fs.writeFileSync(path.join(root, 'codegraph.json'), JSON.stringify({ includeIgnored: ['benchmark/repos/'] })); git(root, 'add', '.gitignore', 'codegraph.json'); git(root, 'commit', '-q', '-m', 'opt the gitignored gitlink back in'); const files = scanDirectory(root); expect(files).toContain('app.ts'); expect(files).toContain('benchmark/repos/ref/ref.ts'); // opted in → indexed expect(discoverEmbeddedRepoRoots(root)).toContain('benchmark/repos/ref/'); }); }); describe('Nested non-submodule git repos', () => { let tempDir: string; beforeEach(() => { tempDir = createTempDir(); }); afterEach(() => { cleanupTempDir(tempDir); }); it('should index files in embedded git repos run from a git super-repo (issue #193)', async () => { const { execFileSync } = await import('child_process'); const git = (cwd: string, ...args: string[]) => execFileSync('git', args, { cwd, stdio: 'pipe' }); // Top-level workspace is itself a git repo, holding no source directly — // the CMake "super-repo" layout from the issue. const root = path.join(tempDir, 'root'); fs.mkdirSync(path.join(root, 'coding'), { recursive: true }); git(root, 'init', '-q'); git(root, 'config', 'user.email', 'test@test.com'); git(root, 'config', 'user.name', 'Test'); fs.writeFileSync(path.join(root, 'CMakeLists.txt'), 'cmake_minimum_required(VERSION 3.10)\n'); // Two independent clones living inside the workspace (NOT submodules): // one with committed source, one with only untracked source. const sub1 = path.join(root, 'sub_repo1', 'src'); fs.mkdirSync(sub1, { recursive: true }); git(path.join(root, 'sub_repo1'), 'init', '-q'); git(path.join(root, 'sub_repo1'), 'config', 'user.email', 'test@test.com'); git(path.join(root, 'sub_repo1'), 'config', 'user.name', 'Test'); fs.writeFileSync(path.join(sub1, 'one.ts'), 'export const one = 1;'); git(path.join(root, 'sub_repo1'), 'add', '-A'); git(path.join(root, 'sub_repo1'), 'commit', '-q', '-m', 'sub1 init'); const sub2 = path.join(root, 'sub_repo2', 'src'); fs.mkdirSync(sub2, { recursive: true }); git(path.join(root, 'sub_repo2'), 'init', '-q'); fs.writeFileSync(path.join(sub2, 'two.ts'), 'export const two = 2;'); const files = scanDirectory(root); // Both committed and untracked source from the nested repos must be found. expect(files).toContain('sub_repo1/src/one.ts'); expect(files).toContain('sub_repo2/src/two.ts'); }); it('should respect each embedded repo\'s own .gitignore', async () => { const { execFileSync } = await import('child_process'); const git = (cwd: string, ...args: string[]) => execFileSync('git', args, { cwd, stdio: 'pipe' }); const root = path.join(tempDir, 'root'); fs.mkdirSync(root, { recursive: true }); git(root, 'init', '-q'); const sub = path.join(root, 'sub_repo', 'src'); fs.mkdirSync(sub, { recursive: true }); git(path.join(root, 'sub_repo'), 'init', '-q'); fs.writeFileSync(path.join(root, 'sub_repo', '.gitignore'), 'src/generated.ts\n'); fs.writeFileSync(path.join(sub, 'real.ts'), 'export const real = 1;'); fs.writeFileSync(path.join(sub, 'generated.ts'), 'export const generated = 1;'); const files = scanDirectory(root); expect(files).toContain('sub_repo/src/real.ts'); expect(files).not.toContain('sub_repo/src/generated.ts'); }); // A .gitignore the `ignore` library can't compile to a regex must not abort // the whole scan — the bad pattern is dropped, valid ones still apply (#682). it('does not crash on a .gitignore with an uncompilable pattern (#682)', () => { fs.mkdirSync(path.join(tempDir, 'src'), { recursive: true }); fs.mkdirSync(path.join(tempDir, 'build'), { recursive: true }); fs.writeFileSync(path.join(tempDir, 'src', 'real.ts'), 'export const x = 1;'); fs.writeFileSync(path.join(tempDir, 'build', 'out.ts'), 'export const y = 2;'); // `\\[` makes the matcher build an unterminated character class — the throw // is lazy (at match time), which is what escaped and killed sync. fs.writeFileSync(path.join(tempDir, '.gitignore'), 'build/\n\\\\[\n'); let files: string[] = []; expect(() => { files = scanDirectory(tempDir); }).not.toThrow(); expect(files).toContain('src/real.ts'); // The still-valid `build/` rule is honored; only the bad line was dropped. expect(files.some((f) => f.startsWith('build/'))).toBe(false); }); // A .gitignore that isn't valid UTF-8 — e.g. encrypted in place by corporate // DLP / endpoint software (UTF-16 header + ciphertext) — is skipped whole, // not fed to the matcher as garbage patterns (#682). it('does not crash on a non-UTF-8 (DLP-encrypted) .gitignore (#682)', () => { fs.mkdirSync(path.join(tempDir, 'src'), { recursive: true }); fs.writeFileSync(path.join(tempDir, 'src', 'real.ts'), 'export const x = 1;'); const header = Buffer.concat([ Buffer.from([0x00, 0x00]), Buffer.from('[notice][user]', 'utf16le'), ]); const junk = Buffer.from([0x5b, 0x99, 0xc3, 0x28, 0x5c, 0x5b, 0xff, 0xfd]); fs.writeFileSync(path.join(tempDir, '.gitignore'), Buffer.concat([header, junk])); let files: string[] = []; expect(() => { files = scanDirectory(tempDir); }).not.toThrow(); expect(files).toContain('src/real.ts'); }); it('buildDefaultIgnore survives a bad .gitignore and still applies valid rules (#682)', () => { fs.writeFileSync(path.join(tempDir, '.gitignore'), 'dist/\n\\\\[\n'); const ig = buildDefaultIgnore(tempDir); expect(() => ig.ignores('src/app.ts')).not.toThrow(); expect(ig.ignores('dist/')).toBe(true); // valid rule survives expect(ig.ignores('src/app.ts')).toBe(false); }); }); // ============================================================================= // Scala // ============================================================================= describe('Scala Extraction', () => { describe('Language detection', () => { it('should detect Scala files', () => { expect(detectLanguage('Main.scala')).toBe('scala'); expect(detectLanguage('script.sc')).toBe('scala'); expect(detectLanguage('src/UserService.scala')).toBe('scala'); }); it('should report Scala as supported', () => { expect(isLanguageSupported('scala')).toBe(true); expect(getSupportedLanguages()).toContain('scala'); }); }); describe('Class extraction', () => { it('should extract class definitions', () => { const code = ` class UserService(private val repo: UserRepository) { def findUser(id: String): Option[String] = Some(id) } `; const result = extractFromSource('UserService.scala', code); const cls = result.nodes.find((n) => n.kind === 'class' && n.name === 'UserService'); expect(cls).toBeDefined(); expect(cls?.language).toBe('scala'); }); it('should extract object definitions as class kind', () => { const code = ` object DatabaseConfig { val url = "jdbc:postgresql://localhost/mydb" } `; const result = extractFromSource('Config.scala', code); const obj = result.nodes.find((n) => n.kind === 'class' && n.name === 'DatabaseConfig'); expect(obj).toBeDefined(); }); it('should extract trait definitions as trait kind', () => { const code = ` trait Repository[A] { def findById(id: String): Option[A] def save(entity: A): Unit } `; const result = extractFromSource('Repository.scala', code); const trait_ = result.nodes.find((n) => n.kind === 'trait' && n.name === 'Repository'); expect(trait_).toBeDefined(); }); }); describe('Method and function extraction', () => { it('should extract method definitions inside a class', () => { const code = ` class Calculator { def add(a: Int, b: Int): Int = a + b def divide(a: Double, b: Double): Double = a / b } `; const result = extractFromSource('Calculator.scala', code); const methods = result.nodes.filter((n) => n.kind === 'method'); expect(methods.find((m) => m.name === 'add')).toBeDefined(); expect(methods.find((m) => m.name === 'divide')).toBeDefined(); }); it('should extract method signatures', () => { const code = ` class Greeter { def greet(name: String): String = s"Hello, \${name}!" } `; const result = extractFromSource('Greeter.scala', code); const method = result.nodes.find((n) => n.name === 'greet'); expect(method?.signature).toContain('name: String'); expect(method?.signature).toContain('String'); }); it('should extract top-level function definitions as functions', () => { const code = ` def factorial(n: Int): Int = if (n <= 1) 1 else n * factorial(n - 1) def greet(name: String): String = s"Hello, \${name}!" `; const result = extractFromSource('utils.scala', code); const fns = result.nodes.filter((n) => n.kind === 'function'); expect(fns.find((f) => f.name === 'factorial')).toBeDefined(); expect(fns.find((f) => f.name === 'greet')).toBeDefined(); }); }); describe('Val and var extraction', () => { it('should extract val inside a class as field', () => { const code = ` class Config { val timeout: Int = 30 val host: String = "localhost" } `; const result = extractFromSource('Config.scala', code); const fields = result.nodes.filter((n) => n.kind === 'field'); expect(fields.find((f) => f.name === 'timeout')).toBeDefined(); expect(fields.find((f) => f.name === 'host')).toBeDefined(); }); it('should extract var inside a class as field', () => { const code = ` class Counter { var count: Int = 0 } `; const result = extractFromSource('Counter.scala', code); const field = result.nodes.find((n) => n.kind === 'field' && n.name === 'count'); expect(field).toBeDefined(); }); it('should extract top-level val as constant', () => { const code = ` val MaxConnections: Int = 100 val DefaultTimeout = 30 `; const result = extractFromSource('constants.scala', code); const consts = result.nodes.filter((n) => n.kind === 'constant'); expect(consts.find((c) => c.name === 'MaxConnections')).toBeDefined(); }); it('should extract top-level var as variable', () => { const code = ` var retries: Int = 3 `; const result = extractFromSource('state.scala', code); const v = result.nodes.find((n) => n.kind === 'variable' && n.name === 'retries'); expect(v).toBeDefined(); }); it('should include type in val/var signature', () => { const code = ` class Service { val timeout: Int = 30 } `; const result = extractFromSource('Service.scala', code); const field = result.nodes.find((n) => n.name === 'timeout'); expect(field?.signature).toContain('timeout'); expect(field?.signature).toContain('Int'); }); }); describe('Enum extraction', () => { it('should extract enum definitions', () => { const code = ` enum Color: case Red case Green case Blue `; const result = extractFromSource('Color.scala', code); const enumNode = result.nodes.find((n) => n.kind === 'enum' && n.name === 'Color'); expect(enumNode).toBeDefined(); }); it('should extract enum cases as enum_member', () => { const code = ` enum Direction: case North case South case East case West `; const result = extractFromSource('Direction.scala', code); const members = result.nodes.filter((n) => n.kind === 'enum_member'); expect(members.find((m) => m.name === 'North')).toBeDefined(); expect(members.find((m) => m.name === 'South')).toBeDefined(); expect(members.length).toBeGreaterThanOrEqual(4); }); }); describe('Type alias extraction', () => { it('should extract type aliases', () => { const code = ` type UserId = String type UserMap = Map[String, String] `; const result = extractFromSource('types.scala', code); const aliases = result.nodes.filter((n) => n.kind === 'type_alias'); expect(aliases.find((a) => a.name === 'UserId')).toBeDefined(); expect(aliases.find((a) => a.name === 'UserMap')).toBeDefined(); }); }); describe('Import extraction', () => { it('should extract import declarations', () => { const code = ` import scala.collection.mutable.ListBuffer import scala.concurrent.Future `; const result = extractFromSource('imports.scala', code); const imports = result.nodes.filter((n) => n.kind === 'import'); expect(imports.length).toBeGreaterThanOrEqual(2); }); }); describe('Visibility modifiers', () => { it('should extract private visibility', () => { const code = ` class Service { private val secret: String = "abc" private def helper(): Unit = {} } `; const result = extractFromSource('Service.scala', code); const secretField = result.nodes.find((n) => n.name === 'secret'); expect(secretField?.visibility).toBe('private'); const helperMethod = result.nodes.find((n) => n.name === 'helper'); expect(helperMethod?.visibility).toBe('private'); }); it('should extract protected visibility', () => { const code = ` class Base { protected def helperMethod(): Unit = {} } `; const result = extractFromSource('Base.scala', code); const method = result.nodes.find((n) => n.name === 'helperMethod'); expect(method?.visibility).toBe('protected'); }); it('should default to public visibility', () => { const code = ` class Greeter { def hello(): Unit = {} } `; const result = extractFromSource('Greeter.scala', code); const method = result.nodes.find((n) => n.name === 'hello'); expect(method?.visibility).toBe('public'); }); }); describe('Inheritance', () => { it('should extract extends relationships', () => { const code = ` class AdminUser extends User { def adminAction(): Unit = {} } `; const result = extractFromSource('AdminUser.scala', code); const extendsRefs = result.unresolvedReferences.filter((r) => r.referenceKind === 'extends'); expect(extendsRefs.find((r) => r.referenceName === 'User')).toBeDefined(); }); }); describe('Call extraction', () => { it('should extract function call expressions', () => { const code = ` def processData(): Unit = { val result = computeResult() println(result) } `; const result = extractFromSource('processor.scala', code); const calls = result.unresolvedReferences.filter((r) => r.referenceKind === 'calls'); expect(calls.length).toBeGreaterThan(0); }); }); }); describe('Vue Extraction', () => { it('should detect Vue files', () => { expect(detectLanguage('App.vue')).toBe('vue'); expect(detectLanguage('components/Button.vue')).toBe('vue'); expect(isLanguageSupported('vue')).toBe(true); }); it('should extract component node from a Vue SFC', () => { const code = ` `; const result = extractFromSource('HelloWorld.vue', code); const componentNode = result.nodes.find((n) => n.kind === 'component'); expect(componentNode).toBeDefined(); expect(componentNode?.name).toBe('HelloWorld'); expect(componentNode?.language).toBe('vue'); expect(componentNode?.isExported).toBe(true); }); it('should extract functions from `; const result = extractFromSource('Button.vue', code); const componentNode = result.nodes.find((n) => n.kind === 'component'); expect(componentNode).toBeDefined(); expect(componentNode?.name).toBe('Button'); const funcNode = result.nodes.find((n) => n.kind === 'function' && n.name === 'handleClick'); expect(funcNode).toBeDefined(); expect(funcNode?.language).toBe('vue'); }); it('should extract from `; const result = extractFromSource('Counter.vue', code); const componentNode = result.nodes.find((n) => n.kind === 'component'); expect(componentNode).toBeDefined(); expect(componentNode?.name).toBe('Counter'); const funcNode = result.nodes.find((n) => n.kind === 'function' && n.name === 'increment'); expect(funcNode).toBeDefined(); expect(funcNode?.language).toBe('vue'); // All nodes should be marked as vue language for (const node of result.nodes) { expect(node.language).toBe('vue'); } }); it('should extract calls from top-level `; const result = extractFromSource('Issue425Setup.vue', code); const call = result.unresolvedReferences.find( (ref) => ref.referenceKind === 'calls' && ref.referenceName === 'getTokenMp' ); expect(call).toBeDefined(); }); it('should extract calls from Vue Options API object methods', () => { const code = ` `; const result = extractFromSource('Issue425Options.vue', code); const calls = result.unresolvedReferences.filter( (ref) => ref.referenceKind === 'calls' && ref.referenceName === 'getTokenMp' ); expect(calls).toHaveLength(2); }); it('should extract component usages from the Vue template (PascalCase + kebab, skipping built-ins) (#629)', () => { const code = ` `; const result = extractFromSource('Host.vue', code); const refs = result.unresolvedReferences .filter((r) => r.referenceKind === 'references') .map((r) => r.referenceName); expect(refs).toContain('UserCard'); // PascalCase tag expect(refs).toContain('MyButton'); // kebab → MyButton expect(refs).not.toContain('Transition'); // Vue built-in skipped expect(refs).not.toContain('Div'); // native HTML element skipped expect(refs).not.toContain('Span'); }); it('should extract from both `; const result = extractFromSource('DualScript.vue', code); const componentNode = result.nodes.find((n) => n.kind === 'component'); expect(componentNode).toBeDefined(); const greetFunc = result.nodes.find((n) => n.kind === 'function' && n.name === 'greet'); expect(greetFunc).toBeDefined(); }); it('should create component node for template-only Vue file', () => { const code = ` `; const result = extractFromSource('Static.vue', code); const componentNode = result.nodes.find((n) => n.kind === 'component'); expect(componentNode).toBeDefined(); expect(componentNode?.name).toBe('Static'); expect(componentNode?.language).toBe('vue'); // Only the component node should exist (no script nodes) expect(result.nodes.length).toBe(1); }); it('should create containment edges from component to script nodes', () => { const code = ` `; const result = extractFromSource('Contained.vue', code); const componentNode = result.nodes.find((n) => n.kind === 'component'); expect(componentNode).toBeDefined(); // Should have containment edges from component to child nodes const containEdges = result.edges.filter( (e) => e.source === componentNode!.id && e.kind === 'contains' ); expect(containEdges.length).toBeGreaterThan(0); }); }); describe('Astro Extraction', () => { it('should detect Astro files', () => { expect(detectLanguage('src/pages/index.astro')).toBe('astro'); expect(detectLanguage('Layout.astro')).toBe('astro'); expect(isLanguageSupported('astro')).toBe(true); }); it('should extract component node from an .astro file', () => { const code = `--- const title = 'Hello'; ---

{title}

`; const result = extractFromSource('Card.astro', code); const componentNode = result.nodes.find((n) => n.kind === 'component'); expect(componentNode).toBeDefined(); expect(componentNode?.name).toBe('Card'); expect(componentNode?.language).toBe('astro'); expect(componentNode?.isExported).toBe(true); }); it('should extract frontmatter symbols with correct line numbers (#768)', () => { const code = `--- import { formatDate } from '../utils/format'; function getIconNode(name: string): string { return name; } const { title } = Astro.props; --- {title} `; const result = extractFromSource('navs.astro', code); // The #768 repro: a function defined in frontmatter must be found const fn = result.nodes.find((n) => n.kind === 'function' && n.name === 'getIconNode'); expect(fn).toBeDefined(); expect(fn?.language).toBe('astro'); expect(fn?.startLine).toBe(4); const imp = result.nodes.find((n) => n.kind === 'import'); expect(imp).toBeDefined(); expect(imp?.startLine).toBe(2); }); it('should extract exported getStaticPaths from frontmatter', () => { const code = `--- export async function getStaticPaths() { return []; } const { slug } = Astro.params; ---

{slug}

`; const result = extractFromSource('[slug].astro', code); const fn = result.nodes.find((n) => n.kind === 'function' && n.name === 'getStaticPaths'); expect(fn).toBeDefined(); expect(fn?.isExported).toBe(true); }); it('should extract calls from template expressions', () => { const code = `--- import { formatDate } from '../utils/format'; const date = new Date(); --- `; const result = extractFromSource('Stamp.astro', code); const call = result.unresolvedReferences.find( (ref) => ref.referenceKind === 'calls' && ref.referenceName === 'formatDate' && ref.line === 5 ); expect(call).toBeDefined(); }); it('should extract calls from a multiline expression opening line', () => { const code = `--- const posts = []; ---
    {posts.map((post) => (
  • {render(post)}
  • ))}
`; const result = extractFromSource('List.astro', code); const mapCall = result.unresolvedReferences.find( (ref) => ref.referenceKind === 'calls' && ref.referenceName === 'posts.map' ); expect(mapCall).toBeDefined(); const innerCall = result.unresolvedReferences.find( (ref) => ref.referenceKind === 'calls' && ref.referenceName === 'render' ); expect(innerCall).toBeDefined(); }); it('should extract PascalCase component usages from the template', () => { const code = `--- import Layout from '../layouts/Layout.astro'; import PostCard from '../components/PostCard.astro'; ---
`; const result = extractFromSource('index.astro', code); const refs = result.unresolvedReferences.filter((r) => r.referenceKind === 'references'); const names = refs.map((r) => r.referenceName); expect(names).toContain('Layout'); expect(names).toContain('PostCard'); // Astro built-ins and lowercase HTML are not component references expect(names).not.toContain('Fragment'); expect(names).not.toContain('div'); }); it('should not extract template patterns from frontmatter, script, or style content', () => { const code = `--- // inside frontmatter comment const x = { y: maybeCall(1) }; ---
real
`; const result = extractFromSource('Guard.astro', code); const templateRefs = result.unresolvedReferences.filter( (r) => r.referenceKind === 'references' && r.referenceName === 'FakeComponent' ); expect(templateRefs).toHaveLength(0); // maybeCall/scriptCall come from the delegated TS extraction (once), // not double-counted by the template scanner const maybeCalls = result.unresolvedReferences.filter( (r) => r.referenceName === 'maybeCall' && r.referenceKind === 'calls' ); expect(maybeCalls.length).toBeLessThanOrEqual(1); }); it('should extract `; const result = extractFromSource('Tracker.astro', code); const fn = result.nodes.find((n) => n.kind === 'function' && n.name === 'trackView'); expect(fn).toBeDefined(); expect(fn?.startLine).toBe(6); expect(fn?.language).toBe('astro'); }); it('should create component node for a frontmatter-less template-only file', () => { const code = `
Static content
`; const result = extractFromSource('Static.astro', code); const componentNode = result.nodes.find((n) => n.kind === 'component'); expect(componentNode).toBeDefined(); expect(componentNode?.name).toBe('Static'); expect(componentNode?.language).toBe('astro'); }); it('should treat an unclosed frontmatter fence as no frontmatter', () => { const code = `--- const broken = true;
never closed
`; const result = extractFromSource('Broken.astro', code); // No TS delegation happened (the fence never closes), but the component // node still exists and nothing throws. const componentNode = result.nodes.find((n) => n.kind === 'component'); expect(componentNode).toBeDefined(); expect(result.nodes.find((n) => n.name === 'broken')).toBeUndefined(); }); it('should create containment edges from component to frontmatter nodes', () => { const code = `--- const value = 42; ---
{value}
`; const result = extractFromSource('Contained.astro', code); const componentNode = result.nodes.find((n) => n.kind === 'component'); expect(componentNode).toBeDefined(); const containEdges = result.edges.filter( (e) => e.source === componentNode!.id && e.kind === 'contains' ); expect(containEdges.length).toBeGreaterThan(0); }); }); describe('Instantiates + Decorates edge extraction', () => { it('emits an instantiates ref for `new Foo()`', () => { const code = ` class Foo {} function bootstrap() { return new Foo(); } `; const result = extractFromSource('app.ts', code); const ref = result.unresolvedReferences.find( (r) => r.referenceKind === 'instantiates' && r.referenceName === 'Foo' ); expect(ref).toBeDefined(); }); it('strips type-argument suffix from generic constructors', () => { const code = ` class Container { constructor(_: T) {} } function go() { return new Container('x'); } `; const result = extractFromSource('app.ts', code); const ref = result.unresolvedReferences.find( (r) => r.referenceKind === 'instantiates' ); expect(ref).toBeDefined(); // Container must be normalised to "Container" — otherwise // resolution can never match the class node. expect(ref!.referenceName).toBe('Container'); }); it('keeps trailing identifier from qualified `new ns.Foo()`', () => { const code = ` const ns = { Foo: class {} }; function go() { return new ns.Foo(); } `; const result = extractFromSource('app.ts', code); const ref = result.unresolvedReferences.find( (r) => r.referenceKind === 'instantiates' ); // We can't always resolve which Foo, but the name should be the // simple identifier so name-matching has a chance. expect(ref?.referenceName).toBe('Foo'); }); it('emits a decorates ref for `@Foo class X {}`', () => { const code = ` function Foo(_arg: string) { return (cls: any) => cls; } @Foo('x') class X {} `; const result = extractFromSource('app.ts', code); const decorClass = result.unresolvedReferences.find( (r) => r.referenceKind === 'decorates' && r.referenceName === 'Foo' ); expect(decorClass).toBeDefined(); }); it('does NOT attribute a prior class\'s decorator to the next class', () => { // Regression: the sibling-walk must stop at the first non- // decorator separator. `@A class Foo {} @B class Bar {}` must // produce `decorates(Foo, A)` and `decorates(Bar, B)` — never // `decorates(Bar, A)`. const code = ` function A(cls: any) { return cls; } function B(cls: any) { return cls; } @A class Foo {} @B class Bar {} `; const result = extractFromSource('app.ts', code); const decoratesEdges = result.unresolvedReferences.filter( (r) => r.referenceKind === 'decorates' ); // Exactly one decorates ref per decorated class, no cross-attribution. const fromBar = decoratesEdges.filter((r) => result.nodes.find((n) => n.id === r.fromNodeId && n.name === 'Bar') ); expect(fromBar.length).toBe(1); expect(fromBar[0]!.referenceName).toBe('B'); }); it('emits a decorates ref for `@Foo method() {}`', () => { const code = ` function Get(p: string) { return (t: any, k: string) => t; } class Svc { @Get('/x') method() { return 1; } } `; const result = extractFromSource('app.ts', code); const decorMethod = result.unresolvedReferences.find( (r) => r.referenceKind === 'decorates' && r.referenceName === 'Get' ); expect(decorMethod).toBeDefined(); // The decorated symbol must be `method`, not the constructor or class. const decoratedNode = result.nodes.find((n) => n.id === decorMethod!.fromNodeId); expect(decoratedNode?.name).toBe('method'); }); }); // ============================================================================= // Lua // ============================================================================= describe('Lua Extraction', () => { describe('Language detection', () => { it('should detect Lua files', () => { expect(detectLanguage('init.lua')).toBe('lua'); expect(detectLanguage('src/util.lua')).toBe('lua'); }); it('should report Lua as supported', () => { expect(isLanguageSupported('lua')).toBe(true); expect(getSupportedLanguages()).toContain('lua'); }); }); describe('Function extraction', () => { it('should extract global and local functions', () => { const code = ` function configure(opts) return opts end local function helper(x) return x * 2 end `; const result = extractFromSource('init.lua', code); const funcs = result.nodes.filter((n) => n.kind === 'function').map((n) => n.name); expect(funcs).toContain('configure'); expect(funcs).toContain('helper'); const configure = result.nodes.find((n) => n.name === 'configure'); expect(configure?.language).toBe('lua'); expect(configure?.signature).toBe('(opts)'); }); it('should split table/method functions into a receiver and method name', () => { const code = ` function M.connect(host, port) return host end function M:send(data) return self end `; const result = extractFromSource('init.lua', code); const methods = result.nodes.filter((n) => n.kind === 'method'); const connect = methods.find((m) => m.name === 'connect'); expect(connect?.qualifiedName).toBe('M::connect'); const send = methods.find((m) => m.name === 'send'); expect(send?.qualifiedName).toBe('M::send'); }); }); describe('Variable extraction', () => { it('should extract local variable declarations', () => { const code = ` local M = {} local count = 0 `; const result = extractFromSource('mod.lua', code); const vars = result.nodes.filter((n) => n.kind === 'variable').map((n) => n.name); expect(vars).toContain('M'); expect(vars).toContain('count'); }); }); describe('Import extraction (require)', () => { it('should extract require() in local declarations and bare calls', () => { const code = ` local socket = require("socket") local http = require "resty.http" require("side.effect") `; const result = extractFromSource('net.lua', code); const imports = result.nodes.filter((n) => n.kind === 'import').map((n) => n.name); expect(imports).toContain('socket'); expect(imports).toContain('resty.http'); expect(imports).toContain('side.effect'); const ref = result.unresolvedReferences.find( (r) => r.referenceKind === 'imports' && r.referenceName === 'socket' ); expect(ref).toBeDefined(); }); // Regression: the tree-sitter-wasms Lua grammar (ABI 13) corrupts the shared // WASM heap under web-tree-sitter 0.25, dropping nested calls/imports on every // parse after the first. We vendor the ABI-15 grammar instead — this guards it // by extracting several sources in sequence and asserting the LAST still works. it('should keep extracting require across many sequential parses', () => { let last; for (let i = 0; i < 8; i++) { last = extractFromSource(`f${i}.lua`, `local m = require("module.${i}")\nreturn m\n`); } const imports = last!.nodes.filter((n) => n.kind === 'import').map((n) => n.name); expect(imports).toContain('module.7'); }); }); describe('Call extraction', () => { it('should record intra-file calls as resolvable references', () => { const code = ` local function helper(x) return x end local function run(y) return helper(y) end `; const result = extractFromSource('calls.lua', code); const call = result.unresolvedReferences.find( (r) => r.referenceKind === 'calls' && r.referenceName === 'helper' ); expect(call).toBeDefined(); }); }); }); // ============================================================================= // Luau (typed superset of Lua — https://luau.org) // ============================================================================= describe('Luau Extraction', () => { describe('Language detection', () => { it('should detect Luau files', () => { expect(detectLanguage('init.luau')).toBe('luau'); expect(detectLanguage('src/Client.luau')).toBe('luau'); }); it('should report Luau as supported', () => { expect(isLanguageSupported('luau')).toBe(true); expect(getSupportedLanguages()).toContain('luau'); }); }); describe('Type aliases', () => { it('should extract `type` and `export type` definitions', () => { const code = ` export type Vector = { x: number, y: number } type Handler = (msg: string) -> boolean `; const result = extractFromSource('types.luau', code); const aliases = result.nodes.filter((n) => n.kind === 'type_alias'); const vector = aliases.find((a) => a.name === 'Vector'); expect(vector).toBeDefined(); expect(vector?.isExported).toBe(true); const handler = aliases.find((a) => a.name === 'Handler'); expect(handler).toBeDefined(); expect(handler?.isExported).toBe(false); }); }); describe('Typed functions and methods', () => { it('should capture typed signatures and split methods by receiver', () => { const code = ` function configure(opts: { debug: boolean }): boolean return opts.debug end function Client:fetch(path: string): Response return path end `; const result = extractFromSource('client.luau', code); const configure = result.nodes.find((n) => n.kind === 'function' && n.name === 'configure'); expect(configure?.language).toBe('luau'); expect(configure?.signature).toBe('(opts: { debug: boolean }): boolean'); const fetch = result.nodes.find((n) => n.kind === 'method' && n.name === 'fetch'); expect(fetch?.qualifiedName).toBe('Client::fetch'); }); }); describe('Imports and variables', () => { it('should extract string and Roblox instance-path require imports', () => { const code = ` local http = require("http") local Signal = require(script.Parent.Signal) local count = 0 `; const result = extractFromSource('mod.luau', code); const imports = result.nodes.filter((n) => n.kind === 'import').map((n) => n.name); expect(imports).toContain('http'); // string require expect(imports).toContain('Signal'); // Roblox instance-path require const vars = result.nodes.filter((n) => n.kind === 'variable').map((n) => n.name); expect(vars).toContain('count'); }); }); }); // ============================================================================= // Objective-C // ============================================================================= describe('Objective-C Extraction', () => { const sample = ` #import #import "MyClass.h" @interface MyClass : NSObject @property (nonatomic, copy) NSString *name; - (void)greet; - (void)doThing:(id)x with:(id)y; + (instancetype)shared; @end @implementation MyClass - (void)greet { NSLog(@"Hello"); [self doWork]; } - (void)doThing:(id)x with:(id)y { [self notify:x]; } + (instancetype)shared { return [[MyClass alloc] init]; } @end void helperFunction(int count) { MyClass *obj = [MyClass shared]; [obj greet]; } `; it('should extract classes, methods, functions, and imports', () => { const result = extractFromSource('App.m', sample); const classes = result.nodes.filter((n) => n.kind === 'class'); expect(classes.filter((c) => c.name === 'MyClass')).toHaveLength(1); const methods = result.nodes.filter((n) => n.kind === 'method'); expect(methods.map((m) => m.name).sort()).toEqual(['doThing:with:', 'greet', 'shared']); const shared = methods.find((m) => m.name === 'shared'); expect(shared?.isStatic).toBe(true); const properties = result.nodes.filter((n) => n.kind === 'property'); expect(properties.some((p) => p.name === 'name')).toBe(true); const functions = result.nodes.filter((n) => n.kind === 'function'); expect(functions.some((f) => f.name === 'helperFunction')).toBe(true); const imports = result.nodes.filter((n) => n.kind === 'import').map((n) => n.name); expect(imports).toContain('Foundation/Foundation.h'); expect(imports).toContain('MyClass.h'); }); it('should record inheritance and protocol conformance', () => { const result = extractFromSource('App.m', sample); const extendsRefs = result.unresolvedReferences.filter((r) => r.referenceKind === 'extends'); const implementsRefs = result.unresolvedReferences.filter((r) => r.referenceKind === 'implements'); expect(extendsRefs.map((r) => r.referenceName)).toContain('NSObject'); expect(implementsRefs.map((r) => r.referenceName)).toContain('NSCopying'); }); it('should record message sends and C calls', () => { const result = extractFromSource('App.m', sample); const calls = result.unresolvedReferences .filter((r) => r.referenceKind === 'calls') .map((r) => r.referenceName); expect(calls).toEqual(expect.arrayContaining(['NSLog', 'doWork', 'MyClass.shared', 'obj.greet'])); }); it('should reconstruct multi-keyword selectors at the call site so they resolve to the method definition', () => { // Regression for the gap discovered post-#165: message_expression's // multi-keyword form `[obj a:1 b:2]` was only emitting the first keyword, // so calls never resolved to multi-part method definitions like // `GET:parameters:headers:progress:success:failure:`. The call-site name // must match the method-definition name with full keywords + trailing colons. const code = ` @implementation Caller - (void)demo { NSMutableDictionary *d = [NSMutableDictionary new]; [d setObject:@"v" forKey:@"k"]; [d setObject:@"v2" forKey:@"k2" withRetry:@YES]; [self touchesBegan:nil withEvent:nil]; } @end `; const result = extractFromSource('Caller.m', code); const calls = result.unresolvedReferences .filter((r) => r.referenceKind === 'calls') .map((r) => r.referenceName); expect(calls).toEqual( expect.arrayContaining([ 'd.setObject:forKey:', 'd.setObject:forKey:withRetry:', 'touchesBegan:withEvent:', ]) ); }); it('should not classify pure C headers with @end in comments as objc', () => { const cHeader = '/* @end of file */\n#ifndef STDIO_H\nvoid printf(const char *);\n#endif\n'; expect(detectLanguage('stdio.h', cHeader)).toBe('c'); }); it('should extract protocol declarations', () => { const code = ` @protocol DataSource - (NSInteger)numberOfItems; @end `; const result = extractFromSource('DataSource.h', code); const protocol = result.nodes.find((n) => n.kind === 'protocol' && n.name === 'DataSource'); expect(protocol).toBeDefined(); }); it('should report Objective-C as supported', () => { expect(isLanguageSupported('objc')).toBe(true); expect(getSupportedLanguages()).toContain('objc'); }); }); describe('Solidity Extraction', () => { const code = `// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; interface IVault { function deposit(uint256 amount) external returns (bool); event Deposited(address indexed user, uint256 amount); } library SafeMath { function add(uint256 a, uint256 b) internal pure returns (uint256) { return a + b; } } contract Vault is IVault { using SafeMath for uint256; enum Status { Active, Frozen, Closed } struct UserInfo { uint256 balance; uint256 lastDeposit; } IERC20 public immutable token; mapping(address => UserInfo) public users; address public owner; event Withdrawn(address indexed user, uint256 amount); error NotOwner(); modifier onlyOwner() { if (msg.sender != owner) revert NotOwner(); _; } constructor(address _token) { token = IERC20(_token); owner = msg.sender; } function deposit(uint256 amount) external override returns (bool) { users[msg.sender].balance = users[msg.sender].balance.add(amount); emit Deposited(msg.sender, amount); return true; } function withdraw(uint256 amount) external onlyOwner { emit Withdrawn(msg.sender, amount); } } `; describe('Language detection', () => { it('should detect Solidity files', () => { expect(detectLanguage('contracts/Vault.sol')).toBe('solidity'); }); it('should report Solidity as supported', () => { expect(isLanguageSupported('solidity')).toBe(true); expect(getSupportedLanguages()).toContain('solidity'); }); }); describe('Container extraction', () => { it('should extract contract / interface / library as class-likes', () => { const result = extractFromSource('Vault.sol', code); // interface_declaration → interface const iface = result.nodes.find((n) => n.kind === 'interface' && n.name === 'IVault'); expect(iface).toBeDefined(); expect(iface?.language).toBe('solidity'); // contract and library both map to 'class' (library has no special semantics // a class node doesn't already cover — they share methodTypes/inheritance). expect(result.nodes.find((n) => n.kind === 'class' && n.name === 'Vault')).toBeDefined(); expect(result.nodes.find((n) => n.kind === 'class' && n.name === 'SafeMath')).toBeDefined(); }); it('should emit extends references for `is X, Y` inheritance', () => { // `Vault is IVault` — Solidity uses one keyword (`is`) for both class // extension and interface implementation, so the extractor emits `extends` // and the resolver's interface-impl synthesizer reclassifies to // `implements` based on the target node kind. const result = extractFromSource('Vault.sol', code); const extendsRefs = result.unresolvedReferences.filter( (r) => r.referenceKind === 'extends' && r.referenceName === 'IVault' ); expect(extendsRefs).toHaveLength(1); const vaultNode = result.nodes.find((n) => n.kind === 'class' && n.name === 'Vault'); expect(extendsRefs[0]?.fromNodeId).toBe(vaultNode?.id); }); }); describe('Method extraction', () => { it('should extract methods, modifiers, and constructor with signatures', () => { const result = extractFromSource('Vault.sol', code); const methods = result.nodes.filter((n) => n.kind === 'method'); const names = methods.map((n) => n.name); expect(names).toContain('deposit'); expect(names).toContain('withdraw'); expect(names).toContain('add'); expect(names).toContain('onlyOwner'); // modifier_definition expect(names).toContain('constructor'); // constructor_definition (synthetic name) // Signature should capture parameters + visibility + state mutability + return type. const add = methods.find((m) => m.name === 'add'); expect(add?.signature).toContain('uint256 a'); expect(add?.signature).toContain('internal'); expect(add?.signature).toContain('pure'); expect(add?.signature).toContain('returns (uint256)'); // `external` visibility should map to 'public' (callable from outside the contract). const deposit = methods.find((m) => m.name === 'deposit'); expect(deposit?.visibility).toBe('public'); }); }); describe('Struct, enum, and field extraction', () => { it('should extract struct, enum, and enum members', () => { const result = extractFromSource('Vault.sol', code); expect(result.nodes.find((n) => n.kind === 'struct' && n.name === 'UserInfo')).toBeDefined(); expect(result.nodes.find((n) => n.kind === 'enum' && n.name === 'Status')).toBeDefined(); const enumMembers = result.nodes.filter((n) => n.kind === 'enum_member').map((n) => n.name); expect(enumMembers).toEqual(expect.arrayContaining(['Active', 'Frozen', 'Closed'])); }); it('should extract state variables, struct members, events, errors as fields', () => { const result = extractFromSource('Vault.sol', code); const fieldNames = result.nodes.filter((n) => n.kind === 'field').map((n) => n.name); // state variables expect(fieldNames).toEqual(expect.arrayContaining(['token', 'users', 'owner'])); // struct members expect(fieldNames).toEqual(expect.arrayContaining(['balance', 'lastDeposit'])); // event + error expect(fieldNames).toEqual(expect.arrayContaining(['Deposited', 'Withdrawn', 'NotOwner'])); }); it('should treat `constant_variable_declaration` as a constant, not a variable', () => { const constCode = `// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; uint256 constant FILE_CONST = 42; `; const result = extractFromSource('consts.sol', constCode); const node = result.nodes.find((n) => n.name === 'FILE_CONST'); expect(node?.kind).toBe('constant'); }); }); describe('Import and call extraction', () => { it('should extract import directives with the source path as the module name', () => { const result = extractFromSource('Vault.sol', code); const imp = result.nodes.find((n) => n.kind === 'import'); expect(imp).toBeDefined(); expect(imp?.name).toBe('@openzeppelin/contracts/token/ERC20/IERC20.sol'); }); it('should produce calls refs for emit, revert, and library/method calls', () => { const result = extractFromSource('Vault.sol', code); const calls = result.unresolvedReferences .filter((r) => r.referenceKind === 'calls') .map((r) => r.referenceName); // emit Deposited(...) expect(calls).toContain('Deposited'); // revert NotOwner() expect(calls).toContain('NotOwner'); // library call: balance.add(amount) — receiver-qualified expect(calls.some((c) => c === 'add' || c === 'balance.add')).toBe(true); }); it('should produce calls refs for modifier invocations and base-constructor invocations', () => { // `withdraw(...) external onlyOwner` — the modifier sits in the function // header, outside the body: field the call walker descends, so it goes // through the decorator-position walk. It must emit `calls` (not // `decorates`) so flow traversal rides the withdraw → onlyOwner → // NotOwner audit path. const result = extractFromSource('Vault.sol', code); const withdrawNode = result.nodes.find((n) => n.kind === 'method' && n.name === 'withdraw'); const modifierCall = result.unresolvedReferences.find( (r) => r.referenceKind === 'calls' && r.referenceName === 'onlyOwner' ); expect(modifierCall).toBeDefined(); expect(modifierCall?.fromNodeId).toBe(withdrawNode?.id); // Base-constructor invocation parses as the same modifier_invocation // node: `constructor(address o) ERC20("T", "TOK") Ownable(o)` — the // constructor-chain hop. const ctorCode = `pragma solidity ^0.8.20; contract MyToken is ERC20, Ownable { constructor(address o) ERC20("Tok", "TOK") Ownable(o) {} function grab(bytes32 r) external onlyRole(ADMIN_ROLE) returns (uint256) { return 1; } } `; const ctorResult = extractFromSource('MyToken.sol', ctorCode); const ctorCalls = ctorResult.unresolvedReferences .filter((r) => r.referenceKind === 'calls') .map((r) => r.referenceName); expect(ctorCalls).toEqual(expect.arrayContaining(['ERC20', 'Ownable'])); // modifier WITH arguments still resolves to the bare modifier name expect(ctorCalls).toContain('onlyRole'); }); }); }); describe('Regression: issue-specific extraction fixes', () => { it('indexes inner functions of an anonymous AMD/CommonJS module wrapper (#528)', () => { const code = ` define(['dep'], function (dep) { function innerHelper(x) { return x + 1; } function compute(y) { return innerHelper(y); } return { compute: compute }; }); `; const result = extractFromSource('amd-module.js', code); const fns = result.nodes.filter((n) => n.kind === 'function').map((n) => n.name); expect(fns).toContain('innerHelper'); expect(fns).toContain('compute'); }); it('attaches Go methods on generic receivers to their type (#583)', () => { const code = ` package main type Stack[T any] struct { items []T } func (s *Stack[T]) Push(v T) { s.items = append(s.items, v) } func (s Stack[T]) Len() int { return len(s.items) } `; const result = extractFromSource('stack.go', code); const methods = result.nodes.filter((n) => n.kind === 'method'); expect(methods.find((m) => m.name === 'Push')?.qualifiedName).toBe('Stack::Push'); expect(methods.find((m) => m.name === 'Len')?.qualifiedName).toBe('Stack::Len'); }); it('indexes new module extensions: .mts/.cts (TS) and .xsjs/.xsjslib (JS) (#366, #556)', () => { expect(isSourceFile('mod.mts')).toBe(true); expect(isSourceFile('mod.cts')).toBe(true); expect(isSourceFile('service.xsjs')).toBe(true); expect(isSourceFile('lib.xsjslib')).toBe(true); expect(detectLanguage('mod.mts')).toBe('typescript'); expect(detectLanguage('service.xsjs')).toBe('javascript'); // End-to-end: a .mts file is parsed as TS, a .xsjs file as JS. const ts = extractFromSource('mod.mts', 'export function hello(): number { return 1; }'); expect(ts.nodes.find((n) => n.name === 'hello' && n.kind === 'function')).toBeDefined(); const js = extractFromSource('service.xsjs', 'function handleRequest() { return 1; }'); expect(js.nodes.find((n) => n.name === 'handleRequest' && n.kind === 'function')).toBeDefined(); }); }); describe('Import / re-export dependency linking (blast-radius recall)', () => { // An import IS a dependency, but extraction only emits references for calls, // instantiations, type annotations, and inheritance — so a symbol imported and // then merely re-exported, placed in a registry array, passed as an argument, // or used in JSX produced no cross-file edge, leaving the providing file with a // false "0 dependents". These tests pin the import/re-export binding linking. it('emits an imports reference per named, aliased, and default import binding', () => { const code = ` import { widget, helper as h } from './foo'; import Thing from './thing'; import * as NS from './ns'; export const registry = [widget]; `; const result = extractFromSource('bar.ts', code); const names = result.unresolvedReferences .filter((r) => r.referenceKind === 'imports') .map((r) => r.referenceName); expect(names).toContain('widget'); // named import → local name expect(names).toContain('h'); // aliased import → local alias expect(names).toContain('Thing'); // default import expect(names).toContain('NS'); // namespace import → linked to the module file as a dependency }); it('emits an imports reference per re-exported binding', () => { const result = extractFromSource('barrel.ts', `export { alpha, beta as b } from './source';`); const names = result.unresolvedReferences .filter((r) => r.referenceKind === 'imports') .map((r) => r.referenceName); // Re-export links the SOURCE-side name, not the local alias. expect(names).toContain('alpha'); expect(names).toContain('beta'); }); it('a value imported/re-exported but never called still makes the importer a dependent', async () => { const dir = createTempDir(); try { fs.mkdirSync(path.join(dir, 'src'), { recursive: true }); fs.writeFileSync( path.join(dir, 'src', 'foo.ts'), `export const widget = { n: 1 };\nexport function helper(): void {}\n` ); // bar uses widget ONLY in an array and re-exports helper — neither is // called/typed, so before import-linking bar had no edge to foo at all. fs.writeFileSync( path.join(dir, 'src', 'bar.ts'), `import { widget } from './foo';\nexport { helper } from './foo';\nexport const registry = [widget];\n` ); const cg = CodeGraph.initSync(dir, { config: { include: ['src/**/*.ts'], exclude: [] } }); await cg.indexAll(); cg.resolveReferences(); expect(cg.getFileDependents('src/foo.ts')).toContain('src/bar.ts'); cg.destroy(); } finally { cleanupTempDir(dir); } }); it('a namespace import touched only via a value-member read still links the module file', async () => { const dir = createTempDir(); try { fs.mkdirSync(path.join(dir, 'src'), { recursive: true }); fs.writeFileSync(path.join(dir, 'src', 'foo.ts'), `export const SOME_CONST = 42;\n`); // `foo` is imported as a namespace and used ONLY via a value-member read // (no call, no type) — `foo.helper()` would link on its own, but a bare // `foo.SOME_CONST` would not, so the module-import backstop must link it. fs.writeFileSync(path.join(dir, 'src', 'bar.ts'), `import * as foo from './foo';\nexport const x = foo.SOME_CONST;\n`); const cg = CodeGraph.initSync(dir, { config: { include: ['src/**/*.ts'], exclude: [] } }); await cg.indexAll(); cg.resolveReferences(); expect(cg.getFileDependents('src/foo.ts')).toContain('src/bar.ts'); cg.destroy(); } finally { cleanupTempDir(dir); } }); }); describe('Python import dependency linking (blast-radius recall)', () => { // Same recall gap as TS: Python only linked called/instantiated imports, so a // name brought in with `from module import X` and then merely stored, used as // a decorator/argument, or re-exported through an `__init__.py` produced no // cross-file edge — the providing module showed a false "0 dependents". it('emits an imports reference per name in a `from module import ...` (incl. value/aliased)', () => { const code = [ 'from foo import helper, widget', 'from foo import Thing as T', 'from . import sibling', 'from bar import *', ].join('\n'); const names = extractFromSource('mod.py', code) .unresolvedReferences.filter((r) => r.referenceKind === 'imports') .map((r) => r.referenceName); expect(names).toContain('helper'); expect(names).toContain('widget'); // value import expect(names).toContain('T'); // aliased import → local name expect(names).toContain('sibling'); // `from . import ` expect(names).not.toContain('*'); // wildcard import has no names }); it('a Python value imported but never called still makes the importer a dependent', async () => { const dir = createTempDir(); try { fs.mkdirSync(path.join(dir, 'pkg'), { recursive: true }); fs.writeFileSync(path.join(dir, 'pkg', 'foo.py'), `widget = {"n": 1}\ndef helper():\n return 1\n`); // bar imports widget+helper but only stores widget in a list — nothing is // called, so before import-linking bar had no edge to foo. fs.writeFileSync(path.join(dir, 'pkg', 'bar.py'), `from foo import widget, helper\nregistry = [widget]\n`); const cg = CodeGraph.initSync(dir, { config: { include: ['pkg/**/*.py'], exclude: [] } }); await cg.indexAll(); cg.resolveReferences(); expect(cg.getFileDependents('pkg/foo.py')).toContain('pkg/bar.py'); cg.destroy(); } finally { cleanupTempDir(dir); } }); it('resolves `from . import submodule` + `submodule.func()` to the submodule', async () => { const dir = createTempDir(); try { fs.mkdirSync(path.join(dir, 'pkg'), { recursive: true }); fs.writeFileSync(path.join(dir, 'pkg', '__init__.py'), ''); fs.writeFileSync(path.join(dir, 'pkg', 'certs.py'), `def where():\n return "/ca.pem"\n`); // certs is an imported MODULE (a file), and certs.where() is a qualified // call through it — the receiver isn't a symbol, so plain name-matching // can't link it. Also exercises the Python relative-dot path fix (`.certs`). fs.writeFileSync(path.join(dir, 'pkg', 'utils.py'), `from . import certs\ndef go():\n return certs.where()\n`); const cg = CodeGraph.initSync(dir, { config: { include: ['pkg/**/*.py'], exclude: [] } }); await cg.indexAll(); cg.resolveReferences(); expect(cg.getFileDependents('pkg/certs.py')).toContain('pkg/utils.py'); cg.destroy(); } finally { cleanupTempDir(dir); } }); it('a module import is a dependency even when the used member is re-exported elsewhere', async () => { const dir = createTempDir(); try { fs.mkdirSync(path.join(dir, 'pkg'), { recursive: true }); fs.writeFileSync(path.join(dir, 'pkg', '__init__.py'), ''); // `where` is NOT defined in certs.py (re-exported from a 3rd-party pkg), so // member resolution can't find it — the module-import backstop must still // record utils -> certs. (Mirrors requests' real `certs.where`.) fs.writeFileSync(path.join(dir, 'pkg', 'certs.py'), `from external_ca import where\n`); fs.writeFileSync(path.join(dir, 'pkg', 'utils.py'), `from . import certs\nCA = certs.where()\n`); const cg = CodeGraph.initSync(dir, { config: { include: ['pkg/**/*.py'], exclude: [] } }); await cg.indexAll(); cg.resolveReferences(); expect(cg.getFileDependents('pkg/certs.py')).toContain('pkg/utils.py'); cg.destroy(); } finally { cleanupTempDir(dir); } }); }); describe('Go cross-package composite literals (blast-radius recall)', () => { // Go function calls and type references across packages already resolved, but // struct composite literals — `render.XML{...}` / `pkga.Widget{...}` — were not // extracted at all, so a package whose types are only INSTANTIATED elsewhere // (gin's render/binding implementations) showed 0 dependents. it('links a cross-package struct composite literal to the defining package', async () => { const dir = createTempDir(); try { fs.writeFileSync(path.join(dir, 'go.mod'), 'module example.com/proj\n\ngo 1.21\n'); fs.mkdirSync(path.join(dir, 'render'), { recursive: true }); fs.writeFileSync(path.join(dir, 'render', 'xml.go'), `package render\n\ntype XML struct { Data any }\n`); fs.writeFileSync(path.join(dir, 'app.go'), `package main\n\nimport "example.com/proj/render"\n\nfunc handle() any { return render.XML{} }\n`); const cg = CodeGraph.initSync(dir, { config: { include: ['**/*.go'], exclude: [] } }); await cg.indexAll(); cg.resolveReferences(); expect(cg.getFileDependents('render/xml.go')).toContain('app.go'); cg.destroy(); } finally { cleanupTempDir(dir); } }); it('links a composite literal in a package-level var registry', async () => { const dir = createTempDir(); try { fs.writeFileSync(path.join(dir, 'go.mod'), 'module example.com/proj\n\ngo 1.21\n'); fs.mkdirSync(path.join(dir, 'render'), { recursive: true }); fs.writeFileSync(path.join(dir, 'render', 'xml.go'), `package render\n\ntype XML struct {}\nfunc (XML) Render() {}\n`); // The implementation is registered only in a top-level `var registry = {...}` // map literal — the body walker doesn't cover top-level declarations, so this // exercises the var-initializer walking added for Go. fs.writeFileSync(path.join(dir, 'reg.go'), `package main\n\nimport "example.com/proj/render"\n\ntype R interface { Render() }\n\nvar registry = map[string]R{ "xml": render.XML{} }\n`); const cg = CodeGraph.initSync(dir, { config: { include: ['**/*.go'], exclude: [] } }); await cg.indexAll(); cg.resolveReferences(); expect(cg.getFileDependents('render/xml.go')).toContain('reg.go'); cg.destroy(); } finally { cleanupTempDir(dir); } }); it('attributes a call inside a top-level closure (cobra RunE) to the var, not the file (#693)', async () => { const dir = createTempDir(); try { fs.writeFileSync(path.join(dir, 'go.mod'), 'module example.com/proj\n\ngo 1.21\n'); // Wire is called ONLY from the anonymous RunE closure inside a top-level // `var rootCmd = &Cmd{...}` — previously the call leaked to the file node, // so `callers(Wire)` surfaced a file (or read as "no caller"). It must now // attribute to the enclosing var. fs.writeFileSync(path.join(dir, 'factory.go'), `package main\n\nfunc Wire() error { return nil }\n`); fs.writeFileSync( path.join(dir, 'root.go'), `package main\n\ntype Cmd struct{ RunE func() error }\n\nvar rootCmd = &Cmd{\n\tRunE: func() error { return Wire() },\n}\n` ); const cg = CodeGraph.initSync(dir, { config: { include: ['**/*.go'], exclude: [] } }); await cg.indexAll(); cg.resolveReferences(); const wire = cg.getNodesByName('Wire').find((n) => n.kind === 'function'); expect(wire).toBeDefined(); const callers = cg.getCallers(wire!.id).map((c) => c.node); expect(callers.some((n) => n.kind === 'variable' && n.name === 'rootCmd')).toBe(true); expect(callers.some((n) => n.kind === 'file')).toBe(false); cg.destroy(); } finally { cleanupTempDir(dir); } }); it('links a parenthesized pointer type conversion `(*T)(x)` to the type', async () => { const dir = createTempDir(); try { fs.writeFileSync(path.join(dir, 'go.mod'), 'module example.com/proj\n\ngo 1.21\n'); fs.writeFileSync(path.join(dir, 'types.go'), `package main\n\ntype Wrapped struct { N int }\n`); // `(*Wrapped)(x)` parses as a call whose callee is the parenthesized type // `(*Wrapped)` — without normalization it dropped on the floor. fs.writeFileSync(path.join(dir, 'use.go'), `package main\n\nfunc run(x *int) { _ = (*Wrapped)(x) }\n`); const cg = CodeGraph.initSync(dir, { config: { include: ['**/*.go'], exclude: [] } }); await cg.indexAll(); cg.resolveReferences(); expect(cg.getFileDependents('types.go')).toContain('use.go'); cg.destroy(); } finally { cleanupTempDir(dir); } }); it('links an implementation reached only through a Go interface (implicit satisfaction, #584)', async () => { const dir = createTempDir(); try { fs.writeFileSync(path.join(dir, 'go.mod'), 'module example.com/proj\n\ngo 1.21\n'); fs.mkdirSync(path.join(dir, 'codec'), { recursive: true }); fs.writeFileSync(path.join(dir, 'codec', 'api.go'), `package codec\n\ntype Core interface {\n\tMarshal(v any) ([]byte, error)\n}\n\nvar API Core\n`); // jsonApi satisfies Core structurally (no `implements` keyword) and is // reached ONLY through the interface (API.Marshal). Without implicit // interface satisfaction + dispatch, json.go shows 0 dependents. fs.writeFileSync(path.join(dir, 'codec', 'json.go'), `package codec\n\ntype jsonApi struct{}\n\nfunc (j jsonApi) Marshal(v any) ([]byte, error) { return nil, nil }\n\nfunc init() { API = jsonApi{} }\n`); const cg = CodeGraph.initSync(dir, { config: { include: ['**/*.go'], exclude: [] } }); await cg.indexAll(); cg.resolveReferences(); expect(cg.getFileDependents('codec/json.go')).toContain('codec/api.go'); cg.destroy(); } finally { cleanupTempDir(dir); } }); }); describe('C# records (blast-radius recall)', () => { // Records are ubiquitous in modern C# (DTOs, value objects, CQRS messages), // but `record` / `record struct` declarations weren't extracted as types — so // every reference, generic-type-argument, and `new` of a record dropped on the // floor and the defining file showed 0 dependents. (#237) it('extracts a record as a graph node (record class + record struct)', () => { const r = extractFromSource('r.cs', `namespace P;\npublic record Box(int N);\npublic record struct Pt(int X);\n`); expect(r.nodes.find((n) => n.name === 'Box' && (n.kind === 'class' || n.kind === 'struct'))).toBeDefined(); expect(r.nodes.find((n) => n.name === 'Pt' && (n.kind === 'class' || n.kind === 'struct'))).toBeDefined(); }); it('resolves references / instantiations of a record across files', async () => { const dir = createTempDir(); try { fs.writeFileSync(path.join(dir, 'types.cs'), `namespace P;\npublic record Box(int N);\n`); // Box is used as a generic type argument and instantiated — both require // Box to be a node to resolve. fs.writeFileSync( path.join(dir, 'use.cs'), `using System.Collections.Generic;\nnamespace P;\npublic class User {\n public IEnumerable Boxes { get; }\n public Box Make() => new Box(1);\n}\n` ); const cg = CodeGraph.initSync(dir, { config: { include: ['**/*.cs'], exclude: [] } }); await cg.indexAll(); cg.resolveReferences(); expect(cg.getFileDependents('types.cs')).toContain('use.cs'); cg.destroy(); } finally { cleanupTempDir(dir); } }); }); describe('Rust cross-module recall', () => { function rustProject(files: Record): string { const dir = createTempDir(); fs.writeFileSync(path.join(dir, 'Cargo.toml'), '[package]\nname = "proj"\nversion = "0.1.0"\nedition = "2021"\n'); fs.mkdirSync(path.join(dir, 'src'), { recursive: true }); for (const [rel, content] of Object.entries(files)) { const full = path.join(dir, 'src', rel); fs.mkdirSync(path.dirname(full), { recursive: true }); fs.writeFileSync(full, content); } return dir; } it('extracts a struct literal `Foo { .. }` as an instantiation across modules', async () => { const dir = rustProject({ 'lib.rs': 'pub mod types;\npub mod consumer;\n', 'types.rs': 'pub struct Widget { pub n: i32 }\n', 'consumer.rs': 'use crate::types::Widget;\npub fn build() -> Widget { Widget { n: 1 } }\n', }); try { const cg = CodeGraph.initSync(dir, { config: { include: ['src/**/*.rs'], exclude: [] } }); await cg.indexAll(); cg.resolveReferences(); expect(cg.getFileDependents('src/types.rs')).toContain('src/consumer.rs'); cg.destroy(); } finally { cleanupTempDir(dir); } }); it('extracts trait method declarations and bridges trait dispatch to the impl', async () => { const dir = rustProject({ 'lib.rs': 'pub mod types;\npub mod consumer;\n', 'types.rs': 'pub trait Render { fn render(&self) -> i32; }\n', // Mine implements Render structurally; reached via &dyn Render dispatch. 'consumer.rs': 'use crate::types::Render;\npub struct Mine { pub x: i32 }\nimpl Render for Mine { fn render(&self) -> i32 { self.x } }\n', }); try { const cg = CodeGraph.initSync(dir, { config: { include: ['src/**/*.rs'], exclude: [] } }); await cg.indexAll(); cg.resolveReferences(); // implements edge (Mine -> Render) makes types.rs a dependent of consumer.rs's struct. expect(cg.getFileDependents('src/types.rs')).toContain('src/consumer.rs'); cg.destroy(); } finally { cleanupTempDir(dir); } }); it('links `pub use` re-export hubs to the modules they re-export', async () => { const dir = rustProject({ 'lib.rs': 'pub mod api;\n', 'api/mod.rs': 'mod widget;\npub use self::widget::Widget;\n', 'api/widget.rs': 'pub struct Widget { pub n: i32 }\n', }); try { const cg = CodeGraph.initSync(dir, { config: { include: ['src/**/*.rs'], exclude: [] } }); await cg.indexAll(); cg.resolveReferences(); // The re-export hub depends on the module it re-exports from. expect(cg.getFileDependents('src/api/widget.rs')).toContain('src/api/mod.rs'); cg.destroy(); } finally { cleanupTempDir(dir); } }); it('resolves a qualified path to the correct module when the leaf name collides', async () => { const dir = rustProject({ 'lib.rs': 'pub mod fast;\npub mod slow;\npub mod hub;\n', 'fast.rs': 'pub fn read() -> i32 { 1 }\n', 'slow.rs': 'pub fn read() -> i32 { 2 }\n', // `read` exists in BOTH fast.rs and slow.rs — module-path resolution must // send this re-export to fast.rs specifically, not name-match either. 'hub.rs': 'pub use crate::fast::read;\n', }); try { const cg = CodeGraph.initSync(dir, { config: { include: ['src/**/*.rs'], exclude: [] } }); await cg.indexAll(); cg.resolveReferences(); expect(cg.getFileDependents('src/fast.rs')).toContain('src/hub.rs'); expect(cg.getFileDependents('src/slow.rs')).not.toContain('src/hub.rs'); cg.destroy(); } finally { cleanupTempDir(dir); } }); }); describe('Java annotations (blast-radius recall)', () => { it('indexes @interface definitions and links @Annotation usages to them', async () => { const dir = createTempDir(); try { fs.mkdirSync(path.join(dir, 'p'), { recursive: true }); // The annotation DEFINITION must be a node, and the @MyAnno usages (which // live inside a `modifiers` node on the class/field/method) must extract. fs.writeFileSync(path.join(dir, 'p', 'MyAnno.java'), `package p;\npublic @interface MyAnno { String value() default ""; }\n`); fs.writeFileSync( path.join(dir, 'p', 'User.java'), `package p;\n@MyAnno("c")\npublic class User {\n @MyAnno("f") int field;\n @MyAnno("m") void go() {}\n}\n` ); const cg = CodeGraph.initSync(dir, { config: { include: ['**/*.java'], exclude: [] } }); await cg.indexAll(); cg.resolveReferences(); expect(cg.getFileDependents('p/MyAnno.java')).toContain('p/User.java'); cg.destroy(); } finally { cleanupTempDir(dir); } }); }); describe('Swift property wrappers / attributes (blast-radius recall)', () => { it('links a @propertyWrapper usage to the wrapper type', async () => { const dir = createTempDir(); try { fs.mkdirSync(path.join(dir, 'Sources', 'M'), { recursive: true }); fs.writeFileSync(path.join(dir, 'Sources', 'M', 'Wrap.swift'), `@propertyWrapper\npublic struct Argument { public var wrappedValue: T }\n`); // `@Argument` is a Swift attribute on a stored property — it lives in the // property's `modifiers` and Swift doesn't extract instance properties as // their own nodes, so without the fix the wrapper type has no users. fs.writeFileSync(path.join(dir, 'Sources', 'M', 'Cmd.swift'), `public struct MyCommand {\n @Argument var name: String\n @Argument var count: Int\n}\n`); const cg = CodeGraph.initSync(dir, { config: { include: ['Sources/**/*.swift'], exclude: [] } }); await cg.indexAll(); cg.resolveReferences(); expect(cg.getFileDependents('Sources/M/Wrap.swift')).toContain('Sources/M/Cmd.swift'); cg.destroy(); } finally { cleanupTempDir(dir); } }); }); describe('R Extraction', () => { describe('Language detection', () => { it('should detect R files (both extension cases)', () => { expect(detectLanguage('analysis.R')).toBe('r'); expect(detectLanguage('scripts/clean.r')).toBe('r'); }); it('should report R as supported', () => { expect(isLanguageSupported('r')).toBe(true); expect(getSupportedLanguages()).toContain('r'); }); }); describe('Function extraction', () => { it('extracts every assignment form, lambdas, and nested functions', () => { const code = ` clean_data <- function(df, threshold = 0.5) { helper <- function(d) scale(d) helper(df) } normalize = function(v) (v - mean(v)) / sd(v) double_it <- \\(x) x * 2 `; const result = extractFromSource('analysis.R', code); const funcs = result.nodes.filter((n) => n.kind === 'function').map((n) => n.name); expect(funcs).toContain('clean_data'); expect(funcs).toContain('normalize'); expect(funcs).toContain('double_it'); expect(funcs).toContain('helper'); // nested, inside clean_data's scope const cleanData = result.nodes.find((n) => n.name === 'clean_data'); expect(cleanData?.language).toBe('r'); expect(cleanData?.signature).toBe('(df, threshold = 0.5)'); }); it('attributes body calls to the enclosing function', () => { const code = ` prep <- function(d) scale(d) fit_model <- function(data) { lm(y ~ x, data = prep(data)) } `; const result = extractFromSource('models.R', code); const prepCall = result.unresolvedReferences.find( (r) => r.referenceName === 'prep' && r.referenceKind === 'calls' ); expect(prepCall).toBeDefined(); const fitModel = result.nodes.find((n) => n.name === 'fit_model'); expect(prepCall?.fromNodeId).toBe(fitModel?.id); }); }); describe('Imports', () => { it('extracts library/require/source as imports, not calls', () => { const code = ` library(dplyr) require(stats) requireNamespace("jsonlite") source("helpers.R") `; const result = extractFromSource('main.R', code); const imports = result.nodes.filter((n) => n.kind === 'import').map((n) => n.name); expect(imports).toContain('dplyr'); expect(imports).toContain('stats'); expect(imports).toContain('jsonlite'); expect(imports).toContain('helpers.R'); // Claimed by the hook — no call references to the import machinery. const libCalls = result.unresolvedReferences.filter( (r) => r.referenceKind === 'calls' && (r.referenceName === 'library' || r.referenceName === 'source') ); expect(libCalls).toHaveLength(0); }); }); describe('Variables and constants', () => { it('extracts top-level assignments; ALL_CAPS as constants; right-assign too', () => { const code = ` ALPHA <- 0.05 max_iter = 100 compute_stats(df) -> stats_result inner <- function() { local_var <- 1 } `; const result = extractFromSource('config.R', code); const constant = result.nodes.find((n) => n.name === 'ALPHA'); expect(constant?.kind).toBe('constant'); const variable = result.nodes.find((n) => n.name === 'max_iter'); expect(variable?.kind).toBe('variable'); const rightAssigned = result.nodes.find((n) => n.name === 'stats_result'); expect(rightAssigned?.kind).toBe('variable'); // Locals inside functions are deliberately NOT extracted. expect(result.nodes.find((n) => n.name === 'local_var')).toBeUndefined(); }); }); describe('Classes', () => { it('extracts S4/R5/R6 class calls as classes with their list methods', () => { const code = ` setClass("Patient", representation(id = "character")) Account <- setRefClass("Account", fields = list(balance = "numeric"), methods = list(deposit = function(x) { balance <<- balance + x }) ) Stack <- R6Class("Stack", public = list(push = function(v) invisible(v)) ) setGeneric("describe", function(obj) standardGeneric("describe")) setMethod("describe", "Patient", function(obj) paste(obj@id)) `; const result = extractFromSource('classes.R', code); const classes = result.nodes.filter((n) => n.kind === 'class').map((n) => n.name); expect(classes).toContain('Patient'); expect(classes).toContain('Account'); expect(classes).toContain('Stack'); const methods = result.nodes.filter((n) => n.kind === 'method').map((n) => n.name); expect(methods).toContain('deposit'); expect(methods).toContain('push'); // setGeneric/setMethod produce functions named by their string argument. const describes = result.nodes.filter((n) => n.name === 'describe' && n.kind === 'function'); expect(describes.length).toBeGreaterThanOrEqual(2); // The class-assignment idiom must not ALSO produce a variable node. expect(result.nodes.find((n) => n.name === 'Account' && n.kind === 'variable')).toBeUndefined(); }); it('extracts ggproto classes with direct-arg methods and the parent as extends', () => { // ggplot2's OO system — every Geom/Stat/Scale in the ecosystem uses it. const code = ` GeomPoint <- ggproto("GeomPoint", Geom, required_aes = c("x", "y"), draw_panel = function(data, panel_params, coord) { coords <- coord$transform(data, panel_params) grid::pointsGrob(coords$x, coords$y) }, draw_key = draw_key_point ) `; const result = extractFromSource('geom-point.R', code); const cls = result.nodes.find((n) => n.name === 'GeomPoint' && n.kind === 'class'); expect(cls).toBeDefined(); const method = result.nodes.find((n) => n.name === 'draw_panel' && n.kind === 'method'); expect(method).toBeDefined(); const ext = result.unresolvedReferences.find( (r) => r.referenceKind === 'extends' && r.referenceName === 'Geom' ); expect(ext?.fromNodeId).toBe(cls?.id); // No twin variable for the assignment. expect(result.nodes.find((n) => n.name === 'GeomPoint' && n.kind === 'variable')).toBeUndefined(); }); }); }); // ============================================================================= // CFML (ColdFusion Markup Language — .cfc/.cfm tag-based and bare-script, .cfs) // ============================================================================= describe('CFML Extraction', () => { describe('Language detection', () => { it('should detect .cfc/.cfm as cfml and .cfs as cfscript', () => { expect(detectLanguage('Service.cfc')).toBe('cfml'); expect(detectLanguage('index.cfm')).toBe('cfml'); expect(detectLanguage('Helper.cfs')).toBe('cfscript'); }); it('should report cfml and cfscript as supported', () => { expect(isLanguageSupported('cfml')).toBe(true); expect(isLanguageSupported('cfscript')).toBe(true); expect(getSupportedLanguages()).toContain('cfml'); expect(getSupportedLanguages()).toContain('cfscript'); }); }); describe('Bare-script .cfc (component { ... })', () => { const code = ` component extends="BaseService" implements="IService" { property name="name" type="string"; function init(required string name) { variables.name = arguments.name; return this; } public string function getName() { return variables.name; } private void function logSomething(required string msg) { writeLog(text=msg); } } `; it('should name the component from the file name (the grammar has no name field)', () => { const result = extractFromSource('SampleService.cfc', code); const cls = result.nodes.find((n) => n.kind === 'class'); expect(cls).toBeDefined(); expect(cls?.name).toBe('SampleService'); expect(cls?.language).toBe('cfml'); }); it('should extract methods with visibility and contains edges to the class', () => { const result = extractFromSource('SampleService.cfc', code); const cls = result.nodes.find((n) => n.kind === 'class'); const methods = result.nodes.filter((n) => n.kind === 'method'); expect(methods.map((m) => m.name)).toEqual( expect.arrayContaining(['init', 'getName', 'logSomething']) ); const logSomething = methods.find((m) => m.name === 'logSomething'); expect(logSomething?.visibility).toBe('private'); const containsLog = result.edges.find( (e) => e.source === cls?.id && e.target === logSomething?.id && e.kind === 'contains' ); expect(containsLog).toBeDefined(); }); it('should extract extends/implements as unresolved references from the class', () => { const result = extractFromSource('SampleService.cfc', code); const cls = result.nodes.find((n) => n.kind === 'class'); const extendsRef = result.unresolvedReferences.find((r) => r.referenceKind === 'extends'); expect(extendsRef?.referenceName).toBe('BaseService'); expect(extendsRef?.fromNodeId).toBe(cls?.id); const implRef = result.unresolvedReferences.find((r) => r.referenceKind === 'implements'); expect(implRef?.referenceName).toBe('IService'); expect(implRef?.fromNodeId).toBe(cls?.id); }); }); describe('Standalone .cfs (pure CFScript)', () => { it('should also name an anonymous component from the file name', () => { const code = ` component { function ping() { return "pong"; } } `; const result = extractFromSource('Sample.cfs', code); const cls = result.nodes.find((n) => n.kind === 'class'); expect(cls).toBeDefined(); expect(cls?.name).toBe('Sample'); expect(cls?.language).toBe('cfscript'); }); it('should extract top-level imports with no enclosing component', () => { const code = ` import com.foo.Bar; import foo.cfm; `; const result = extractFromSource('Includes.cfs', code); const imports = result.nodes.filter((n) => n.kind === 'import').map((n) => n.name); expect(imports).toContain('com.foo.Bar'); expect(imports).toContain('foo.cfm'); }); }); describe('Tag-based .cfc (/)', () => { const code = ` \t \t\t \t \t \t\t \t\t\tvar x = helper(); \t\t\tanotherCall(x); \t\t \t `; it('should name the component from the file name when the tag has no name attribute', () => { const result = extractFromSource('TagStyle.cfc', code); const cls = result.nodes.find((n) => n.kind === 'class'); expect(cls?.name).toBe('TagStyle'); expect(cls?.language).toBe('cfml'); }); it('should prefer an explicit name attribute on the cfcomponent tag', () => { const named = `\n\n`; const result = extractFromSource('File.cfc', named); expect(result.nodes.find((n) => n.kind === 'class')?.name).toBe('ExplicitName'); }); it('should extract cffunction tags as methods with access-derived visibility', () => { const result = extractFromSource('TagStyle.cfc', code); const methods = result.nodes.filter((n) => n.kind === 'method'); expect(methods.map((m) => m.name)).toEqual(expect.arrayContaining(['getName', 'doWork'])); const getName = methods.find((m) => m.name === 'getName'); expect(getName?.visibility).toBe('public'); expect(getName?.returnType).toBe('string'); const doWork = methods.find((m) => m.name === 'doWork'); expect(doWork?.visibility).toBe('private'); }); it('should not double-extract symbols from the component body (implicit-end-tag walk)', () => { const result = extractFromSource('TagStyle.cfc', code); const methods = result.nodes.filter((n) => n.kind === 'method' && n.name === 'getName'); expect(methods).toHaveLength(1); const doWorkMethods = result.nodes.filter((n) => n.kind === 'method' && n.name === 'doWork'); expect(doWorkMethods).toHaveLength(1); }); it('should delegate tag bodies to the cfscript grammar and attribute calls to the enclosing method', () => { const result = extractFromSource('TagStyle.cfc', code); const doWork = result.nodes.find((n) => n.kind === 'method' && n.name === 'doWork'); const helperCall = result.unresolvedReferences.find( (r) => r.referenceKind === 'calls' && r.referenceName === 'helper' ); expect(helperCall?.fromNodeId).toBe(doWork?.id); }); it('should produce exactly one correctly-ranged file node, not a leaked snippet-scoped one', () => { const result = extractFromSource('TagStyle.cfc', code); const fileNodes = result.nodes.filter((n) => n.kind === 'file'); expect(fileNodes).toHaveLength(1); expect(fileNodes[0].startLine).toBe(1); const cls = result.nodes.find((n) => n.kind === 'class'); const containsClass = result.edges.find( (e) => e.source === fileNodes[0].id && e.target === cls?.id && e.kind === 'contains' ); expect(containsClass).toBeDefined(); }); }); describe('Top-level cffunction with no enclosing cfcomponent (.cfm template)', () => { it('should extract as a top-level function contained by the file', () => { const code = ` \t `; const result = extractFromSource('helper.cfm', code); const fn = result.nodes.find((n) => n.kind === 'function' && n.name === 'helper'); expect(fn).toBeDefined(); const fileNode = result.nodes.find((n) => n.kind === 'file'); const containsFn = result.edges.find( (e) => e.source === fileNode?.id && e.target === fn?.id && e.kind === 'contains' ); expect(containsFn).toBeDefined(); }); }); describe(' nested inside control-flow tags (//)', () => { it('should delegate a body nested inside within a ', () => { const code = ` helper(); `; const result = extractFromSource('Nested.cfc', code); const doStuff = result.nodes.find((n) => n.kind === 'method' && n.name === 'doStuff'); expect(doStuff).toBeDefined(); const helperCall = result.unresolvedReferences.find( (r) => r.referenceKind === 'calls' && r.referenceName === 'helper' ); expect(helperCall?.fromNodeId).toBe(doStuff?.id); }); it('should delegate a body nested inside at top-level component scope', () => { const code = ` topLevelHelper(); `; const result = extractFromSource('Nested2.cfc', code); const cls = result.nodes.find((n) => n.kind === 'class'); expect(cls).toBeDefined(); const helperCall = result.unresolvedReferences.find( (r) => r.referenceKind === 'calls' && r.referenceName === 'topLevelHelper' ); expect(helperCall?.fromNodeId).toBe(cls?.id); }); }); describe(' SQL bodies (cfquery grammar)', () => { it('should extract a call expression embedded in a #hash# inside the SQL body', () => { const code = ` SELECT id, name FROM users WHERE owner = #getCurrentUser().getId()# `; const result = extractFromSource('Query.cfc', code); const getUsers = result.nodes.find((n) => n.kind === 'method' && n.name === 'getUsers'); expect(getUsers).toBeDefined(); const getCurrentUserCall = result.unresolvedReferences.find( (r) => r.referenceKind === 'calls' && r.referenceName === 'getCurrentUser' ); expect(getCurrentUserCall?.fromNodeId).toBe(getUsers?.id); const getIdCall = result.unresolvedReferences.find( (r) => r.referenceKind === 'calls' && r.referenceName === 'getId' ); expect(getIdCall?.fromNodeId).toBe(getUsers?.id); }); }); describe('UTF-8 BOM handling (common in CFML saved by Windows editors)', () => { it('should route a BOM-prefixed tag-based .cfc to the tag grammar, not the script grammar', () => { const code = `\uFEFF\n\n\n\n\n`; const result = extractFromSource('ModuleConfig.cfc', code); const cls = result.nodes.find((n) => n.kind === 'class'); expect(cls?.name).toBe('ModuleConfig'); const configure = result.nodes.find((n) => n.kind === 'method' && n.name === 'configure'); expect(configure).toBeDefined(); }); it('should still treat a BOM-prefixed bare-script .cfc as script', () => { const code = `\uFEFFcomponent {\n function ping() { return "pong"; }\n}\n`; const result = extractFromSource('Ping.cfc', code); expect(result.nodes.find((n) => n.kind === 'class')?.name).toBe('Ping'); expect(result.nodes.find((n) => n.kind === 'method')?.name).toBe('ping'); }); }); describe('Unquoted tag attribute values (legal in older CFML)', () => { it('should extract functions and inheritance from unquoted attributes', () => { const code = `\n\n\n\n\n`; const result = extractFromSource('Unquoted.cfc', code); const doThing = result.nodes.find((n) => n.kind === 'method' && n.name === 'doThing'); expect(doThing).toBeDefined(); expect(doThing?.visibility).toBe('private'); const extendsRef = result.unresolvedReferences.find((r) => r.referenceKind === 'extends'); expect(extendsRef?.referenceName).toBe('Base'); }); }); describe('Functions in a component-level block', () => { it('should classify them as methods of the component (ColdBox ModuleConfig shape)', () => { const code = `\n\nfunction configure() {\n return settings();\n}\nfunction onLoad() {\n return 1;\n}\n\n\n`; const result = extractFromSource('ModuleConfig.cfc', code); const cls = result.nodes.find((n) => n.kind === 'class'); const methods = result.nodes.filter((n) => n.kind === 'method').map((n) => n.name); expect(methods).toEqual(expect.arrayContaining(['configure', 'onLoad'])); expect(result.nodes.filter((n) => n.kind === 'function')).toHaveLength(0); const configure = result.nodes.find((n) => n.kind === 'method' && n.name === 'configure'); const containsEdge = result.edges.find( (e) => e.source === cls?.id && e.target === configure?.id && e.kind === 'contains' ); expect(containsEdge).toBeDefined(); }); it('should keep kind function for a inside a cffunction body', () => { const code = `\n\n\nfunction innerHelper() { return 1; }\n\n\n\n`; const result = extractFromSource('Outer.cfc', code); expect(result.nodes.find((n) => n.name === 'outer')?.kind).toBe('method'); expect(result.nodes.find((n) => n.name === 'innerHelper')?.kind).toBe('function'); }); }); }); describe('COBOL Extraction', () => { it('should detect .cbl/.cob/.cpy as cobol (case-insensitive)', () => { expect(detectLanguage('app/cbl/CBACT01C.cbl')).toBe('cobol'); expect(detectLanguage('app/cbl/CBSTM03A.CBL')).toBe('cobol'); expect(detectLanguage('prog.cob')).toBe('cobol'); expect(detectLanguage('app/cpy/CVACT01Y.cpy')).toBe('cobol'); expect(isSourceFile('CBACT01C.cbl')).toBe(true); }); const FIXED = (body: string) => body .split('\n') .map((l) => (l.length > 0 ? ' ' + l : l)) .join('\n'); const PROGRAM = FIXED(`IDENTIFICATION DIVISION. PROGRAM-ID. TESTPROG. DATA DIVISION. WORKING-STORAGE SECTION. 01 WS-TOTALS. 05 WS-COUNT PIC 9(4) VALUE ZERO. 88 WS-DONE VALUE 'Y'. 77 WS-FLAG PIC X. COPY CVACT01Y. PROCEDURE DIVISION. MAIN-SECTION SECTION. 0000-MAIN. PERFORM 1000-INIT PERFORM 2000-PROCESS THRU 2000-EXIT CALL 'CBACT01C' USING WS-TOTALS CALL WS-FLAG GO TO 9999-END . 1000-INIT. MOVE ZERO TO WS-COUNT. 2000-PROCESS. EXEC CICS LINK PROGRAM('COCOM01C') COMMAREA(WS-TOTALS) END-EXEC. 2000-EXIT. EXIT. 9999-END. GOBACK. `); it('should extract the program as a module node', () => { const result = extractFromSource('TESTPROG.cbl', PROGRAM); const moduleNode = result.nodes.find((n) => n.kind === 'module'); expect(moduleNode).toBeDefined(); expect(moduleNode?.name).toBe('TESTPROG'); }); it('should extract sections and paragraphs as functions with reconstructed extents', () => { const result = extractFromSource('TESTPROG.cbl', PROGRAM); const fns = result.nodes.filter((n) => n.kind === 'function'); const names = fns.map((f) => f.name); expect(names).toContain('MAIN-SECTION'); expect(names).toContain('0000-MAIN'); expect(names).toContain('2000-PROCESS'); // A paragraph spans from its header to the next header, not just one line. const main = fns.find((f) => f.name === '0000-MAIN'); expect(main).toBeDefined(); expect(main!.endLine).toBeGreaterThan(main!.startLine + 3); // Paragraphs are contained in their section (qualified name includes it). expect(main!.qualifiedName).toContain('MAIN-SECTION'); }); it('should extract PERFORM, PERFORM THRU, GO TO, and CALL literal as calls references', () => { const result = extractFromSource('TESTPROG.cbl', PROGRAM); const calls = result.unresolvedReferences.filter((r) => r.referenceKind === 'calls'); const targets = calls.map((c) => c.referenceName); expect(targets).toContain('1000-INIT'); expect(targets).toContain('2000-PROCESS'); // PERFORM ... THRU start expect(targets).toContain('2000-EXIT'); // PERFORM ... THRU end expect(targets).toContain('9999-END'); // GO TO expect(targets).toContain('CBACT01C'); // CALL 'literal' expect(targets).toContain('COCOM01C'); // EXEC CICS LINK PROGRAM('...') // Dynamic CALL through a data name is skipped — announce, don't guess. expect(targets).not.toContain('WS-FLAG'); }); it('should extract COPY as an import node and imports reference', () => { const result = extractFromSource('TESTPROG.cbl', PROGRAM); const importNode = result.nodes.find((n) => n.kind === 'import'); expect(importNode).toBeDefined(); expect(importNode?.name).toBe('CVACT01Y'); const importRefs = result.unresolvedReferences.filter((r) => r.referenceKind === 'imports'); expect(importRefs.map((r) => r.referenceName)).toContain('CVACT01Y'); }); it('should extract data items as variables, fields, and 88-level constants', () => { const result = extractFromSource('TESTPROG.cbl', PROGRAM); const group = result.nodes.find((n) => n.name === 'WS-TOTALS'); expect(group?.kind).toBe('variable'); const nested = result.nodes.find((n) => n.name === 'WS-COUNT'); expect(nested?.kind).toBe('field'); expect(nested?.qualifiedName).toContain('WS-TOTALS'); const condition = result.nodes.find((n) => n.name === 'WS-DONE'); expect(condition?.kind).toBe('constant'); const standalone = result.nodes.find((n) => n.name === 'WS-FLAG'); expect(standalone?.kind).toBe('variable'); }); it('should extract EXEC SQL INCLUDE as an import', () => { const code = FIXED(`IDENTIFICATION DIVISION. PROGRAM-ID. SQLPROG. DATA DIVISION. WORKING-STORAGE SECTION. EXEC SQL INCLUDE SQLCA END-EXEC. 01 WS-X PIC X. PROCEDURE DIVISION. P1. GOBACK. `); const result = extractFromSource('SQLPROG.cbl', code); const importNode = result.nodes.find((n) => n.kind === 'import' && n.name === 'SQLCA'); expect(importNode).toBeDefined(); // The EXEC block must not break the rest of the file. expect(result.nodes.find((n) => n.name === 'WS-X')).toBeDefined(); }); it('should extract a standalone data copybook (.cpy fragment)', () => { const code = FIXED(`01 ACCOUNT-RECORD. 05 ACCT-ID PIC 9(11). 05 ACCT-CURR-BAL PIC S9(10)V99. `); const result = extractFromSource('CVACT01Y.cpy', code); const record = result.nodes.find((n) => n.name === 'ACCOUNT-RECORD'); expect(record?.kind).toBe('variable'); const field = result.nodes.find((n) => n.name === 'ACCT-ID'); expect(field?.kind).toBe('field'); }); it('should extract a procedure copybook (.cpy fragment with paragraphs)', () => { const code = FIXED(`EDIT-DATE. MOVE 1 TO WS-X PERFORM VALIDATE-YEAR. VALIDATE-YEAR. CONTINUE. `); const result = extractFromSource('CSUTLDPY.cpy', code); const fns = result.nodes.filter((n) => n.kind === 'function').map((n) => n.name); expect(fns).toContain('EDIT-DATE'); expect(fns).toContain('VALIDATE-YEAR'); const calls = result.unresolvedReferences.filter((r) => r.referenceKind === 'calls'); expect(calls.map((c) => c.referenceName)).toContain('VALIDATE-YEAR'); }); it('should emit write-site references for MOVE/ADD/COMPUTE targets', () => { const code = FIXED(`IDENTIFICATION DIVISION. PROGRAM-ID. WRITEREF. DATA DIVISION. WORKING-STORAGE SECTION. 01 WS-TOTAL PIC S9(7)V99. 01 WS-COUNT PIC 9(4). PROCEDURE DIVISION. P1. MOVE ZERO TO WS-TOTAL ADD 1 TO WS-COUNT COMPUTE WS-TOTAL = WS-TOTAL + 1 SUBTRACT 1 FROM WS-COUNT MOVE 1 TO RETURN-CODE. `); const result = extractFromSource('WRITEREF.cbl', code); const writes = result.unresolvedReferences.filter((r) => r.referenceKind === 'references'); const names = writes.map((w) => w.referenceName); expect(names.filter((n) => n === 'WS-TOTAL').length).toBeGreaterThanOrEqual(2); // MOVE + COMPUTE expect(names).toContain('WS-COUNT'); // ADD and SUBTRACT targets // Special registers carry no declaration — never referenced. expect(names).not.toContain('RETURN-CODE'); }); it('should emit cics-transid references for RETURN TRANSID, literal and via same-file VALUE', () => { const code = FIXED(`IDENTIFICATION DIVISION. PROGRAM-ID. TXPROG. DATA DIVISION. WORKING-STORAGE SECTION. 01 WS-TRANID PIC X(04) VALUE 'CB00'. PROCEDURE DIVISION. P1. EXEC CICS RETURN TRANSID('CC00') COMMAREA(WS-X) END-EXEC . P2. EXEC CICS RETURN TRANSID(WS-TRANID) END-EXEC . P3. EXEC CICS XCTL PROGRAM(WS-UNKNOWN-VAR) END-EXEC . `); const result = extractFromSource('TXPROG.cbl', code); const calls = result.unresolvedReferences.filter((r) => r.referenceKind === 'calls'); const names = calls.map((c) => c.referenceName); expect(names).toContain('cics-transid:CC00'); // literal expect(names).toContain('cics-transid:CB00'); // dereferenced through WS-TRANID // Un-derefable program variable: dynamic dispatch, no guessed edge. expect(names.filter((n) => n.startsWith('cics-transid:')).length).toBe(2); }); it('should shift free-format source so it still extracts (preParse)', () => { const code = `IDENTIFICATION DIVISION. PROGRAM-ID. FREEPROG. PROCEDURE DIVISION. DO-WORK. DISPLAY 'HI'. `; const result = extractFromSource('freeprog.cbl', code); expect(result.nodes.find((n) => n.kind === 'module')?.name).toBe('FREEPROG'); expect(result.nodes.find((n) => n.kind === 'function')?.name).toBe('DO-WORK'); }); }); // ============================================================================= // VB.NET (.vb) — vendored patched govindbanura/tree-sitter-vbnet grammar // ============================================================================= describe('VB.NET Extraction', () => { it('should detect .vb as vbnet', () => { expect(detectLanguage('Service.vb')).toBe('vbnet'); expect(detectLanguage('app/Forms/MainForm.vb')).toBe('vbnet'); expect(isSourceFile('Service.vb')).toBe(true); }); const SAMPLE = `Imports System Imports System.Collections.Generic Namespace Acme.Billing Public Interface IRepository Function GetById(ByVal id As Integer) As Invoice End Interface Public Enum InvoiceState Draft = 0 Sent Paid End Enum Public Structure Money Public Amount As Decimal End Structure Public MustInherit Class EntityBase Public Property Id As Integer End Class Public Class Invoice Inherits EntityBase Implements IRepository Private ReadOnly _lines As New List(Of String) Public Const MaxLines As Integer = 100 Public Event Paid(ByVal amount As Decimal) Public Property State As InvoiceState Public Sub New(ByVal id As Integer) Me.Id = id End Sub Public Function GetById(ByVal id As Integer) As Invoice Implements IRepository.GetById Return New Invoice(id) End Function Public Sub AddLine(ByVal description As String) _lines.Add(description) Validate(description) End Sub Private Sub Validate(ByVal text As String) If text.Length > MaxLines Then Throw New ArgumentException("too long") End Sub End Class ' lowercase keywords: VB is case-insensitive public module Helpers public function Twice(byval n as integer) as integer return n * 2 end function Public Sub Run() Dim inv = New Invoice(1) inv.AddLine("widget") Dim d As New Dictionary(Of String, Integer) Helpers.Twice(21) End Sub end module End Namespace `; it('should extract classes, modules, interfaces, structures, and enums', () => { const result = extractFromSource('Invoice.vb', SAMPLE); const kinds = (kind: string) => result.nodes.filter((n) => n.kind === kind).map((n) => n.name); expect(kinds('class')).toEqual(expect.arrayContaining(['EntityBase', 'Invoice', 'Helpers'])); expect(kinds('interface')).toContain('IRepository'); expect(kinds('struct')).toContain('Money'); expect(kinds('enum')).toContain('InvoiceState'); expect(kinds('enum_member')).toEqual(expect.arrayContaining(['Draft', 'Sent', 'Paid'])); }); it('should extract methods, constructors, properties, fields, and events (case-insensitive keywords)', () => { const result = extractFromSource('Invoice.vb', SAMPLE); const methods = result.nodes.filter((n) => n.kind === 'method').map((n) => n.name); expect(methods).toEqual(expect.arrayContaining(['GetById', 'AddLine', 'Validate', 'Twice', 'Run'])); const props = result.nodes.filter((n) => n.kind === 'property').map((n) => n.name); expect(props).toEqual(expect.arrayContaining(['Id', 'State'])); const fields = result.nodes.filter((n) => n.kind === 'field' || n.kind === 'constant').map((n) => n.name); expect(fields).toEqual(expect.arrayContaining(['_lines', 'MaxLines'])); // Event declarations index as findable members expect(fields).toContain('Paid'); }); it('should qualify types with their namespace', () => { const result = extractFromSource('Invoice.vb', SAMPLE); const invoice = result.nodes.find((n) => n.kind === 'class' && n.name === 'Invoice'); expect(invoice?.qualifiedName).toContain('Acme.Billing'); }); it('should emit Inherits as extends and Implements as implements references', () => { const result = extractFromSource('Invoice.vb', SAMPLE); const extendsRefs = result.unresolvedReferences.filter((r) => r.referenceKind === 'extends'); expect(extendsRefs.map((r) => r.referenceName)).toContain('EntityBase'); const implementsRefs = result.unresolvedReferences.filter((r) => r.referenceKind === 'implements'); expect(implementsRefs.map((r) => r.referenceName)).toContain('IRepository'); }); it('should extract calls through both invocation and index-shaped parens', () => { const result = extractFromSource('Invoice.vb', SAMPLE); const calls = result.unresolvedReferences.filter((r) => r.referenceKind === 'calls').map((r) => r.referenceName); // `_lines.Add(description)` parses as array_access (non-empty parens) — still a call site expect(calls).toContain('_lines.Add'); // bare call with args expect(calls).toContain('Validate'); // qualified module call expect(calls).toContain('Helpers.Twice'); }); it('should emit instantiates for New, with VB generic syntax stripped', () => { const result = extractFromSource('Invoice.vb', SAMPLE); const insts = result.unresolvedReferences.filter((r) => r.referenceKind === 'instantiates').map((r) => r.referenceName); expect(insts).toContain('Invoice'); // `As New Dictionary(Of String, Integer)` → bare type name, not `Dictionary(Of ...)` expect(insts.some((n) => n.includes('(') || /\bOf\b/.test(n))).toBe(false); }); it('should extract Imports as import nodes', () => { const result = extractFromSource('Invoice.vb', SAMPLE); const imports = result.nodes.filter((n) => n.kind === 'import').map((n) => n.name); expect(imports).toEqual(expect.arrayContaining(['System', 'System.Collections.Generic'])); }); it('should parse a file without a trailing newline (preParse guard)', () => { const code = 'Class Tail\n Sub Go()\n Log("x")\n End Sub\nEnd Class'; const result = extractFromSource('Tail.vb', code); expect(result.nodes.find((n) => n.kind === 'class')?.name).toBe('Tail'); expect(result.nodes.find((n) => n.kind === 'method')?.name).toBe('Go'); }); }); describe('VB.NET Extraction — scanner-backed constructs', () => { it('should parse XML literals as opaque literals without breaking siblings', () => { const code = `Class Muxer Function WriteTags() As Object Dim xml = <%= From tag In Tags Select <%= tag.Name %> %> Return xml End Function Sub After() Log("still extracted") End Sub End Class `; const result = extractFromSource('Muxer.vb', code); const methods = result.nodes.filter((n) => n.kind === 'method').map((n) => n.name); expect(methods).toEqual(expect.arrayContaining(['WriteTags', 'After'])); }); it('should parse multi-line LINQ query clauses', () => { const code = `Class T Function Big() As Integer Dim big = From l In _lines Where l.Length > 3 Select l.Length Return big.Sum() End Function End Class `; const result = extractFromSource('Linq.vb', code); const calls = result.unresolvedReferences.filter((r) => r.referenceKind === 'calls').map((r) => r.referenceName); expect(calls).toContain('big.Sum'); expect(result.nodes.find((n) => n.kind === 'method')?.name).toBe('Big'); }); it('should extract MustOverride members without derailing following members', () => { const code = `MustInherit Class VideoEncoder MustOverride ReadOnly Property OutputExt As String Public MustOverride Sub ShowConfigDialog(Optional param As Object = Nothing) MustOverride Function GetError() As String Sub New() CanEdit = True End Sub End Class `; const result = extractFromSource('VideoEncoder.vb', code); const methods = result.nodes.filter((n) => n.kind === 'method').map((n) => n.name); expect(methods).toEqual(expect.arrayContaining(['ShowConfigDialog', 'GetError', 'New'])); const props = result.nodes.filter((n) => n.kind === 'property').map((n) => n.name); expect(props).toContain('OutputExt'); }); it('should parse nullable declarator shorthand (Dim x? = expr)', () => { const code = `Class T Sub M(folderInfo As Object) Dim SteamFolderData? = Parser.GetSteamNameAndID(folderInfo) Use(SteamFolderData) End Sub End Class `; const result = extractFromSource('Factory.vb', code); const calls = result.unresolvedReferences.filter((r) => r.referenceKind === 'calls').map((r) => r.referenceName); expect(calls).toContain('Parser.GetSteamNameAndID'); }); }); // ============================================================================= // Erlang (vendored WhatsApp/tree-sitter-erlang grammar — the ELP grammar) // ============================================================================= describe('Erlang Extraction', () => { describe('Language detection', () => { it('should report Erlang as supported', () => { expect(isLanguageSupported('erlang')).toBe(true); expect(getSupportedLanguages()).toContain('erlang'); expect(isSourceFile('apps/app/src/foo.erl')).toBe(true); expect(isSourceFile('include/foo.hrl')).toBe(true); }); }); describe('Function extraction', () => { it('should merge multi-clause functions into one node spanning all clauses', () => { const code = `-module(m). -export([classify/1]). classify(X) when is_atom(X) -> atom; classify(X) when is_binary(X) -> binary; classify(_X) -> other. `; const result = extractFromSource('src/m.erl', code); const fns = result.nodes.filter((n) => n.kind === 'function' && n.name === 'classify'); expect(fns).toHaveLength(1); expect(fns[0]!.startLine).toBe(4); expect(fns[0]!.endLine).toBe(9); expect(fns[0]!.language).toBe('erlang'); }); it('should qualify functions with the module namespace', () => { const code = `-module(my_server). -export([start/0]). start() -> ok. helper() -> ok. `; const result = extractFromSource('src/my_server.erl', code); const ns = result.nodes.find((n) => n.kind === 'namespace'); expect(ns?.name).toBe('my_server'); const start = result.nodes.find((n) => n.kind === 'function' && n.name === 'start'); expect(start?.qualifiedName).toBe('my_server::start'); }); it('should flag exported functions and honor -compile(export_all)', () => { const code = `-module(m). -export([api/0]). api() -> internal(). internal() -> ok. `; const result = extractFromSource('src/m.erl', code); const api = result.nodes.find((n) => n.name === 'api'); const internal = result.nodes.find((n) => n.name === 'internal'); expect(api?.isExported).toBe(true); expect(internal?.isExported).toBe(false); const all = extractFromSource('src/all.erl', `-module(all). -compile(export_all). anything() -> ok. `); expect(all.nodes.find((n) => n.name === 'anything')?.isExported).toBe(true); }); it('should use the preceding -spec as the signature and capture doc comments', () => { const code = `-module(m). %% Fetches a value by key. -spec fetch(binary()) -> {ok, term()} | not_found. fetch(Key) -> lookup(Key). lookup(_K) -> not_found. `; const result = extractFromSource('src/m.erl', code); const fetch = result.nodes.find((n) => n.name === 'fetch'); expect(fetch?.signature).toBe('-spec fetch(binary()) -> {ok, term()} | not_found.'); expect(fetch?.docstring).toBe('Fetches a value by key.'); }); it('should fall back to the clause header as the signature', () => { const code = `-module(m). resize(W, H) when W > 0, H > 0 -> {W, H}. `; const result = extractFromSource('src/m.erl', code); const resize = result.nodes.find((n) => n.name === 'resize'); expect(resize?.signature).toBe('resize(W, H) when W > 0, H > 0'); }); }); describe('Record and type extraction', () => { it('should extract records as structs with fields', () => { const code = `-module(m). -record(state, { store = #{} :: map(), counter = 0 :: non_neg_integer() }). `; const result = extractFromSource('src/m.erl', code); const rec = result.nodes.find((n) => n.kind === 'struct'); expect(rec?.name).toBe('state'); const fields = result.nodes.filter((n) => n.kind === 'field').map((n) => n.name); expect(fields).toContain('store'); expect(fields).toContain('counter'); }); it('should extract -type and -opaque as type aliases, without bogus type-call refs', () => { const code = `-module(m). -type key() :: atom() | binary(). -opaque handle() :: reference(). -spec noop(key()) -> ok. noop(_K) -> ok. `; const result = extractFromSource('src/m.erl', code); const aliases = result.nodes.filter((n) => n.kind === 'type_alias').map((n) => n.name); expect(aliases).toContain('key'); expect(aliases).toContain('handle'); // Type-position expressions parse as `call` nodes — the spec/type subtrees // must not leak `calls` refs to type names like atom()/binary(). const bogus = result.unresolvedReferences.filter( (r) => r.referenceKind === 'calls' && ['atom', 'binary', 'reference', 'key'].includes(r.referenceName) ); expect(bogus).toHaveLength(0); }); it('should extract -define macros as constants', () => { const code = `-module(m). -define(TIMEOUT, 5000). -define(WRAP(X), {ok, X}). `; const result = extractFromSource('src/m.erl', code); const consts = result.nodes.filter((n) => n.kind === 'constant').map((n) => n.name); expect(consts).toContain('TIMEOUT'); expect(consts).toContain('WRAP'); }); }); describe('Import extraction', () => { it('should extract -include/-include_lib and -import', () => { const code = `-module(m). -include("records.hrl"). -include_lib("kernel/include/logger.hrl"). -import(lists, [map/2]). `; const result = extractFromSource('src/m.erl', code); const imports = result.nodes.filter((n) => n.kind === 'import').map((n) => n.name); expect(imports).toContain('records.hrl'); expect(imports).toContain('kernel/include/logger.hrl'); expect(imports).toContain('lists'); const ref = result.unresolvedReferences.find( (r) => r.referenceKind === 'imports' && r.referenceName === 'records.hrl' ); expect(ref).toBeDefined(); }); }); describe('Call extraction', () => { it('should record local calls bare and remote calls module-qualified', () => { const code = `-module(m). -export([run/1]). run(X) -> Y = prepare(X), other_mod:process(Y). prepare(X) -> X. `; const result = extractFromSource('src/m.erl', code); const calls = result.unresolvedReferences.filter((r) => r.referenceKind === 'calls').map((r) => r.referenceName); expect(calls).toContain('prepare'); // `mod:fn(...)` is emitted as `mod::fn` — the same shape the module // namespace gives every function's qualifiedName, so it resolves via // the qualified-name matcher. expect(calls).toContain('other_mod::process'); }); it('should not emit calls for dynamic dispatch (var module / var fun)', () => { const code = `-module(m). -export([run/2]). run(Mod, F) -> Mod:handle(x), F(y). `; const result = extractFromSource('src/m.erl', code); const calls = result.unresolvedReferences.filter((r) => r.referenceKind === 'calls').map((r) => r.referenceName); expect(calls).not.toContain('handle'); expect(calls).not.toContain('Mod::handle'); expect(calls).not.toContain('F'); }); it('should connect gen_server self-calls to the module handlers', () => { const code = `-module(kv_store). -behaviour(gen_server). -export([get/1, put/2, drop/1]). -export([init/1, handle_call/3, handle_cast/2]). -define(SERVER, ?MODULE). get(Key) -> gen_server:call(?SERVER, {get, Key}). put(Key, Value) -> gen_server:cast(?MODULE, {put, Key, Value}). drop(Key) -> gen_server:call(kv_store, {drop, Key}). init(_) -> {ok, #{}}. handle_call({get, K}, _From, S) -> {reply, maps:find(K, S), S}; handle_call({drop, K}, _From, S) -> {reply, ok, maps:remove(K, S)}. handle_cast({put, K, V}, S) -> {noreply, maps:put(K, V, S)}. `; const result = extractFromSource('src/kv_store.erl', code); const calls = result.unresolvedReferences.filter((r) => r.referenceKind === 'calls').map((r) => r.referenceName); // ?SERVER (defined as ?MODULE), ?MODULE, and the module's own atom all // count as self — public API wrappers connect to their handlers. expect(calls.filter((c) => c === 'kv_store::handle_call')).toHaveLength(2); expect(calls).toContain('kv_store::handle_cast'); }); it('should connect gen_server calls to a registered-name module, directly or via an atom macro', () => { const code = `-module(kv_client). -export([fetch/1, evict/1]). -define(STORE, kv_store). fetch(Key) -> gen_server:call(kv_store, {get, Key}). evict(Key) -> gen_server:cast(?STORE, {evict, Key}). `; const result = extractFromSource('src/kv_client.erl', code); const calls = result.unresolvedReferences.filter((r) => r.referenceKind === 'calls').map((r) => r.referenceName); // OTP's {local, ?MODULE} convention names a server after its module — // a cross-module registered name targets that module's handlers. A name // matching no module simply never resolves downstream. expect(calls).toContain('kv_store::handle_call'); expect(calls).toContain('kv_store::handle_cast'); }); it('should not connect gen_server calls with dynamic targets', () => { const code = `-module(m). -export([go/2]). go(Pid, Msg) -> gen_server:call(Pid, Msg), gen_server:cast({global, some_name}, Msg), gen_server:call({some_name, node()}, Msg). `; const result = extractFromSource('src/m.erl', code); const calls = result.unresolvedReferences.filter((r) => r.referenceKind === 'calls').map((r) => r.referenceName); expect(calls.filter((c) => c.includes('handle_call') || c.includes('handle_cast'))).toHaveLength(0); }); it('should lift static MFA arguments of the spawn/apply family into call refs', () => { const code = `-module(m). -export([boot/2]). boot(Req, Env) -> Pid = proc_lib:spawn_link(?MODULE, request_process, [Req, Env]), spawn(?MODULE, monitor_loop, [Pid]), apply(other_mod, handle, [Req]), timer:apply_after(500, other_mod, tick, []), Pid. request_process(_R, _E) -> ok. monitor_loop(_P) -> ok. `; const result = extractFromSource('src/m.erl', code); const calls = result.unresolvedReferences.filter((r) => r.referenceKind === 'calls').map((r) => r.referenceName); expect(calls).toContain('request_process'); // ?MODULE → bare, same-file resolution expect(calls).toContain('monitor_loop'); expect(calls).toContain('other_mod::handle'); expect(calls).toContain('other_mod::tick'); }); it('should stay silent on dynamic spawn/apply (var module, fun value, or plain fun)', () => { const code = `-module(m). -export([go/3]). go(M, F, A) -> spawn(M, F, A), spawn(fun() -> helper() end), apply(M, F, A). helper() -> ok. `; const result = extractFromSource('src/m.erl', code); const calls = result.unresolvedReferences.filter((r) => r.referenceKind === 'calls').map((r) => r.referenceName); // The fun body's call is still walked; no phantom MFA targets appear. expect(calls).toContain('helper'); expect(calls.filter((c) => c !== 'spawn' && c !== 'apply' && c !== 'helper')).toHaveLength(0); }); it('should treat ?MODULE:fn calls as local calls', () => { const code = `-module(m). -export([kick/0]). kick() -> ?MODULE:work(). work() -> ok. `; const result = extractFromSource('src/m.erl', code); const calls = result.unresolvedReferences.filter((r) => r.referenceKind === 'calls').map((r) => r.referenceName); expect(calls).toContain('work'); }); it('should capture fun name/arity values as function references', () => { const code = `-module(m). -export([wire/1]). wire(Pids) -> lists:foreach(fun notify/1, Pids), lists:map(fun m:notify/1, Pids). notify(_P) -> ok. `; const result = extractFromSource('src/m.erl', code); const refs = result.unresolvedReferences.filter((r) => r.referenceKind === 'references').map((r) => r.referenceName); expect(refs).toContain('notify'); expect(refs).toContain('m::notify'); }); it('should reference records used in bodies and argument patterns', () => { const code = `-module(m). -export([mk/1, get_id/1]). -record(req, {id, payload}). mk(Id) -> #req{id = Id}. get_id(#req{id = Id}) -> Id. `; const result = extractFromSource('src/m.erl', code); const refs = result.unresolvedReferences.filter( (r) => r.referenceKind === 'references' && r.referenceName === 'req' ); expect(refs.length).toBeGreaterThanOrEqual(2); }); it('should attribute calls from every clause of a multi-clause function', () => { const code = `-module(m). -export([handle/1]). handle({a, X}) -> first(X); handle({b, X}) -> second(X). first(X) -> X. second(X) -> X. `; const result = extractFromSource('src/m.erl', code); const handle = result.nodes.find((n) => n.kind === 'function' && n.name === 'handle'); const calls = result.unresolvedReferences.filter( (r) => r.referenceKind === 'calls' && r.fromNodeId === handle?.id ).map((r) => r.referenceName); expect(calls).toContain('first'); expect(calls).toContain('second'); }); }); describe('escript and app resource files', () => { it('should extract functions and calls from an escript behind a shebang', () => { const code = `#!/usr/bin/env escript %%! -smp enable main([Path]) -> Result = analyze(Path), io:format("~p~n", [Result]). analyze(Path) -> {ok, Bin} = file:read_file(Path), byte_size(Bin). `; const result = extractFromSource('bin/tool.escript', code); const fns = result.nodes.filter((n) => n.kind === 'function').map((n) => n.name); expect(fns).toContain('main'); expect(fns).toContain('analyze'); const calls = result.unresolvedReferences.filter((r) => r.referenceKind === 'calls').map((r) => r.referenceName); expect(calls).toContain('analyze'); expect(calls).toContain('io::format'); }); it('should link an app resource file to its callback module and dependency apps', () => { const code = `{application, sample, [ {description, "Sample application"}, {vsn, "1.0.0"}, {registered, [sample_server]}, {mod, {sample_app, []}}, {applications, [kernel, stdlib, sample_core]}, {included_applications, [sample_extra]}, {env, [{limit, 100}]}, {modules, []} ]}. `; const result = extractFromSource('src/sample.app.src', code); const refs = result.unresolvedReferences.map((r) => `${r.referenceKind}:${r.referenceName}`); // The application-callback module is the app's entry point. expect(refs).toContain('references:sample_app'); // Dependencies resolve to umbrella siblings; kernel/stdlib just drop. expect(refs).toContain('imports:kernel'); expect(refs).toContain('imports:sample_core'); expect(refs).toContain('imports:sample_extra'); // Registered names, env values, and the like carry no graph structure. expect(refs.filter((r) => r.endsWith(':sample_server'))).toHaveLength(0); expect(refs.filter((r) => r.endsWith(':limit'))).toHaveLength(0); }); }); describe('Macro linkage', () => { it('should attribute macro-body calls to the macro and link function-like uses into the chain', () => { const code = `-module(m). -export([do_thing/1]). -define(LOG_AUDIT(Event), audit_logger:log(Event, ?MODULE)). do_thing(X) -> ?LOG_AUDIT({thing, X}), ok. `; const result = extractFromSource('src/m.erl', code); const macro = result.nodes.find((n) => n.kind === 'constant' && n.name === 'LOG_AUDIT'); const doThing = result.nodes.find((n) => n.kind === 'function' && n.name === 'do_thing'); const refsFrom = (id?: string) => result.unresolvedReferences.filter((r) => r.fromNodeId === id).map((r) => `${r.referenceKind}:${r.referenceName}`); // The body's remote call belongs to the macro node — true exactly once. expect(refsFrom(macro?.id)).toContain('calls:audit_logger::log'); // The use site joins the call chain: do_thing -calls→ LOG_AUDIT. expect(refsFrom(doThing?.id)).toContain('calls:LOG_AUDIT'); }); it('should reference bare macro reads without polluting call chains', () => { const code = `-module(m). -export([wait/0]). -define(TIMEOUT, 5000). wait() -> receive after ?TIMEOUT -> ok end. `; const result = extractFromSource('src/m.erl', code); const refs = result.unresolvedReferences.map((r) => `${r.referenceKind}:${r.referenceName}`); expect(refs).toContain('references:TIMEOUT'); expect(refs).not.toContain('calls:TIMEOUT'); }); it('should skip compiler-predefined macros and keep walking macro-use arguments', () => { const code = `-module(m). -export([check/0]). check() -> ?assertEqual(ok, prepare()), {?MODULE, ?LINE, ?FUNCTION_NAME}. prepare() -> ok. `; const result = extractFromSource('src/m.erl', code); const refs = result.unresolvedReferences.map((r) => r.referenceName); // The nested call inside the macro's arguments still attributes to check/0. expect(refs).toContain('prepare'); // ?assertEqual (an OTP header macro) is emitted and simply never resolves… expect(refs).toContain('assertEqual'); // …but predefined macros have no definition to link. expect(refs).not.toContain('MODULE'); expect(refs).not.toContain('LINE'); expect(refs).not.toContain('FUNCTION_NAME'); }); it('should chain macro-to-macro uses', () => { const code = `-module(m). -define(TARGET, target_fn()). -define(ALIAS, ?TARGET). `; const result = extractFromSource('src/m.erl', code); const target = result.nodes.find((n) => n.kind === 'constant' && n.name === 'TARGET'); const alias = result.nodes.find((n) => n.kind === 'constant' && n.name === 'ALIAS'); const refsFrom = (id?: string) => result.unresolvedReferences.filter((r) => r.fromNodeId === id).map((r) => `${r.referenceKind}:${r.referenceName}`); expect(refsFrom(target?.id)).toContain('calls:target_fn'); expect(refsFrom(alias?.id)).toContain('references:TARGET'); }); }); describe('Behaviour extraction', () => { it('should emit an implements reference for -behaviour', () => { const code = `-module(m). -behaviour(gen_server). init(_) -> {ok, #{}}. `; const result = extractFromSource('src/m.erl', code); const impl = result.unresolvedReferences.find((r) => r.referenceKind === 'implements'); expect(impl?.referenceName).toBe('gen_server'); }); it('should not create symbols from -callback declarations', () => { const code = `-module(b). -callback handle_thing(term()) -> ok. -callback init(list()) -> {ok, term()}. `; const result = extractFromSource('src/b.erl', code); const fns = result.nodes.filter((n) => n.kind === 'function'); expect(fns).toHaveLength(0); }); }); }); describe('Terraform Extraction', () => { describe('Language detection', () => { it('should detect Terraform files', () => { expect(detectLanguage('main.tf')).toBe('terraform'); expect(detectLanguage('terraform.tfvars')).toBe('terraform'); expect(detectLanguage('versions.tofu')).toBe('terraform'); }); it('should report Terraform as supported', () => { expect(isLanguageSupported('terraform')).toBe(true); expect(getSupportedLanguages()).toContain('terraform'); }); }); describe('Block extraction', () => { it('should extract a resource block as a class with qualified type.name', () => { const code = ` resource "aws_s3_bucket" "my_bucket" { bucket = "example" } `; const result = extractFromSource('main.tf', code); const res = result.nodes.find((n) => n.name === 'aws_s3_bucket.my_bucket'); expect(res).toBeDefined(); expect(res?.kind).toBe('class'); expect(res?.qualifiedName).toBe('aws_s3_bucket.my_bucket'); expect(res?.signature).toBe('resource "aws_s3_bucket" "my_bucket"'); expect(res?.language).toBe('terraform'); }); it('should extract a data block under the data.* qualified name', () => { const code = ` data "aws_caller_identity" "current" {} `; const result = extractFromSource('main.tf', code); const node = result.nodes.find((n) => n.qualifiedName === 'data.aws_caller_identity.current'); expect(node).toBeDefined(); expect(node?.kind).toBe('class'); }); it('should extract a variable block as variable with qualified name var.X', () => { const code = ` variable "region" { type = string default = "us-east-1" } `; const result = extractFromSource('variables.tf', code); const v = result.nodes.find((n) => n.qualifiedName === 'var.region'); expect(v).toBeDefined(); expect(v?.kind).toBe('variable'); expect(v?.name).toBe('region'); }); it('should extract an output block as variable with qualified name output.X', () => { const code = ` output "bucket_arn" { value = aws_s3_bucket.my_bucket.arn } `; const result = extractFromSource('outputs.tf', code); const out = result.nodes.find((n) => n.qualifiedName === 'output.bucket_arn'); expect(out).toBeDefined(); expect(out?.kind).toBe('variable'); }); it('should extract a module block as module with qualified name module.X', () => { const code = ` module "vpc" { source = "./modules/vpc" cidr = var.vpc_cidr } `; const result = extractFromSource('main.tf', code); const m = result.nodes.find((n) => n.qualifiedName === 'module.vpc'); expect(m).toBeDefined(); expect(m?.kind).toBe('module'); }); it('should extract a provider block as namespace', () => { const code = ` provider "aws" { region = "us-east-1" } `; const result = extractFromSource('main.tf', code); const p = result.nodes.find((n) => n.qualifiedName === 'provider.aws'); expect(p).toBeDefined(); expect(p?.kind).toBe('namespace'); }); it('should extract every locals attribute as its own constant with local.K qualified name', () => { const code = ` locals { prefix = "prod" full_name = "\${local.prefix}-app" max_retries = 3 } `; const result = extractFromSource('locals.tf', code); const names = result.nodes .filter((n) => n.kind === 'constant') .map((n) => n.qualifiedName) .sort(); expect(names).toEqual(['local.full_name', 'local.max_retries', 'local.prefix']); }); it('should ignore a terraform settings block', () => { const code = ` terraform { required_version = ">= 1.5" } `; const result = extractFromSource('versions.tf', code); const symbols = result.nodes.filter((n) => n.kind !== 'file'); expect(symbols).toHaveLength(0); }); it('should index .tfvars top-level attributes via the same parser path', () => { // .tfvars files have no blocks — just bare attributes, each of which // SETS the root module variable of that name. No symbols are declared, // but every top-level assignment references its variable so "what sets // var.region" is answerable. const code = ` region = "us-east-1" environment = "prod" `; const result = extractFromSource('terraform.tfvars', code); expect(result.errors.filter((e) => e.severity === 'error')).toHaveLength(0); const symbols = result.nodes.filter((n) => n.kind !== 'file'); expect(symbols).toHaveLength(0); const refs = result.unresolvedReferences.map((r) => r.referenceName); expect(refs).toContain('var.region'); expect(refs).toContain('var.environment'); }); }); describe('Reference extraction', () => { it('should emit a reference for var.X used inside a resource', () => { const code = ` variable "region" {} resource "aws_s3_bucket" "b" { bucket = var.region } `; const result = extractFromSource('main.tf', code); const refs = result.unresolvedReferences.map((r) => r.referenceName); expect(refs).toContain('var.region'); }); it('should emit a reference for module.M. as module.M', () => { const code = ` output "vpc_id" { value = module.vpc.vpc_id } `; const result = extractFromSource('outputs.tf', code); const refs = result.unresolvedReferences.map((r) => r.referenceName); expect(refs).toContain('module.vpc'); }); it('should emit a scoped module.M:output.X ref alongside module.M for output chains', () => { const code = ` output "vpc_id" { value = module.vpc.vpc_id } `; const result = extractFromSource('outputs.tf', code); const refs = result.unresolvedReferences.map((r) => r.referenceName); expect(refs).toContain('module.vpc:output.vpc_id'); // A bare module.M use (no output segment) stays a single ref. const bare = extractFromSource('main.tf', 'output "m" {\n value = module.vpc\n}\n'); const bareRefs = bare.unresolvedReferences.map((r) => r.referenceName); expect(bareRefs).toContain('module.vpc'); expect(bareRefs.some((r) => r.includes(':output.'))).toBe(false); }); it('should wire module blocks: scoped input refs, meta-args skipped, local source imported', () => { const code = ` module "vpc" { source = "./modules/vpc" version = "1.0.0" count = 2 depends_on = [aws_iam_role.net] cidr = var.vpc_cidr name = "prod" } `; const result = extractFromSource('main.tf', code); const refs = result.unresolvedReferences.map((r) => r.referenceName); // Input attributes wire to the child module's variables (scoped spelling). expect(refs).toContain('module.vpc:var.cidr'); expect(refs).toContain('module.vpc:var.name'); // Meta-arguments configure the call, not child variables. expect(refs).not.toContain('module.vpc:var.source'); expect(refs).not.toContain('module.vpc:var.version'); expect(refs).not.toContain('module.vpc:var.count'); expect(refs).not.toContain('module.vpc:var.depends_on'); // A local ./ source emits the module→file imports ref. const fileRef = result.unresolvedReferences.find((r) => r.referenceName === 'module.vpc:file'); expect(fileRef).toBeDefined(); expect(fileRef?.referenceKind).toBe('imports'); // Attribute VALUES still reference the parent scope as before. expect(refs).toContain('var.vpc_cidr'); expect(refs).toContain('aws_iam_role.net'); }); it('should not emit a module.M:file ref for registry or git sources', () => { const code = ` module "s3" { source = "terraform-aws-modules/s3-bucket/aws" version = "4.0.0" bucket = "x" } module "net" { source = "git::https://example.com/net.git" cidr = "10.0.0.0/16" } `; const result = extractFromSource('main.tf', code); const refs = result.unresolvedReferences.map((r) => r.referenceName); expect(refs.some((r) => r.endsWith(':file'))).toBe(false); // Input wiring is still emitted — the resolver drops it when the // source turns out to be out-of-repo. expect(refs).toContain('module.s3:var.bucket'); }); it('should emit a remote-output candidate for module.M.outputs.X chains', () => { const code = ` resource "aws_eks_cluster" "this" { vpc_id = module.vpc.outputs.vpc_id } `; const result = extractFromSource('main.tf', code); const refs = result.unresolvedReferences.map((r) => r.referenceName); expect(refs).toContain('module.vpc'); expect(refs).toContain('module.vpc:remote-output.vpc_id'); // A plain two-segment chain must NOT produce a remote-output candidate. const plain = extractFromSource('o.tf', 'output "x" {\n value = module.vpc.vpc_id\n}\n'); const plainRefs = plain.unresolvedReferences.map((r) => r.referenceName); expect(plainRefs.some((r) => r.includes(':remote-output.'))).toBe(false); }); it('should reference resource addresses from moved/import/removed blocks, anchored to the file', () => { const code = ` resource "aws_instance" "new" {} moved { from = aws_instance.old to = aws_instance.new } import { to = aws_s3_bucket.b id = "bucket-name" } removed { from = module.legacy.aws_iam_role.r } `; const result = extractFromSource('main.tf', code); const refs = result.unresolvedReferences; const names = refs.map((r) => r.referenceName); expect(names).toContain('aws_instance.old'); expect(names).toContain('aws_instance.new'); expect(names).toContain('aws_s3_bucket.b'); expect(names).toContain('module.legacy'); // Scoped module refs are suppressed here: module.legacy.aws_iam_role.r // names a resource inside a module instance, never a module output. expect(names.some((n) => n.includes(':'))).toBe(false); // Anchored to the file node, and no phantom symbols were declared. const fileNode = result.nodes.find((n) => n.kind === 'file'); for (const r of refs.filter((x) => x.referenceName === 'aws_instance.old')) { expect(r.fromNodeId).toBe(fileNode?.id); } expect(result.nodes.filter((n) => n.kind !== 'file')).toHaveLength(1); // just aws_instance.new }); it('should collect check-assert condition references and still index check-scoped data blocks', () => { const code = ` check "health" { data "http" "ping" { url = var.endpoint } assert { condition = data.http.ping.status_code == 200 && var.strict error_message = "unhealthy" } } `; const result = extractFromSource('checks.tf', code); const names = result.unresolvedReferences.map((r) => r.referenceName); expect(names).toContain('data.http.ping'); expect(names).toContain('var.strict'); expect(names).toContain('var.endpoint'); // The scoped data source inside the check is a real symbol. expect(result.nodes.find((n) => n.qualifiedName === 'data.http.ping')).toBeDefined(); }); it('should qualify aliased provider blocks and reference provider selections', () => { const code = ` provider "aws" { region = "us-east-1" } provider "aws" { alias = "east" region = "us-east-2" } resource "aws_s3_bucket" "b" { provider = aws.east bucket = "x" } resource "google_service_account" "sa" { provider = google-beta } `; const result = extractFromSource('main.tf', code); const providers = result.nodes.filter((n) => n.kind === 'namespace').map((n) => n.qualifiedName).sort(); expect(providers).toEqual(['provider.aws', 'provider.aws.east']); const names = result.unresolvedReferences.map((r) => r.referenceName); expect(names).toContain('provider.aws.east'); expect(names).toContain('provider.google-beta'); // The selection must not be misread as a resource reference. expect(names).not.toContain('aws.east'); }); it('should reference the values (not keys) of a module providers map', () => { const code = ` module "vpc" { source = "./modules/vpc" providers = { aws = aws.east } } `; const result = extractFromSource('main.tf', code); const names = result.unresolvedReferences.map((r) => r.referenceName); expect(names).toContain('provider.aws.east'); expect(names).not.toContain('provider.aws'); expect(names).not.toContain('aws.east'); // providers is a meta-argument — no input wiring for it. expect(names).not.toContain('module.vpc:var.providers'); }); it('should emit data.T.N references stripped of the trailing attribute', () => { const code = ` output "account" { value = data.aws_caller_identity.current.account_id } `; const result = extractFromSource('outputs.tf', code); const refs = result.unresolvedReferences.map((r) => r.referenceName); expect(refs).toContain('data.aws_caller_identity.current'); }); it('should emit T.N references for managed-resource attribute access', () => { const code = ` resource "aws_iam_policy" "p" { policy = aws_s3_bucket.my.arn } `; const result = extractFromSource('main.tf', code); const refs = result.unresolvedReferences.map((r) => r.referenceName); expect(refs).toContain('aws_s3_bucket.my'); }); it('should emit local.K references from locals attribute expressions', () => { const code = ` locals { prefix = "prod" name = "\${local.prefix}-app" } `; const result = extractFromSource('locals.tf', code); const refs = result.unresolvedReferences.map((r) => r.referenceName); expect(refs).toContain('local.prefix'); }); it('should skip built-in heads (each, count, self, path, terraform.workspace)', () => { const code = ` resource "aws_instance" "x" { count = each.value name = path.module workspace = terraform.workspace self_ref = self.id index_value = count.index } `; const result = extractFromSource('main.tf', code); const refs = result.unresolvedReferences.map((r) => r.referenceName); // None of the built-ins should produce project references. expect(refs.some((r) => r.startsWith('each.'))).toBe(false); expect(refs.some((r) => r.startsWith('count.'))).toBe(false); expect(refs.some((r) => r.startsWith('self.'))).toBe(false); expect(refs.some((r) => r.startsWith('path.'))).toBe(false); expect(refs.some((r) => r.startsWith('terraform.'))).toBe(false); }); }); }); // ============================================================================= // ArkTS (HarmonyOS / OpenHarmony declarative UI — `.ets`) // ============================================================================= describe('ArkTS Extraction', () => { it('reports ArkTS as supported', () => { expect(isLanguageSupported('arkts')).toBe(true); expect(getSupportedLanguages()).toContain('arkts'); }); describe('@Component struct extraction', () => { const code = ` import { TodoItem } from '../model/TodoItem'; @Entry @Component struct Index { @State message: string = 'Hello'; @Prop count: number = 0; @StorageLink('theme') theme: string = 'light'; private service: TodoService = new TodoService(); aboutToAppear(): void { this.load(); } load(): void { this.message = 'loaded'; } build() { Column() { Text(this.message).fontSize(50) } .height('100%') } } `; it('extracts the struct with its ArkUI decorators', () => { const result = extractFromSource('pages/Index.ets', code); const comp = result.nodes.find((n) => n.kind === 'struct' && n.name === 'Index'); expect(comp).toBeDefined(); expect(comp?.language).toBe('arkts'); expect(comp?.decorators).toEqual(expect.arrayContaining(['Entry', 'Component'])); }); it('extracts an EXPORTED struct whose decorators sit on the export statement', () => { const result = extractFromSource( 'components/Card.ets', `@Component\nexport struct Card {\n build() {\n Row() {}\n }\n}\n` ); const card = result.nodes.find((n) => n.kind === 'struct' && n.name === 'Card'); expect(card).toBeDefined(); expect(card?.isExported).toBe(true); expect(card?.decorators).toContain('Component'); }); it('extracts struct members: build(), lifecycle + regular methods with qualified names', () => { const result = extractFromSource('pages/Index.ets', code); const methods = result.nodes.filter((n) => n.kind === 'method'); expect(methods.find((m) => m.qualifiedName === 'Index::build')).toBeDefined(); expect(methods.find((m) => m.qualifiedName === 'Index::aboutToAppear')).toBeDefined(); expect(methods.find((m) => m.qualifiedName === 'Index::load')).toBeDefined(); }); it('extracts @State/@Prop/@StorageLink members as properties with their decorators', () => { const result = extractFromSource('pages/Index.ets', code); const message = result.nodes.find((n) => n.kind === 'property' && n.qualifiedName === 'Index::message'); expect(message).toBeDefined(); expect(message?.decorators).toContain('State'); const count = result.nodes.find((n) => n.kind === 'property' && n.qualifiedName === 'Index::count'); expect(count?.decorators).toContain('Prop'); // Decorator-with-args: the decorator NAME is captured, not its argument. const theme = result.nodes.find((n) => n.kind === 'property' && n.qualifiedName === 'Index::theme'); expect(theme?.decorators).toContain('StorageLink'); }); it('emits intra-struct method call refs (this.load())', () => { const result = extractFromSource('pages/Index.ets', code); const call = result.unresolvedReferences.find( (r) => r.referenceKind === 'calls' && r.referenceName === 'load' ); expect(call).toBeDefined(); }); }); describe('build() DSL call surface', () => { const code = ` @Extend(Text) function titleStyle(size: number) { .fontSize(size) } @Component struct Page { count: number = 0; handleTap(): void { this.count += 1; } @Builder headerBar(title: string) { Row() { Text(title).titleStyle(24) Button('Go').onClick(this.handleTap) } } build() { Column({ space: 8 }) { this.headerBar('Home') ChildCard({ label: 'hi' }) } .height('100%') } } `; function callRefsFrom(result: ReturnType, methodName: string): string[] { const from = result.nodes.find((n) => n.kind === 'method' && n.name === methodName); return result.unresolvedReferences .filter((r) => r.referenceKind === 'calls' && r.fromNodeId === from?.id) .map((r) => r.referenceName); } it('emits a call ref for a custom component instantiation inside build()', () => { const result = extractFromSource('pages/Page.ets', code); expect(callRefsFrom(result, 'build')).toContain('ChildCard'); }); it('emits dot-prefixed call refs for chained attributes (@Extend/@Styles-only resolution)', () => { const result = extractFromSource('pages/Page.ets', code); // `.titleStyle(24)` chains on the Text component — one node, repeated // property/arguments field pairs, NOT nested call_expressions. The // leading dot routes the ref to the decorator-gated matcher strategy so // framework attributes (`.height` below) can never hit an arbitrary // same-named symbol. expect(callRefsFrom(result, 'headerBar')).toContain('.titleStyle'); expect(callRefsFrom(result, 'build')).toContain('.height'); expect(callRefsFrom(result, 'build')).not.toContain('height'); }); it('recovers the detached-chain shape (chain on the line after a nested component)', () => { // Inside arkui_children, a chain starting after the closing `}` is // detached by the grammar into sibling leading_dot_expression + // parenthesized_expression statements — the close-button idiom. const detached = ` @Component struct Panel { close(): void {} build() { Column() { Row() { Text('x') } .width(10) .onClick(this.close) .id('close_button') } } } `; const result = extractFromSource('components/Panel.ets', detached); const refs = callRefsFrom(result, 'build'); expect(refs).toContain('close'); expect(refs).toContain('.width'); expect(refs).not.toContain('width'); }); it('dot-prefixes the innermost call of a proper-form detached chain', () => { // `.alignItems(x).layoutWeight(1)` under a leading_dot_expression: the // wrapper consumes the dot, so the innermost call has a bare identifier // function and would otherwise emit as a plain `alignItems(...)` call. const chained = ` @Component struct Card { build() { Column() { List() { Text('x') } .alignItems(HorizontalAlign.Start) .layoutWeight(1) .height('100%') } } } `; const result = extractFromSource('components/Card.ets', chained); const refs = callRefsFrom(result, 'build'); expect(refs).toContain('.alignItems'); expect(refs).not.toContain('alignItems'); expect(refs).toContain('.layoutWeight'); expect(refs).not.toContain('layoutWeight'); }); it('emits a call ref for an .onClick(this.handler) method-reference binding', () => { const result = extractFromSource('pages/Page.ets', code); expect(callRefsFrom(result, 'headerBar')).toContain('handleTap'); }); it('emits a call ref for a @Builder method invoked as this.headerBar()', () => { const result = extractFromSource('pages/Page.ets', code); expect(callRefsFrom(result, 'build')).toContain('headerBar'); }); it('extracts a global @Extend function with its decorator', () => { const result = extractFromSource('pages/Page.ets', code); const fn = result.nodes.find((n) => n.kind === 'function' && n.name === 'titleStyle'); expect(fn).toBeDefined(); expect(fn?.decorators).toContain('Extend'); }); }); describe('Global @Builder functions', () => { it('extracts a decorated global @Builder function with signature and decorator', () => { const result = extractFromSource( 'common/builders.ets', `@Builder\nfunction EmptyHint(message: string) {\n Column() {\n Text(message).fontSize(16)\n }\n}\n` ); const fn = result.nodes.find((n) => n.kind === 'function' && n.name === 'EmptyHint'); expect(fn).toBeDefined(); expect(fn?.signature).toBe('(message: string)'); expect(fn?.decorators).toContain('Builder'); }); }); describe('Standard TypeScript constructs in .ets', () => { it('extracts classes, interfaces, enums, type aliases and their members', () => { const code = ` export enum Priority { Low, Medium = 2, High } export interface Shape { area(): number; } export type Handler = (e: string) => void; export class Service { private count: number = 0; doWork(x: number): number { return this.helper(x); } helper(n: number): number { return n * 2; } } `; const result = extractFromSource('common/service.ets', code); expect(result.nodes.find((n) => n.kind === 'class' && n.name === 'Service')).toBeDefined(); expect(result.nodes.find((n) => n.kind === 'enum' && n.name === 'Priority')).toBeDefined(); const members = result.nodes.filter((n) => n.kind === 'enum_member').map((n) => n.qualifiedName); expect(members).toEqual(expect.arrayContaining(['Priority::Low', 'Priority::Medium', 'Priority::High'])); expect(result.nodes.find((n) => n.kind === 'interface' && n.name === 'Shape')).toBeDefined(); expect(result.nodes.find((n) => n.kind === 'type_alias' && n.name === 'Handler')).toBeDefined(); const doWork = result.nodes.find((n) => n.qualifiedName === 'Service::doWork'); expect(doWork?.kind).toBe('method'); expect(doWork?.signature).toBe('(x: number): number'); expect( result.unresolvedReferences.find((r) => r.referenceKind === 'calls' && r.referenceName === 'helper') ).toBeDefined(); }); }); describe('Import extraction', () => { it('extracts relative, SDK (@ohos/@kit) and default imports', () => { const code = ` import router from '@ohos.router'; import { promptAction } from '@kit.ArkUI'; import { TodoItem } from '../model/TodoItem'; import DataStore from '../data/DataStore'; `; const result = extractFromSource('pages/imports.ets', code); const imports = result.nodes.filter((n) => n.kind === 'import').map((n) => n.name); expect(imports).toContain('@ohos.router'); expect(imports).toContain('@kit.ArkUI'); expect(imports).toContain('../model/TodoItem'); expect(imports).toContain('../data/DataStore'); }); }); });