mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-07 08:26:11 +00:00
* feat: Phase 3 — return type inference, generic args extraction, Ruby YARD type extractor
Three architectural improvements to the type resolution system:
1. Return type inference — wire extractMethodSignature returnType through
SymbolDefinition into call-processor. When var = callee() and callee
has a known return type, bind var to that type. Handles Promise<T>
unwrapping, nullable stripping, pointer/reference removal.
2. Generic type argument extraction — new extractGenericTypeArgs() utility
that extracts type parameters from List<User> → ['User']. Handles
TS/Java/Kotlin/C#/Rust generic syntax. Building block for for-loop
variable typing.
3. Ruby dedicated type extractor — replaces the stub with YARD annotation
parsing (@param name [Type]), handling qualified types, nullable types,
and singleton methods. Ruby now has real type resolution.
Unit tests: 127 → 192+ (type-env) + 65 (symbol-table, call-processor) + 18 (generics)
Integration tests: 8+ new test cases with fixtures across TS/Python/Go/Java/Ruby
* fix: Phase 3 gaps — WRAPPER_GENERICS correctness, Ruby :: qualifier, namespaced constructors
- Remove collection types (List, Array, Vec, Set) from WRAPPER_GENERICS to prevent
false CALLS edges (e.g. List<User> no longer unwraps to User)
- Add :: qualifier handling in extractReturnTypeName for Ruby/C++/Rust namespaced types
- Add Ruby `constant` and `scope_resolution` node types to shared extractors
- Extract shared extractRubyConstructorAssignment helper (dedup type-env.ts + ruby.ts)
- Add integration tests for return type inference: Python, TypeScript, Go, Java, Ruby
- Add Ruby namespaced constructor fixture (Models::UserService.new)
- Add unit tests for collection reclassification and :: qualifiers
* feat: Phase 4 — CONSTRUCTOR_BINDING_SCANNERS for all languages + return type inference tests
Add CONSTRUCTOR_BINDING_SCANNERS for 6 missing languages, completing
return type inference coverage across all 11 supported languages:
- TypeScript/JS: variable_declarator with call_expression, unwraps await
- Go: short_var_declaration single-assignment (skips multi-return, new/make)
- Java: local_variable_declaration with `var` type + method_invocation
- C#: variable_declaration with implicit_type (var) + invocation_expression
- Rust: let_declaration without type annotation, handles mut_pattern
- PHP: assignment_expression with function_call_expression
Also adds property_identifier to extractSimpleTypeName for qualified
member calls (repo.getUser → getUser), fixing namespaced constructor
inference that was previously a known limitation.
Integration tests added for all 11 languages with correct label
assertions (Function vs Method per language's tree-sitter queries).
* refactor: merge CONSTRUCTOR_BINDING_SCANNERS into per-language LanguageTypeConfig
Eliminates the parallel dispatch map in type-env.ts by moving all 11
constructor binding scanners into their respective type-extractors/*.ts
files as `scanConstructorBinding` on LanguageTypeConfig.
- Add ConstructorBindingScanner type to types.ts
- Add shared helpers: hasTypeAnnotation, unwrapAwait, extractCalleeName
- Move scanners to typescript.ts, jvm.ts, python.ts, php.ts, go.ts,
rust.ts, swift.ts, c-cpp.ts, csharp.ts, ruby.ts
- Fix `any` types in C# scanner → SyntaxNode | null
- Delete ~300 lines from type-env.ts (CONSTRUCTOR_BINDING_SCANNERS map)
- Update buildTypeEnv to use config.scanConstructorBinding
All 143 type-env unit tests and all 10 language integration suites pass.
* fix: remove unused import, fix any type in Java scanner, update stale comment
- Remove unused extractCalleeName import from jvm.ts
- Fix (c: any) → (c: SyntaxNode) in Java scanner
- Update stale CONSTRUCTOR_BINDING_SCANNERS reference in ruby.ts comment
* fix: C# and PHP return type inference — scanner fixes, method signature extraction, and cross-file resolution
Addresses code review findings on PR #284:
C# scanner (csharp.ts):
- Fix type node lookup: iterate children instead of childForFieldName('type')
which returns undefined in tree-sitter-c-sharp
- Fix initializer lookup: handle direct invocation_expression children
(no equals_value_clause wrapper in tree-sitter-c-sharp)
C# return type extraction (utils.ts):
- Add 'returns' field check to extractMethodSignature — tree-sitter-c-sharp
uses 'returns', not 'type', for method return types
C# cross-file resolution (call-processor.ts + fixture):
- Add constructor binding verification to sequential processCalls path
(was only in the worker processCallsFromExtracted path)
- Add ReturnType.csproj to csharp-return-type fixture
- Update fixture namespaces to use ReturnType.Models/ReturnType.Services
prefix (matches real C# project conventions)
PHP scanner (php.ts):
- Extend scanConstructorBinding to handle member_call_expression
($this->getUser() patterns), not just function_call_expression
Shared (shared.ts):
- Add member_access_expression to extractSimpleTypeName qualified-names
block (C# method calls like svc.GetUser())
Tests:
- Add Repo.cs/Repo.php disambiguation fixtures (two Save methods)
- Strengthen C# and PHP return type tests with hard disambiguation assertions
- Add C# scanner unit tests and return type extraction test
* feat: per-language ReturnTypeExtractor + doc-comment @param parsing for PHP, JS, Ruby
Add ReturnTypeExtractor to LanguageTypeConfig interface with implementations
for Ruby (YARD @return), PHP (PHPDoc @return), and JS/TS (JSDoc @returns).
The fallback is wired in both parsing-processor and parse-worker paths,
activating only when extractMethodSignature finds no AST-based return type.
Also add doc-comment @param type extraction for PHP and JS/TS, following
Ruby's existing collectYardParams pattern. This enables parameter.method()
resolution in loosely-typed codebases using PHPDoc @param or JSDoc @param.
Additional fixes from PR #284 code review:
- Go: add selector_expression + field_identifier to extractSimpleTypeName
(enables package-qualified factory calls like models.NewUser())
- Ruby: broaden scanConstructorBinding to capture plain call assignments
(user = get_user()) in addition to Class.new patterns
- Ruby: harden return-type fixture with disambiguation (two save methods)
Test coverage: +14 new integration tests across Go, Ruby, PHP, JS/TS
* fix: JSDoc async return type, PHP attribute walkers, and $this receiver disambiguation
Three fixes from fourth-pass code review on PR #284:
1. JSDoc `@returns {Promise<User>}` no longer stripped to `Promise` — extractReturnType
now uses sanitizeReturnType (preserves generics) instead of normalizeJsDocType
(which stripped them before extractReturnTypeName could unwrap WRAPPER_GENERICS).
2. PHP 8+ `#[Attribute]` and JS `@decorator` nodes no longer break doc-comment walkers.
Both extractReturnType and collect*Params functions now skip attribute_list/decorator
nodes instead of breaking on them as named siblings.
3. PHP `$this->method()` now provides receiverClassName for disambiguation.
When two classes define the same method, the enclosing class narrows candidates
via ownerId matching in call-processor, preventing false no-binding results.
* fix: sanitizeReturnType dot corruption, JS test assertions, Ruby constant receiver
- Remove redundant dot-path stripping from sanitizeReturnType that corrupted
qualified names inside generics (e.g. Promise<models.User> → User>)
- Split JS async fixture into separate files and add negative assertions
to properly verify disambiguation (mirroring PHP test pattern)
- Accept 'constant' node type in Ruby scanConstructorBinding for factory
call assignments (SERVICE = build_service())
- Add 'constant' to SIMPLE_RECEIVER_TYPES so extractReceiverName handles
Ruby constant receivers (SERVICE.process)
* fix: nested generic arg splitting, JS/Ruby test false positives
- Replace naive comma split in extractReturnTypeName with bracket-balanced
extractFirstGenericArg so nested types like Future<Result<User, Error>>
unwrap correctly instead of producing malformed "Result<User"
- Add CompletableFuture to WRAPPER_GENERICS for Java async unwrapping
- Split js-jsdoc-return-type fixture models.js into user.js/repo.js and
add negative assertions to prove disambiguation (not just file match)
- Split ruby-constant-factory-call fixture into separate service files
and add negative assertions against AdminService resolution
* fix: review findings — receiverClassName parity, Rust wrappers, Go multi-return, Kotlin/Swift qualified calls
P1: Sequential path now includes receiverClassName narrowing for PHP
$this->method() disambiguation (was missing vs worker path).
P2: Added Rc/Arc/Weak/MutexGuard/Cow + 6 more Rust Deref types to
WRAPPER_GENERICS (Box excluded — Java Swing collision). Extended
Kotlin/Swift scanners to handle navigation_expression callees.
Added Go multi-return support (user, err := f()) with blank/_/err/ok
guard + AST-level first-return extraction in extractMethodSignature.
P3: Extracted shared verifyConstructorBindings() eliminating 60 lines
of duplication between sequential and worker paths. Added return-type
inference integration tests for C++, Rust, Swift with competing
methods and negative disambiguation assertions.
* fix: Swift navigation_suffix unwrapping, Rust lifetime skipping, Kotlin disambiguation tests
- Swift scanConstructorBinding: handle tree-sitter wrapping qualified
identifiers in navigation_suffix nodes
- Add extractFirstTypeArg to skip Rust lifetime parameters ('a, '_)
when unwrapping wrapper generics like Ref<'_, User>
- Kotlin tests: add Repo class fixture with competing save() methods
to prove disambiguation; assert no spurious edges on known gap
- Remove tree-sitter-kotlin from optionalDependencies (now regular dep)
* fix: C# null-conditional calls, Ruby YARD bracket-balanced split, PHPDoc alternate order, escapeValue hardening
- Add C# null-conditional call support (user?.Save()): tree-sitter query for
conditional_access_expression, member_binding_expression in MEMBER_ACCESS_NODE_TYPES,
receiver extraction via conditional_access_expression parent walk
- Fix Ruby YARD type parsing for nested generics (Hash<Symbol, User>): replace
naive split(',') with bracket-balanced splitter respecting <> depth
- Add alternate YARD format (@param [Type] name) alongside standard (@param name [Type])
- Add alternate PHPDoc format (@param $name Type) alongside standard (@param Type $name)
- Harden escapeValue in kuzu-adapter.ts: escape \n and \r to prevent Cypher injection
- Integration tests: C# null-conditional fixture (5 tests), Ruby YARD generics fixture (6 tests)
- Unit tests: PHPDoc alternate order (2 tests), C# null-conditional call-form (updated)
* test: add Python static/classmethod integration tests (issue #289)
Verifies that classes using only @staticmethod/@classmethod have HAS_METHOD
edges connecting them to their child methods. This was the root cause of
issue #289 where context() and impact() returned empty for such classes.
Tests cover: HAS_METHOD edge emission, unique static method resolution
(create_user, delete_user), and ambiguous same-named method handling
(find_user on both UserService and AdminService — safely refused).
* fix: lbug batch escapeValue newline hardening, Rust ::default() scanner exclusion
- Apply \n/\r escaping to batch upsert escapeValue in lbug-adapter.ts:429
(missed instance of the CREATE-path fix from ec4dca4)
- Exclude Rust ::default() from scanConstructorBinding to match
extractInitializer behavior — avoids wasted cross-file lookups on
the broadly-implemented Default trait
- Unit tests: 2 new scanner exclusion tests (::default and ::new)
- Integration tests: 6 new Rust ::default() constructor resolution tests
with disambiguation fixture (User::default vs Repo::default)
* fix: C#/Rust async await unwrap, PHP backslash namespace, fallback escaping
- C# scanConstructorBinding: unwrap await_expression to find invocation_expression
(var user = await svc.GetUserAsync() now produces constructor binding)
- Rust scanConstructorBinding: unwrap .await postfix via shared unwrapAwait helper
(let user = get_user().await now produces constructor binding)
- extractReturnTypeName: handle PHP backslash namespace separator (\App\Models\User → User)
- fallbackRelationshipInserts: match batch escapeValue hardening with \n/\r escaping
Tests: 2 unit (type-env), 3 unit (call-processor), 7 integration (csharp+rust), 7 fixtures
* fix: C#/Rust async-binding test false positives — add competing types and negative assertions
C# fixture: add Order.cs with Order.Save(), change OrderService to return
Task<Order> via GetOrderAsync, add negative assertion proving user.Save()
does not resolve to Order#Save.
Rust fixture: split models.rs into user.rs/repo.rs, make process_user and
process_repo async fn, add bidirectional negative assertions proving no
cross-contamination between User#save and Repo#save.
* fix: C# async-binding broken assertion, bare wrapper type leak, JSDoc optional params
- Split Program.cs Main into ProcessUser/ProcessOrder so negative
assertions use strict toBeUndefined() (matching Rust pattern)
- Guard bare wrapper types (Task, Promise, Option…) in
extractReturnTypeName — return undefined instead of the wrapper name
- Update JSDOC_PARAM_RE to capture @param {Type} [optionalName] syntax
* fix: update symbol and relationship counts in documentation
456 lines
18 KiB
TypeScript
456 lines
18 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { inferCallForm, extractReceiverName, type SyntaxNode } from '../../src/core/ingestion/utils.js';
|
|
import { createSymbolTable } from '../../src/core/ingestion/symbol-table.js';
|
|
import Parser from 'tree-sitter';
|
|
import TypeScript from 'tree-sitter-typescript';
|
|
import Python from 'tree-sitter-python';
|
|
import Java from 'tree-sitter-java';
|
|
import CSharp from 'tree-sitter-c-sharp';
|
|
import Kotlin from 'tree-sitter-kotlin';
|
|
import Go from 'tree-sitter-go';
|
|
import Rust from 'tree-sitter-rust';
|
|
import CPP from 'tree-sitter-cpp';
|
|
import PHP from 'tree-sitter-php';
|
|
import { LANGUAGE_QUERIES } from '../../src/core/ingestion/tree-sitter-queries.js';
|
|
import { SupportedLanguages } from '../../src/config/supported-languages.js';
|
|
|
|
/**
|
|
* Helper: parse code, run the language query, and return all @call captures
|
|
* as { callNode, nameNode } pairs.
|
|
*/
|
|
function extractCallCaptures(
|
|
parser: Parser,
|
|
code: string,
|
|
language: string,
|
|
): Array<{ callNode: SyntaxNode; nameNode: SyntaxNode; calledName: string }> {
|
|
const queryStr = LANGUAGE_QUERIES[language];
|
|
if (!queryStr) throw new Error(`No query for ${language}`);
|
|
|
|
const tree = parser.parse(code);
|
|
const lang = parser.getLanguage();
|
|
const query = new Parser.Query(lang, queryStr);
|
|
const matches = query.matches(tree.rootNode);
|
|
|
|
const results: Array<{ callNode: SyntaxNode; nameNode: SyntaxNode; calledName: string }> = [];
|
|
|
|
for (const match of matches) {
|
|
const captureMap: Record<string, SyntaxNode> = {};
|
|
for (const c of match.captures) {
|
|
captureMap[c.name] = c.node;
|
|
}
|
|
if (captureMap['call'] && captureMap['call.name']) {
|
|
results.push({
|
|
callNode: captureMap['call'],
|
|
nameNode: captureMap['call.name'],
|
|
calledName: captureMap['call.name'].text,
|
|
});
|
|
}
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
describe('inferCallForm', () => {
|
|
const parser = new Parser();
|
|
|
|
describe('TypeScript', () => {
|
|
it('detects free call', () => {
|
|
parser.setLanguage(TypeScript.typescript);
|
|
const captures = extractCallCaptures(parser, 'doStuff()', SupportedLanguages.TypeScript);
|
|
const match = captures.find(c => c.calledName === 'doStuff');
|
|
expect(match).toBeDefined();
|
|
expect(inferCallForm(match!.callNode, match!.nameNode)).toBe('free');
|
|
});
|
|
|
|
it('detects member call', () => {
|
|
parser.setLanguage(TypeScript.typescript);
|
|
const captures = extractCallCaptures(parser, 'user.save()', SupportedLanguages.TypeScript);
|
|
const match = captures.find(c => c.calledName === 'save');
|
|
expect(match).toBeDefined();
|
|
expect(inferCallForm(match!.callNode, match!.nameNode)).toBe('member');
|
|
});
|
|
});
|
|
|
|
describe('Python', () => {
|
|
it('detects free call', () => {
|
|
parser.setLanguage(Python);
|
|
const captures = extractCallCaptures(parser, 'print_result()', SupportedLanguages.Python);
|
|
const match = captures.find(c => c.calledName === 'print_result');
|
|
expect(match).toBeDefined();
|
|
expect(inferCallForm(match!.callNode, match!.nameNode)).toBe('free');
|
|
});
|
|
|
|
it('detects member call', () => {
|
|
parser.setLanguage(Python);
|
|
const captures = extractCallCaptures(parser, 'self.save()', SupportedLanguages.Python);
|
|
const match = captures.find(c => c.calledName === 'save');
|
|
expect(match).toBeDefined();
|
|
expect(inferCallForm(match!.callNode, match!.nameNode)).toBe('member');
|
|
});
|
|
});
|
|
|
|
describe('Java', () => {
|
|
it('detects free call (no object)', () => {
|
|
parser.setLanguage(Java);
|
|
const code = `class Foo { void run() { doStuff(); } }`;
|
|
const captures = extractCallCaptures(parser, code, SupportedLanguages.Java);
|
|
const match = captures.find(c => c.calledName === 'doStuff');
|
|
expect(match).toBeDefined();
|
|
expect(inferCallForm(match!.callNode, match!.nameNode)).toBe('free');
|
|
});
|
|
|
|
it('detects member call (with object)', () => {
|
|
parser.setLanguage(Java);
|
|
const code = `class Foo { void run() { user.save(); } }`;
|
|
const captures = extractCallCaptures(parser, code, SupportedLanguages.Java);
|
|
const match = captures.find(c => c.calledName === 'save');
|
|
expect(match).toBeDefined();
|
|
expect(inferCallForm(match!.callNode, match!.nameNode)).toBe('member');
|
|
});
|
|
});
|
|
|
|
describe('C#', () => {
|
|
it('detects free call', () => {
|
|
parser.setLanguage(CSharp);
|
|
const code = `class Foo { void Run() { DoStuff(); } }`;
|
|
const captures = extractCallCaptures(parser, code, SupportedLanguages.CSharp);
|
|
const match = captures.find(c => c.calledName === 'DoStuff');
|
|
expect(match).toBeDefined();
|
|
expect(inferCallForm(match!.callNode, match!.nameNode)).toBe('free');
|
|
});
|
|
|
|
it('detects member call', () => {
|
|
parser.setLanguage(CSharp);
|
|
const code = `class Foo { void Run() { user.Save(); } }`;
|
|
const captures = extractCallCaptures(parser, code, SupportedLanguages.CSharp);
|
|
const match = captures.find(c => c.calledName === 'Save');
|
|
expect(match).toBeDefined();
|
|
expect(inferCallForm(match!.callNode, match!.nameNode)).toBe('member');
|
|
});
|
|
});
|
|
|
|
describe('Go', () => {
|
|
it('detects free call', () => {
|
|
parser.setLanguage(Go);
|
|
const code = `package main\nfunc main() { doStuff() }`;
|
|
const captures = extractCallCaptures(parser, code, SupportedLanguages.Go);
|
|
const match = captures.find(c => c.calledName === 'doStuff');
|
|
expect(match).toBeDefined();
|
|
expect(inferCallForm(match!.callNode, match!.nameNode)).toBe('free');
|
|
});
|
|
|
|
it('detects member call via selector', () => {
|
|
parser.setLanguage(Go);
|
|
const code = `package main\nfunc main() { user.Save() }`;
|
|
const captures = extractCallCaptures(parser, code, SupportedLanguages.Go);
|
|
const match = captures.find(c => c.calledName === 'Save');
|
|
expect(match).toBeDefined();
|
|
expect(inferCallForm(match!.callNode, match!.nameNode)).toBe('member');
|
|
});
|
|
});
|
|
|
|
describe('Rust', () => {
|
|
it('detects free call', () => {
|
|
parser.setLanguage(Rust);
|
|
const code = `fn main() { do_stuff(); }`;
|
|
const captures = extractCallCaptures(parser, code, SupportedLanguages.Rust);
|
|
const match = captures.find(c => c.calledName === 'do_stuff');
|
|
expect(match).toBeDefined();
|
|
expect(inferCallForm(match!.callNode, match!.nameNode)).toBe('free');
|
|
});
|
|
|
|
it('detects member call via field_expression', () => {
|
|
parser.setLanguage(Rust);
|
|
const code = `fn main() { user.save(); }`;
|
|
const captures = extractCallCaptures(parser, code, SupportedLanguages.Rust);
|
|
const match = captures.find(c => c.calledName === 'save');
|
|
expect(match).toBeDefined();
|
|
expect(inferCallForm(match!.callNode, match!.nameNode)).toBe('member');
|
|
});
|
|
|
|
it('detects scoped call as free (Foo::new)', () => {
|
|
parser.setLanguage(Rust);
|
|
const code = `fn main() { Foo::new(); }`;
|
|
const captures = extractCallCaptures(parser, code, SupportedLanguages.Rust);
|
|
const match = captures.find(c => c.calledName === 'new');
|
|
expect(match).toBeDefined();
|
|
expect(inferCallForm(match!.callNode, match!.nameNode)).toBe('free');
|
|
});
|
|
});
|
|
|
|
describe('C++', () => {
|
|
it('detects free call', () => {
|
|
parser.setLanguage(CPP);
|
|
const code = `void main() { doStuff(); }`;
|
|
const captures = extractCallCaptures(parser, code, SupportedLanguages.CPlusPlus);
|
|
const match = captures.find(c => c.calledName === 'doStuff');
|
|
expect(match).toBeDefined();
|
|
expect(inferCallForm(match!.callNode, match!.nameNode)).toBe('free');
|
|
});
|
|
|
|
it('detects member call via field_expression', () => {
|
|
parser.setLanguage(CPP);
|
|
const code = `void main() { obj.run(); }`;
|
|
const captures = extractCallCaptures(parser, code, SupportedLanguages.CPlusPlus);
|
|
const match = captures.find(c => c.calledName === 'run');
|
|
expect(match).toBeDefined();
|
|
expect(inferCallForm(match!.callNode, match!.nameNode)).toBe('member');
|
|
});
|
|
});
|
|
|
|
describe('PHP', () => {
|
|
it('detects free function call', () => {
|
|
parser.setLanguage(PHP.php);
|
|
const code = `<?php doStuff(); ?>`;
|
|
const captures = extractCallCaptures(parser, code, SupportedLanguages.PHP);
|
|
const match = captures.find(c => c.calledName === 'doStuff');
|
|
expect(match).toBeDefined();
|
|
expect(inferCallForm(match!.callNode, match!.nameNode)).toBe('free');
|
|
});
|
|
|
|
it('detects member call', () => {
|
|
parser.setLanguage(PHP.php);
|
|
const code = `<?php $user->save(); ?>`;
|
|
const captures = extractCallCaptures(parser, code, SupportedLanguages.PHP);
|
|
const match = captures.find(c => c.calledName === 'save');
|
|
expect(match).toBeDefined();
|
|
expect(inferCallForm(match!.callNode, match!.nameNode)).toBe('member');
|
|
});
|
|
|
|
it('detects static call as member', () => {
|
|
parser.setLanguage(PHP.php);
|
|
const code = `<?php Foo::bar(); ?>`;
|
|
const captures = extractCallCaptures(parser, code, SupportedLanguages.PHP);
|
|
const match = captures.find(c => c.calledName === 'bar');
|
|
expect(match).toBeDefined();
|
|
expect(inferCallForm(match!.callNode, match!.nameNode)).toBe('member');
|
|
});
|
|
});
|
|
|
|
describe('Kotlin', () => {
|
|
it('detects free call', () => {
|
|
parser.setLanguage(Kotlin);
|
|
const code = `fun main() { doStuff() }`;
|
|
const captures = extractCallCaptures(parser, code, SupportedLanguages.Kotlin);
|
|
const match = captures.find(c => c.calledName === 'doStuff');
|
|
expect(match).toBeDefined();
|
|
expect(inferCallForm(match!.callNode, match!.nameNode)).toBe('free');
|
|
});
|
|
|
|
it('detects member call via navigation_expression', () => {
|
|
parser.setLanguage(Kotlin);
|
|
const code = `fun main() { user.save() }`;
|
|
const captures = extractCallCaptures(parser, code, SupportedLanguages.Kotlin);
|
|
const match = captures.find(c => c.calledName === 'save');
|
|
expect(match).toBeDefined();
|
|
expect(inferCallForm(match!.callNode, match!.nameNode)).toBe('member');
|
|
});
|
|
|
|
it('Foo() is a free call (constructor_invocation only in heritage context)', () => {
|
|
parser.setLanguage(Kotlin);
|
|
const code = `fun main() { val x = Foo() }`;
|
|
const captures = extractCallCaptures(parser, code, SupportedLanguages.Kotlin);
|
|
const match = captures.find(c => c.calledName === 'Foo');
|
|
expect(match).toBeDefined();
|
|
// Kotlin Foo() is syntactically a call_expression, not constructor_invocation
|
|
// Constructor discrimination happens in Phase 2 via symbol kind matching
|
|
expect(inferCallForm(match!.callNode, match!.nameNode)).toBe('free');
|
|
});
|
|
|
|
it('detects constructor_invocation in heritage delegation as constructor', () => {
|
|
parser.setLanguage(Kotlin);
|
|
const code = `open class Base\nclass Derived : Base()`;
|
|
const captures = extractCallCaptures(parser, code, SupportedLanguages.Kotlin);
|
|
const match = captures.find(c => c.calledName === 'Base');
|
|
// constructor_invocation is captured by heritage queries, not call queries
|
|
// If it happens to be captured, it should be 'constructor'
|
|
if (match) {
|
|
expect(inferCallForm(match.callNode, match.nameNode)).toBe('constructor');
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('extractReceiverName', () => {
|
|
const parser = new Parser();
|
|
|
|
describe('TypeScript', () => {
|
|
it('extracts simple identifier receiver', () => {
|
|
parser.setLanguage(TypeScript.typescript);
|
|
const captures = extractCallCaptures(parser, 'user.save()', SupportedLanguages.TypeScript);
|
|
const match = captures.find(c => c.calledName === 'save');
|
|
expect(match).toBeDefined();
|
|
expect(extractReceiverName(match!.nameNode)).toBe('user');
|
|
});
|
|
|
|
it('extracts "this" as receiver', () => {
|
|
parser.setLanguage(TypeScript.typescript);
|
|
const code = `class Foo { run() { this.save(); } }`;
|
|
const captures = extractCallCaptures(parser, code, SupportedLanguages.TypeScript);
|
|
const match = captures.find(c => c.calledName === 'save');
|
|
expect(match).toBeDefined();
|
|
expect(extractReceiverName(match!.nameNode)).toBe('this');
|
|
});
|
|
|
|
it('returns undefined for chained call receiver', () => {
|
|
parser.setLanguage(TypeScript.typescript);
|
|
const captures = extractCallCaptures(parser, 'getUser().save()', SupportedLanguages.TypeScript);
|
|
const match = captures.find(c => c.calledName === 'save');
|
|
expect(match).toBeDefined();
|
|
expect(extractReceiverName(match!.nameNode)).toBeUndefined();
|
|
});
|
|
|
|
it('returns undefined for free call', () => {
|
|
parser.setLanguage(TypeScript.typescript);
|
|
const captures = extractCallCaptures(parser, 'doStuff()', SupportedLanguages.TypeScript);
|
|
const match = captures.find(c => c.calledName === 'doStuff');
|
|
expect(match).toBeDefined();
|
|
expect(extractReceiverName(match!.nameNode)).toBeUndefined();
|
|
});
|
|
|
|
it('extracts receiver from optional chain call user?.save()', () => {
|
|
parser.setLanguage(TypeScript.typescript);
|
|
const captures = extractCallCaptures(parser, 'user?.save()', SupportedLanguages.TypeScript);
|
|
const match = captures.find(c => c.calledName === 'save');
|
|
expect(match).toBeDefined();
|
|
expect(extractReceiverName(match!.nameNode)).toBe('user');
|
|
});
|
|
|
|
it('extracts "this" from optional chain call this?.save()', () => {
|
|
parser.setLanguage(TypeScript.typescript);
|
|
const code = `class Foo { run() { this?.save(); } }`;
|
|
const captures = extractCallCaptures(parser, code, SupportedLanguages.TypeScript);
|
|
const match = captures.find(c => c.calledName === 'save');
|
|
expect(match).toBeDefined();
|
|
expect(extractReceiverName(match!.nameNode)).toBe('this');
|
|
});
|
|
});
|
|
|
|
describe('Python', () => {
|
|
it('extracts simple identifier receiver', () => {
|
|
parser.setLanguage(Python);
|
|
const captures = extractCallCaptures(parser, 'user.save()', SupportedLanguages.Python);
|
|
const match = captures.find(c => c.calledName === 'save');
|
|
expect(match).toBeDefined();
|
|
expect(extractReceiverName(match!.nameNode)).toBe('user');
|
|
});
|
|
|
|
it('extracts "self" as receiver', () => {
|
|
parser.setLanguage(Python);
|
|
const captures = extractCallCaptures(parser, 'self.save()', SupportedLanguages.Python);
|
|
const match = captures.find(c => c.calledName === 'save');
|
|
expect(match).toBeDefined();
|
|
expect(extractReceiverName(match!.nameNode)).toBe('self');
|
|
});
|
|
});
|
|
|
|
describe('Java', () => {
|
|
it('extracts receiver from method_invocation', () => {
|
|
parser.setLanguage(Java);
|
|
const code = `class Foo { void run() { user.save(); } }`;
|
|
const captures = extractCallCaptures(parser, code, SupportedLanguages.Java);
|
|
const match = captures.find(c => c.calledName === 'save');
|
|
expect(match).toBeDefined();
|
|
expect(extractReceiverName(match!.nameNode)).toBe('user');
|
|
});
|
|
});
|
|
|
|
describe('Go', () => {
|
|
it('extracts receiver from selector_expression', () => {
|
|
parser.setLanguage(Go);
|
|
const code = `package main\nfunc main() { user.Save() }`;
|
|
const captures = extractCallCaptures(parser, code, SupportedLanguages.Go);
|
|
const match = captures.find(c => c.calledName === 'Save');
|
|
expect(match).toBeDefined();
|
|
expect(extractReceiverName(match!.nameNode)).toBe('user');
|
|
});
|
|
});
|
|
|
|
describe('Rust', () => {
|
|
it('extracts receiver from field_expression', () => {
|
|
parser.setLanguage(Rust);
|
|
const code = `fn main() { user.save(); }`;
|
|
const captures = extractCallCaptures(parser, code, SupportedLanguages.Rust);
|
|
const match = captures.find(c => c.calledName === 'save');
|
|
expect(match).toBeDefined();
|
|
expect(extractReceiverName(match!.nameNode)).toBe('user');
|
|
});
|
|
});
|
|
|
|
describe('C#', () => {
|
|
it('extracts receiver from member_access_expression', () => {
|
|
parser.setLanguage(CSharp);
|
|
const code = `class Foo { void Run() { user.Save(); } }`;
|
|
const captures = extractCallCaptures(parser, code, SupportedLanguages.CSharp);
|
|
const match = captures.find(c => c.calledName === 'Save');
|
|
expect(match).toBeDefined();
|
|
expect(extractReceiverName(match!.nameNode)).toBe('user');
|
|
});
|
|
|
|
it('captures null-conditional user?.Save() and extracts receiver', () => {
|
|
parser.setLanguage(CSharp);
|
|
const code = `class Foo { void Run() { user?.Save(); } }`;
|
|
const captures = extractCallCaptures(parser, code, SupportedLanguages.CSharp);
|
|
const match = captures.find(c => c.calledName === 'Save');
|
|
// C# conditional_access_expression (user?.Save()) is now captured via member_binding_expression
|
|
expect(match).toBeDefined();
|
|
expect(inferCallForm(match!.callNode, match!.nameNode)).toBe('member');
|
|
expect(extractReceiverName(match!.nameNode)).toBe('user');
|
|
});
|
|
});
|
|
|
|
describe('Kotlin', () => {
|
|
it('extracts receiver from navigation_expression', () => {
|
|
parser.setLanguage(Kotlin);
|
|
const code = `fun main() { user.save() }`;
|
|
const captures = extractCallCaptures(parser, code, SupportedLanguages.Kotlin);
|
|
const match = captures.find(c => c.calledName === 'save');
|
|
expect(match).toBeDefined();
|
|
expect(extractReceiverName(match!.nameNode)).toBe('user');
|
|
});
|
|
|
|
it('extracts receiver from safe navigation user?.save()', () => {
|
|
parser.setLanguage(Kotlin);
|
|
const code = `fun main() { user?.save() }`;
|
|
const captures = extractCallCaptures(parser, code, SupportedLanguages.Kotlin);
|
|
const match = captures.find(c => c.calledName === 'save');
|
|
expect(match).toBeDefined();
|
|
expect(extractReceiverName(match!.nameNode)).toBe('user');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('ownerId on SymbolDefinition', () => {
|
|
it('is set for Method symbols via symbolTable.add()', () => {
|
|
const st = createSymbolTable();
|
|
st.add('src/foo.ts', 'save', 'Method:src/foo.ts:save', 'Method', {
|
|
parameterCount: 1,
|
|
ownerId: 'Class:src/foo.ts:User',
|
|
});
|
|
|
|
const def = st.lookupExactFull('src/foo.ts', 'save');
|
|
expect(def).toBeDefined();
|
|
expect(def!.ownerId).toBe('Class:src/foo.ts:User');
|
|
expect(def!.parameterCount).toBe(1);
|
|
});
|
|
|
|
it('is undefined for Function symbols (no owner)', () => {
|
|
const st = createSymbolTable();
|
|
st.add('src/foo.ts', 'helper', 'Function:src/foo.ts:helper', 'Function');
|
|
|
|
const def = st.lookupExactFull('src/foo.ts', 'helper');
|
|
expect(def).toBeDefined();
|
|
expect(def!.ownerId).toBeUndefined();
|
|
});
|
|
|
|
it('propagates ownerId through lookupFuzzy', () => {
|
|
const st = createSymbolTable();
|
|
st.add('src/foo.ts', 'save', 'Method:src/foo.ts:save', 'Method', {
|
|
ownerId: 'Class:src/foo.ts:User',
|
|
});
|
|
|
|
const defs = st.lookupFuzzy('save');
|
|
expect(defs).toHaveLength(1);
|
|
expect(defs[0].ownerId).toBe('Class:src/foo.ts:User');
|
|
});
|
|
});
|