diff --git a/CLAUDE.md b/CLAUDE.md index 94efe84d..d54ad954 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -198,6 +198,7 @@ Multiple Claude Code instances run concurrently in this repo. **Every session mu **Rules:** - Run `/worktree` before starting work +- **Always sync with `origin/main` before starting feature work.** Run `git fetch origin && git log --oneline origin/main -10` to check recent merges. If the current branch is behind main, create a new branch from `origin/main`. Never implement features on stale branches — the work may already exist on main. - Stage only files you explicitly changed - Commit with specific file paths: `git commit -m "msg"` - Ignore unexpected dirty files — they belong to another session diff --git a/docs/roadmap/ROADMAP.md b/docs/roadmap/ROADMAP.md index 1586be32..6365dbad 100644 --- a/docs/roadmap/ROADMAP.md +++ b/docs/roadmap/ROADMAP.md @@ -17,7 +17,7 @@ Codegraph is a strong local-first code graph CLI. This roadmap describes planned | [**2.5**](#phase-25--analysis-expansion) | Analysis Expansion | Complexity metrics, community detection, flow tracing, co-change, manifesto, boundary rules, check, triage, audit, batch, hybrid search | **Complete** (v2.7.0) | | [**2.7**](#phase-27--deep-analysis--graph-enrichment) | Deep Analysis & Graph Enrichment | Dataflow analysis, intraprocedural CFG, AST node storage, expanded node/edge types, extractors refactoring, CLI consolidation, interactive viewer, exports command, normalizeSymbol | **Complete** (v3.0.0) | | [**3**](#phase-3--architectural-refactoring) | Architectural Refactoring (Vertical Slice) | Unified AST analysis framework, command/query separation, repository pattern, queries.js decomposition, composable MCP, CLI commands, domain errors, builder pipeline, presentation layer, domain grouping, curated API, unified graph model, qualified names, CLI composability | **Complete** (v3.1.5) | -| [**4**](#phase-4--resolution-accuracy) | Resolution Accuracy | Dead role sub-categories, receiver type tracking, interface/trait implementation edges, resolution precision/recall benchmarks, `package.json` exports field, monorepo workspace resolution | Planned | +| [**4**](#phase-4--resolution-accuracy) | Resolution Accuracy | Dead role sub-categories, receiver type tracking, interface/trait implementation edges, resolution precision/recall benchmarks, `package.json` exports field, monorepo workspace resolution | **In Progress** (4.2 complete) | | [**5**](#phase-5--typescript-migration) | TypeScript Migration | Project setup, core type definitions, leaf -> core -> orchestration module migration, test migration, supply-chain security, CI coverage gates | Planned | | [**6**](#phase-6--native-analysis-acceleration) | Native Analysis Acceleration | Move JS-only build phases (AST nodes, CFG, dataflow, insert nodes, structure, roles, complexity) to Rust; fix incremental rebuild data loss on native; sub-100ms 1-file rebuilds | Planned | | [**7**](#phase-7--runtime--extensibility) | Runtime & Extensibility | Event-driven pipeline, unified engine strategy, subgraph export filtering, transitive confidence, query caching, configuration profiles, pagination, plugin system, DX & onboarding, confidence annotations, shell completion | Planned | @@ -1005,17 +1005,21 @@ src/domain/ The coarse `dead` role is now sub-classified into four categories: `dead-leaf` (parameters, properties, constants), `dead-entry` (CLI commands, MCP tools, route/handler files), `dead-ffi` (cross-language FFI — `.rs`, `.c`, `.go`, etc.), and `dead-unresolved` (genuinely unreferenced callables). The `--role dead` filter matches all sub-roles for backward compatibility. Risk weights are tuned per sub-role. `VALID_ROLES`, `DEAD_SUB_ROLES` exported from `shared/kinds.js`. Stats, MCP `node_roles`, CLI `roles`/`triage` all updated. -### 4.2 -- Receiver Type Tracking for Method Dispatch +### 4.2 -- Receiver Type Tracking for Method Dispatch ✅ -The single highest-impact resolution improvement. Currently `obj.method()` resolves to ANY exported `method` in scope — no receiver type tracking. This misses repository pattern calls (`repo.findCallers()`), builder chains, and visitor dispatch. +The single highest-impact resolution improvement. Previously `obj.method()` resolved to ANY exported `method` in scope — no receiver type tracking. This missed repository pattern calls (`repo.findCallers()`), builder chains, and visitor dispatch. -- Track variable-to-type assignments: when `const x = new SomeClass()` or `const x: SomeClass = ...`, record `x → SomeClass` in a per-file type map -- During edge building, resolve `x.method()` to `SomeClass.method` using the type map -- Leverage the existing `qualified_name` and `scope` columns (Phase 3.12) for matching -- Confidence: `1.0` for explicit constructor, `0.9` for type annotation, `0.7` for factory function return -- Covers: TypeScript annotated variables, constructor assignments, factory patterns +**Implemented:** +- ✅ Upgraded `typeMap` from `Map` to `Map` across all 8 language extractors +- ✅ Graded confidence per type source: `1.0` constructor (`new Foo()`), `0.9` type annotation / typed parameter, `0.7` factory method (`Foo.create()`) +- ✅ Factory pattern extraction: JS/TS (`Foo.create()`), Go (`NewFoo()`, `&Struct{}`, `Struct{}`), Python (`Foo()`, `Foo.create()`) +- ✅ Edge builder uses type map for precise `ClassName.method` qualified-name lookup in both JS fallback and native supplement paths +- ✅ Receiver edges carry type-source confidence instead of hardcoded 0.9/0.7 +- ✅ `setIfHigher` logic ensures highest-confidence assignment wins per variable +- ✅ Incremental build path updated to consume new format +- ✅ Backwards-compatible: `typeof entry === 'string'` guards handle mixed old/new formats -**Affected files:** `src/domain/graph/builder/stages/build-edges.js`, `src/extractors/*.js` +**Affected files:** `src/domain/graph/builder/stages/build-edges.js`, `src/domain/graph/builder/incremental.js`, `src/extractors/*.js` (all 8 languages) ### 4.3 -- Interface and Trait Implementation Tracking diff --git a/src/domain/graph/builder/incremental.js b/src/domain/graph/builder/incremental.js index c2ea1509..77835624 100644 --- a/src/domain/graph/builder/incremental.js +++ b/src/domain/graph/builder/incremental.js @@ -101,7 +101,12 @@ function resolveCallTargets(stmts, call, relPath, importedNames, typeMap) { } // Type-aware resolution: translate variable receiver to declared type if ((!targets || targets.length === 0) && call.receiver && typeMap) { - const typeName = typeMap.get(call.receiver); + const typeEntry = typeMap.get(call.receiver); + const typeName = typeEntry + ? typeof typeEntry === 'string' + ? typeEntry + : typeEntry.type + : null; if (typeName) { const qualified = `${typeName}.${call.name}`; targets = stmts.findNodeByName.all(qualified); diff --git a/src/domain/graph/builder/stages/build-edges.js b/src/domain/graph/builder/stages/build-edges.js index 47d75320..3dd6ea78 100644 --- a/src/domain/graph/builder/stages/build-edges.js +++ b/src/domain/graph/builder/stages/build-edges.js @@ -104,7 +104,11 @@ function buildCallEdgesNative(ctx, getNodeIdStmt, allEdgeRows, allNodes, native) const importedNames = buildImportedNamesForNative(ctx, relPath, symbols, rootDir); const typeMap = symbols.typeMap instanceof Map - ? [...symbols.typeMap.entries()].map(([name, typeName]) => ({ name, typeName })) + ? [...symbols.typeMap.entries()].map(([name, entry]) => ({ + name, + typeName: typeof entry === 'string' ? entry : entry.type, + confidence: typeof entry === 'object' ? entry.confidence : 0.9, + })) : Array.isArray(symbols.typeMap) ? symbols.typeMap : []; @@ -166,7 +170,9 @@ function supplementReceiverEdges(ctx, nativeFiles, getNodeIdStmt, allEdgeRows) { for (const nf of nativeFiles) { const relPath = nf.file; - const typeMap = new Map(nf.typeMap.map((t) => [t.name, t.typeName])); + const typeMap = new Map( + nf.typeMap.map((t) => [t.name, { type: t.typeName, confidence: t.confidence ?? 0.9 }]), + ); const fileNodeRow = { id: nf.fileNodeId }; for (const call of nf.calls) { @@ -180,7 +186,8 @@ function supplementReceiverEdges(ctx, nativeFiles, getNodeIdStmt, allEdgeRows) { buildReceiverEdge(ctx, call, caller, relPath, seenCallEdges, allEdgeRows, typeMap); // Type-resolved method call: caller → Type.method - const typeName = typeMap.get(call.receiver); + const typeEntry = typeMap.get(call.receiver); + const typeName = typeEntry ? typeEntry.type : null; if (typeName) { const qualifiedName = `${typeName}.${call.name}`; const targets = (ctx.nodesByName.get(qualifiedName) || []).filter( @@ -298,7 +305,12 @@ function resolveCallTargets(ctx, call, relPath, importedNames, typeMap) { function resolveByMethodOrGlobal(ctx, call, relPath, typeMap) { // Type-aware resolution: translate variable receiver to its declared type if (call.receiver && typeMap) { - const typeName = typeMap.get(call.receiver); + const typeEntry = typeMap.get(call.receiver); + const typeName = typeEntry + ? typeof typeEntry === 'string' + ? typeEntry + : typeEntry.type + : null; if (typeName) { const qualifiedName = `${typeName}.${call.name}`; const typed = (ctx.nodesByName.get(qualifiedName) || []).filter((n) => n.kind === 'method'); @@ -367,7 +379,10 @@ function buildFileCallEdges( function buildReceiverEdge(ctx, call, caller, relPath, seenCallEdges, allEdgeRows, typeMap) { const receiverKinds = new Set(['class', 'struct', 'interface', 'type', 'module']); - const effectiveReceiver = typeMap?.get(call.receiver) || call.receiver; + const typeEntry = typeMap?.get(call.receiver); + const typeName = typeEntry ? (typeof typeEntry === 'string' ? typeEntry : typeEntry.type) : null; + const typeConfidence = typeEntry && typeof typeEntry === 'object' ? typeEntry.confidence : null; + const effectiveReceiver = typeName || call.receiver; const samefile = ctx.nodesByNameAndFile.get(`${effectiveReceiver}|${relPath}`) || []; const candidates = samefile.length > 0 ? samefile : ctx.nodesByName.get(effectiveReceiver) || []; const receiverNodes = candidates.filter((n) => receiverKinds.has(n.kind)); @@ -376,7 +391,8 @@ function buildReceiverEdge(ctx, call, caller, relPath, seenCallEdges, allEdgeRow const recvKey = `recv|${caller.id}|${recvTarget.id}`; if (!seenCallEdges.has(recvKey)) { seenCallEdges.add(recvKey); - const confidence = effectiveReceiver !== call.receiver ? 0.9 : 0.7; + // Use type source confidence when available, otherwise 0.7 for untyped receiver + const confidence = typeConfidence ?? (typeName ? 0.9 : 0.7); allEdgeRows.push([caller.id, recvTarget.id, 'receiver', confidence, 0]); } } diff --git a/src/extractors/csharp.js b/src/extractors/csharp.js index 68fd3121..c7957c92 100644 --- a/src/extractors/csharp.js +++ b/src/extractors/csharp.js @@ -330,7 +330,7 @@ function extractCSharpTypeMapDepth(node, ctx, depth) { if (child && child.type === 'variable_declarator') { const nameNode = child.childForFieldName('name') || child.child(0); if (nameNode && nameNode.type === 'identifier') { - ctx.typeMap.set(nameNode.text, typeName); + ctx.typeMap.set(nameNode.text, { type: typeName, confidence: 0.9 }); } } } @@ -344,7 +344,7 @@ function extractCSharpTypeMapDepth(node, ctx, depth) { const nameNode = node.childForFieldName('name'); if (typeNode && nameNode) { const typeName = extractCSharpTypeName(typeNode); - if (typeName) ctx.typeMap.set(nameNode.text, typeName); + if (typeName) ctx.typeMap.set(nameNode.text, { type: typeName, confidence: 0.9 }); } } diff --git a/src/extractors/go.js b/src/extractors/go.js index 23b5f1b0..069e687e 100644 --- a/src/extractors/go.js +++ b/src/extractors/go.js @@ -208,10 +208,17 @@ function extractGoTypeMap(node, ctx) { extractGoTypeMapDepth(node, ctx, 0); } +function setIfHigher(typeMap, name, type, confidence) { + const existing = typeMap.get(name); + if (!existing || confidence > existing.confidence) { + typeMap.set(name, { type, confidence }); + } +} + function extractGoTypeMapDepth(node, ctx, depth) { if (depth >= 200) return; - // var x MyType = ... or var x, y MyType → var_declaration > var_spec + // var x MyType = ... or var x, y MyType → var_declaration > var_spec (confidence 0.9) if (node.type === 'var_spec') { const typeNode = node.childForFieldName('type'); if (typeNode) { @@ -220,14 +227,14 @@ function extractGoTypeMapDepth(node, ctx, depth) { for (let i = 0; i < node.childCount; i++) { const child = node.child(i); if (child && child.type === 'identifier') { - ctx.typeMap.set(child.text, typeName); + setIfHigher(ctx.typeMap, child.text, typeName, 0.9); } } } } } - // Function/method parameter types: parameter_declaration has identifiers then a type + // Function/method parameter types: parameter_declaration (confidence 0.9) if (node.type === 'parameter_declaration') { const typeNode = node.childForFieldName('type'); if (typeNode) { @@ -236,7 +243,70 @@ function extractGoTypeMapDepth(node, ctx, depth) { for (let i = 0; i < node.childCount; i++) { const child = node.child(i); if (child && child.type === 'identifier') { - ctx.typeMap.set(child.text, typeName); + setIfHigher(ctx.typeMap, child.text, typeName, 0.9); + } + } + } + } + } + + // short_var_declaration: x := Struct{}, x := &Struct{}, x := NewFoo() + // Handles multi-variable forms: x, y := A{}, B{} + if (node.type === 'short_var_declaration') { + const left = node.childForFieldName('left'); + const right = node.childForFieldName('right'); + if (left && right) { + const lefts = + left.type === 'expression_list' + ? Array.from({ length: left.childCount }, (_, i) => left.child(i)).filter( + (c) => c?.type === 'identifier', + ) + : left.type === 'identifier' + ? [left] + : []; + const rights = + right.type === 'expression_list' + ? Array.from({ length: right.childCount }, (_, i) => right.child(i)).filter( + (c) => c?.isNamed, + ) + : [right]; + + for (let idx = 0; idx < lefts.length; idx++) { + const varNode = lefts[idx]; + const rhs = rights[idx]; + if (!varNode || !rhs) continue; + + // x := Struct{...} — composite literal (confidence 1.0) + if (rhs.type === 'composite_literal') { + const typeNode = rhs.childForFieldName('type'); + if (typeNode) { + const typeName = extractGoTypeName(typeNode); + if (typeName) setIfHigher(ctx.typeMap, varNode.text, typeName, 1.0); + } + } + // x := &Struct{...} — address-of composite literal (confidence 1.0) + if (rhs.type === 'unary_expression') { + const operand = rhs.childForFieldName('operand'); + if (operand && operand.type === 'composite_literal') { + const typeNode = operand.childForFieldName('type'); + if (typeNode) { + const typeName = extractGoTypeName(typeNode); + if (typeName) setIfHigher(ctx.typeMap, varNode.text, typeName, 1.0); + } + } + } + // x := NewFoo() or x := pkg.NewFoo() — factory function (confidence 0.7) + if (rhs.type === 'call_expression') { + const fn = rhs.childForFieldName('function'); + if (fn && fn.type === 'selector_expression') { + const field = fn.childForFieldName('field'); + if (field?.text.startsWith('New')) { + const typeName = field.text.slice(3); + if (typeName) setIfHigher(ctx.typeMap, varNode.text, typeName, 0.7); + } + } else if (fn && fn.type === 'identifier' && fn.text.startsWith('New')) { + const typeName = fn.text.slice(3); + if (typeName) setIfHigher(ctx.typeMap, varNode.text, typeName, 0.7); } } } diff --git a/src/extractors/java.js b/src/extractors/java.js index f1c024d6..d44a0b8f 100644 --- a/src/extractors/java.js +++ b/src/extractors/java.js @@ -235,7 +235,7 @@ function handleJavaLocalVarDecl(node, ctx) { const child = node.child(i); if (child?.type === 'variable_declarator') { const nameNode = child.childForFieldName('name'); - if (nameNode) ctx.typeMap.set(nameNode.text, typeName); + if (nameNode) ctx.typeMap.set(nameNode.text, { type: typeName, confidence: 0.9 }); } } } @@ -280,7 +280,7 @@ function extractJavaParameters(paramListNode, typeMap) { if (typeNode) { const typeName = typeNode.type === 'generic_type' ? typeNode.child(0)?.text : typeNode.text; - if (typeName) typeMap.set(nameNode.text, typeName); + if (typeName) typeMap.set(nameNode.text, { type: typeName, confidence: 0.9 }); } } } diff --git a/src/extractors/javascript.js b/src/extractors/javascript.js index 7762959c..9b0dfd8a 100644 --- a/src/extractors/javascript.js +++ b/src/extractors/javascript.js @@ -1,6 +1,63 @@ import { debug } from '../infrastructure/logger.js'; import { findChild, nodeEndLine } from './helpers.js'; +/** Built-in globals that start with uppercase but are not user-defined types. */ +const BUILTIN_GLOBALS = new Set([ + 'Math', + 'JSON', + 'Promise', + 'Array', + 'Object', + 'Date', + 'Error', + 'Symbol', + 'Map', + 'Set', + 'RegExp', + 'Number', + 'String', + 'Boolean', + 'WeakMap', + 'WeakSet', + 'WeakRef', + 'Proxy', + 'Reflect', + 'Intl', + 'ArrayBuffer', + 'SharedArrayBuffer', + 'DataView', + 'Atomics', + 'BigInt', + 'Float32Array', + 'Float64Array', + 'Int8Array', + 'Int16Array', + 'Int32Array', + 'Uint8Array', + 'Uint16Array', + 'Uint32Array', + 'Uint8ClampedArray', + 'URL', + 'URLSearchParams', + 'TextEncoder', + 'TextDecoder', + 'AbortController', + 'AbortSignal', + 'Headers', + 'Request', + 'Response', + 'FormData', + 'Blob', + 'File', + 'ReadableStream', + 'WritableStream', + 'TransformStream', + 'console', + 'Buffer', + 'EventEmitter', + 'Stream', +]); + /** * Extract symbols from a JS/TS parsed AST. * When a compiled tree-sitter Query is provided (from parser.js), @@ -824,7 +881,24 @@ function extractNewExprTypeName(newExprNode) { return null; } +/** + * Extract variable-to-type assignments into a per-file type map. + * + * Values are `{ type: string, confidence: number }`: + * - 1.0: explicit constructor (`new Foo()`) + * - 0.9: type annotation (`: Foo`) or typed parameter + * - 0.7: factory method call (`Foo.create()` — uppercase-first heuristic) + * + * Higher-confidence entries take priority when the same variable is seen twice. + */ function extractTypeMapWalk(rootNode, typeMap) { + function setIfHigher(name, type, confidence) { + const existing = typeMap.get(name); + if (!existing || confidence > existing.confidence) { + typeMap.set(name, { type, confidence }); + } + } + function walk(node, depth) { if (depth >= 200) return; const t = node.type; @@ -834,12 +908,27 @@ function extractTypeMapWalk(rootNode, typeMap) { const typeAnno = findChild(node, 'type_annotation'); if (typeAnno) { const typeName = extractSimpleTypeName(typeAnno); - if (typeName) typeMap.set(nameN.text, typeName); - } else { - const valueN = node.childForFieldName('value'); - if (valueN && valueN.type === 'new_expression') { + if (typeName) setIfHigher(nameN.text, typeName, 0.9); + } + const valueN = node.childForFieldName('value'); + if (valueN) { + // Constructor: const x = new Foo() → confidence 1.0 + if (valueN.type === 'new_expression') { const ctorType = extractNewExprTypeName(valueN); - if (ctorType) typeMap.set(nameN.text, ctorType); + if (ctorType) setIfHigher(nameN.text, ctorType, 1.0); + } + // Factory method: const x = Foo.create() → confidence 0.7 + else if (valueN.type === 'call_expression') { + const fn = valueN.childForFieldName('function'); + if (fn && fn.type === 'member_expression') { + const obj = fn.childForFieldName('object'); + if (obj && obj.type === 'identifier') { + const objName = obj.text; + if (objName[0] !== objName[0].toLowerCase() && !BUILTIN_GLOBALS.has(objName)) { + setIfHigher(nameN.text, objName, 0.7); + } + } + } } } } @@ -850,7 +939,7 @@ function extractTypeMapWalk(rootNode, typeMap) { const typeAnno = findChild(node, 'type_annotation'); if (typeAnno) { const typeName = extractSimpleTypeName(typeAnno); - if (typeName) typeMap.set(nameNode.text, typeName); + if (typeName) setIfHigher(nameNode.text, typeName, 0.9); } } } diff --git a/src/extractors/php.js b/src/extractors/php.js index fa1cfe04..d59d1410 100644 --- a/src/extractors/php.js +++ b/src/extractors/php.js @@ -339,7 +339,7 @@ function extractPhpTypeMapDepth(node, ctx, depth) { const nameNode = node.childForFieldName('name') || findChild(node, 'variable_name'); if (typeNode && nameNode) { const typeName = extractPhpTypeName(typeNode); - if (typeName) ctx.typeMap.set(nameNode.text, typeName); + if (typeName) ctx.typeMap.set(nameNode.text, { type: typeName, confidence: 0.9 }); } } diff --git a/src/extractors/python.js b/src/extractors/python.js index 28c8d308..416b9737 100644 --- a/src/extractors/python.js +++ b/src/extractors/python.js @@ -1,5 +1,49 @@ import { findChild, nodeEndLine, pythonVisibility } from './helpers.js'; +/** Built-in globals that start with uppercase but are not user-defined types. */ +const BUILTIN_GLOBALS_PY = new Set([ + // Uppercase builtins that would false-positive on the factory heuristic + 'Exception', + 'BaseException', + 'ValueError', + 'TypeError', + 'KeyError', + 'IndexError', + 'AttributeError', + 'RuntimeError', + 'OSError', + 'IOError', + 'FileNotFoundError', + 'PermissionError', + 'NotImplementedError', + 'StopIteration', + 'GeneratorExit', + 'SystemExit', + 'KeyboardInterrupt', + 'ArithmeticError', + 'LookupError', + 'UnicodeError', + 'UnicodeDecodeError', + 'UnicodeEncodeError', + 'ImportError', + 'ModuleNotFoundError', + 'ConnectionError', + 'TimeoutError', + 'OverflowError', + 'ZeroDivisionError', + 'NameError', + 'SyntaxError', + 'RecursionError', + 'MemoryError', + // Common standard library uppercase classes + 'Path', + 'PurePath', + 'OrderedDict', + 'Counter', + 'Decimal', + 'Fraction', +]); + /** * Extract symbols from Python files. */ @@ -290,29 +334,61 @@ function extractPythonTypeMap(node, ctx) { extractPythonTypeMapDepth(node, ctx, 0); } +function setIfHigherPy(typeMap, name, type, confidence) { + const existing = typeMap.get(name); + if (!existing || confidence > existing.confidence) { + typeMap.set(name, { type, confidence }); + } +} + function extractPythonTypeMapDepth(node, ctx, depth) { if (depth >= 200) return; - // typed_parameter: identifier : type + // typed_parameter: identifier : type (confidence 0.9) if (node.type === 'typed_parameter') { const nameNode = node.child(0); const typeNode = node.childForFieldName('type'); if (nameNode && nameNode.type === 'identifier' && typeNode) { const typeName = extractPythonTypeName(typeNode); if (typeName && nameNode.text !== 'self' && nameNode.text !== 'cls') { - ctx.typeMap.set(nameNode.text, typeName); + setIfHigherPy(ctx.typeMap, nameNode.text, typeName, 0.9); } } } - // typed_default_parameter: name : type = default + // typed_default_parameter: name : type = default (confidence 0.9) if (node.type === 'typed_default_parameter') { const nameNode = node.childForFieldName('name'); const typeNode = node.childForFieldName('type'); if (nameNode && nameNode.type === 'identifier' && typeNode) { const typeName = extractPythonTypeName(typeNode); if (typeName && nameNode.text !== 'self' && nameNode.text !== 'cls') { - ctx.typeMap.set(nameNode.text, typeName); + setIfHigherPy(ctx.typeMap, nameNode.text, typeName, 0.9); + } + } + } + + // assignment: x = SomeClass(...) → constructor (confidence 1.0) + // x = SomeClass.create(...) → factory (confidence 0.7) + if (node.type === 'assignment') { + const left = node.childForFieldName('left'); + const right = node.childForFieldName('right'); + if (left && left.type === 'identifier' && right && right.type === 'call') { + const fn = right.childForFieldName('function'); + if (fn && fn.type === 'identifier') { + const name = fn.text; + if (name[0] !== name[0].toLowerCase()) { + setIfHigherPy(ctx.typeMap, left.text, name, 1.0); + } + } + if (fn && fn.type === 'attribute') { + const obj = fn.childForFieldName('object'); + if (obj && obj.type === 'identifier') { + const objName = obj.text; + if (objName[0] !== objName[0].toLowerCase() && !BUILTIN_GLOBALS_PY.has(objName)) { + setIfHigherPy(ctx.typeMap, left.text, objName, 0.7); + } + } } } } diff --git a/src/extractors/rust.js b/src/extractors/rust.js index e0f8fe33..e601e8bc 100644 --- a/src/extractors/rust.js +++ b/src/extractors/rust.js @@ -272,7 +272,7 @@ function extractRustTypeMapDepth(node, ctx, depth) { const typeNode = node.childForFieldName('type'); if (pattern && pattern.type === 'identifier' && typeNode) { const typeName = extractRustTypeName(typeNode); - if (typeName) ctx.typeMap.set(pattern.text, typeName); + if (typeName) ctx.typeMap.set(pattern.text, { type: typeName, confidence: 0.9 }); } } @@ -284,7 +284,7 @@ function extractRustTypeMapDepth(node, ctx, depth) { const name = pattern.type === 'identifier' ? pattern.text : null; if (name && name !== 'self' && name !== '&self' && name !== '&mut self') { const typeName = extractRustTypeName(typeNode); - if (typeName) ctx.typeMap.set(name, typeName); + if (typeName) ctx.typeMap.set(name, { type: typeName, confidence: 0.9 }); } } } diff --git a/tests/integration/build.test.js b/tests/integration/build.test.js index a4148642..cfa1ebac 100644 --- a/tests/integration/build.test.js +++ b/tests/integration/build.test.js @@ -537,7 +537,8 @@ describe('typed method call resolution', () => { db.close(); const receiverEdges = edges.filter((e) => e.target === 'Router'); expect(receiverEdges.length).toBeGreaterThan(0); - // Type-resolved receiver edges should have 0.9 confidence - expect(receiverEdges[0].confidence).toBe(0.9); + // Type-resolved receiver edges carry the type source confidence + // (1.0 for constructor `new Router()`, 0.9 for annotation, 0.7 for factory) + expect(receiverEdges[0].confidence).toBe(1.0); }); }); diff --git a/tests/parsers/java.test.js b/tests/parsers/java.test.js index 83d05683..39bcf118 100644 --- a/tests/parsers/java.test.js +++ b/tests/parsers/java.test.js @@ -119,16 +119,16 @@ public class Foo {}`); } }`); expect(symbols.typeMap).toBeInstanceOf(Map); - expect(symbols.typeMap.get('items')).toBe('List'); - expect(symbols.typeMap.get('router')).toBe('Router'); + expect(symbols.typeMap.get('items')).toEqual({ type: 'List', confidence: 0.9 }); + expect(symbols.typeMap.get('router')).toEqual({ type: 'Router', confidence: 0.9 }); }); it('extracts typeMap from method parameters', () => { const symbols = parseJava(`public class Foo { void handle(Request req, Response res) {} }`); - expect(symbols.typeMap.get('req')).toBe('Request'); - expect(symbols.typeMap.get('res')).toBe('Response'); + expect(symbols.typeMap.get('req')).toEqual({ type: 'Request', confidence: 0.9 }); + expect(symbols.typeMap.get('res')).toEqual({ type: 'Response', confidence: 0.9 }); }); }); }); diff --git a/tests/parsers/javascript.test.js b/tests/parsers/javascript.test.js index 00a04547..0550ad70 100644 --- a/tests/parsers/javascript.test.js +++ b/tests/parsers/javascript.test.js @@ -103,26 +103,28 @@ describe('JavaScript parser', () => { return extractSymbols(tree, 'test.ts'); } - it('extracts typeMap from type annotations', () => { + it('extracts typeMap from type annotations with confidence 0.9', () => { const symbols = parseTS(`const x: Router = express.Router();`); expect(symbols.typeMap).toBeInstanceOf(Map); - expect(symbols.typeMap.get('x')).toBe('Router'); + expect(symbols.typeMap.get('x')).toEqual({ type: 'Router', confidence: 0.9 }); }); it('extracts typeMap from generic types', () => { const symbols = parseTS(`const m: Map = new Map();`); - expect(symbols.typeMap.get('m')).toBe('Map'); + expect(symbols.typeMap.get('m')).toEqual( + expect.objectContaining({ type: 'Map', confidence: 1.0 }), + ); }); - it('infers type from new expressions', () => { + it('infers type from new expressions with confidence 1.0', () => { const symbols = parseTS(`const r = new Router();`); - expect(symbols.typeMap.get('r')).toBe('Router'); + expect(symbols.typeMap.get('r')).toEqual({ type: 'Router', confidence: 1.0 }); }); - it('extracts parameter types into typeMap', () => { + it('extracts parameter types into typeMap with confidence 0.9', () => { const symbols = parseTS(`function process(req: Request, res: Response) {}`); - expect(symbols.typeMap.get('req')).toBe('Request'); - expect(symbols.typeMap.get('res')).toBe('Response'); + expect(symbols.typeMap.get('req')).toEqual({ type: 'Request', confidence: 0.9 }); + expect(symbols.typeMap.get('res')).toEqual({ type: 'Response', confidence: 0.9 }); }); it('returns empty typeMap when no annotations', () => { @@ -138,12 +140,37 @@ describe('JavaScript parser', () => { it('handles let/var declarations with type annotations', () => { const symbols = parseTS(`let app: Express = createApp();`); - expect(symbols.typeMap.get('app')).toBe('Express'); + expect(symbols.typeMap.get('app')).toEqual({ type: 'Express', confidence: 0.9 }); }); - it('prefers type annotation over new expression', () => { + it('prefers constructor (1.0) over type annotation (0.9)', () => { const symbols = parseTS(`const x: Base = new Derived();`); - expect(symbols.typeMap.get('x')).toBe('Base'); + // Deliberate: constructor (1.0) beats annotation (0.9) because the runtime + // type is what matters for call resolution. In `const x: Base = new Derived()`, + // x.method() dispatches to Derived.method, not Base.method. This is a + // semantic reversal from the previous behaviour (annotation-first). + expect(symbols.typeMap.get('x')).toEqual({ type: 'Derived', confidence: 1.0 }); + }); + + it('extracts factory method patterns with confidence 0.7', () => { + const symbols = parseJS(`const client = HttpClient.create();`); + expect(symbols.typeMap.get('client')).toEqual({ type: 'HttpClient', confidence: 0.7 }); + }); + + it('ignores lowercase factory calls', () => { + const symbols = parseJS(`const result = utils.create();`); + expect(symbols.typeMap.has('result')).toBe(false); + }); + + it('ignores built-in globals like Math, JSON, Promise', () => { + const symbols = parseJS(` + const r = Math.random(); + const d = JSON.parse('{}'); + const p = Promise.resolve(42); + `); + expect(symbols.typeMap.has('r')).toBe(false); + expect(symbols.typeMap.has('d')).toBe(false); + expect(symbols.typeMap.has('p')).toBe(false); }); });