GitNexus/gitnexus/test/unit/has-method.test.ts
Gergő Magyar 63fc4c795f
feat: MethodExtractor configs for Python, PHP, Swift, Dart, Rust, Ruby (#624)
* 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>
2026-04-03 16:11:31 +01:00

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();
});
});
});