mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-24 00:51:53 +00:00
* feat(type-env): constructor-call type inference for TypeEnv (Phase 1)
Add extractInitializer as a Tier 1 fallback in buildTypeEnv: when a
declaration node has no explicit type annotation, infer the type from
constructor-call patterns (new X(), X::new(), X::default(), $x = new X()).
Languages covered: TypeScript/JS, Java (var), Rust, PHP, C++ (auto).
Python/Kotlin/Swift deferred — need symbol-table access to distinguish
class constructors from function calls.
Adds 20 new unit tests covering constructor inference, annotation
precedence, and known limitations across all supported languages.
* fix(type-env): class-aware constructor resolution, multi-declarator fix
- Add collectClassNames pre-scan: walks AST to build Set<string> of
class/struct names defined in the file
- C++ extractInitializer uses classNames.has() to verify identifier is
a known class before inferring (auto x = User() resolves, auto x =
getUser() does not — no false positives)
- Add InitializerExtractor type that receives classNames parameter
- Fix env.size gating: always call extractInitializer when available,
so mixed declarators like const a: A = x, b = new B() resolve both
- Add env.has() guard in Java extractInitializer to skip already-bound vars
- Document Rust new/default whitelist rationale
- Pin all test assertions, add mixed multi-declarator test case
* fix(type-env): resolve Self/self/static/parent to actual type names
- Rust: Self::new()/Self::default() resolves to enclosing impl type
- PHP: new self()/static() resolves to enclosing class, parent() to superclass
- Rust: Tier 0 annotation guard prevents overwrite by constructor inference
- Rust: mut_pattern handling in extractVarName for let mut bindings
- TS: fix misleading comment in extractInitializer
- 58 tests passing (3 new Self/self resolution tests)
* perf(type-env): single-pass AST walk with closure-scoped state
Refactors buildTypeEnv to use closures instead of passing mutable state
as parameters. classNames, env, and config are captured by the inner
walk and extractTypeBinding functions — no parameter mutation.
- Eliminates separate collectClassNames pre-scan (O(2n) → O(n))
- config looked up once per file instead of per-node
- 29 fewer lines
* feat(type-env): constructor-inferred type resolution for all languages
Add cross-file constructor type inference to the ingestion pipeline,
enabling receiver-type disambiguation for member calls like
`user.save()` when the variable is assigned from a constructor without
explicit type annotations.
Pipeline changes:
- Add extractInitializer to Python and Swift type extractors
- Add CONSTRUCTOR_BINDING_SCANNERS for Python, Swift, C/C++ in type-env
- Wire constructorBindings through parse-worker → parsing-processor →
pipeline → processCallsFromExtracted
- Rewrite resolveCallTarget receiver-type filtering (step D) to use
tiered import resolution (same-file → import-scoped → global) before
falling back to fuzzy ownerId matching
- Use collectTieredCandidates for constructor binding verification
instead of raw lookupFuzzy
Bug fixes:
- Fix C++ inline method query: @definition.method was captured on
field_declaration_list instead of function_definition, causing wrong
parameterCount for all inline class methods
- Fix parse-worker accumulated/flush results missing constructorBindings
CI changes:
- Add swift.test.ts to ci-integration pipeline group and coverage job
- Update ci-report to fetch base branch (main) coverage for delta
reporting instead of showing config thresholds
- Add per-suite timing breakdown table (unit/integration/total)
- Add expandable skipped test details section
Tests: 288 passed, 4 skipped (swift — macOS only) across 10 languages
- 36 new constructor-inferred integration tests (4 per language)
- 10 fixture directories with cross-file constructor patterns
- TypeScript, JavaScript, Java, Kotlin, Python, PHP, Rust, Go, C++, Swift
* fix(type-extractors): add type assertion for LanguageTypeConfig
* feat(ruby): constructor-inferred type resolution and self-receiver mapping
Add Ruby User.new constructor binding scanner to type-env, enabling
receiver-type disambiguation for member calls like user.save vs repo.save.
Add self/this → enclosing class resolution in lookupTypeEnv so self.method()
calls resolve to the correct class even when the method name is ambiguous.
* docs: update README with constructor inference and self/this resolution details
* refactor(ingestion): unified ResolutionContext replaces fragmented map passing
Introduce createResolutionContext() as the single resolution API for all
processors. Eliminates duplicated tier-selection logic, fixes heritage
namedImportMap bug, and adds per-file resolution caching.
- NEW resolution-context.ts: closure-factory with resolve(), per-file cache,
TIER_CONFIDENCE constant, and shared ResolutionTier type
- DELETE symbol-resolver.ts: zero production importers, logic now in
resolution-context.ts
- call-processor: all functions take ctx instead of 6 separate maps,
collectTieredCandidates removed (ctx.resolve replaces it),
D4 redundant re-resolve eliminated
- heritage-processor: takes ctx, resolveHeritageId helper extracts
repeated 14-line fallback pattern, namedImportMap now included
- import-processor: takes ctx, dead createImportMap/createPackageMap/
createNamedImportMap factories removed
- pipeline: creates single ctx, wires onProgress to all processors,
logs cache hit rate in dev mode
- Tier renamed: unique-global → global (honest about returning all candidates)
- Tests migrated: 1178 unit + 84 integration passing
* feat(type-env): self/this/super resolution, TypeEnvironment API, and review fixes
Add cross-language receiver keyword resolution:
- self/this/$this → enclosing class name via AST walk
- super/base/parent → parent class name via heritage AST extraction
(8 grammar variants: TS/JS, Java, Python, Ruby, C#, PHP, Kotlin, C++, Swift)
- D-phase widening in resolveCallTarget for super→parent method dispatch
Introduce TypeEnvironment API replacing loose TypeEnvResult + lookupTypeEnv:
- buildTypeEnv() returns TypeEnvironment with .lookup() method
- Single-pass AST walk merges constructor binding scan (was separate traversal)
- ClassNameLookup type replaces over-broad ReadonlySet<string> facade
- Memoized class name lookups to avoid redundant SymbolTable scans
Code review fixes (6 agents, 11 findings):
- Replace ctx.resolve(name, '') hack with direct symbols.lookupFuzzy()
- Extract scope key helpers (extractFuncNameFromScope, receiverKey)
- Simplify D-phase from 5 steps to 4 with deduped typeNodeIds
- Remove C from CONSTRUCTOR_BINDING_SCANNERS (YAGNI — C has no constructors)
- Cache Map reuse in ResolutionContext to reduce GC pressure
- Remove unused TieredCandidates import
Integration tests for self/this, parent, and super resolution across all
12 supported languages with per-language fixture directories.
* fix(type-env): generic parent resolution, TS cast inference, C++ brace-init
Fix generic parent class breaking super resolution:
- extractParentClassFromNode now uses extractSimpleTypeName to strip
generic params (Base<T> → Base) and qualified names (models.Model → Model)
- Affects TS, Java, Python, C# heritage extraction
Fix TypeScript new X() as T / new X()! missed inference:
- Unwrap as_expression and non_null_expression before checking for
new_expression in extractInitializer
Fix C++ brace-init User{} missed inference:
- Handle compound_literal_expression with type_identifier child
in extractInitializer
Clean up deprecated lookupTypeEnv:
- Remove standalone lookupTypeEnv export, migrate all callers to
TypeEnvironment.lookup() method
- Update all 80+ test assertions to use the new API
Integration test fixtures added:
- typescript-cast-constructor-inference (new X() as T, new X()!)
- typescript/java/csharp/kotlin-generic-parent-resolution
- cpp-brace-init-inference (auto x = User{})
* fix(type-extractors): Go &User{}, TS double-cast, Swift .init inference
Fix Go pointer-to-struct literal not inferred:
- Unwrap unary_expression (address-of &) before composite_literal check
- user := &User{} now correctly infers type User
Fix TypeScript double-cast only unwrapping one level:
- Change if to while loop for nested as_expression/non_null_expression
- new User() as unknown as Admin now correctly infers type User
Fix Swift User.init(name:) explicit init call missed:
- Handle navigation_expression callee with .init suffix in extractInitializer
Integration test fixtures:
- go-pointer-constructor-inference (&User{}, &Repo{})
- typescript-double-cast-inference (as unknown as T)
* feat: Rust struct literal, Python qualified ctor, Go new(), Swift .init scanner
- Rust: handle struct_expression in extractInitializer (User { name: "alice" })
- Python: support attribute nodes in extractInitializer (models.User("alice"))
and the cross-file scanner — extractSimpleTypeName handles qualified names
- Go: handle new(User) built-in in extractGoShortVarDeclaration
- Swift: extend CONSTRUCTOR_BINDING_SCANNERS to handle navigation_expression
callee for User.init(name:) cross-file resolution
Unit tests: 87 → 96 (Rust struct literal, Go new(), Python qualified ctor,
Python scanner qualified, plus edge cases)
Integration tests: 4 new describe blocks with fixtures
* fix: Rust Self{} resolution, C++ scoped brace-init, PHP promotion params, Ruby constants
- Rust: resolve Self {} struct literal to enclosing impl type (was stored as "Self")
- C++: replace type_identifier guard with extractSimpleTypeName for compound_literal_expression,
enabling ns::User{} scoped brace-init (closes previously deferred gap)
- PHP: add property_promotion_parameter to TYPED_PARAMETER_TYPES for PHP 8.0+
constructor property promotion (__construct(private Foo $x))
- Ruby: extend extractRubyConstructorBinding to accept constant left-hand side
(REPO = Repo.new)
Unit tests: 96 → 101 (+5: Rust Self{} ×2, C++ ns::User{} ×1, PHP promotion ×1,
Ruby constant ×1)
Integration tests: 4 new describe blocks with fixtures
* feat: Phase 1 type resolution gaps — walrus, PHP properties, nullable, Go make/assert
Phase 1 quick wins from the type resolution gap analysis:
1. Python walrus operator := (named_expression) — extractInitializer + scanner
2. PHP 7.4+ typed class properties — property_declaration in extractDeclaration
3. Nullable union unwrapping — User | null → User in extractSimpleTypeName
4. Go make() builtin — slice/map element type extraction
5. Go type assertions — iface.(User) type extraction
Also: PHP primitive_type handling in extractSimpleTypeName (string, int, etc.)
Unit tests: 101 → 114 (+13)
Integration tests: 8 new describe blocks with fixtures
* feat: Phase 2 type resolution gaps — C++ range-for, Rust if-let, C# pattern matching, Python class annotations
Phase 2 medium-effort improvements:
1. C++ range-for with explicit type — for (User& u : vec) binds u: User
2. Rust if-let/while-let captured_pattern — user @ User { .. } binds user: User
3. C# is-pattern matching — if (obj is User user) binds user: User
4. Python class-level annotations — confirmed already working, added tests
Unit tests: 114 → 127 (+13)
Integration tests: 11 new test cases with fixtures
379 lines
14 KiB
TypeScript
379 lines
14 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
import { processCallsFromExtracted } from '../../src/core/ingestion/call-processor.js';
|
|
import { createResolutionContext, type ResolutionContext } from '../../src/core/ingestion/resolution-context.js';
|
|
import { createKnowledgeGraph } from '../../src/core/graph/graph.js';
|
|
import type { ExtractedCall, FileConstructorBindings } from '../../src/core/ingestion/workers/parse-worker.js';
|
|
|
|
describe('processCallsFromExtracted', () => {
|
|
let graph: ReturnType<typeof createKnowledgeGraph>;
|
|
let ctx: ResolutionContext;
|
|
|
|
beforeEach(() => {
|
|
graph = createKnowledgeGraph();
|
|
ctx = createResolutionContext();
|
|
});
|
|
|
|
it('creates CALLS relationship for same-file resolution', async () => {
|
|
ctx.symbols.add('src/index.ts', 'helper', 'Function:src/index.ts:helper', 'Function');
|
|
|
|
const calls: ExtractedCall[] = [{
|
|
filePath: 'src/index.ts',
|
|
calledName: 'helper',
|
|
sourceId: 'Function:src/index.ts:main',
|
|
}];
|
|
|
|
await processCallsFromExtracted(graph, calls, ctx);
|
|
|
|
const rels = graph.relationships.filter(r => r.type === 'CALLS');
|
|
expect(rels).toHaveLength(1);
|
|
expect(rels[0].sourceId).toBe('Function:src/index.ts:main');
|
|
expect(rels[0].targetId).toBe('Function:src/index.ts:helper');
|
|
expect(rels[0].confidence).toBe(0.95);
|
|
expect(rels[0].reason).toBe('same-file');
|
|
});
|
|
|
|
it('creates CALLS relationship for import-resolved resolution', async () => {
|
|
ctx.symbols.add('src/utils.ts', 'format', 'Function:src/utils.ts:format', 'Function');
|
|
ctx.importMap.set('src/index.ts', new Set(['src/utils.ts']));
|
|
|
|
const calls: ExtractedCall[] = [{
|
|
filePath: 'src/index.ts',
|
|
calledName: 'format',
|
|
sourceId: 'Function:src/index.ts:main',
|
|
}];
|
|
|
|
await processCallsFromExtracted(graph, calls, ctx);
|
|
|
|
const rels = graph.relationships.filter(r => r.type === 'CALLS');
|
|
expect(rels).toHaveLength(1);
|
|
expect(rels[0].confidence).toBe(0.9);
|
|
expect(rels[0].reason).toBe('import-resolved');
|
|
});
|
|
|
|
it('resolves unique global symbol with moderate confidence', async () => {
|
|
ctx.symbols.add('src/other.ts', 'uniqueFunc', 'Function:src/other.ts:uniqueFunc', 'Function');
|
|
|
|
const calls: ExtractedCall[] = [{
|
|
filePath: 'src/index.ts',
|
|
calledName: 'uniqueFunc',
|
|
sourceId: 'Function:src/index.ts:main',
|
|
}];
|
|
|
|
await processCallsFromExtracted(graph, calls, ctx);
|
|
|
|
const rels = graph.relationships.filter(r => r.type === 'CALLS');
|
|
expect(rels).toHaveLength(1);
|
|
expect(rels[0].confidence).toBe(0.5);
|
|
expect(rels[0].reason).toBe('global');
|
|
});
|
|
|
|
it('refuses ambiguous global symbols — no CALLS edge created', async () => {
|
|
ctx.symbols.add('src/a.ts', 'render', 'Function:src/a.ts:render', 'Function');
|
|
ctx.symbols.add('src/b.ts', 'render', 'Function:src/b.ts:render', 'Function');
|
|
|
|
const calls: ExtractedCall[] = [{
|
|
filePath: 'src/index.ts',
|
|
calledName: 'render',
|
|
sourceId: 'Function:src/index.ts:main',
|
|
}];
|
|
|
|
await processCallsFromExtracted(graph, calls, ctx);
|
|
|
|
const rels = graph.relationships.filter(r => r.type === 'CALLS');
|
|
expect(rels).toHaveLength(0);
|
|
});
|
|
|
|
it('skips unresolvable calls', async () => {
|
|
const calls: ExtractedCall[] = [{
|
|
filePath: 'src/index.ts',
|
|
calledName: 'nonExistent',
|
|
sourceId: 'Function:src/index.ts:main',
|
|
}];
|
|
|
|
await processCallsFromExtracted(graph, calls, ctx);
|
|
expect(graph.relationshipCount).toBe(0);
|
|
});
|
|
|
|
it('refuses non-callable symbols even when the name resolves', async () => {
|
|
ctx.symbols.add('src/index.ts', 'Widget', 'Class:src/index.ts:Widget', 'Class');
|
|
|
|
const calls: ExtractedCall[] = [{
|
|
filePath: 'src/index.ts',
|
|
calledName: 'Widget',
|
|
sourceId: 'Function:src/index.ts:main',
|
|
}];
|
|
|
|
await processCallsFromExtracted(graph, calls, ctx);
|
|
expect(graph.relationshipCount).toBe(0);
|
|
});
|
|
|
|
it('refuses CALLS edges to Interface symbols', async () => {
|
|
ctx.symbols.add('src/types.ts', 'Serializable', 'Interface:src/types.ts:Serializable', 'Interface');
|
|
ctx.importMap.set('src/index.ts', new Set(['src/types.ts']));
|
|
|
|
const calls: ExtractedCall[] = [{
|
|
filePath: 'src/index.ts',
|
|
calledName: 'Serializable',
|
|
sourceId: 'Function:src/index.ts:main',
|
|
}];
|
|
|
|
await processCallsFromExtracted(graph, calls, ctx);
|
|
expect(graph.relationships.filter(r => r.type === 'CALLS')).toHaveLength(0);
|
|
});
|
|
|
|
it('refuses CALLS edges to Enum symbols', async () => {
|
|
ctx.symbols.add('src/status.ts', 'Status', 'Enum:src/status.ts:Status', 'Enum');
|
|
ctx.importMap.set('src/index.ts', new Set(['src/status.ts']));
|
|
|
|
const calls: ExtractedCall[] = [{
|
|
filePath: 'src/index.ts',
|
|
calledName: 'Status',
|
|
sourceId: 'Function:src/index.ts:main',
|
|
}];
|
|
|
|
await processCallsFromExtracted(graph, calls, ctx);
|
|
expect(graph.relationships.filter(r => r.type === 'CALLS')).toHaveLength(0);
|
|
});
|
|
|
|
it('prefers same-file over import-resolved', async () => {
|
|
ctx.symbols.add('src/index.ts', 'render', 'Function:src/index.ts:render', 'Function');
|
|
ctx.symbols.add('src/utils.ts', 'render', 'Function:src/utils.ts:render', 'Function');
|
|
ctx.importMap.set('src/index.ts', new Set(['src/utils.ts']));
|
|
|
|
const calls: ExtractedCall[] = [{
|
|
filePath: 'src/index.ts',
|
|
calledName: 'render',
|
|
sourceId: 'Function:src/index.ts:main',
|
|
}];
|
|
|
|
await processCallsFromExtracted(graph, calls, ctx);
|
|
|
|
const rels = graph.relationships.filter(r => r.type === 'CALLS');
|
|
expect(rels).toHaveLength(1);
|
|
expect(rels[0].targetId).toBe('Function:src/index.ts:render');
|
|
expect(rels[0].reason).toBe('same-file');
|
|
});
|
|
|
|
it('handles multiple calls from the same file', async () => {
|
|
ctx.symbols.add('src/index.ts', 'foo', 'Function:src/index.ts:foo', 'Function');
|
|
ctx.symbols.add('src/index.ts', 'bar', 'Function:src/index.ts:bar', 'Function');
|
|
|
|
const calls: ExtractedCall[] = [
|
|
{ filePath: 'src/index.ts', calledName: 'foo', sourceId: 'Function:src/index.ts:main' },
|
|
{ filePath: 'src/index.ts', calledName: 'bar', sourceId: 'Function:src/index.ts:main' },
|
|
];
|
|
|
|
await processCallsFromExtracted(graph, calls, ctx);
|
|
expect(graph.relationships.filter(r => r.type === 'CALLS')).toHaveLength(2);
|
|
});
|
|
|
|
it('uses arity to disambiguate import-scoped callable candidates', async () => {
|
|
ctx.symbols.add('src/logger.ts', 'log', 'Function:src/logger.ts:log', 'Function', { parameterCount: 0 });
|
|
ctx.symbols.add('src/formatter.ts', 'log', 'Function:src/formatter.ts:log', 'Function', { parameterCount: 1 });
|
|
ctx.importMap.set('src/index.ts', new Set(['src/logger.ts', 'src/formatter.ts']));
|
|
|
|
const calls: ExtractedCall[] = [{
|
|
filePath: 'src/index.ts',
|
|
calledName: 'log',
|
|
sourceId: 'Function:src/index.ts:main',
|
|
argCount: 1,
|
|
}];
|
|
|
|
await processCallsFromExtracted(graph, calls, ctx);
|
|
|
|
const rels = graph.relationships.filter(r => r.type === 'CALLS');
|
|
expect(rels).toHaveLength(1);
|
|
expect(rels[0].targetId).toBe('Function:src/formatter.ts:log');
|
|
expect(rels[0].reason).toBe('import-resolved');
|
|
});
|
|
|
|
it('refuses ambiguous call targets when arity does not produce a unique match', async () => {
|
|
ctx.symbols.add('src/logger.ts', 'log', 'Function:src/logger.ts:log', 'Function', { parameterCount: 1 });
|
|
ctx.symbols.add('src/formatter.ts', 'log', 'Function:src/formatter.ts:log', 'Function', { parameterCount: 1 });
|
|
ctx.importMap.set('src/index.ts', new Set(['src/logger.ts', 'src/formatter.ts']));
|
|
|
|
const calls: ExtractedCall[] = [{
|
|
filePath: 'src/index.ts',
|
|
calledName: 'log',
|
|
sourceId: 'Function:src/index.ts:main',
|
|
argCount: 1,
|
|
}];
|
|
|
|
await processCallsFromExtracted(graph, calls, ctx);
|
|
expect(graph.relationships.filter(r => r.type === 'CALLS')).toHaveLength(0);
|
|
});
|
|
|
|
it('calls progress callback', async () => {
|
|
ctx.symbols.add('src/index.ts', 'foo', 'Function:src/index.ts:foo', 'Function');
|
|
|
|
const calls: ExtractedCall[] = [
|
|
{ filePath: 'src/index.ts', calledName: 'foo', sourceId: 'Function:src/index.ts:main' },
|
|
];
|
|
|
|
const onProgress = vi.fn();
|
|
await processCallsFromExtracted(graph, calls, ctx, onProgress);
|
|
|
|
expect(onProgress).toHaveBeenCalledWith(1, 1);
|
|
});
|
|
|
|
it('handles empty calls array', async () => {
|
|
await processCallsFromExtracted(graph, [], ctx);
|
|
expect(graph.relationshipCount).toBe(0);
|
|
});
|
|
|
|
// ---- Constructor-aware resolution (Phase 2) ----
|
|
|
|
it('resolves constructor call to Class when no Constructor node exists', async () => {
|
|
ctx.symbols.add('src/models.ts', 'User', 'Class:src/models.ts:User', 'Class');
|
|
ctx.importMap.set('src/index.ts', new Set(['src/models.ts']));
|
|
|
|
const calls: ExtractedCall[] = [{
|
|
filePath: 'src/index.ts',
|
|
calledName: 'User',
|
|
sourceId: 'Function:src/index.ts:main',
|
|
callForm: 'constructor',
|
|
}];
|
|
|
|
await processCallsFromExtracted(graph, calls, ctx);
|
|
|
|
const rels = graph.relationships.filter(r => r.type === 'CALLS');
|
|
expect(rels).toHaveLength(1);
|
|
expect(rels[0].targetId).toBe('Class:src/models.ts:User');
|
|
expect(rels[0].reason).toBe('import-resolved');
|
|
});
|
|
|
|
it('resolves constructor call to Constructor node over Class node', async () => {
|
|
ctx.symbols.add('src/models.ts', 'User', 'Class:src/models.ts:User', 'Class');
|
|
ctx.symbols.add('src/models.ts', 'User', 'Constructor:src/models.ts:User', 'Constructor', { parameterCount: 1 });
|
|
ctx.importMap.set('src/index.ts', new Set(['src/models.ts']));
|
|
|
|
const calls: ExtractedCall[] = [{
|
|
filePath: 'src/index.ts',
|
|
calledName: 'User',
|
|
sourceId: 'Function:src/index.ts:main',
|
|
argCount: 1,
|
|
callForm: 'constructor',
|
|
}];
|
|
|
|
await processCallsFromExtracted(graph, calls, ctx);
|
|
|
|
const rels = graph.relationships.filter(r => r.type === 'CALLS');
|
|
expect(rels).toHaveLength(1);
|
|
expect(rels[0].targetId).toBe('Constructor:src/models.ts:User');
|
|
});
|
|
|
|
it('refuses Class target without callForm=constructor (existing behavior)', async () => {
|
|
ctx.symbols.add('src/models.ts', 'User', 'Class:src/models.ts:User', 'Class');
|
|
ctx.importMap.set('src/index.ts', new Set(['src/models.ts']));
|
|
|
|
const calls: ExtractedCall[] = [{
|
|
filePath: 'src/index.ts',
|
|
calledName: 'User',
|
|
sourceId: 'Function:src/index.ts:main',
|
|
}];
|
|
|
|
await processCallsFromExtracted(graph, calls, ctx);
|
|
|
|
const rels = graph.relationships.filter(r => r.type === 'CALLS');
|
|
expect(rels).toHaveLength(0);
|
|
});
|
|
|
|
it('constructor call falls back to callable types when no Constructor/Class found', async () => {
|
|
ctx.symbols.add('src/utils.ts', 'Widget', 'Function:src/utils.ts:Widget', 'Function');
|
|
ctx.importMap.set('src/index.ts', new Set(['src/utils.ts']));
|
|
|
|
const calls: ExtractedCall[] = [{
|
|
filePath: 'src/index.ts',
|
|
calledName: 'Widget',
|
|
sourceId: 'Function:src/index.ts:main',
|
|
callForm: 'constructor',
|
|
}];
|
|
|
|
await processCallsFromExtracted(graph, calls, ctx);
|
|
|
|
const rels = graph.relationships.filter(r => r.type === 'CALLS');
|
|
expect(rels).toHaveLength(1);
|
|
expect(rels[0].targetId).toBe('Function:src/utils.ts:Widget');
|
|
});
|
|
|
|
it('constructor arity filtering narrows overloaded constructors', async () => {
|
|
ctx.symbols.add('src/models.ts', 'User', 'Constructor:src/models.ts:User(0)', 'Constructor', { parameterCount: 0 });
|
|
ctx.symbols.add('src/models.ts', 'User', 'Constructor:src/models.ts:User(2)', 'Constructor', { parameterCount: 2 });
|
|
ctx.importMap.set('src/index.ts', new Set(['src/models.ts']));
|
|
|
|
const calls: ExtractedCall[] = [{
|
|
filePath: 'src/index.ts',
|
|
calledName: 'User',
|
|
sourceId: 'Function:src/index.ts:main',
|
|
argCount: 2,
|
|
callForm: 'constructor',
|
|
}];
|
|
|
|
await processCallsFromExtracted(graph, calls, ctx);
|
|
|
|
const rels = graph.relationships.filter(r => r.type === 'CALLS');
|
|
expect(rels).toHaveLength(1);
|
|
expect(rels[0].targetId).toBe('Constructor:src/models.ts:User(2)');
|
|
});
|
|
|
|
it('cannot discriminate same-arity overloads by parameter type (known limitation)', async () => {
|
|
ctx.symbols.add('src/UserDao.ts', 'save', 'Function:src/UserDao.ts:save', 'Function', { parameterCount: 1 });
|
|
ctx.symbols.add('src/RepoDao.ts', 'save', 'Function:src/RepoDao.ts:save', 'Function', { parameterCount: 1 });
|
|
ctx.importMap.set('src/index.ts', new Set(['src/UserDao.ts', 'src/RepoDao.ts']));
|
|
|
|
const calls: ExtractedCall[] = [{
|
|
filePath: 'src/index.ts',
|
|
calledName: 'save',
|
|
sourceId: 'Function:src/index.ts:main',
|
|
argCount: 1,
|
|
}];
|
|
|
|
await processCallsFromExtracted(graph, calls, ctx);
|
|
const rels = graph.relationships.filter(r => r.type === 'CALLS');
|
|
expect(rels).toHaveLength(0);
|
|
});
|
|
|
|
// ---- Scope-aware constructor bindings (Phase 3) ----
|
|
|
|
it('scope-aware bindings: same varName in different functions resolves to correct type', async () => {
|
|
ctx.symbols.add('src/models.ts', 'User', 'Class:src/models.ts:User', 'Class');
|
|
ctx.symbols.add('src/models.ts', 'Repo', 'Class:src/models.ts:Repo', 'Class');
|
|
ctx.symbols.add('src/models.ts', 'save', 'Function:src/models.ts:save', 'Function');
|
|
ctx.importMap.set('src/index.ts', new Set(['src/models.ts']));
|
|
|
|
const constructorBindings: FileConstructorBindings[] = [{
|
|
filePath: 'src/index.ts',
|
|
bindings: [
|
|
{ scope: 'processUser@12', varName: 'obj', calleeName: 'User' },
|
|
{ scope: 'processRepo@89', varName: 'obj', calleeName: 'Repo' },
|
|
],
|
|
}];
|
|
|
|
const calls: ExtractedCall[] = [
|
|
{
|
|
filePath: 'src/index.ts',
|
|
calledName: 'save',
|
|
sourceId: 'Function:src/index.ts:processUser',
|
|
receiverName: 'obj',
|
|
callForm: 'member',
|
|
},
|
|
{
|
|
filePath: 'src/index.ts',
|
|
calledName: 'save',
|
|
sourceId: 'Function:src/index.ts:processRepo',
|
|
receiverName: 'obj',
|
|
callForm: 'member',
|
|
},
|
|
];
|
|
|
|
await processCallsFromExtracted(graph, calls, ctx, undefined, constructorBindings);
|
|
|
|
const rels = graph.relationships.filter(r => r.type === 'CALLS');
|
|
expect(rels).toHaveLength(2);
|
|
// Both calls should resolve, each with the correct receiver type from their scope
|
|
// (the important thing is they don't collide — without scope awareness,
|
|
// last-write-wins would give both calls the same receiver type)
|
|
expect(rels[0].sourceId).toBe('Function:src/index.ts:processUser');
|
|
expect(rels[1].sourceId).toBe('Function:src/index.ts:processRepo');
|
|
});
|
|
});
|