mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
* feat: MethodExtractor configs for Python, PHP, Swift, Dart, Rust, Ruby with exhaustive integration tests Add per-language MethodExtractionConfig for all remaining tree-sitter languages (RFC #568 PR 2). Each config follows the established createMethodExtractor() factory pattern — no new types, no parse-worker changes. Configs: - Python: @abstractmethod, @staticmethod/@classmethod, *args/**kwargs, type hints, _/__ visibility - PHP: abstract/final/static keywords, PHP 8 #[] attributes, __construct/__destruct - Swift: 5-level visibility, protocol-as-abstract, static/class methods, @ attributes - Dart: _ convention visibility, abstract (no body), method_signature unwrapping - Rust: pub visibility, &self receiver, trait_item + impl_item, #[] attributes - Ruby: positional visibility via sibling-walk, singleton_method as static Integration fixtures (18 directories) covering 3 resolution patterns: - Method enrichment: parameterTypes, isAbstract, isFinal, annotations on graph nodes - Overload dispatch: arity-based CALLS resolution via parameterTypes - Abstract dispatch: abstract/concrete method distinction (Python, PHP, Rust, Swift) Go deferred — requires factory changes for receiver-based method extraction. Closes #571 * fix: address code review findings across 6 MethodExtractor configs Fix all actionable items from the PR #624 deep-dive review: Dart (critical — fixes 6 CI failures): - isDartStatic: check children first, siblings as fallback - isDartAbstract: handle declaration nodes for abstract methods - extractSingleParam: detect required keyword as sibling token - Add declaration to methodNodeTypes, mixin_declaration to typeDeclarationNodes - Add member call query for variable assignments in tree-sitter-queries Python: - hasDecorator now matches dotted paths (e.g. @abc.abstractmethod) - Fix version comment from ^0.23.6 to 0.23.4 PHP: - Add enum_declaration to typeDeclarationNodes (PHP 8.1+) - Add version comment for 0.23.12 Swift: - Add isOverride using hasKeyword/hasModifier pattern Rust: - Fix version comment from ^0.23.2 to 0.23.1 Also: identifier fallback in generic.ts for mixin owner names, Dart integration test label fix (Method vs Function), version comment for tree-sitter-dart 1.0.0. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: Dart extension_declaration and Ruby module_function support Dart: - Add extension_declaration to typeDeclarationNodes and extension_body to bodyNodeTypes — extension methods are now extracted into the graph - Add extension_declaration and mixin_declaration to CLASS_CONTAINER_TYPES for HAS_METHOD edge resolution Ruby: - module_function now maps to visibility 'private' in extractRubyVisibility - module_function methods marked isStatic via backward-walk in isStatic - Override semantics: private/public after module_function resets isStatic Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(go): Go MethodExtractor config with receiver-based extraction Add Go as the 13th language with a per-language MethodExtractor config. Go methods are top-level (not nested in struct bodies), so this adds extractFromNode() to the MethodExtractor interface for direct method node extraction without an enclosing class. Config extracts: - Name from field_identifier (methods) / identifier (functions) - Return type including multi-return (first type from parameter_list) - Parameters with variadic support - Visibility via uppercase/lowercase convention - Receiver type with pointer unwrapping (*User → User) - isStatic for functions (no receiver) Infrastructure: - extractOwnerName optional hook on MethodExtractionConfig - extractFromNode on MethodExtractor (factory auto-implements) - Parse-worker uses extractFromNode when no enclosing class found - method_declaration added to CLASS_CONTAINER_TYPES Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test: method enrichment integration tests for 7 languages + TS abstract class fix Add method-enrichment integration test fixtures and test blocks for Go, C++, Java, Kotlin, TypeScript, JavaScript, and C#. Each fixture tests: class detection, HAS_METHOD edges, EXTENDS edges, isAbstract, isStatic, annotations, parameterTypes, and CALLS edge resolution. Fixes found during testing: - Remove method_declaration from CLASS_CONTAINER_TYPES (added for Go but broke Java/C# HAS_METHOD edge resolution — method_declaration is also Java's method node type) - Add abstract_class_declaration query to TypeScript tree-sitter queries (was missing, so abstract classes were invisible to pipeline) 1699 integration tests pass across 20 test files, 0 regressions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * style: format typeDeclarationNodes array for better readability in PHP config * fix: Go interface methods + Rust impl-for-Struct owner resolution Go: - Add method_elem to methodNodeTypes so interface method signatures are extractable as abstract methods - Integration test: Animal interface detected, Speak isAbstract, CALLS edges from app.go Rust: - Add extractOwnerName to resolve impl Trait for Struct to the concrete Struct (not the Trait) — fixes method misattribution - Fix findEnclosingClassId to generate Struct: label (not Impl:) for impl blocks so HAS_METHOD edges resolve to struct nodes - Tighten abstract-dispatch test: assert SqlRepo owns find/save generic.ts: - Fix extractOwnerName fallback: when hook returns a value, skip both name-field and type_identifier scan (was overwriting result) 1703 integration tests pass, 0 regressions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: code review response — Rust impl label, Swift params, Dart async, sequential methodExtractor Address code review findings from PR #624: - ast-helpers: Rust `impl Trait for Struct` uses Struct label (matches existing graph node), plain `impl Struct` uses Impl label (matches definition.impl) - swift: fix parameter type extraction (user_type not type_annotation), detect default values as function_declaration siblings, add version comment - dart: isDartAsync now detects async*/sync* generators, add clarifying comment for declaration nodes in extension bodies - python: correct isFinal comment (PEP 591 @typing.final exists, just not modeled) - parsing-processor: port methodExtractor enrichment to sequential path so isAbstract/isStatic/visibility/annotations/isFinal populate on <15-file repos - tests: remove silent `if (prop !== undefined)` guards, assert properties directly, fix label queries (Dart Method vs Function, Swift Method for protocol methods), add Rust HAS_METHOD sourceLabel tests, Swift parameterTypes tests, and Dart async/sync* integration tests with fixture Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: Rust grammar gap + qualified method IDs to resolve same-file collisions Phase 1 — Rust grammar: - Add function_signature_item query to RUST_QUERIES so abstract trait methods (fn speak(&self) -> String;) become graph nodes with isAbstract=true Phase 2 — Qualified method IDs: - findEnclosingClassInfo returns {classId, className} for AST-based class lookup - Both parsing paths (sequential + worker) qualify method/property IDs with enclosing class: Method:file:ClassName.method instead of Method:file:method - extractFuncNameFromSourceId handles ClassName.method format - Fixes silent data loss when same-name methods in different classes shared a file (e.g., Animal.speak and Dog.speak both now exist as distinct graph nodes) Test updates: - Rust: abstract+concrete trait methods both verified, function count adjusted - Python: static method disambiguation now emits 2 CALLS edges (correct — no more ID collision masking the second call) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: owner-aware resolution for qualified method IDs Address Codex adversarial review findings after qualified ID change: - findEnclosingFunction: disambiguate candidates by ownerId when multiple same-name methods exist in file; qualify fallback-generated IDs - findEnclosingFunctionId (worker): qualify sourceIds with enclosing class name so CALLS source attribution matches definition-phase node IDs - buildExportedTypeMapFromGraph: use lookupExactAll + nodeId match instead of lookupExactFull which returns first definition for bare name Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: methodExtractor variadic arity, return type preservation, PHP abstract dispatch Three bugs in the methodExtractor enrichment path broke 17 integration tests: 1. Variadic parameterCount: buildMethodProps and parse-worker set parameterCount = info.parameters.length even for variadic functions, causing arity filtering to reject valid calls. Now checks isVariadic and sets parameterCount = undefined (matching extractMethodSignature). 2. C++ bare `...` token: extractCppParameters only iterated named children, missing the unnamed `...` token in C-style variadics like log_entry(const char* fmt, ...). Added fallback scan of all children. 3. Return type stripping: All 11 language extractReturnType functions used extractSimpleTypeName() which strips generic parameters (List<User> → "List", Task<User> → "Task"). Changed to .text?.trim() to preserve full generic types needed for for-loop iterable resolution, async-await binding, and return-type inference. Also fixes PHP abstract dispatch test that matched SqlRepository instead of the interface due to ambiguous filePath.includes('Repository') filter, and adds parent-walk fallback in PHP isAbstract for extractFromNode path. * chore: remove plan and review artifacts from PR * fix: address Round 4 review findings + infrastructure improvements - Ruby: add singleton_class support for class << self methods (4 new tests) - PHP: add enum_declaration to CLASS_CONTAINER_TYPES - Dart: add mixin/extension labels to CONTAINER_TYPE_TO_LABEL - Swift: add TODO for unverifiable struct/enum node types on Node 22 - C#: add grammar version comment (0.23.1) - Ruby: fix version comment range to pin (0.23.1) - Rust/ast-helpers: add cross-reference comments for impl_item duplication - ast-helpers: document CLASS_CONTAINER_TYPES ↔ typeDeclarationNodes invariant - generic.ts: replace Array.includes with Set for O(1) dedup in addNestedBodies - Go/Python/Ruby: align isAbstract signature with 2-param interface contract - CLAUDE.md: fix malformed backtick around gitnexus:start HTML comment - parsing-processor: add per-class method extraction cache (eliminates O(N*M)) - ast-helpers: add scoped_type_identifier to impl_item resolution - call-processor: add dev-mode warnings at silent candidates[0] fallbacks - MCP context(): surface methodMetadata for Method/Function/Constructor nodes - resources.ts: update schema to list all stored Method properties * fix: singleton_class HAS_METHOD edge regression in findEnclosingClassInfo singleton_class (class << self) was added to CLASS_CONTAINER_TYPES but has no name field — its receiver `self` has node type 'self', not 'identifier'. findEnclosingClassInfo now walks up to the enclosing class/module to inherit its name, matching ruby.ts:extractOwnerName. Also fixes findEnclosingClassNode in parse-worker.ts to skip singleton_class and return the actual class/module node. Adds integration test assertions for from_habitat (class << self method): HAS_METHOD edge from Animal, isStatic=true, parameterCount=1. --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
534 lines
16 KiB
TypeScript
534 lines
16 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import Parser from 'tree-sitter';
|
|
import TypeScript from 'tree-sitter-typescript';
|
|
import Python from 'tree-sitter-python';
|
|
import Java from 'tree-sitter-java';
|
|
import CPP from 'tree-sitter-cpp';
|
|
import Rust from 'tree-sitter-rust';
|
|
import CSharp from 'tree-sitter-c-sharp';
|
|
import Go from 'tree-sitter-go';
|
|
import {
|
|
findEnclosingClassId,
|
|
CLASS_CONTAINER_TYPES,
|
|
CONTAINER_TYPE_TO_LABEL,
|
|
} from '../../src/core/ingestion/utils/ast-helpers.js';
|
|
|
|
function parseCode(language: any, code: string): Parser.Tree {
|
|
const parser = new Parser();
|
|
parser.setLanguage(language);
|
|
return parser.parse(code);
|
|
}
|
|
|
|
/** Find the first descendant node matching a predicate (BFS). */
|
|
function findNode(
|
|
root: Parser.SyntaxNode,
|
|
predicate: (n: Parser.SyntaxNode) => boolean,
|
|
): Parser.SyntaxNode | null {
|
|
const queue: Parser.SyntaxNode[] = [root];
|
|
while (queue.length > 0) {
|
|
const node = queue.shift()!;
|
|
if (predicate(node)) return node;
|
|
for (let i = 0; i < node.childCount; i++) {
|
|
queue.push(node.child(i)!);
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
describe('CLASS_CONTAINER_TYPES', () => {
|
|
it('contains expected class-like AST node types', () => {
|
|
expect(CLASS_CONTAINER_TYPES.has('class_declaration')).toBe(true);
|
|
expect(CLASS_CONTAINER_TYPES.has('interface_declaration')).toBe(true);
|
|
expect(CLASS_CONTAINER_TYPES.has('struct_declaration')).toBe(true);
|
|
expect(CLASS_CONTAINER_TYPES.has('impl_item')).toBe(true);
|
|
expect(CLASS_CONTAINER_TYPES.has('class_specifier')).toBe(true);
|
|
expect(CLASS_CONTAINER_TYPES.has('class_definition')).toBe(true);
|
|
});
|
|
|
|
it('does not contain function types', () => {
|
|
expect(CLASS_CONTAINER_TYPES.has('function_declaration')).toBe(false);
|
|
expect(CLASS_CONTAINER_TYPES.has('function_definition')).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('CONTAINER_TYPE_TO_LABEL', () => {
|
|
it('maps class-like types to correct labels', () => {
|
|
expect(CONTAINER_TYPE_TO_LABEL['class_declaration']).toBe('Class');
|
|
expect(CONTAINER_TYPE_TO_LABEL['interface_declaration']).toBe('Interface');
|
|
expect(CONTAINER_TYPE_TO_LABEL['struct_declaration']).toBe('Struct');
|
|
expect(CONTAINER_TYPE_TO_LABEL['impl_item']).toBe('Impl');
|
|
expect(CONTAINER_TYPE_TO_LABEL['trait_item']).toBe('Trait');
|
|
expect(CONTAINER_TYPE_TO_LABEL['record_declaration']).toBe('Record');
|
|
expect(CONTAINER_TYPE_TO_LABEL['protocol_declaration']).toBe('Interface');
|
|
});
|
|
});
|
|
|
|
describe('findEnclosingClassId', () => {
|
|
const filePath = 'test/example.ts';
|
|
|
|
describe('TypeScript', () => {
|
|
it('finds enclosing class for a method', () => {
|
|
const tree = parseCode(
|
|
TypeScript.typescript,
|
|
`
|
|
class MyService {
|
|
getData() {
|
|
return 42;
|
|
}
|
|
}
|
|
`,
|
|
);
|
|
// Find the method_definition node for getData
|
|
const methodNode = findNode(tree.rootNode, (n) => n.type === 'method_definition');
|
|
expect(methodNode).not.toBeNull();
|
|
|
|
// Find the identifier 'getData' inside the method
|
|
const nameNode = findNode(
|
|
methodNode!,
|
|
(n) => n.type === 'property_identifier' && n.text === 'getData',
|
|
);
|
|
expect(nameNode).not.toBeNull();
|
|
|
|
const result = findEnclosingClassId(nameNode!, filePath);
|
|
expect(result).toBe('Class:test/example.ts:MyService');
|
|
});
|
|
|
|
it('finds enclosing interface for a method signature', () => {
|
|
const tree = parseCode(
|
|
TypeScript.typescript,
|
|
`
|
|
interface MyInterface {
|
|
doSomething(): void;
|
|
}
|
|
`,
|
|
);
|
|
// In TS, interface methods are method_signature nodes — find method name
|
|
const nameNode = findNode(
|
|
tree.rootNode,
|
|
(n) => n.type === 'property_identifier' && n.text === 'doSomething',
|
|
);
|
|
if (nameNode) {
|
|
const result = findEnclosingClassId(nameNode, filePath);
|
|
expect(result).toBe('Interface:test/example.ts:MyInterface');
|
|
}
|
|
});
|
|
|
|
it('returns null for a top-level function', () => {
|
|
const tree = parseCode(
|
|
TypeScript.typescript,
|
|
`
|
|
function topLevel() {
|
|
return 1;
|
|
}
|
|
`,
|
|
);
|
|
const nameNode = findNode(
|
|
tree.rootNode,
|
|
(n) => n.type === 'identifier' && n.text === 'topLevel',
|
|
);
|
|
expect(nameNode).not.toBeNull();
|
|
|
|
const result = findEnclosingClassId(nameNode!, filePath);
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it('returns null when node has no parent', () => {
|
|
// Root node's parent is null
|
|
const tree = parseCode(TypeScript.typescript, 'const x = 1;');
|
|
const result = findEnclosingClassId(tree.rootNode, filePath);
|
|
expect(result).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('Python', () => {
|
|
it('finds enclosing class for a method', () => {
|
|
const tree = parseCode(
|
|
Python,
|
|
`
|
|
class Calculator:
|
|
def add(self, a, b):
|
|
return a + b
|
|
`,
|
|
);
|
|
const methodNode = findNode(tree.rootNode, (n) => n.type === 'function_definition');
|
|
expect(methodNode).not.toBeNull();
|
|
|
|
const nameNode = findNode(methodNode!, (n) => n.type === 'identifier' && n.text === 'add');
|
|
expect(nameNode).not.toBeNull();
|
|
|
|
const result = findEnclosingClassId(nameNode!, 'test/calc.py');
|
|
expect(result).toBe('Class:test/calc.py:Calculator');
|
|
});
|
|
});
|
|
|
|
describe('Java', () => {
|
|
it('finds enclosing class for a method', () => {
|
|
const tree = parseCode(
|
|
Java,
|
|
`
|
|
class UserService {
|
|
public void save(User user) {}
|
|
}
|
|
`,
|
|
);
|
|
const methodNode = findNode(tree.rootNode, (n) => n.type === 'method_declaration');
|
|
expect(methodNode).not.toBeNull();
|
|
|
|
const nameNode = findNode(methodNode!, (n) => n.type === 'identifier' && n.text === 'save');
|
|
expect(nameNode).not.toBeNull();
|
|
|
|
const result = findEnclosingClassId(nameNode!, 'test/UserService.java');
|
|
expect(result).toBe('Class:test/UserService.java:UserService');
|
|
});
|
|
|
|
it('finds enclosing interface for a method', () => {
|
|
const tree = parseCode(
|
|
Java,
|
|
`
|
|
interface Repository {
|
|
void findById(int id);
|
|
}
|
|
`,
|
|
);
|
|
const methodNode = findNode(tree.rootNode, (n) => n.type === 'method_declaration');
|
|
expect(methodNode).not.toBeNull();
|
|
|
|
const nameNode = findNode(
|
|
methodNode!,
|
|
(n) => n.type === 'identifier' && n.text === 'findById',
|
|
);
|
|
expect(nameNode).not.toBeNull();
|
|
|
|
const result = findEnclosingClassId(nameNode!, 'test/Repository.java');
|
|
expect(result).toBe('Interface:test/Repository.java:Repository');
|
|
});
|
|
});
|
|
|
|
describe('C++', () => {
|
|
it('finds enclosing class_specifier for a method', () => {
|
|
const tree = parseCode(
|
|
CPP,
|
|
`
|
|
class Vector {
|
|
public:
|
|
int size() { return _size; }
|
|
private:
|
|
int _size;
|
|
};
|
|
`,
|
|
);
|
|
// In C++ tree-sitter, the class is a class_specifier
|
|
const classNode = findNode(tree.rootNode, (n) => n.type === 'class_specifier');
|
|
expect(classNode).not.toBeNull();
|
|
|
|
// Find a function_definition inside the class
|
|
const funcDef = findNode(classNode!, (n) => n.type === 'function_definition');
|
|
expect(funcDef).not.toBeNull();
|
|
|
|
const nameNode = findNode(funcDef!, (n) => n.type === 'identifier' && n.text === 'size');
|
|
if (nameNode) {
|
|
const result = findEnclosingClassId(nameNode, 'test/vector.h');
|
|
expect(result).toBe('Class:test/vector.h:Vector');
|
|
}
|
|
});
|
|
|
|
it('finds enclosing struct_specifier for a method', () => {
|
|
const tree = parseCode(
|
|
CPP,
|
|
`
|
|
struct Point {
|
|
double distance() { return 0; }
|
|
};
|
|
`,
|
|
);
|
|
const funcDef = findNode(tree.rootNode, (n) => n.type === 'function_definition');
|
|
if (funcDef) {
|
|
const nameNode = findNode(funcDef, (n) => n.type === 'identifier' && n.text === 'distance');
|
|
if (nameNode) {
|
|
const result = findEnclosingClassId(nameNode, 'test/point.h');
|
|
expect(result).toBe('Struct:test/point.h:Point');
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('Rust', () => {
|
|
it('finds enclosing impl_item for a method', () => {
|
|
const tree = parseCode(
|
|
Rust,
|
|
`
|
|
struct Counter {
|
|
count: u32,
|
|
}
|
|
impl Counter {
|
|
fn increment(&mut self) {
|
|
self.count += 1;
|
|
}
|
|
}
|
|
`,
|
|
);
|
|
const implNode = findNode(tree.rootNode, (n) => n.type === 'impl_item');
|
|
expect(implNode).not.toBeNull();
|
|
|
|
const funcItem = findNode(implNode!, (n) => n.type === 'function_item');
|
|
expect(funcItem).not.toBeNull();
|
|
|
|
const nameNode = findNode(
|
|
funcItem!,
|
|
(n) => n.type === 'identifier' && n.text === 'increment',
|
|
);
|
|
expect(nameNode).not.toBeNull();
|
|
|
|
const result = findEnclosingClassId(nameNode!, 'test/counter.rs');
|
|
expect(result).toBe('Impl:test/counter.rs:Counter');
|
|
});
|
|
|
|
it('picks struct name (not trait name) for impl Trait for Struct', () => {
|
|
const tree = parseCode(
|
|
Rust,
|
|
`
|
|
struct MyStruct {
|
|
value: i32,
|
|
}
|
|
|
|
trait MyTrait {
|
|
fn do_something(&self);
|
|
}
|
|
|
|
impl MyTrait for MyStruct {
|
|
fn do_something(&self) {
|
|
println!("{}", self.value);
|
|
}
|
|
}
|
|
`,
|
|
);
|
|
// Find the impl_item that has a `for` keyword (impl Trait for Struct)
|
|
const implNode = findNode(
|
|
tree.rootNode,
|
|
(n) => n.type === 'impl_item' && n.children?.some((c: any) => c.text === 'for'),
|
|
);
|
|
expect(implNode).not.toBeNull();
|
|
|
|
const funcItem = findNode(implNode!, (n) => n.type === 'function_item');
|
|
expect(funcItem).not.toBeNull();
|
|
|
|
const nameNode = findNode(
|
|
funcItem!,
|
|
(n) => n.type === 'identifier' && n.text === 'do_something',
|
|
);
|
|
expect(nameNode).not.toBeNull();
|
|
|
|
const result = findEnclosingClassId(nameNode!, 'test/my_struct.rs');
|
|
// Should resolve to MyStruct (the implementing type), NOT MyTrait.
|
|
// Uses Struct label because trait impls don't create Impl nodes in the graph
|
|
// (the definition.impl query uses !trait filter).
|
|
expect(result).toBe('Struct:test/my_struct.rs:MyStruct');
|
|
});
|
|
|
|
it('still picks struct name for plain impl Struct (no trait)', () => {
|
|
const tree = parseCode(
|
|
Rust,
|
|
`
|
|
struct Counter {
|
|
count: u32,
|
|
}
|
|
impl Counter {
|
|
fn increment(&mut self) {
|
|
self.count += 1;
|
|
}
|
|
}
|
|
`,
|
|
);
|
|
const implNode = findNode(tree.rootNode, (n) => n.type === 'impl_item');
|
|
expect(implNode).not.toBeNull();
|
|
|
|
const funcItem = findNode(implNode!, (n) => n.type === 'function_item');
|
|
expect(funcItem).not.toBeNull();
|
|
|
|
const nameNode = findNode(
|
|
funcItem!,
|
|
(n) => n.type === 'identifier' && n.text === 'increment',
|
|
);
|
|
expect(nameNode).not.toBeNull();
|
|
|
|
const result = findEnclosingClassId(nameNode!, 'test/counter.rs');
|
|
expect(result).toBe('Impl:test/counter.rs:Counter');
|
|
});
|
|
|
|
it('finds enclosing trait_item for a method', () => {
|
|
const tree = parseCode(
|
|
Rust,
|
|
`
|
|
trait Drawable {
|
|
fn draw(&self);
|
|
}
|
|
`,
|
|
);
|
|
const traitNode = findNode(tree.rootNode, (n) => n.type === 'trait_item');
|
|
expect(traitNode).not.toBeNull();
|
|
|
|
const funcItem = findNode(
|
|
traitNode!,
|
|
(n) => n.type === 'function_signature_item' || n.type === 'function_item',
|
|
);
|
|
if (funcItem) {
|
|
const nameNode = findNode(funcItem, (n) => n.type === 'identifier' && n.text === 'draw');
|
|
if (nameNode) {
|
|
const result = findEnclosingClassId(nameNode, 'test/draw.rs');
|
|
expect(result).toBe('Trait:test/draw.rs:Drawable');
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('C#', () => {
|
|
it('finds enclosing class for a method', () => {
|
|
const tree = parseCode(
|
|
CSharp,
|
|
`
|
|
class OrderService {
|
|
public void Process() {}
|
|
}
|
|
`,
|
|
);
|
|
const methodNode = findNode(tree.rootNode, (n) => n.type === 'method_declaration');
|
|
expect(methodNode).not.toBeNull();
|
|
|
|
const nameNode = findNode(
|
|
methodNode!,
|
|
(n) => n.type === 'identifier' && n.text === 'Process',
|
|
);
|
|
expect(nameNode).not.toBeNull();
|
|
|
|
const result = findEnclosingClassId(nameNode!, 'test/OrderService.cs');
|
|
expect(result).toBe('Class:test/OrderService.cs:OrderService');
|
|
});
|
|
|
|
it('finds enclosing record for a method', () => {
|
|
const tree = parseCode(
|
|
CSharp,
|
|
`
|
|
record Person {
|
|
public string GetName() { return ""; }
|
|
}
|
|
`,
|
|
);
|
|
const methodNode = findNode(tree.rootNode, (n) => n.type === 'method_declaration');
|
|
if (methodNode) {
|
|
const nameNode = findNode(
|
|
methodNode,
|
|
(n) => n.type === 'identifier' && n.text === 'GetName',
|
|
);
|
|
if (nameNode) {
|
|
const result = findEnclosingClassId(nameNode, 'test/Person.cs');
|
|
expect(result).toBe('Record:test/Person.cs:Person');
|
|
}
|
|
}
|
|
});
|
|
|
|
it('finds enclosing struct for a method', () => {
|
|
const tree = parseCode(
|
|
CSharp,
|
|
`
|
|
struct Vector2 {
|
|
public float Length() { return 0; }
|
|
}
|
|
`,
|
|
);
|
|
const methodNode = findNode(tree.rootNode, (n) => n.type === 'method_declaration');
|
|
if (methodNode) {
|
|
const nameNode = findNode(
|
|
methodNode,
|
|
(n) => n.type === 'identifier' && n.text === 'Length',
|
|
);
|
|
if (nameNode) {
|
|
const result = findEnclosingClassId(nameNode, 'test/Vector2.cs');
|
|
expect(result).toBe('Struct:test/Vector2.cs:Vector2');
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('Go', () => {
|
|
it('returns receiver struct ID for Go methods', () => {
|
|
// Go methods have receiver parameter: func (s *Server) Start() {}
|
|
// findEnclosingClassId extracts the receiver type to link method → struct
|
|
const tree = parseCode(
|
|
Go,
|
|
`
|
|
package main
|
|
|
|
type Server struct {
|
|
port int
|
|
}
|
|
|
|
func (s *Server) Start() {}
|
|
`,
|
|
);
|
|
const methodNode = findNode(tree.rootNode, (n) => n.type === 'method_declaration');
|
|
expect(methodNode).not.toBeNull();
|
|
|
|
const nameNode = findNode(
|
|
methodNode!,
|
|
(n) => n.type === 'field_identifier' && n.text === 'Start',
|
|
);
|
|
if (nameNode) {
|
|
const result = findEnclosingClassId(nameNode, 'test/server.go');
|
|
expect(result).not.toBeNull();
|
|
// Should generate a Struct ID for "Server"
|
|
expect(result).toContain('Server');
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('edge cases', () => {
|
|
it('handles nested classes — returns innermost enclosing class', () => {
|
|
const tree = parseCode(
|
|
TypeScript.typescript,
|
|
`
|
|
class Outer {
|
|
inner = class Inner {
|
|
doWork() {}
|
|
}
|
|
}
|
|
`,
|
|
);
|
|
const methodNode = findNode(tree.rootNode, (n) => n.type === 'method_definition');
|
|
if (methodNode) {
|
|
const nameNode = findNode(
|
|
methodNode,
|
|
(n) => n.type === 'property_identifier' && n.text === 'doWork',
|
|
);
|
|
if (nameNode) {
|
|
const result = findEnclosingClassId(nameNode, filePath);
|
|
// Should find the innermost class (Inner, which is a class node)
|
|
expect(result).not.toBeNull();
|
|
// The result should reference the inner class, not the outer
|
|
}
|
|
}
|
|
});
|
|
|
|
it('returns null for a node without parent', () => {
|
|
// Simulate a node with null parent
|
|
const fakeNode = { parent: null };
|
|
const result = findEnclosingClassId(fakeNode, filePath);
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it('skips containers without a name node', () => {
|
|
// Simulate AST nodes where the class container has no name
|
|
const fakeClassNode = {
|
|
type: 'class_declaration',
|
|
childForFieldName: () => null,
|
|
children: [],
|
|
parent: null,
|
|
};
|
|
const fakeChild = {
|
|
parent: fakeClassNode,
|
|
};
|
|
const result = findEnclosingClassId(fakeChild, filePath);
|
|
// The class has no name, so should return null
|
|
expect(result).toBeNull();
|
|
});
|
|
});
|
|
});
|