mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
* feat(cpp): C/C++ MethodExtractor config with pure virtual detection (#572) - Pure virtual (= 0) detected as isAbstract via token scanning - virtual/final/override via hasKeyword and virtual_specifier children - Access specifier visibility via backward sibling walk (public:/private:/protected:) - Pointer/reference parameter types extracted correctly - Constructor and destructor support via declaration node type - Static detection via storage_class_specifier - 16 new tests covering all acceptance criteria * fix(cpp): isVirtual infers from override/final + out-of-class resolution - isVirtual returns true for override/final methods (C++ mandates these are virtual) - Add findClassNodeByQualifiedName to parse-worker: resolves Foo::bar() back to the Foo class declaration for method extractor enrichment - Handles pointer/ref return types, constructors, destructors - Integration test for virtual/static/constructor inline methods - 233 unit+integration tests pass, 97 C++ resolver tests pass * fix(cpp): address review — deep pointers, templates, unions, trailing returns - Fix extractParamName: recursive unwrap for int** ptr → "ptr" (not "**ptr") - Fix findFunctionDeclarator: recursive unwrap for multi-level pointer chains - Template methods: generic extractor unwraps template_declaration to inner node - union_specifier: added to typeDeclarationNodes, visibility defaults to public - Trailing return type: auto foo() -> T now extracts T instead of "auto" - Fix version comment: ^0.22.4 → ^0.23.4 to match package.json - 4 new tests: double pointer params, template methods, union methods, trailing returns * fix(cpp): template method visibility + union isTypeDeclaration test extractCppVisibility now walks from the template_declaration parent when the node is wrapped by a template, restoring correct access- specifier resolution for templated class methods. Also adds missing isTypeDeclaration assertion for union_specifier and expands the template method test with explicit visibility checks. * fix(cpp): address deep gap analysis review findings - findClassNodeByQualifiedName: recursive pointer/reference declarator unwrap, fixing out-of-class linking for deep pointer return types (e.g. int** Foo::bar()) - findClassNodeByQualifiedName: recurse into namespace_definition blocks so namespace-wrapped classes resolve correctly - Suppress = delete / = default special members from extraction via delete_method_clause / default_method_clause node detection - Update known-gaps: namespace-wrapped classes, const-overload collapse - Add tree-sitter-c version comment for consistency - toBeFalsy() → toBe(undefined) for precise isVirtual assertion - Tests: = delete, = default, = 0 non-regression, operator overloads, deep pointer return types, default visibility (class vs struct), multiple access specifier sections
718 lines
22 KiB
TypeScript
718 lines
22 KiB
TypeScript
/**
|
|
* Integration tests for HAS_METHOD edge extraction.
|
|
*
|
|
* These tests exercise findEnclosingClassId against real tree-sitter ASTs
|
|
* produced by the actual parser pipeline (loadParser + loadLanguage + queries).
|
|
* Unlike the unit tests that test findEnclosingClassId in isolation with simple
|
|
* snippets, these focus on multi-class files, interface vs class disambiguation,
|
|
* and cross-language pipeline correctness.
|
|
*/
|
|
import { describe, it, expect, beforeAll } from 'vitest';
|
|
import Parser from 'tree-sitter';
|
|
import { loadParser, loadLanguage } from '../../src/core/tree-sitter/parser-loader.js';
|
|
import { SupportedLanguages } from '../../src/config/supported-languages.js';
|
|
import { getProvider } from '../../src/core/ingestion/languages/index.js';
|
|
import {
|
|
findEnclosingClassId,
|
|
getDefinitionNodeFromCaptures,
|
|
} from '../../src/core/ingestion/utils/ast-helpers.js';
|
|
|
|
let parser: Parser;
|
|
|
|
beforeAll(async () => {
|
|
parser = await loadParser();
|
|
});
|
|
|
|
/** Parse code with given language, run definition queries, return matched definitions with their enclosing class IDs. */
|
|
function parseAndExtractMethods(
|
|
code: string,
|
|
lang: SupportedLanguages,
|
|
filePath: string,
|
|
): { name: string; defType: string; enclosingClassId: string | null }[] {
|
|
const tree = parser.parse(code);
|
|
const provider = getProvider(lang);
|
|
const query = new Parser.Query(parser.getLanguage(), provider.treeSitterQueries);
|
|
const matches = query.matches(tree.rootNode);
|
|
|
|
const results: { name: string; defType: string; enclosingClassId: string | null }[] = [];
|
|
|
|
for (const match of matches) {
|
|
const captureMap: Record<string, any> = {};
|
|
let nameNode: any = null;
|
|
|
|
for (const capture of match.captures) {
|
|
captureMap[capture.name] = capture.node;
|
|
if (capture.name === 'name') {
|
|
nameNode = capture.node;
|
|
}
|
|
}
|
|
|
|
const defNode = getDefinitionNodeFromCaptures(captureMap);
|
|
if (!defNode || !nameNode) continue;
|
|
|
|
const defType = Object.keys(captureMap).find((k) => k.startsWith('definition.')) || 'unknown';
|
|
const enclosingClassId = findEnclosingClassId(nameNode, filePath);
|
|
|
|
results.push({
|
|
name: nameNode.text,
|
|
defType,
|
|
enclosingClassId,
|
|
});
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
describe('HAS_METHOD integration — C#: class with interface', () => {
|
|
beforeAll(async () => {
|
|
await loadLanguage(SupportedLanguages.CSharp);
|
|
});
|
|
|
|
it('methods link to correct owner (interface vs class)', () => {
|
|
const code = `
|
|
interface IRepository {
|
|
void FindById(int id);
|
|
void Save(object entity);
|
|
}
|
|
|
|
class SqlRepository {
|
|
public void FindById(int id) {}
|
|
public void Save(object entity) {}
|
|
private void Connect() {}
|
|
}
|
|
`;
|
|
const results = parseAndExtractMethods(code, SupportedLanguages.CSharp, 'src/Repo.cs');
|
|
|
|
// Interface methods should be enclosed by the interface
|
|
const ifaceFindById = results.find(
|
|
(r) => r.name === 'FindById' && r.enclosingClassId?.startsWith('Interface:'),
|
|
);
|
|
expect(ifaceFindById).toBeDefined();
|
|
expect(ifaceFindById!.enclosingClassId).toBe('Interface:src/Repo.cs:IRepository');
|
|
|
|
const ifaceSave = results.find(
|
|
(r) => r.name === 'Save' && r.enclosingClassId?.startsWith('Interface:'),
|
|
);
|
|
expect(ifaceSave).toBeDefined();
|
|
expect(ifaceSave!.enclosingClassId).toBe('Interface:src/Repo.cs:IRepository');
|
|
|
|
// Class methods should be enclosed by the class
|
|
const classFindById = results.find(
|
|
(r) => r.name === 'FindById' && r.enclosingClassId?.startsWith('Class:'),
|
|
);
|
|
expect(classFindById).toBeDefined();
|
|
expect(classFindById!.enclosingClassId).toBe('Class:src/Repo.cs:SqlRepository');
|
|
|
|
const classConnect = results.find((r) => r.name === 'Connect');
|
|
expect(classConnect).toBeDefined();
|
|
expect(classConnect!.enclosingClassId).toBe('Class:src/Repo.cs:SqlRepository');
|
|
});
|
|
|
|
it('class/interface name captures point to their own container (self-referential)', () => {
|
|
const code = `
|
|
interface IService {
|
|
void Execute();
|
|
}
|
|
|
|
class ServiceImpl {
|
|
public void Execute() {}
|
|
}
|
|
`;
|
|
const results = parseAndExtractMethods(code, SupportedLanguages.CSharp, 'src/Service.cs');
|
|
|
|
// The name node for IService sits inside the interface_declaration, so
|
|
// findEnclosingClassId returns the interface itself. This is expected —
|
|
// the pipeline uses defType (definition.interface vs definition.method) to
|
|
// distinguish container declarations from methods, not enclosingClassId.
|
|
const ifaceDecl = results.find((r) => r.name === 'IService');
|
|
expect(ifaceDecl).toBeDefined();
|
|
expect(ifaceDecl!.defType).toBe('definition.interface');
|
|
|
|
const classDecl = results.find((r) => r.name === 'ServiceImpl');
|
|
expect(classDecl).toBeDefined();
|
|
expect(classDecl!.defType).toBe('definition.class');
|
|
|
|
// Methods should still correctly reference their container
|
|
const execMethods = results.filter((r) => r.name === 'Execute');
|
|
expect(execMethods.length).toBe(2);
|
|
expect(
|
|
execMethods.some((r) => r.enclosingClassId === 'Interface:src/Service.cs:IService'),
|
|
).toBe(true);
|
|
expect(execMethods.some((r) => r.enclosingClassId === 'Class:src/Service.cs:ServiceImpl')).toBe(
|
|
true,
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('HAS_METHOD integration — Rust: impl + trait', () => {
|
|
beforeAll(async () => {
|
|
await loadLanguage(SupportedLanguages.Rust);
|
|
});
|
|
|
|
it('methods link to impl vs trait nodes', () => {
|
|
const code = `
|
|
trait Drawable {
|
|
fn draw(&self);
|
|
fn resize(&self, w: u32, h: u32);
|
|
}
|
|
|
|
struct Circle {
|
|
radius: f64,
|
|
}
|
|
|
|
impl Circle {
|
|
fn new(radius: f64) -> Circle {
|
|
Circle { radius }
|
|
}
|
|
|
|
fn area(&self) -> f64 {
|
|
3.14 * self.radius * self.radius
|
|
}
|
|
}
|
|
`;
|
|
const results = parseAndExtractMethods(code, SupportedLanguages.Rust, 'src/shapes.rs');
|
|
|
|
// Trait methods should be enclosed by the trait
|
|
const traitDraw = results.find((r) => r.name === 'draw');
|
|
if (traitDraw) {
|
|
expect(traitDraw.enclosingClassId).toBe('Trait:src/shapes.rs:Drawable');
|
|
}
|
|
|
|
const traitResize = results.find((r) => r.name === 'resize');
|
|
if (traitResize) {
|
|
expect(traitResize.enclosingClassId).toBe('Trait:src/shapes.rs:Drawable');
|
|
}
|
|
|
|
// Impl methods should be enclosed by the impl block
|
|
const implNew = results.find((r) => r.name === 'new');
|
|
if (implNew) {
|
|
expect(implNew.enclosingClassId).toBe('Impl:src/shapes.rs:Circle');
|
|
}
|
|
|
|
const implArea = results.find((r) => r.name === 'area');
|
|
if (implArea) {
|
|
expect(implArea.enclosingClassId).toBe('Impl:src/shapes.rs:Circle');
|
|
}
|
|
});
|
|
|
|
it('standalone functions do not get HAS_METHOD', () => {
|
|
const code = `
|
|
fn helper() -> bool {
|
|
true
|
|
}
|
|
|
|
struct Foo;
|
|
|
|
impl Foo {
|
|
fn bar(&self) {}
|
|
}
|
|
`;
|
|
const results = parseAndExtractMethods(code, SupportedLanguages.Rust, 'src/lib.rs');
|
|
|
|
const helper = results.find((r) => r.name === 'helper');
|
|
expect(helper).toBeDefined();
|
|
expect(helper!.enclosingClassId).toBeNull();
|
|
|
|
const bar = results.find((r) => r.name === 'bar');
|
|
if (bar) {
|
|
expect(bar.enclosingClassId).toBe('Impl:src/lib.rs:Foo');
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('HAS_METHOD integration — Python: class methods vs standalone functions', () => {
|
|
beforeAll(async () => {
|
|
await loadLanguage(SupportedLanguages.Python);
|
|
});
|
|
|
|
it('methods link to class, standalone functions get null', () => {
|
|
const code = `
|
|
def standalone_helper():
|
|
return 42
|
|
|
|
class Calculator:
|
|
def __init__(self):
|
|
self.value = 0
|
|
|
|
def add(self, x):
|
|
self.value += x
|
|
return self
|
|
|
|
def result(self):
|
|
return self.value
|
|
|
|
def another_standalone():
|
|
pass
|
|
`;
|
|
const results = parseAndExtractMethods(code, SupportedLanguages.Python, 'src/calc.py');
|
|
|
|
// Standalone functions should not be enclosed
|
|
const standaloneHelper = results.find((r) => r.name === 'standalone_helper');
|
|
expect(standaloneHelper).toBeDefined();
|
|
expect(standaloneHelper!.enclosingClassId).toBeNull();
|
|
|
|
const anotherStandalone = results.find((r) => r.name === 'another_standalone');
|
|
expect(anotherStandalone).toBeDefined();
|
|
expect(anotherStandalone!.enclosingClassId).toBeNull();
|
|
|
|
// Class methods should be enclosed
|
|
const init = results.find((r) => r.name === '__init__');
|
|
expect(init).toBeDefined();
|
|
expect(init!.enclosingClassId).toBe('Class:src/calc.py:Calculator');
|
|
|
|
const add = results.find((r) => r.name === 'add');
|
|
expect(add).toBeDefined();
|
|
expect(add!.enclosingClassId).toBe('Class:src/calc.py:Calculator');
|
|
|
|
const resultMethod = results.find((r) => r.name === 'result');
|
|
expect(resultMethod).toBeDefined();
|
|
expect(resultMethod!.enclosingClassId).toBe('Class:src/calc.py:Calculator');
|
|
});
|
|
});
|
|
|
|
describe('HAS_METHOD integration — Multiple classes in one file', () => {
|
|
describe('TypeScript', () => {
|
|
beforeAll(async () => {
|
|
await loadLanguage(SupportedLanguages.TypeScript, 'multi.ts');
|
|
});
|
|
|
|
it('methods associate with their owning class', () => {
|
|
const code = `
|
|
class UserService {
|
|
findUser(id: number) {
|
|
return null;
|
|
}
|
|
deleteUser(id: number) {}
|
|
}
|
|
|
|
class OrderService {
|
|
createOrder(data: any) {
|
|
return data;
|
|
}
|
|
cancelOrder(id: number) {}
|
|
}
|
|
|
|
function topLevelUtil() {
|
|
return true;
|
|
}
|
|
`;
|
|
const results = parseAndExtractMethods(
|
|
code,
|
|
SupportedLanguages.TypeScript,
|
|
'src/services.ts',
|
|
);
|
|
|
|
// UserService methods
|
|
const findUser = results.find((r) => r.name === 'findUser');
|
|
expect(findUser).toBeDefined();
|
|
expect(findUser!.enclosingClassId).toBe('Class:src/services.ts:UserService');
|
|
|
|
const deleteUser = results.find((r) => r.name === 'deleteUser');
|
|
expect(deleteUser).toBeDefined();
|
|
expect(deleteUser!.enclosingClassId).toBe('Class:src/services.ts:UserService');
|
|
|
|
// OrderService methods
|
|
const createOrder = results.find((r) => r.name === 'createOrder');
|
|
expect(createOrder).toBeDefined();
|
|
expect(createOrder!.enclosingClassId).toBe('Class:src/services.ts:OrderService');
|
|
|
|
const cancelOrder = results.find((r) => r.name === 'cancelOrder');
|
|
expect(cancelOrder).toBeDefined();
|
|
expect(cancelOrder!.enclosingClassId).toBe('Class:src/services.ts:OrderService');
|
|
|
|
// Top-level function
|
|
const topLevelUtil = results.find((r) => r.name === 'topLevelUtil');
|
|
expect(topLevelUtil).toBeDefined();
|
|
expect(topLevelUtil!.enclosingClassId).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('Java', () => {
|
|
beforeAll(async () => {
|
|
await loadLanguage(SupportedLanguages.Java);
|
|
});
|
|
|
|
it('methods associate with their owning class', () => {
|
|
const code = `
|
|
class Logger {
|
|
public void info(String msg) {}
|
|
public void error(String msg) {}
|
|
}
|
|
|
|
class Formatter {
|
|
public String format(String template) { return template; }
|
|
private String escape(String input) { return input; }
|
|
}
|
|
`;
|
|
const results = parseAndExtractMethods(
|
|
code,
|
|
SupportedLanguages.Java,
|
|
'src/util/Logging.java',
|
|
);
|
|
|
|
const info = results.find((r) => r.name === 'info');
|
|
expect(info).toBeDefined();
|
|
expect(info!.enclosingClassId).toBe('Class:src/util/Logging.java:Logger');
|
|
|
|
const error = results.find((r) => r.name === 'error');
|
|
expect(error).toBeDefined();
|
|
expect(error!.enclosingClassId).toBe('Class:src/util/Logging.java:Logger');
|
|
|
|
const format = results.find((r) => r.name === 'format');
|
|
expect(format).toBeDefined();
|
|
expect(format!.enclosingClassId).toBe('Class:src/util/Logging.java:Formatter');
|
|
|
|
const escape = results.find((r) => r.name === 'escape');
|
|
expect(escape).toBeDefined();
|
|
expect(escape!.enclosingClassId).toBe('Class:src/util/Logging.java:Formatter');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('HAS_METHOD integration — Java: class with interface', () => {
|
|
beforeAll(async () => {
|
|
await loadLanguage(SupportedLanguages.Java);
|
|
});
|
|
|
|
it('methods link to correct owner (interface vs class)', () => {
|
|
const code = `
|
|
interface Validator {
|
|
boolean validate(Object input);
|
|
String getMessage();
|
|
}
|
|
|
|
class EmailValidator {
|
|
public boolean validate(Object input) { return true; }
|
|
public String getMessage() { return "invalid email"; }
|
|
private boolean checkFormat(String email) { return true; }
|
|
}
|
|
`;
|
|
const results = parseAndExtractMethods(
|
|
code,
|
|
SupportedLanguages.Java,
|
|
'src/validation/Validator.java',
|
|
);
|
|
|
|
// Interface methods
|
|
const ifaceValidate = results.find(
|
|
(r) => r.name === 'validate' && r.enclosingClassId?.startsWith('Interface:'),
|
|
);
|
|
expect(ifaceValidate).toBeDefined();
|
|
expect(ifaceValidate!.enclosingClassId).toBe(
|
|
'Interface:src/validation/Validator.java:Validator',
|
|
);
|
|
|
|
const ifaceGetMessage = results.find(
|
|
(r) => r.name === 'getMessage' && r.enclosingClassId?.startsWith('Interface:'),
|
|
);
|
|
expect(ifaceGetMessage).toBeDefined();
|
|
expect(ifaceGetMessage!.enclosingClassId).toBe(
|
|
'Interface:src/validation/Validator.java:Validator',
|
|
);
|
|
|
|
// Class methods
|
|
const classValidate = results.find(
|
|
(r) => r.name === 'validate' && r.enclosingClassId?.startsWith('Class:'),
|
|
);
|
|
expect(classValidate).toBeDefined();
|
|
expect(classValidate!.enclosingClassId).toBe(
|
|
'Class:src/validation/Validator.java:EmailValidator',
|
|
);
|
|
|
|
const classCheckFormat = results.find((r) => r.name === 'checkFormat');
|
|
expect(classCheckFormat).toBeDefined();
|
|
expect(classCheckFormat!.enclosingClassId).toBe(
|
|
'Class:src/validation/Validator.java:EmailValidator',
|
|
);
|
|
});
|
|
|
|
it('class/interface declarations are captured with correct defType', () => {
|
|
const code = `
|
|
interface Repository {
|
|
void save(Object entity);
|
|
}
|
|
|
|
class UserRepository {
|
|
public void save(Object entity) {}
|
|
}
|
|
`;
|
|
const results = parseAndExtractMethods(code, SupportedLanguages.Java, 'src/repo/Repo.java');
|
|
|
|
// The pipeline distinguishes containers from methods via defType, not enclosingClassId
|
|
const repoDecl = results.find((r) => r.name === 'Repository');
|
|
expect(repoDecl).toBeDefined();
|
|
expect(repoDecl!.defType).toBe('definition.interface');
|
|
|
|
const userRepoDecl = results.find((r) => r.name === 'UserRepository');
|
|
expect(userRepoDecl).toBeDefined();
|
|
expect(userRepoDecl!.defType).toBe('definition.class');
|
|
|
|
// Methods associate correctly
|
|
const saveMethods = results.filter((r) => r.name === 'save');
|
|
expect(saveMethods.length).toBe(2);
|
|
expect(
|
|
saveMethods.some((r) => r.enclosingClassId === 'Interface:src/repo/Repo.java:Repository'),
|
|
).toBe(true);
|
|
expect(
|
|
saveMethods.some((r) => r.enclosingClassId === 'Class:src/repo/Repo.java:UserRepository'),
|
|
).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('HAS_METHOD integration — C++ class methods', () => {
|
|
beforeAll(async () => {
|
|
await loadLanguage(SupportedLanguages.CPlusPlus);
|
|
});
|
|
|
|
it('inline methods link to their owning class_specifier', () => {
|
|
const code = `
|
|
class Stack {
|
|
public:
|
|
void push(int val) { data[top++] = val; }
|
|
int pop() { return data[--top]; }
|
|
int size() { return top; }
|
|
private:
|
|
int data[100];
|
|
int top;
|
|
};
|
|
|
|
class Queue {
|
|
public:
|
|
void enqueue(int val) {}
|
|
int dequeue() { return 0; }
|
|
};
|
|
`;
|
|
const results = parseAndExtractMethods(code, SupportedLanguages.CPlusPlus, 'src/containers.h');
|
|
|
|
// Stack methods
|
|
const push = results.find((r) => r.name === 'push');
|
|
if (push) {
|
|
expect(push.enclosingClassId).toBe('Class:src/containers.h:Stack');
|
|
}
|
|
|
|
const pop = results.find((r) => r.name === 'pop');
|
|
if (pop) {
|
|
expect(pop.enclosingClassId).toBe('Class:src/containers.h:Stack');
|
|
}
|
|
|
|
const size = results.find((r) => r.name === 'size');
|
|
if (size) {
|
|
expect(size.enclosingClassId).toBe('Class:src/containers.h:Stack');
|
|
}
|
|
|
|
// Queue methods
|
|
const enqueue = results.find((r) => r.name === 'enqueue');
|
|
if (enqueue) {
|
|
expect(enqueue.enclosingClassId).toBe('Class:src/containers.h:Queue');
|
|
}
|
|
|
|
const dequeue = results.find((r) => r.name === 'dequeue');
|
|
if (dequeue) {
|
|
expect(dequeue.enclosingClassId).toBe('Class:src/containers.h:Queue');
|
|
}
|
|
});
|
|
|
|
it('free functions have null enclosingClassId', () => {
|
|
const code = `
|
|
void freeFunction() {}
|
|
|
|
class Foo {
|
|
public:
|
|
void method() {}
|
|
};
|
|
`;
|
|
const results = parseAndExtractMethods(code, SupportedLanguages.CPlusPlus, 'src/mixed.cpp');
|
|
|
|
const freeFn = results.find((r) => r.name === 'freeFunction');
|
|
if (freeFn) {
|
|
expect(freeFn.enclosingClassId).toBeNull();
|
|
}
|
|
|
|
const method = results.find((r) => r.name === 'method');
|
|
if (method) {
|
|
expect(method.enclosingClassId).toBe('Class:src/mixed.cpp:Foo');
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('HAS_METHOD integration — C++ virtual/static/constructor inline methods', () => {
|
|
beforeAll(async () => {
|
|
await loadLanguage(SupportedLanguages.CPlusPlus);
|
|
});
|
|
|
|
it('virtual, override, static, and constructor methods are captured from inline class body', () => {
|
|
const code = `
|
|
class Shape {
|
|
public:
|
|
Shape() {}
|
|
virtual ~Shape() {}
|
|
virtual double area() = 0;
|
|
static Shape* create();
|
|
};
|
|
|
|
class Circle : public Shape {
|
|
public:
|
|
Circle(double r) : radius(r) {}
|
|
double area() override { return 3.14 * radius * radius; }
|
|
private:
|
|
double radius;
|
|
};
|
|
`;
|
|
const results = parseAndExtractMethods(code, SupportedLanguages.CPlusPlus, 'src/shapes.h');
|
|
|
|
// Shape methods
|
|
const shapeCtor = results.find(
|
|
(r) => r.name === 'Shape' && r.enclosingClassId === 'Class:src/shapes.h:Shape',
|
|
);
|
|
expect(shapeCtor).toBeDefined();
|
|
|
|
const shapeDtor = results.find((r) => r.name === '~Shape');
|
|
expect(shapeDtor).toBeDefined();
|
|
expect(shapeDtor!.enclosingClassId).toBe('Class:src/shapes.h:Shape');
|
|
|
|
const area = results.find(
|
|
(r) => r.name === 'area' && r.enclosingClassId === 'Class:src/shapes.h:Shape',
|
|
);
|
|
expect(area).toBeDefined();
|
|
|
|
const create = results.find((r) => r.name === 'create');
|
|
expect(create).toBeDefined();
|
|
expect(create!.enclosingClassId).toBe('Class:src/shapes.h:Shape');
|
|
|
|
// Circle methods
|
|
const circleCtor = results.find(
|
|
(r) => r.name === 'Circle' && r.enclosingClassId === 'Class:src/shapes.h:Circle',
|
|
);
|
|
expect(circleCtor).toBeDefined();
|
|
|
|
const circleArea = results.find(
|
|
(r) => r.name === 'area' && r.enclosingClassId === 'Class:src/shapes.h:Circle',
|
|
);
|
|
expect(circleArea).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe('HAS_METHOD integration — TypeScript: abstract, interface, and #private methods', () => {
|
|
beforeAll(async () => {
|
|
await loadLanguage(SupportedLanguages.TypeScript, 'methods.ts');
|
|
});
|
|
|
|
it('abstract method signatures are captured and link to abstract class', () => {
|
|
const code = `
|
|
abstract class Shape {
|
|
abstract area(): number;
|
|
describe(): string { return "shape"; }
|
|
}
|
|
`;
|
|
const results = parseAndExtractMethods(code, SupportedLanguages.TypeScript, 'src/shapes.ts');
|
|
|
|
const area = results.find((r) => r.name === 'area' && r.defType === 'definition.method');
|
|
expect(area).toBeDefined();
|
|
expect(area!.enclosingClassId).toBe('Class:src/shapes.ts:Shape');
|
|
|
|
const describe = results.find(
|
|
(r) => r.name === 'describe' && r.defType === 'definition.method',
|
|
);
|
|
expect(describe).toBeDefined();
|
|
expect(describe!.enclosingClassId).toBe('Class:src/shapes.ts:Shape');
|
|
});
|
|
|
|
it('interface method signatures are captured and link to interface', () => {
|
|
const code = `
|
|
interface Printable {
|
|
print(format: string): void;
|
|
getLabel(): string;
|
|
}
|
|
`;
|
|
const results = parseAndExtractMethods(code, SupportedLanguages.TypeScript, 'src/printable.ts');
|
|
|
|
const print = results.find((r) => r.name === 'print' && r.defType === 'definition.method');
|
|
expect(print).toBeDefined();
|
|
expect(print!.enclosingClassId).toBe('Interface:src/printable.ts:Printable');
|
|
|
|
const getLabel = results.find(
|
|
(r) => r.name === 'getLabel' && r.defType === 'definition.method',
|
|
);
|
|
expect(getLabel).toBeDefined();
|
|
expect(getLabel!.enclosingClassId).toBe('Interface:src/printable.ts:Printable');
|
|
});
|
|
|
|
it('ES2022 #private methods are captured and link to class', () => {
|
|
const code = `
|
|
class Vault {
|
|
#decrypt(data: string): string { return data; }
|
|
read(): string { return this.#decrypt("x"); }
|
|
}
|
|
`;
|
|
const results = parseAndExtractMethods(code, SupportedLanguages.TypeScript, 'src/vault.ts');
|
|
|
|
const decrypt = results.find((r) => r.name === '#decrypt');
|
|
expect(decrypt).toBeDefined();
|
|
expect(decrypt!.defType).toBe('definition.method');
|
|
expect(decrypt!.enclosingClassId).toBe('Class:src/vault.ts:Vault');
|
|
|
|
const read = results.find((r) => r.name === 'read');
|
|
expect(read).toBeDefined();
|
|
expect(read!.enclosingClassId).toBe('Class:src/vault.ts:Vault');
|
|
});
|
|
});
|
|
|
|
describe('HAS_METHOD integration — JavaScript: ES2022 #private methods', () => {
|
|
beforeAll(async () => {
|
|
await loadLanguage(SupportedLanguages.JavaScript);
|
|
});
|
|
|
|
it('#private methods are captured and link to class', () => {
|
|
const code = `
|
|
class Encapsulated {
|
|
#internal() { return 42; }
|
|
expose() { return this.#internal(); }
|
|
}
|
|
`;
|
|
const results = parseAndExtractMethods(
|
|
code,
|
|
SupportedLanguages.JavaScript,
|
|
'src/encapsulated.js',
|
|
);
|
|
|
|
const internal = results.find((r) => r.name === '#internal');
|
|
expect(internal).toBeDefined();
|
|
expect(internal!.defType).toBe('definition.method');
|
|
expect(internal!.enclosingClassId).toBe('Class:src/encapsulated.js:Encapsulated');
|
|
});
|
|
});
|
|
|
|
describe('HAS_METHOD integration — C# struct and record', () => {
|
|
beforeAll(async () => {
|
|
await loadLanguage(SupportedLanguages.CSharp);
|
|
});
|
|
|
|
it('struct methods link to struct, record methods link to record', () => {
|
|
const code = `
|
|
struct Vector2 {
|
|
public float Length() { return 0; }
|
|
public Vector2 Normalize() { return this; }
|
|
}
|
|
|
|
record Person {
|
|
public string GetFullName() { return ""; }
|
|
}
|
|
`;
|
|
const results = parseAndExtractMethods(code, SupportedLanguages.CSharp, 'src/Types.cs');
|
|
|
|
const length = results.find((r) => r.name === 'Length');
|
|
if (length) {
|
|
expect(length.enclosingClassId).toBe('Struct:src/Types.cs:Vector2');
|
|
}
|
|
|
|
const normalize = results.find((r) => r.name === 'Normalize');
|
|
if (normalize) {
|
|
expect(normalize.enclosingClassId).toBe('Struct:src/Types.cs:Vector2');
|
|
}
|
|
|
|
const getFullName = results.find((r) => r.name === 'GetFullName');
|
|
if (getFullName) {
|
|
expect(getFullName.enclosingClassId).toBe('Record:src/Types.cs:Person');
|
|
}
|
|
});
|
|
});
|