mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
This commit is contained in:
parent
153262304c
commit
0561d24efd
79 changed files with 3482 additions and 1583 deletions
|
|
@ -58,8 +58,40 @@ This repository is a **monorepo** with two main products: the **CLI / MCP packag
|
|||
| Web UI behavior | `gitnexus-web/src/` (components, workers, graph client). |
|
||||
| CI | `.github/workflows/*.yml`, `.github/actions/setup-gitnexus/`. |
|
||||
|
||||
## Known limitations
|
||||
|
||||
### Overloaded method resolution
|
||||
|
||||
Method and Constructor node IDs include an arity suffix (`#<paramCount>`) to
|
||||
disambiguate overloaded methods. Two overloads with different parameter counts
|
||||
produce distinct graph nodes: `Method:file:Class.method#1` vs
|
||||
`Method:file:Class.method#2`.
|
||||
|
||||
**Remaining limitation — same-arity overloads:** When two overloads share the
|
||||
same parameter count but differ only in types (e.g. `save(int)` vs
|
||||
`save(String)`), they still share a node ID. This is rare in practice; a future
|
||||
enhancement may add type-hash disambiguation for languages with reliable type
|
||||
extraction (see issue #574).
|
||||
|
||||
**Variadic method matching:** When one side is variadic (`parameterCount`
|
||||
undefined) and the other has a fixed count, `METHOD_IMPLEMENTS` edges are
|
||||
emitted with confidence 0.7 instead of 1.0. Variadic methods like
|
||||
`foo(String... args)` may superficially match `foo(String s)` by type but
|
||||
are not guaranteed to be interchangeable across all languages (Java/Kotlin
|
||||
accept this via varargs sugar; TypeScript, C#, Rust do not).
|
||||
|
||||
**Confidence tiering** for `METHOD_IMPLEMENTS` edges:
|
||||
|
||||
| Match quality | Confidence | When |
|
||||
|---|---|---|
|
||||
| Exact parameter types match | 1.0 | Both sides have `parameterTypes` arrays and they match |
|
||||
| Arity (count) matches | 1.0 | Both sides have `parameterCount`, types unavailable |
|
||||
| Variadic vs fixed | 0.7 | One side is variadic, other has fixed count |
|
||||
| Lenient (insufficient info) | 0.7 | One or both sides lack type and count data |
|
||||
|
||||
## Related docs
|
||||
|
||||
- [MIGRATION.md](MIGRATION.md) — breaking changes and migration guidance.
|
||||
- [RUNBOOK.md](RUNBOOK.md) — operational commands and recovery.
|
||||
- [GUARDRAILS.md](GUARDRAILS.md) — safety boundaries for humans and agents.
|
||||
- [TESTING.md](TESTING.md) — how to run tests.
|
||||
|
|
|
|||
27
MIGRATION.md
Normal file
27
MIGRATION.md
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# Migration Guide
|
||||
|
||||
## OVERRIDES → METHOD_OVERRIDES (PR #642)
|
||||
|
||||
The `OVERRIDES` relationship type has been renamed to `METHOD_OVERRIDES` for
|
||||
consistency with the new `METHOD_IMPLEMENTS` edge type.
|
||||
|
||||
### Do I need to migrate?
|
||||
|
||||
**No.** Backward compatibility is handled automatically at runtime:
|
||||
|
||||
- `local-backend.ts` dual-reads both `OVERRIDES` and `METHOD_OVERRIDES` in all
|
||||
impact-analysis and context queries. Existing stored graphs with `OVERRIDES`
|
||||
edges continue to return correct results without any manual intervention.
|
||||
- The `REL_TYPES` array in `schema-constants.ts` includes both names so Cypher
|
||||
queries that reference either will work.
|
||||
|
||||
### What happens on re-index?
|
||||
|
||||
Running `npx gitnexus analyze` on a repository produces `METHOD_OVERRIDES`
|
||||
edges going forward. The old `OVERRIDES` edges are replaced as part of the
|
||||
normal full re-index.
|
||||
|
||||
### When will the legacy alias be removed?
|
||||
|
||||
The `OVERRIDES` compat alias will remain until a future major version. Removal
|
||||
will be announced in this file and in the changelog before it happens.
|
||||
|
|
@ -97,7 +97,8 @@ export type RelationshipType =
|
|||
| 'CONTAINS'
|
||||
| 'CALLS'
|
||||
| 'INHERITS'
|
||||
| 'OVERRIDES'
|
||||
| 'METHOD_OVERRIDES'
|
||||
| 'METHOD_IMPLEMENTS'
|
||||
| 'IMPORTS'
|
||||
| 'USES'
|
||||
| 'DEFINES'
|
||||
|
|
|
|||
|
|
@ -55,7 +55,9 @@ export const REL_TYPES = [
|
|||
'HAS_METHOD',
|
||||
'HAS_PROPERTY',
|
||||
'ACCESSES',
|
||||
'OVERRIDES',
|
||||
'METHOD_OVERRIDES',
|
||||
'OVERRIDES', // Legacy compat alias — kept until all stored indexes are migrated
|
||||
'METHOD_IMPLEMENTS',
|
||||
'MEMBER_OF',
|
||||
'STEP_IN_PROCESS',
|
||||
'HANDLES_ROUTE',
|
||||
|
|
|
|||
|
|
@ -12,9 +12,10 @@ import { isVerboseIngestionEnabled } from './utils/verbose.js';
|
|||
import { yieldToEventLoop } from './utils/event-loop.js';
|
||||
import {
|
||||
FUNCTION_NODE_TYPES,
|
||||
extractFunctionName,
|
||||
findEnclosingClassId,
|
||||
findEnclosingClassInfo,
|
||||
genericFuncName,
|
||||
inferFunctionLabel,
|
||||
} from './utils/ast-helpers.js';
|
||||
import {
|
||||
countCallArguments,
|
||||
|
|
@ -234,7 +235,9 @@ const findEnclosingFunction = (
|
|||
|
||||
while (current) {
|
||||
if (FUNCTION_NODE_TYPES.has(current.type)) {
|
||||
const { funcName, label } = extractFunctionName(current);
|
||||
const efnResult = provider.methodExtractor?.extractFunctionName?.(current);
|
||||
const funcName = efnResult?.funcName ?? genericFuncName(current);
|
||||
const label = efnResult?.label ?? inferFunctionLabel(current.type);
|
||||
|
||||
if (funcName) {
|
||||
const resolved = ctx.resolve(funcName, filePath);
|
||||
|
|
@ -264,7 +267,20 @@ const findEnclosingFunction = (
|
|||
}
|
||||
const classInfo = findEnclosingClassInfo(current, filePath);
|
||||
const qualifiedName = classInfo ? `${classInfo.className}.${funcName}` : funcName;
|
||||
return generateId(finalLabel, `${filePath}:${qualifiedName}`);
|
||||
// Include #<arity> suffix to match definition-phase Method/Constructor IDs.
|
||||
// Use provider.methodExtractor.extractFromNode — same extractor as definition phase.
|
||||
let arity: number | undefined;
|
||||
if (finalLabel === 'Method' || finalLabel === 'Constructor') {
|
||||
const language = getLanguageFromFilename(filePath);
|
||||
const info = language
|
||||
? provider.methodExtractor?.extractFromNode?.(current, { filePath, language })
|
||||
: undefined;
|
||||
if (info) {
|
||||
arity = info.parameters.some((p) => p.isVariadic) ? undefined : info.parameters.length;
|
||||
}
|
||||
}
|
||||
const arityTag = arity !== undefined ? `#${arity}` : '';
|
||||
return generateId(finalLabel, `${filePath}:${qualifiedName}${arityTag}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -299,7 +315,20 @@ const findEnclosingFunction = (
|
|||
const qualifiedName = classInfo
|
||||
? `${classInfo.className}.${customResult.funcName}`
|
||||
: customResult.funcName;
|
||||
return generateId(finalLabel, `${filePath}:${qualifiedName}`);
|
||||
// Include #<arity> suffix to match definition-phase Method/Constructor IDs.
|
||||
const sigNode = current.previousSibling ?? current;
|
||||
let arity2: number | undefined;
|
||||
if (finalLabel === 'Method' || finalLabel === 'Constructor') {
|
||||
const language = getLanguageFromFilename(filePath);
|
||||
const info = language
|
||||
? provider.methodExtractor?.extractFromNode?.(sigNode, { filePath, language })
|
||||
: undefined;
|
||||
if (info) {
|
||||
arity2 = info.parameters.some((p) => p.isVariadic) ? undefined : info.parameters.length;
|
||||
}
|
||||
}
|
||||
const arityTag2 = arity2 !== undefined ? `#${arity2}` : '';
|
||||
return generateId(finalLabel, `${filePath}:${qualifiedName}${arityTag2}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -600,6 +629,7 @@ export const processCalls = async (
|
|||
importedReturnTypes,
|
||||
importedRawReturnTypes,
|
||||
enclosingFunctionFinder: provider?.enclosingFunctionFinder,
|
||||
extractFunctionName: provider?.methodExtractor?.extractFunctionName,
|
||||
});
|
||||
if (typeEnv && exportedTypeMap) {
|
||||
const fileExports = collectExportedBindings(typeEnv, file.path, ctx.symbols, graph);
|
||||
|
|
@ -840,7 +870,8 @@ export const processCalls = async (
|
|||
let p = callNode.parent;
|
||||
while (p) {
|
||||
if (FUNCTION_NODE_TYPES.has(p.type)) {
|
||||
const { funcName } = extractFunctionName(p);
|
||||
const funcName =
|
||||
provider.methodExtractor?.extractFunctionName?.(p)?.funcName ?? genericFuncName(p);
|
||||
if (funcName) {
|
||||
scope = `${funcName}@${p.startIndex}`;
|
||||
break;
|
||||
|
|
@ -1384,12 +1415,16 @@ const extractFuncNameFromScope = (scope: string): string => scope.slice(0, scope
|
|||
|
||||
/** Extract the bare function name from a sourceId.
|
||||
* Handles both unqualified ("Function:filepath:funcName" → "funcName")
|
||||
* and qualified ("Function:filepath:ClassName.funcName" → "funcName"). */
|
||||
* and qualified ("Function:filepath:ClassName.funcName" → "funcName").
|
||||
* Strips any trailing #<arity> suffix from Method/Constructor IDs. */
|
||||
const extractFuncNameFromSourceId = (sourceId: string): string => {
|
||||
const lastColon = sourceId.lastIndexOf(':');
|
||||
const segment = lastColon >= 0 ? sourceId.slice(lastColon + 1) : '';
|
||||
const dotIdx = segment.lastIndexOf('.');
|
||||
return dotIdx >= 0 ? segment.slice(dotIdx + 1) : segment;
|
||||
const raw = dotIdx >= 0 ? segment.slice(dotIdx + 1) : segment;
|
||||
// Strip #<arity> suffix (e.g. "save#2" → "save")
|
||||
const hashIdx = raw.indexOf('#');
|
||||
return hashIdx >= 0 ? raw.slice(0, hashIdx) : raw;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -15,7 +15,19 @@ import { cCppExportChecker } from '../export-detection.js';
|
|||
import { resolveCImport, resolveCppImport } from '../import-resolvers/standard.js';
|
||||
import { C_QUERIES, CPP_QUERIES } from '../tree-sitter-queries.js';
|
||||
|
||||
import { isCppInsideClassOrStruct } from '../utils/ast-helpers.js';
|
||||
/**
|
||||
* Node types for standard function declarations that need C/C++ declarator handling.
|
||||
* Used by cCppExtractFunctionName to determine how to extract the function name.
|
||||
*/
|
||||
const FUNCTION_DECLARATION_TYPES = new Set([
|
||||
'function_declaration',
|
||||
'function_definition',
|
||||
'async_function_declaration',
|
||||
'generator_function_declaration',
|
||||
'function_item',
|
||||
]);
|
||||
import type { SyntaxNode } from '../utils/ast-helpers.js';
|
||||
import type { NodeLabel } from 'gitnexus-shared';
|
||||
import type { LanguageProvider } from '../language-provider.js';
|
||||
import { createFieldExtractor } from '../field-extractors/generic.js';
|
||||
import {
|
||||
|
|
@ -132,6 +144,154 @@ const C_BUILT_INS: ReadonlySet<string> = new Set([
|
|||
'put',
|
||||
]);
|
||||
|
||||
/**
|
||||
* C/C++ function name extraction — unwraps pointer_declarator / reference_declarator /
|
||||
* function_declarator / qualified_identifier chains to find the actual function name.
|
||||
* Handles field_identifier (method inside class body) and parenthesized_declarator.
|
||||
*/
|
||||
const cCppExtractFunctionName = (
|
||||
node: SyntaxNode,
|
||||
): { funcName: string | null; label: NodeLabel } | null => {
|
||||
if (!FUNCTION_DECLARATION_TYPES.has(node.type)) return null;
|
||||
|
||||
let funcName: string | null = null;
|
||||
let label: NodeLabel = 'Function';
|
||||
|
||||
// C/C++: function_definition -> [pointer_declarator ->] function_declarator -> qualified_identifier/identifier
|
||||
// Unwrap pointer_declarator / reference_declarator wrappers to reach function_declarator
|
||||
let declarator = node.childForFieldName?.('declarator');
|
||||
if (!declarator) {
|
||||
for (let i = 0; i < node.childCount; i++) {
|
||||
const c = node.child(i);
|
||||
if (c?.type === 'function_declarator') {
|
||||
declarator = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
while (
|
||||
declarator &&
|
||||
(declarator.type === 'pointer_declarator' || declarator.type === 'reference_declarator')
|
||||
) {
|
||||
let nextDeclarator = declarator.childForFieldName?.('declarator');
|
||||
if (!nextDeclarator) {
|
||||
for (let i = 0; i < declarator.childCount; i++) {
|
||||
const c = declarator.child(i);
|
||||
if (
|
||||
c?.type === 'function_declarator' ||
|
||||
c?.type === 'pointer_declarator' ||
|
||||
c?.type === 'reference_declarator'
|
||||
) {
|
||||
nextDeclarator = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
declarator = nextDeclarator;
|
||||
}
|
||||
if (declarator) {
|
||||
let innerDeclarator = declarator.childForFieldName?.('declarator');
|
||||
if (!innerDeclarator) {
|
||||
for (let i = 0; i < declarator.childCount; i++) {
|
||||
const c = declarator.child(i);
|
||||
if (
|
||||
c?.type === 'qualified_identifier' ||
|
||||
c?.type === 'identifier' ||
|
||||
c?.type === 'field_identifier' ||
|
||||
c?.type === 'parenthesized_declarator'
|
||||
) {
|
||||
innerDeclarator = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (innerDeclarator?.type === 'qualified_identifier') {
|
||||
let nameNode = innerDeclarator.childForFieldName?.('name');
|
||||
if (!nameNode) {
|
||||
for (let i = 0; i < innerDeclarator.childCount; i++) {
|
||||
const c = innerDeclarator.child(i);
|
||||
if (c?.type === 'identifier') {
|
||||
nameNode = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (nameNode?.text) {
|
||||
funcName = nameNode.text;
|
||||
label = 'Method';
|
||||
}
|
||||
} else if (
|
||||
innerDeclarator?.type === 'identifier' ||
|
||||
innerDeclarator?.type === 'field_identifier'
|
||||
) {
|
||||
// field_identifier is used for method names inside C++ class bodies
|
||||
funcName = innerDeclarator.text;
|
||||
if (innerDeclarator.type === 'field_identifier') label = 'Method';
|
||||
} else if (innerDeclarator?.type === 'parenthesized_declarator') {
|
||||
let nestedId: SyntaxNode | null = null;
|
||||
for (let i = 0; i < innerDeclarator.childCount; i++) {
|
||||
const c = innerDeclarator.child(i);
|
||||
if (c?.type === 'qualified_identifier' || c?.type === 'identifier') {
|
||||
nestedId = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (nestedId?.type === 'qualified_identifier') {
|
||||
let nameNode = nestedId.childForFieldName?.('name');
|
||||
if (!nameNode) {
|
||||
for (let i = 0; i < nestedId.childCount; i++) {
|
||||
const c = nestedId.child(i);
|
||||
if (c?.type === 'identifier') {
|
||||
nameNode = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (nameNode?.text) {
|
||||
funcName = nameNode.text;
|
||||
label = 'Method';
|
||||
}
|
||||
} else if (nestedId?.type === 'identifier') {
|
||||
funcName = nestedId.text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback for other node types in FUNCTION_DECLARATION_TYPES (e.g. function_item for Rust in C++ tree)
|
||||
if (!funcName) {
|
||||
let nameNode = node.childForFieldName?.('name');
|
||||
if (!nameNode) {
|
||||
for (let i = 0; i < node.childCount; i++) {
|
||||
const c = node.child(i);
|
||||
if (
|
||||
c?.type === 'identifier' ||
|
||||
c?.type === 'property_identifier' ||
|
||||
c?.type === 'simple_identifier'
|
||||
) {
|
||||
nameNode = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
funcName = nameNode?.text ?? null;
|
||||
}
|
||||
|
||||
return { funcName, label };
|
||||
};
|
||||
|
||||
/** Check if a C/C++ function_definition is inside a class or struct body.
|
||||
* Used by cppLabelOverride to skip duplicate function captures
|
||||
* that are already covered by definition.method queries. */
|
||||
function isCppInsideClassOrStruct(functionNode: SyntaxNode): boolean {
|
||||
let ancestor: SyntaxNode | null = functionNode?.parent ?? null;
|
||||
while (ancestor) {
|
||||
if (ancestor.type === 'class_specifier' || ancestor.type === 'struct_specifier') return true;
|
||||
ancestor = ancestor.parent;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Label override shared by C and C++: skip function_definition captures inside class/struct
|
||||
* bodies (they're duplicates of definition.method captures). */
|
||||
const cppLabelOverride: NonNullable<LanguageProvider['labelOverride']> = (
|
||||
|
|
@ -151,7 +311,10 @@ export const cProvider = defineLanguage({
|
|||
importResolver: resolveCImport,
|
||||
importSemantics: 'wildcard',
|
||||
fieldExtractor: createFieldExtractor(cFieldConfig),
|
||||
methodExtractor: createMethodExtractor(cMethodConfig),
|
||||
methodExtractor: createMethodExtractor({
|
||||
...cMethodConfig,
|
||||
extractFunctionName: cCppExtractFunctionName,
|
||||
}),
|
||||
labelOverride: cppLabelOverride,
|
||||
builtInNames: C_BUILT_INS,
|
||||
});
|
||||
|
|
@ -166,7 +329,10 @@ export const cppProvider = defineLanguage({
|
|||
importSemantics: 'wildcard',
|
||||
mroStrategy: 'leftmost-base',
|
||||
fieldExtractor: createFieldExtractor(cppFieldConfig),
|
||||
methodExtractor: createMethodExtractor(cppMethodConfig),
|
||||
methodExtractor: createMethodExtractor({
|
||||
...cppMethodConfig,
|
||||
extractFunctionName: cCppExtractFunctionName,
|
||||
}),
|
||||
labelOverride: cppLabelOverride,
|
||||
builtInNames: C_BUILT_INS,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
|
||||
import type { SyntaxNode } from '../utils/ast-helpers.js';
|
||||
import type { NodeLabel } from 'gitnexus-shared';
|
||||
import { FUNCTION_NODE_TYPES, extractFunctionName } from '../utils/ast-helpers.js';
|
||||
import { FUNCTION_NODE_TYPES } from '../utils/ast-helpers.js';
|
||||
import { SupportedLanguages } from 'gitnexus-shared';
|
||||
import { defineLanguage } from '../language-provider.js';
|
||||
import { typeConfig as dartConfig } from '../type-extractors/dart.js';
|
||||
|
|
@ -30,8 +30,8 @@ import { dartMethodConfig } from '../method-extractors/configs/dart.js';
|
|||
* function_body are siblings under program or class_body, unlike most languages
|
||||
* where the function declaration wraps both.
|
||||
*
|
||||
* Delegates name extraction to the shared `extractFunctionName` which already
|
||||
* handles Dart's function_signature and method_signature node types.
|
||||
* Extracts the function name inline — Dart uses function_signature and
|
||||
* method_signature (which wraps function_signature) as its FUNCTION_NODE_TYPES.
|
||||
*/
|
||||
const dartEnclosingFunctionFinder = (
|
||||
node: SyntaxNode,
|
||||
|
|
@ -39,7 +39,21 @@ const dartEnclosingFunctionFinder = (
|
|||
if (node.type !== 'function_body') return null;
|
||||
const prev = node.previousSibling;
|
||||
if (!prev || !FUNCTION_NODE_TYPES.has(prev.type)) return null;
|
||||
const { funcName, label } = extractFunctionName(prev);
|
||||
|
||||
// method_signature wraps function_signature — unwrap to reach the name
|
||||
let target = prev;
|
||||
let label: NodeLabel = 'Function';
|
||||
if (prev.type === 'method_signature') {
|
||||
label = 'Method';
|
||||
for (let i = 0; i < prev.childCount; i++) {
|
||||
const c = prev.child(i);
|
||||
if (c?.type === 'function_signature') {
|
||||
target = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
const funcName = target.childForFieldName?.('name')?.text ?? null;
|
||||
return funcName ? { funcName, label } : null;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -15,12 +15,26 @@ import { resolveKotlinImport } from '../import-resolvers/jvm.js';
|
|||
import { extractKotlinNamedBindings } from '../named-bindings/kotlin.js';
|
||||
import { appendKotlinWildcard } from '../import-resolvers/jvm.js';
|
||||
import { KOTLIN_QUERIES } from '../tree-sitter-queries.js';
|
||||
import { isKotlinClassMethod } from '../utils/ast-helpers.js';
|
||||
import type { SyntaxNode } from '../utils/ast-helpers.js';
|
||||
import { createFieldExtractor } from '../field-extractors/generic.js';
|
||||
import { kotlinConfig } from '../field-extractors/configs/jvm.js';
|
||||
import { createMethodExtractor } from '../method-extractors/generic.js';
|
||||
import { kotlinMethodConfig } from '../method-extractors/configs/jvm.js';
|
||||
|
||||
/** Check if a Kotlin function_declaration capture is inside a class_body (i.e., a method).
|
||||
* Kotlin grammar uses function_declaration for both top-level functions and class methods.
|
||||
* Returns true when the captured definition node has a class_body ancestor. */
|
||||
function isKotlinClassMethod(
|
||||
captureNode: { parent?: SyntaxNode | null } | null | undefined,
|
||||
): boolean {
|
||||
let ancestor = captureNode?.parent;
|
||||
while (ancestor) {
|
||||
if (ancestor.type === 'class_body') return true;
|
||||
ancestor = ancestor.parent;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const BUILT_INS: ReadonlySet<string> = new Set([
|
||||
'println',
|
||||
'print',
|
||||
|
|
|
|||
|
|
@ -8,7 +8,9 @@
|
|||
*/
|
||||
|
||||
import { SupportedLanguages } from 'gitnexus-shared';
|
||||
import type { NodeLabel } from 'gitnexus-shared';
|
||||
import { defineLanguage } from '../language-provider.js';
|
||||
import type { SyntaxNode } from '../utils/ast-helpers.js';
|
||||
import { typeConfig as rubyConfig } from '../type-extractors/ruby.js';
|
||||
import { routeRubyCall } from '../call-routing.js';
|
||||
import { rubyExportChecker } from '../export-detection.js';
|
||||
|
|
@ -19,6 +21,25 @@ import { rubyConfig as rubyFieldConfig } from '../field-extractors/configs/ruby.
|
|||
import { createMethodExtractor } from '../method-extractors/generic.js';
|
||||
import { rubyMethodConfig } from '../method-extractors/configs/ruby.js';
|
||||
|
||||
/** Ruby method/singleton_method: extract name from 'name' field, label as Method. */
|
||||
const rubyExtractFunctionName = (
|
||||
node: SyntaxNode,
|
||||
): { funcName: string | null; label: NodeLabel } | null => {
|
||||
if (node.type !== 'method' && node.type !== 'singleton_method') return null;
|
||||
|
||||
let nameNode = node.childForFieldName?.('name');
|
||||
if (!nameNode) {
|
||||
for (let i = 0; i < node.childCount; i++) {
|
||||
const c = node.child(i);
|
||||
if (c?.type === 'identifier') {
|
||||
nameNode = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return { funcName: nameNode?.text ?? null, label: 'Method' };
|
||||
};
|
||||
|
||||
const BUILT_INS: ReadonlySet<string> = new Set([
|
||||
'puts',
|
||||
'p',
|
||||
|
|
@ -87,6 +108,9 @@ export const rubyProvider = defineLanguage({
|
|||
callRouter: routeRubyCall,
|
||||
importSemantics: 'wildcard',
|
||||
fieldExtractor: createFieldExtractor(rubyFieldConfig),
|
||||
methodExtractor: createMethodExtractor(rubyMethodConfig),
|
||||
methodExtractor: createMethodExtractor({
|
||||
...rubyMethodConfig,
|
||||
extractFunctionName: rubyExtractFunctionName,
|
||||
}),
|
||||
builtInNames: BUILT_INS,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,7 +11,9 @@
|
|||
*/
|
||||
|
||||
import { SupportedLanguages } from 'gitnexus-shared';
|
||||
import type { NodeLabel } from 'gitnexus-shared';
|
||||
import { defineLanguage } from '../language-provider.js';
|
||||
import type { SyntaxNode } from '../utils/ast-helpers.js';
|
||||
import { typeConfig as rustConfig } from '../type-extractors/rust.js';
|
||||
import { rustExportChecker } from '../export-detection.js';
|
||||
import { resolveRustImport } from '../import-resolvers/rust.js';
|
||||
|
|
@ -22,6 +24,35 @@ import { rustConfig as rustFieldConfig } from '../field-extractors/configs/rust.
|
|||
import { createMethodExtractor } from '../method-extractors/generic.js';
|
||||
import { rustMethodConfig } from '../method-extractors/configs/rust.js';
|
||||
|
||||
/** Rust impl_item: find the function_item child and extract its name as a Method. */
|
||||
const rustExtractFunctionName = (
|
||||
node: SyntaxNode,
|
||||
): { funcName: string | null; label: NodeLabel } | null => {
|
||||
if (node.type !== 'impl_item') return null;
|
||||
|
||||
let funcItem: SyntaxNode | null = null;
|
||||
for (let i = 0; i < node.childCount; i++) {
|
||||
const c = node.child(i);
|
||||
if (c?.type === 'function_item') {
|
||||
funcItem = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!funcItem) return null;
|
||||
|
||||
let nameNode = funcItem.childForFieldName?.('name');
|
||||
if (!nameNode) {
|
||||
for (let i = 0; i < funcItem.childCount; i++) {
|
||||
const c = funcItem.child(i);
|
||||
if (c?.type === 'identifier') {
|
||||
nameNode = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return { funcName: nameNode?.text ?? null, label: 'Method' };
|
||||
};
|
||||
|
||||
const BUILT_INS: ReadonlySet<string> = new Set([
|
||||
'unwrap',
|
||||
'expect',
|
||||
|
|
@ -89,6 +120,9 @@ export const rustProvider = defineLanguage({
|
|||
namedBindingExtractor: extractRustNamedBindings,
|
||||
mroStrategy: 'qualified-syntax',
|
||||
fieldExtractor: createFieldExtractor(rustFieldConfig),
|
||||
methodExtractor: createMethodExtractor(rustMethodConfig),
|
||||
methodExtractor: createMethodExtractor({
|
||||
...rustMethodConfig,
|
||||
extractFunctionName: rustExtractFunctionName,
|
||||
}),
|
||||
builtInNames: BUILT_INS,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,12 +11,14 @@
|
|||
*/
|
||||
|
||||
import { SupportedLanguages } from 'gitnexus-shared';
|
||||
import type { NodeLabel } from 'gitnexus-shared';
|
||||
import { defineLanguage } from '../language-provider.js';
|
||||
import { typeConfig as swiftConfig } from '../type-extractors/swift.js';
|
||||
import { swiftExportChecker } from '../export-detection.js';
|
||||
import { resolveSwiftImport } from '../import-resolvers/swift.js';
|
||||
import { SWIFT_QUERIES } from '../tree-sitter-queries.js';
|
||||
import type { SwiftPackageConfig } from '../language-config.js';
|
||||
import type { SyntaxNode } from '../utils/ast-helpers.js';
|
||||
import { createFieldExtractor } from '../field-extractors/generic.js';
|
||||
import { swiftConfig as swiftFieldConfig } from '../field-extractors/configs/swift.js';
|
||||
import { createMethodExtractor } from '../method-extractors/generic.js';
|
||||
|
|
@ -109,6 +111,15 @@ function wireSwiftImplicitImports(
|
|||
}
|
||||
}
|
||||
|
||||
/** Swift init/deinit declarations have special names and Constructor label. */
|
||||
const swiftExtractFunctionName = (
|
||||
node: SyntaxNode,
|
||||
): { funcName: string | null; label: NodeLabel } | null => {
|
||||
if (node.type === 'init_declaration') return { funcName: 'init', label: 'Constructor' };
|
||||
if (node.type === 'deinit_declaration') return { funcName: 'deinit', label: 'Constructor' };
|
||||
return null; // fall through to generic
|
||||
};
|
||||
|
||||
const BUILT_INS: ReadonlySet<string> = new Set([
|
||||
'print',
|
||||
'debugPrint',
|
||||
|
|
@ -229,7 +240,10 @@ export const swiftProvider = defineLanguage({
|
|||
importSemantics: 'wildcard',
|
||||
heritageDefaultEdge: 'IMPLEMENTS',
|
||||
fieldExtractor: createFieldExtractor(swiftFieldConfig),
|
||||
methodExtractor: createMethodExtractor(swiftMethodConfig),
|
||||
methodExtractor: createMethodExtractor({
|
||||
...swiftMethodConfig,
|
||||
extractFunctionName: swiftExtractFunctionName,
|
||||
}),
|
||||
implicitImportWirer: wireSwiftImplicitImports,
|
||||
builtInNames: BUILT_INS,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,7 +8,9 @@
|
|||
*/
|
||||
|
||||
import { SupportedLanguages } from 'gitnexus-shared';
|
||||
import type { NodeLabel } from 'gitnexus-shared';
|
||||
import { defineLanguage } from '../language-provider.js';
|
||||
import type { SyntaxNode } from '../utils/ast-helpers.js';
|
||||
import { typeConfig as typescriptConfig } from '../type-extractors/typescript.js';
|
||||
import { tsExportChecker } from '../export-detection.js';
|
||||
import { resolveTypescriptImport, resolveJavascriptImport } from '../import-resolvers/standard.js';
|
||||
|
|
@ -23,6 +25,31 @@ import {
|
|||
javascriptMethodConfig,
|
||||
} from '../method-extractors/configs/typescript-javascript.js';
|
||||
|
||||
/**
|
||||
* TypeScript/JavaScript: arrow_function and function_expression get their name
|
||||
* from the parent variable_declarator (e.g. `const foo = () => {}`).
|
||||
*/
|
||||
const tsExtractFunctionName = (
|
||||
node: SyntaxNode,
|
||||
): { funcName: string | null; label: NodeLabel } | null => {
|
||||
if (node.type !== 'arrow_function' && node.type !== 'function_expression') return null;
|
||||
|
||||
const parent = node.parent;
|
||||
if (parent?.type !== 'variable_declarator') return null;
|
||||
|
||||
let nameNode = parent.childForFieldName?.('name');
|
||||
if (!nameNode) {
|
||||
for (let i = 0; i < parent.childCount; i++) {
|
||||
const c = parent.child(i);
|
||||
if (c?.type === 'identifier') {
|
||||
nameNode = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return { funcName: nameNode?.text ?? null, label: 'Function' };
|
||||
};
|
||||
|
||||
export const BUILT_INS: ReadonlySet<string> = new Set([
|
||||
'console',
|
||||
'log',
|
||||
|
|
@ -129,7 +156,10 @@ export const typescriptProvider = defineLanguage({
|
|||
importResolver: resolveTypescriptImport,
|
||||
namedBindingExtractor: extractTsNamedBindings,
|
||||
fieldExtractor: typescriptFieldExtractor,
|
||||
methodExtractor: createMethodExtractor(typescriptMethodConfig),
|
||||
methodExtractor: createMethodExtractor({
|
||||
...typescriptMethodConfig,
|
||||
extractFunctionName: tsExtractFunctionName,
|
||||
}),
|
||||
builtInNames: BUILT_INS,
|
||||
});
|
||||
|
||||
|
|
@ -142,6 +172,9 @@ export const javascriptProvider = defineLanguage({
|
|||
importResolver: resolveJavascriptImport,
|
||||
namedBindingExtractor: extractTsNamedBindings,
|
||||
fieldExtractor: createFieldExtractor(javascriptConfig),
|
||||
methodExtractor: createMethodExtractor(javascriptMethodConfig),
|
||||
methodExtractor: createMethodExtractor({
|
||||
...javascriptMethodConfig,
|
||||
extractFunctionName: tsExtractFunctionName,
|
||||
}),
|
||||
builtInNames: BUILT_INS,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -187,6 +187,7 @@ export const csharpMethodConfig: MethodExtractionConfig = {
|
|||
'destructor_declaration',
|
||||
'operator_declaration',
|
||||
'conversion_operator_declaration',
|
||||
'local_function_statement',
|
||||
],
|
||||
bodyNodeTypes: ['declaration_list'],
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,57 @@ import type { SyntaxNode } from '../../utils/ast-helpers.js';
|
|||
// PHP helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Regex to extract PHPDoc @return annotations: `@return User` */
|
||||
const PHPDOC_RETURN_RE = /@return\s+(\S+)/;
|
||||
|
||||
/** Node types to skip when walking backwards through siblings for PHPDoc. */
|
||||
const PHPDOC_SKIP_NODE_TYPES: ReadonlySet<string> = new Set(['attribute_list', 'attribute']);
|
||||
|
||||
/**
|
||||
* Normalize a PHPDoc return type for the MethodExtractor.
|
||||
* Strips nullable prefix, null/false/void unions, namespace prefixes, and
|
||||
* rejects uninformative types (mixed, void, self, static, object, array).
|
||||
*/
|
||||
function normalizePhpReturnType(raw: string): string | undefined {
|
||||
let type = raw.startsWith('?') ? raw.slice(1) : raw;
|
||||
const parts = type
|
||||
.split('|')
|
||||
.filter((p) => p !== 'null' && p !== 'false' && p !== 'void' && p !== 'mixed');
|
||||
if (parts.length !== 1) return undefined;
|
||||
type = parts[0];
|
||||
const segments = type.split('\\');
|
||||
type = segments[segments.length - 1];
|
||||
if (
|
||||
type === 'mixed' ||
|
||||
type === 'void' ||
|
||||
type === 'self' ||
|
||||
type === 'static' ||
|
||||
type === 'object' ||
|
||||
type === 'array'
|
||||
)
|
||||
return undefined;
|
||||
if (/^\w+(\[\])?$/.test(type) || /^\w+\s*</.test(type)) return type;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Walk backwards through preceding siblings of `node` to find a PHPDoc
|
||||
* `@return Type` annotation. Skips `attribute_list` nodes (PHP 8 attributes).
|
||||
*/
|
||||
function extractPhpDocReturnType(node: SyntaxNode): string | undefined {
|
||||
let sibling = node.previousSibling;
|
||||
while (sibling) {
|
||||
if (sibling.type === 'comment') {
|
||||
const match = PHPDOC_RETURN_RE.exec(sibling.text);
|
||||
if (match) return normalizePhpReturnType(match[1]);
|
||||
} else if (sibling.isNamed && !PHPDOC_SKIP_NODE_TYPES.has(sibling.type)) {
|
||||
break;
|
||||
}
|
||||
sibling = sibling.previousSibling;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const PHP_VIS = new Set<MethodVisibility>(['public', 'private', 'protected']);
|
||||
|
||||
/**
|
||||
|
|
@ -52,6 +103,9 @@ function hasModifierNode(node: SyntaxNode, modifierType: string): boolean {
|
|||
* It appears as a type node (primitive_type, named_type, union_type,
|
||||
* optional_type, nullable_type, intersection_type) after the formal_parameters
|
||||
* and a `:` token separator.
|
||||
*
|
||||
* When the AST return type is missing or uninformative (`array` / `iterable`),
|
||||
* falls back to parsing PHPDoc `@return Type` from preceding doc comments.
|
||||
*/
|
||||
function extractPhpReturnType(node: SyntaxNode): string | undefined {
|
||||
const TYPE_NODE_TYPES = new Set([
|
||||
|
|
@ -63,6 +117,7 @@ function extractPhpReturnType(node: SyntaxNode): string | undefined {
|
|||
'intersection_type',
|
||||
]);
|
||||
|
||||
let astType: string | undefined;
|
||||
let seenParams = false;
|
||||
for (let i = 0; i < node.childCount; i++) {
|
||||
const child = node.child(i);
|
||||
|
|
@ -73,14 +128,22 @@ function extractPhpReturnType(node: SyntaxNode): string | undefined {
|
|||
}
|
||||
// After the parameters node, look for the colon and then the type
|
||||
if (seenParams && child.isNamed && TYPE_NODE_TYPES.has(child.type)) {
|
||||
return child.text?.trim();
|
||||
astType = child.text?.trim();
|
||||
break;
|
||||
}
|
||||
// Stop at body or semicolon
|
||||
if (child.type === 'compound_statement' || (!child.isNamed && child.text === ';')) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
|
||||
// If AST type is missing or uninformative, try PHPDoc @return fallback
|
||||
if (!astType || astType === 'array' || astType === 'iterable') {
|
||||
const docType = extractPhpDocReturnType(node);
|
||||
if (docType) return docType;
|
||||
}
|
||||
|
||||
return astType;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -208,7 +271,7 @@ export const phpMethodConfig: MethodExtractionConfig = {
|
|||
'trait_declaration',
|
||||
'enum_declaration',
|
||||
],
|
||||
methodNodeTypes: ['method_declaration'],
|
||||
methodNodeTypes: ['method_declaration', 'function_definition'],
|
||||
bodyNodeTypes: ['declaration_list'],
|
||||
|
||||
extractName(node) {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,50 @@ import type { SyntaxNode } from '../../utils/ast-helpers.js';
|
|||
|
||||
const VISIBILITY_MODIFIERS = new Set(['private', 'protected', 'public']);
|
||||
|
||||
/** Regex to extract YARD `@return [Type]` annotations from comments. */
|
||||
const YARD_RETURN_RE = /@return\s+\[([^\]]+)\]/;
|
||||
|
||||
/**
|
||||
* Extract the simple type name from a YARD type string.
|
||||
* Handles qualified types ("Models::User" -> "User"), generics ("Array<User>"
|
||||
* -> "Array"), nullable ("String, nil" -> "String"), and rejects ambiguous
|
||||
* unions ("String, Integer" -> undefined).
|
||||
*/
|
||||
function extractYardTypeName(yardType: string): string | undefined {
|
||||
const trimmed = yardType.trim();
|
||||
|
||||
// Bracket-balanced split on commas to handle generics like Hash<Symbol, User>
|
||||
const parts: string[] = [];
|
||||
let depth = 0,
|
||||
start = 0;
|
||||
for (let i = 0; i < trimmed.length; i++) {
|
||||
if (trimmed[i] === '<') depth++;
|
||||
else if (trimmed[i] === '>') depth--;
|
||||
else if (trimmed[i] === ',' && depth === 0) {
|
||||
parts.push(trimmed.slice(start, i).trim());
|
||||
start = i + 1;
|
||||
}
|
||||
}
|
||||
parts.push(trimmed.slice(start).trim());
|
||||
const filtered = parts.filter((p) => p !== '' && p !== 'nil');
|
||||
if (filtered.length !== 1) return undefined; // ambiguous union
|
||||
|
||||
const typePart = filtered[0];
|
||||
|
||||
// Qualified: "Models::User" -> "User"
|
||||
const segments = typePart.split('::');
|
||||
const last = segments[segments.length - 1];
|
||||
|
||||
// Generic: "Array<User>" -> "Array"
|
||||
const genericMatch = last.match(/^(\w+)\s*[<{(]/);
|
||||
if (genericMatch) return genericMatch[1];
|
||||
|
||||
// Simple identifier
|
||||
if (/^\w+$/.test(last)) return last;
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract visibility for a Ruby method by walking backwards through the
|
||||
* parent body_statement's named children from the method node's position.
|
||||
|
|
@ -166,8 +210,30 @@ export const rubyMethodConfig: MethodExtractionConfig = {
|
|||
return nameNode?.text;
|
||||
},
|
||||
|
||||
extractReturnType(_node) {
|
||||
// Ruby has no type annotations — return type is always null
|
||||
extractReturnType(node) {
|
||||
// Walk backwards through preceding siblings looking for YARD @return [Type].
|
||||
// Try direct siblings first, then fall back to parent (body_statement) siblings
|
||||
// for class methods where the comment may be a sibling of the body_statement.
|
||||
const search = (startNode: SyntaxNode): string | undefined => {
|
||||
let sibling = startNode.previousSibling;
|
||||
while (sibling) {
|
||||
if (sibling.type === 'comment') {
|
||||
const match = YARD_RETURN_RE.exec(sibling.text);
|
||||
if (match) return extractYardTypeName(match[1]);
|
||||
} else if (sibling.isNamed) {
|
||||
break;
|
||||
}
|
||||
sibling = sibling.previousSibling;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const result = search(node);
|
||||
if (result) return result;
|
||||
|
||||
if (node.parent?.type === 'body_statement') {
|
||||
return search(node.parent);
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -125,8 +125,46 @@ function extractTsJsParameters(node: SyntaxNode): ParameterInfo[] {
|
|||
return params;
|
||||
}
|
||||
|
||||
/** Regex to extract @returns or @return from JSDoc comments: `@returns {Type}` */
|
||||
const JSDOC_RETURN_RE = /@returns?\s*\{([^}]+)\}/;
|
||||
|
||||
/**
|
||||
* Minimal sanitization for JSDoc return types — preserves generic wrappers
|
||||
* (e.g. `Promise<User>`) so that extractReturnTypeName in call-processor
|
||||
* can apply WRAPPER_GENERICS unwrapping. Only strips JSDoc-specific syntax markers.
|
||||
*/
|
||||
function sanitizeJsDocReturnType(raw: string): string | undefined {
|
||||
let type = raw.trim();
|
||||
// Strip JSDoc nullable/non-nullable prefixes: ?User → User, !User → User
|
||||
if (type.startsWith('?') || type.startsWith('!')) type = type.slice(1);
|
||||
// Strip module: prefix — module:models.User → models.User
|
||||
if (type.startsWith('module:')) type = type.slice(7);
|
||||
// Reject unions (ambiguous)
|
||||
if (type.includes('|')) return undefined;
|
||||
if (!type) return undefined;
|
||||
return type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Walk backwards through preceding siblings looking for a JSDoc comment containing
|
||||
* `@returns {Type}` or `@return {Type}`. Stops at the first non-comment named node
|
||||
* (excluding decorators, which precede methods in TS/JS).
|
||||
*/
|
||||
function extractJsDocReturnType(node: SyntaxNode): string | undefined {
|
||||
let sibling = node.previousSibling;
|
||||
while (sibling) {
|
||||
if (sibling.type === 'comment') {
|
||||
const match = JSDOC_RETURN_RE.exec(sibling.text);
|
||||
if (match) return sanitizeJsDocReturnType(match[1]);
|
||||
} else if (sibling.isNamed && sibling.type !== 'decorator') break;
|
||||
sibling = sibling.previousSibling;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract return type from return_type field, unwrapping type_annotation.
|
||||
* Falls back to JSDoc `@returns {Type}` when the AST has no return type annotation.
|
||||
*
|
||||
* tree-sitter-typescript uses `return_type` as the field name (not `type` like JVM).
|
||||
* The return_type field points to a type_annotation node that must be unwrapped.
|
||||
|
|
@ -140,7 +178,8 @@ function extractTsJsReturnType(node: SyntaxNode): string | undefined {
|
|||
}
|
||||
return returnType.text?.trim();
|
||||
}
|
||||
return undefined;
|
||||
// AST has no return type annotation — try JSDoc fallback
|
||||
return extractJsDocReturnType(node);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -227,7 +266,14 @@ const shared: Omit<MethodExtractionConfig, 'language'> = {
|
|||
// are not discovered because class_expression is not in typeDeclarationNodes.
|
||||
// - declare module / declare global augmentations — methods inside ambient_module_declaration
|
||||
// wrappers are not surfaced because the top-level walker doesn't descend into them.
|
||||
methodNodeTypes: ['method_definition', 'method_signature', 'abstract_method_signature'],
|
||||
methodNodeTypes: [
|
||||
'method_definition',
|
||||
'method_signature',
|
||||
'abstract_method_signature',
|
||||
'function_declaration',
|
||||
'generator_function_declaration',
|
||||
'function_signature',
|
||||
],
|
||||
bodyNodeTypes: ['class_body', 'interface_body'],
|
||||
|
||||
extractName(node) {
|
||||
|
|
|
|||
|
|
@ -86,6 +86,8 @@ export function createMethodExtractor(config: MethodExtractionConfig): MethodExt
|
|||
if (!methodNodeSet.has(node.type)) return null;
|
||||
return buildMethod(node, node, context, config);
|
||||
},
|
||||
|
||||
...(config.extractFunctionName ? { extractFunctionName: config.extractFunctionName } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -48,6 +48,14 @@ export interface MethodExtractor {
|
|||
isTypeDeclaration(node: SyntaxNode): boolean;
|
||||
/** Extract method info from a standalone method node (e.g. Go top-level method_declaration). */
|
||||
extractFromNode?(node: SyntaxNode, context: MethodExtractorContext): MethodInfo | null;
|
||||
/** Extract function name + label from an AST node during parent-walk.
|
||||
* Languages with non-standard AST structures (e.g. C/C++ declarator
|
||||
* unwrapping, Swift init/deinit, Rust impl_item) provide this hook
|
||||
* to replace the generic name-field lookup.
|
||||
* Return null to fall through to the generic extractor. */
|
||||
extractFunctionName?(
|
||||
node: SyntaxNode,
|
||||
): { funcName: string | null; label: import('gitnexus-shared').NodeLabel } | null;
|
||||
}
|
||||
|
||||
export interface MethodExtractionConfig {
|
||||
|
|
@ -75,4 +83,9 @@ export interface MethodExtractionConfig {
|
|||
ownerNode: SyntaxNode,
|
||||
context: MethodExtractorContext,
|
||||
) => MethodInfo | null;
|
||||
/** Extract function name + label from an AST node during parent-walk.
|
||||
* Passed through to the MethodExtractor by createMethodExtractor. */
|
||||
extractFunctionName?: (
|
||||
node: SyntaxNode,
|
||||
) => { funcName: string | null; label: import('gitnexus-shared').NodeLabel } | null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
*
|
||||
* Walks the inheritance DAG (EXTENDS/IMPLEMENTS edges), collects methods from
|
||||
* each ancestor via HAS_METHOD edges, detects method-name collisions across
|
||||
* parents, and applies language-specific resolution rules to emit OVERRIDES edges.
|
||||
* parents, and applies language-specific resolution rules to emit METHOD_OVERRIDES edges.
|
||||
*
|
||||
* Language-specific rules:
|
||||
* - C++: leftmost base class in declaration order wins
|
||||
|
|
@ -13,10 +13,10 @@
|
|||
* - Rust: no auto-resolution — requires qualified syntax, resolvedTo = null
|
||||
* - Default: single inheritance — first definition wins
|
||||
*
|
||||
* OVERRIDES edge direction: Class → Method (not Method → Method).
|
||||
* METHOD_OVERRIDES edge direction: Class → Method (not Method → Method).
|
||||
* The source is the child class that inherits conflicting methods,
|
||||
* the target is the winning ancestor method node.
|
||||
* Cypher: MATCH (c:Class)-[r:CodeRelation {type: 'OVERRIDES'}]->(m:Method)
|
||||
* Cypher: MATCH (c:Class)-[r:CodeRelation {type: 'METHOD_OVERRIDES'}]->(m:Method)
|
||||
*/
|
||||
|
||||
import { KnowledgeGraph } from '../graph/types.js';
|
||||
|
|
@ -47,6 +47,7 @@ export interface MROResult {
|
|||
entries: MROEntry[];
|
||||
overrideEdges: number;
|
||||
ambiguityCount: number;
|
||||
methodImplementsEdges: number;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -289,6 +290,10 @@ export function computeMRO(graph: KnowledgeGraph): MROResult {
|
|||
let overrideEdges = 0;
|
||||
let ambiguityCount = 0;
|
||||
|
||||
// Pre-computed maps to avoid redundant BFS in emitMethodImplementsEdges
|
||||
const ancestorsMap = new Map<string, string[]>();
|
||||
const edgeTypesMap = new Map<string, Map<string, 'EXTENDS' | 'IMPLEMENTS'>>();
|
||||
|
||||
// Process every class that has at least one parent
|
||||
for (const [classId, directParents] of parentMap) {
|
||||
if (directParents.length === 0) continue;
|
||||
|
|
@ -302,12 +307,16 @@ export function computeMRO(graph: KnowledgeGraph): MROResult {
|
|||
|
||||
// Compute linearized MRO depending on language strategy
|
||||
const provider = getProvider(language);
|
||||
const ancestors = gatherAncestors(classId, parentMap);
|
||||
ancestorsMap.set(classId, ancestors);
|
||||
edgeTypesMap.set(classId, buildTransitiveEdgeTypes(classId, parentMap, parentEdgeType));
|
||||
|
||||
let mroOrder: string[];
|
||||
if (provider.mroStrategy === 'c3') {
|
||||
const c3Result = c3Linearize(classId, parentMap, c3Cache);
|
||||
mroOrder = c3Result ?? gatherAncestors(classId, parentMap);
|
||||
mroOrder = c3Result ?? ancestors;
|
||||
} else {
|
||||
mroOrder = gatherAncestors(classId, parentMap);
|
||||
mroOrder = ancestors;
|
||||
}
|
||||
|
||||
// Get the parent names for the MRO entry
|
||||
|
|
@ -348,11 +357,9 @@ export function computeMRO(graph: KnowledgeGraph): MROResult {
|
|||
// Detect collisions: methods defined in 2+ different ancestors
|
||||
const ambiguities: MethodAmbiguity[] = [];
|
||||
|
||||
// Compute transitive edge types once per class (only needed for implements-split languages)
|
||||
// Use pre-computed transitive edge types (only needed for implements-split languages)
|
||||
const needsEdgeTypes = provider.mroStrategy === 'implements-split';
|
||||
const classEdgeTypes = needsEdgeTypes
|
||||
? buildTransitiveEdgeTypes(classId, parentMap, parentEdgeType)
|
||||
: undefined;
|
||||
const classEdgeTypes = needsEdgeTypes ? edgeTypesMap.get(classId) : undefined;
|
||||
|
||||
for (const [methodName, defs] of methodsByName) {
|
||||
if (defs.length < 2) continue;
|
||||
|
|
@ -401,13 +408,13 @@ export function computeMRO(graph: KnowledgeGraph): MROResult {
|
|||
ambiguityCount++;
|
||||
}
|
||||
|
||||
// Emit OVERRIDES edge if resolution found
|
||||
// Emit METHOD_OVERRIDES edge if resolution found
|
||||
if (resolution.resolvedTo !== null) {
|
||||
graph.addRelationship({
|
||||
id: generateId('OVERRIDES', `${classId}->${resolution.resolvedTo}`),
|
||||
id: generateId('METHOD_OVERRIDES', `${classId}->${resolution.resolvedTo}`),
|
||||
sourceId: classId,
|
||||
targetId: resolution.resolvedTo,
|
||||
type: 'OVERRIDES',
|
||||
type: 'METHOD_OVERRIDES',
|
||||
confidence: resolution.confidence,
|
||||
reason: resolution.reason,
|
||||
});
|
||||
|
|
@ -424,7 +431,389 @@ export function computeMRO(graph: KnowledgeGraph): MROResult {
|
|||
});
|
||||
}
|
||||
|
||||
return { entries, overrideEdges, ambiguityCount };
|
||||
const methodImplementsEdges = emitMethodImplementsEdges(
|
||||
graph,
|
||||
parentMap,
|
||||
methodMap,
|
||||
parentEdgeType,
|
||||
ancestorsMap,
|
||||
edgeTypesMap,
|
||||
);
|
||||
|
||||
return { entries, overrideEdges, ambiguityCount, methodImplementsEdges };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// METHOD_IMPLEMENTS edge emission
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Check if two parameter type arrays match.
|
||||
* When either side has no type info, fall back to parameterCount comparison
|
||||
* (arity-compatible matching). If both have parameterCount and they differ,
|
||||
* return no match. If counts match, return confident match. If either count
|
||||
* is undefined, return lenient (non-confident) match.
|
||||
*
|
||||
* Returns `{ match, confident }`:
|
||||
* - Exact type match → `{ match: true, confident: true }`
|
||||
* - Arity match (both have parameterCount, counts equal) → `{ match: true, confident: true }`
|
||||
* - Lenient (either side lacks types AND lacks parameterCount) → `{ match: true, confident: false }`
|
||||
* - No match → `{ match: false, confident: false }`
|
||||
*/
|
||||
function parameterTypesMatch(
|
||||
a: string[],
|
||||
b: string[],
|
||||
aParamCount?: number,
|
||||
bParamCount?: number,
|
||||
): { match: boolean; confident: boolean } {
|
||||
// If one side is variadic and the other isn't, types may match superficially
|
||||
// but the methods aren't guaranteed to be interchangeable
|
||||
if ((aParamCount === undefined) !== (bParamCount === undefined)) {
|
||||
return { match: true, confident: false };
|
||||
}
|
||||
|
||||
if (a.length === 0 || b.length === 0) {
|
||||
// Fall back to arity check when type info is missing
|
||||
if (aParamCount !== undefined && bParamCount !== undefined) {
|
||||
return { match: aParamCount === bParamCount, confident: aParamCount === bParamCount };
|
||||
}
|
||||
return { match: true, confident: false }; // lenient when either count is unknown
|
||||
}
|
||||
if (a.length !== b.length) return { match: false, confident: false };
|
||||
const exact = a.every((t, i) => t === b[i]);
|
||||
return { match: exact, confident: exact };
|
||||
}
|
||||
|
||||
/**
|
||||
* For each concrete class that implements/extends an interface or trait,
|
||||
* find methods in the class that implement methods defined in the interface
|
||||
* and emit METHOD_IMPLEMENTS edges: ConcreteMethod → InterfaceMethod.
|
||||
*
|
||||
* Method node IDs include a `#<paramCount>` arity suffix, so overloaded
|
||||
* methods with different parameter counts are distinct nodes in the graph.
|
||||
*
|
||||
* **Remaining limitation — same-arity overloads:** When two overloads share
|
||||
* the same parameter count but differ only in types (e.g. `save(int)` vs
|
||||
* `save(String)`), they still collapse to one node ID. This is rare in
|
||||
* practice; a future enhancement may add type-hash disambiguation for
|
||||
* languages with reliable type extraction (see issue #574).
|
||||
*/
|
||||
function emitMethodImplementsEdges(
|
||||
graph: KnowledgeGraph,
|
||||
parentMap: Map<string, string[]>,
|
||||
methodMap: Map<string, string[]>,
|
||||
parentEdgeType: Map<string, Map<string, 'EXTENDS' | 'IMPLEMENTS'>>,
|
||||
ancestorsMap: Map<string, string[]>,
|
||||
edgeTypesMap: Map<string, Map<string, 'EXTENDS' | 'IMPLEMENTS'>>,
|
||||
): number {
|
||||
let edgeCount = 0;
|
||||
|
||||
for (const [classId, parentIds] of parentMap) {
|
||||
const classNode = graph.getNode(classId);
|
||||
if (!classNode) continue;
|
||||
|
||||
// Interfaces and traits declare contracts — they don't implement them
|
||||
if (classNode.label === 'Interface' || classNode.label === 'Trait') continue;
|
||||
|
||||
// Get this class's own methods
|
||||
const ownMethodIds = methodMap.get(classId) ?? [];
|
||||
|
||||
// Build a lookup: methodName → Array<{methodId, parameterTypes, parameterCount}> for own methods
|
||||
const ownMethodsByName = new Map<
|
||||
string,
|
||||
Array<{ methodId: string; parameterTypes: string[]; parameterCount?: number }>
|
||||
>();
|
||||
for (const methodId of ownMethodIds) {
|
||||
const methodNode = graph.getNode(methodId);
|
||||
if (!methodNode || methodNode.label === 'Property') continue;
|
||||
// Abstract methods don't satisfy interface contracts
|
||||
if (methodNode.properties.isAbstract === true) continue;
|
||||
const name = methodNode.properties.name as string;
|
||||
const parameterTypes = (methodNode.properties.parameterTypes as string[] | undefined) ?? [];
|
||||
const parameterCount = methodNode.properties.parameterCount as number | undefined;
|
||||
let bucket = ownMethodsByName.get(name);
|
||||
if (!bucket) {
|
||||
bucket = [];
|
||||
ownMethodsByName.set(name, bucket);
|
||||
}
|
||||
bucket.push({ methodId, parameterTypes, parameterCount });
|
||||
}
|
||||
|
||||
// Use pre-computed ancestors and edge types; fall back to computing if missing (safety)
|
||||
const allAncestors = ancestorsMap.get(classId) ?? gatherAncestors(classId, parentMap);
|
||||
const ancestorEdgeTypes =
|
||||
edgeTypesMap.get(classId) ?? buildTransitiveEdgeTypes(classId, parentMap, parentEdgeType);
|
||||
|
||||
// Dedup set: avoid duplicate edges from diamond paths
|
||||
const emitted = new Set<string>();
|
||||
|
||||
// For each ancestor, check if it's an interface/trait or classified as IMPLEMENTS
|
||||
for (const ancestorId of allAncestors) {
|
||||
const ancestorNode = graph.getNode(ancestorId);
|
||||
if (!ancestorNode) continue;
|
||||
|
||||
const isInterfaceLike = ancestorNode.label === 'Interface' || ancestorNode.label === 'Trait';
|
||||
const classifiedEdgeType = ancestorEdgeTypes.get(ancestorId);
|
||||
if (!isInterfaceLike && classifiedEdgeType !== 'IMPLEMENTS') continue;
|
||||
|
||||
// Get ancestor's methods
|
||||
const ancestorMethodIds = methodMap.get(ancestorId) ?? [];
|
||||
|
||||
for (const ancestorMethodId of ancestorMethodIds) {
|
||||
const ancestorMethodNode = graph.getNode(ancestorMethodId);
|
||||
if (!ancestorMethodNode || ancestorMethodNode.label === 'Property') continue;
|
||||
|
||||
const ancestorName = ancestorMethodNode.properties.name as string;
|
||||
const ancestorParamTypes =
|
||||
(ancestorMethodNode.properties.parameterTypes as string[] | undefined) ?? [];
|
||||
const ancestorParamCount = ancestorMethodNode.properties.parameterCount as
|
||||
| number
|
||||
| undefined;
|
||||
|
||||
// Find matching method in own class by name + parameterTypes/arity
|
||||
const candidates = ownMethodsByName.get(ancestorName);
|
||||
|
||||
// Unit 3: If no own method matches, walk the EXTENDS chain to find inherited concrete method
|
||||
if (!candidates || candidates.length === 0) {
|
||||
const inherited = findInheritedMethod(
|
||||
classId,
|
||||
ancestorName,
|
||||
ancestorParamTypes,
|
||||
ancestorParamCount,
|
||||
graph,
|
||||
parentMap,
|
||||
methodMap,
|
||||
parentEdgeType,
|
||||
ancestorMethodId,
|
||||
);
|
||||
if (inherited) {
|
||||
const edgeKey = `${inherited.methodId}->${ancestorMethodId}`;
|
||||
if (!emitted.has(edgeKey)) {
|
||||
emitted.add(edgeKey);
|
||||
graph.addRelationship({
|
||||
id: generateId('METHOD_IMPLEMENTS', edgeKey),
|
||||
sourceId: inherited.methodId,
|
||||
targetId: ancestorMethodId,
|
||||
type: 'METHOD_IMPLEMENTS',
|
||||
confidence: inherited.confident ? 1.0 : 0.7,
|
||||
reason: '',
|
||||
});
|
||||
edgeCount++;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Unit 4: Filter candidates by type/arity match, then check for ambiguity
|
||||
const matching: Array<{
|
||||
methodId: string;
|
||||
parameterTypes: string[];
|
||||
parameterCount?: number;
|
||||
confident: boolean;
|
||||
}> = [];
|
||||
for (const c of candidates) {
|
||||
const result = parameterTypesMatch(
|
||||
c.parameterTypes,
|
||||
ancestorParamTypes,
|
||||
c.parameterCount,
|
||||
ancestorParamCount,
|
||||
);
|
||||
if (result.match) {
|
||||
matching.push({ ...c, confident: result.confident });
|
||||
}
|
||||
}
|
||||
|
||||
if (matching.length === 0) continue;
|
||||
|
||||
// If multiple candidates match at name+arity level, emit no edge (ambiguous)
|
||||
if (matching.length > 1) continue;
|
||||
|
||||
const winner = matching[0];
|
||||
const edgeKey = `${winner.methodId}->${ancestorMethodId}`;
|
||||
if (emitted.has(edgeKey)) continue;
|
||||
emitted.add(edgeKey);
|
||||
|
||||
graph.addRelationship({
|
||||
id: generateId('METHOD_IMPLEMENTS', edgeKey),
|
||||
sourceId: winner.methodId,
|
||||
targetId: ancestorMethodId,
|
||||
type: 'METHOD_IMPLEMENTS',
|
||||
confidence: winner.confident ? 1.0 : 0.7,
|
||||
reason: '',
|
||||
});
|
||||
edgeCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return edgeCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Walk the class's EXTENDS chain to find the nearest concrete method matching
|
||||
* the given name and parameter signature. If the EXTENDS chain yields no match,
|
||||
* fall back to IMPLEMENTS parents and check for non-abstract default methods
|
||||
* (e.g. Java default interface methods, Kotlin interface defaults).
|
||||
* Returns the first matching method found in BFS order, or null.
|
||||
*/
|
||||
function findInheritedMethod(
|
||||
classId: string,
|
||||
methodName: string,
|
||||
targetParamTypes: string[],
|
||||
targetParamCount: number | undefined,
|
||||
graph: KnowledgeGraph,
|
||||
parentMap: Map<string, string[]>,
|
||||
methodMap: Map<string, string[]>,
|
||||
parentEdgeType: Map<string, Map<string, 'EXTENDS' | 'IMPLEMENTS'>>,
|
||||
/** Method ID to exclude from results (prevents self-edges when the ancestor
|
||||
* method being matched lives on an IMPLEMENTS parent). */
|
||||
excludeMethodId?: string,
|
||||
): { methodId: string; parameterTypes: string[]; confident: boolean } | null {
|
||||
const visited = new Set<string>();
|
||||
const queue: string[] = [];
|
||||
|
||||
// Seed with direct EXTENDS parents only
|
||||
const directParents = parentMap.get(classId) ?? [];
|
||||
const directEdges = parentEdgeType.get(classId);
|
||||
for (const pid of directParents) {
|
||||
const et = directEdges?.get(pid);
|
||||
if (et === 'EXTENDS') {
|
||||
// Also check that the parent is not an Interface/Trait
|
||||
const parentNode = graph.getNode(pid);
|
||||
if (parentNode && parentNode.label !== 'Interface' && parentNode.label !== 'Trait') {
|
||||
queue.push(pid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Level-order BFS: process all ancestors at the current depth before
|
||||
// advancing. Once any match is found at depth D, finish that depth and stop.
|
||||
// Diamond dedup: same methodId via two paths at the same depth = 1 match.
|
||||
let currentLevel = [...queue];
|
||||
|
||||
while (currentLevel.length > 0) {
|
||||
const matches = new Map<
|
||||
string,
|
||||
{ methodId: string; parameterTypes: string[]; confident: boolean }
|
||||
>();
|
||||
const nextLevel: string[] = [];
|
||||
|
||||
for (const ancestorId of currentLevel) {
|
||||
if (visited.has(ancestorId)) continue;
|
||||
visited.add(ancestorId);
|
||||
|
||||
// Check this ancestor's methods
|
||||
const methods = methodMap.get(ancestorId) ?? [];
|
||||
for (const mid of methods) {
|
||||
const mNode = graph.getNode(mid);
|
||||
if (!mNode || mNode.label === 'Property') continue;
|
||||
// Abstract inherited methods don't count as concrete implementations
|
||||
if (mNode.properties.isAbstract === true) continue;
|
||||
if (mNode.properties.name !== methodName) continue;
|
||||
|
||||
const mParamTypes = (mNode.properties.parameterTypes as string[] | undefined) ?? [];
|
||||
const mParamCount = mNode.properties.parameterCount as number | undefined;
|
||||
const ptResult = parameterTypesMatch(
|
||||
mParamTypes,
|
||||
targetParamTypes,
|
||||
mParamCount,
|
||||
targetParamCount,
|
||||
);
|
||||
if (ptResult.match) {
|
||||
matches.set(mid, {
|
||||
methodId: mid,
|
||||
parameterTypes: mParamTypes,
|
||||
confident: ptResult.confident,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Collect EXTENDS parents for the next depth level
|
||||
const grandparents = parentMap.get(ancestorId) ?? [];
|
||||
const ancestorEdges = parentEdgeType.get(ancestorId);
|
||||
for (const gp of grandparents) {
|
||||
if (visited.has(gp)) continue;
|
||||
const gpEdge = ancestorEdges?.get(gp);
|
||||
if (gpEdge === 'EXTENDS') {
|
||||
const gpNode = graph.getNode(gp);
|
||||
if (gpNode && gpNode.label !== 'Interface' && gpNode.label !== 'Trait') {
|
||||
nextLevel.push(gp);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If any matches found at this depth, decide and stop
|
||||
if (matches.size === 1) return matches.values().next().value!;
|
||||
if (matches.size > 1) return null; // ambiguous at same depth
|
||||
|
||||
currentLevel = nextLevel;
|
||||
}
|
||||
|
||||
// ── Second pass: walk IMPLEMENTS parents AND their interface ancestry ──
|
||||
// Only reached when the EXTENDS chain yielded no match.
|
||||
// BFS through interface/trait hierarchy to find default (non-abstract) methods.
|
||||
const implBfsQueue: string[] = [];
|
||||
for (const pid of directParents) {
|
||||
const et = directEdges?.get(pid);
|
||||
if (et === 'IMPLEMENTS') {
|
||||
implBfsQueue.push(pid);
|
||||
}
|
||||
}
|
||||
|
||||
// Collect all matches from the IMPLEMENTS BFS — return null if ambiguous (>1 match)
|
||||
const implMatches: Array<{
|
||||
methodId: string;
|
||||
parameterTypes: string[];
|
||||
confident: boolean;
|
||||
}> = [];
|
||||
const implVisited = new Set<string>();
|
||||
while (implBfsQueue.length > 0) {
|
||||
const ifaceId = implBfsQueue.shift()!;
|
||||
if (implVisited.has(ifaceId)) continue;
|
||||
implVisited.add(ifaceId);
|
||||
|
||||
// Only process Interface/Trait nodes — Dart `implements Class` does not
|
||||
// inherit method bodies, so Class/Struct/Enum parents must be skipped.
|
||||
const ifaceNode = graph.getNode(ifaceId);
|
||||
if (!ifaceNode || (ifaceNode.label !== 'Interface' && ifaceNode.label !== 'Trait')) continue;
|
||||
|
||||
// Check this interface/trait's methods for a non-abstract default
|
||||
const methods = methodMap.get(ifaceId) ?? [];
|
||||
for (const mid of methods) {
|
||||
if (mid === excludeMethodId) continue; // prevent self-edges
|
||||
const mNode = graph.getNode(mid);
|
||||
if (!mNode || mNode.label === 'Property') continue;
|
||||
if (mNode.properties.isAbstract === true) continue;
|
||||
if (mNode.properties.name !== methodName) continue;
|
||||
|
||||
const mParamTypes = (mNode.properties.parameterTypes as string[] | undefined) ?? [];
|
||||
const mParamCount = mNode.properties.parameterCount as number | undefined;
|
||||
const ptResult = parameterTypesMatch(
|
||||
mParamTypes,
|
||||
targetParamTypes,
|
||||
mParamCount,
|
||||
targetParamCount,
|
||||
);
|
||||
if (ptResult.match) {
|
||||
implMatches.push({
|
||||
methodId: mid,
|
||||
parameterTypes: mParamTypes,
|
||||
confident: ptResult.confident,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Walk this interface's parents (interface-extends-interface chains)
|
||||
const ifaceParents = parentMap.get(ifaceId) ?? [];
|
||||
for (const gp of ifaceParents) {
|
||||
if (!implVisited.has(gp)) implBfsQueue.push(gp);
|
||||
}
|
||||
}
|
||||
|
||||
// Ambiguous: multiple interfaces provide the same default method
|
||||
if (implMatches.length === 1) return implMatches[0];
|
||||
return null; // 0 matches or ambiguous (>1)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import { yieldToEventLoop } from './utils/event-loop.js';
|
|||
import {
|
||||
getDefinitionNodeFromCaptures,
|
||||
findEnclosingClassInfo,
|
||||
extractMethodSignature,
|
||||
getLabelFromCaptures,
|
||||
CLASS_CONTAINER_TYPES,
|
||||
type SyntaxNode,
|
||||
|
|
@ -22,6 +21,7 @@ import { detectFrameworkFromAST } from './framework-detection.js';
|
|||
import { buildTypeEnv } from './type-env.js';
|
||||
import type { FieldInfo, FieldExtractorContext } from './field-types.js';
|
||||
import type { MethodInfo } from './method-types.js';
|
||||
import { buildMethodProps, arityForIdFromInfo } from './utils/method-props.js';
|
||||
import type { LanguageProvider } from './language-provider.js';
|
||||
import { WorkerPool } from './workers/worker-pool.js';
|
||||
import type {
|
||||
|
|
@ -232,35 +232,6 @@ function seqFindEnclosingClassNode(node: SyntaxNode): SyntaxNode | null {
|
|||
return null;
|
||||
}
|
||||
|
||||
/** Convert MethodInfo from methodExtractor into flat properties for a graph node. */
|
||||
function buildMethodProps(info: MethodInfo): Record<string, unknown> {
|
||||
const types: string[] = [];
|
||||
let optionalCount = 0;
|
||||
let hasVariadic = false;
|
||||
for (const p of info.parameters) {
|
||||
if (p.type !== null) types.push(p.type);
|
||||
if (p.isOptional) optionalCount++;
|
||||
if (p.isVariadic) hasVariadic = true;
|
||||
}
|
||||
return {
|
||||
parameterCount: hasVariadic ? undefined : info.parameters.length,
|
||||
...(!hasVariadic && optionalCount > 0
|
||||
? { requiredParameterCount: info.parameters.length - optionalCount }
|
||||
: {}),
|
||||
...(types.length > 0 ? { parameterTypes: types } : {}),
|
||||
returnType: info.returnType ?? undefined,
|
||||
visibility: info.visibility,
|
||||
isStatic: info.isStatic,
|
||||
isAbstract: info.isAbstract,
|
||||
isFinal: info.isFinal,
|
||||
...(info.isVirtual ? { isVirtual: info.isVirtual } : {}),
|
||||
...(info.isOverride ? { isOverride: info.isOverride } : {}),
|
||||
...(info.isAsync ? { isAsync: info.isAsync } : {}),
|
||||
...(info.isPartial ? { isPartial: info.isPartial } : {}),
|
||||
...(info.annotations.length > 0 ? { annotations: info.annotations } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
/** Minimal no-op SymbolTable stub for FieldExtractorContext (sequential path has a real
|
||||
* SymbolTable, but it's incomplete at this stage — use the stub for safety). */
|
||||
const NOOP_SYMBOL_TABLE_SEQ = {
|
||||
|
|
@ -372,7 +343,10 @@ const processParsingSequential = async (
|
|||
|
||||
// Build per-file type environment for FieldExtractor context (lightweight — skipped if no fieldExtractor)
|
||||
const typeEnv = provider.fieldExtractor
|
||||
? buildTypeEnv(tree, language, { enclosingFunctionFinder: provider.enclosingFunctionFinder })
|
||||
? buildTypeEnv(tree, language, {
|
||||
enclosingFunctionFinder: provider.enclosingFunctionFinder,
|
||||
extractFunctionName: provider.methodExtractor?.extractFunctionName,
|
||||
})
|
||||
: null;
|
||||
|
||||
matches.forEach((match) => {
|
||||
|
|
@ -414,18 +388,15 @@ const processParsingSequential = async (
|
|||
const qualifiedName = enclosingClassInfo
|
||||
? `${enclosingClassInfo.className}.${nodeName}`
|
||||
: nodeName;
|
||||
const nodeId = generateId(nodeLabel, `${file.path}:${qualifiedName}`);
|
||||
const frameworkHint = definitionNode
|
||||
? detectFrameworkFromAST(language, (definitionNode.text || '').slice(0, 300))
|
||||
: null;
|
||||
|
||||
// Extract method metadata for Function/Method/Constructor nodes.
|
||||
// Try the per-language methodExtractor first (provides isAbstract, isStatic,
|
||||
// visibility, annotations, etc.). Fall back to extractMethodSignature for
|
||||
// basic parameterCount/parameterTypes/returnType when no methodExtractor exists.
|
||||
// Extract method metadata for Function/Method/Constructor nodes BEFORE generating
|
||||
// the node ID — parameterCount is needed to disambiguate overloaded methods.
|
||||
// Use the per-language MethodExtractor for method metadata (isAbstract, isStatic,
|
||||
// visibility, annotations, parameterCount, parameterTypes, returnType, etc.).
|
||||
const isMethodLike =
|
||||
nodeLabel === 'Function' || nodeLabel === 'Method' || nodeLabel === 'Constructor';
|
||||
let methodProps: Record<string, unknown> = {};
|
||||
let arityForId: number | undefined; // raw param count for ID, even for variadic
|
||||
if (isMethodLike && definitionNode) {
|
||||
let enriched = false;
|
||||
|
||||
|
|
@ -452,6 +423,7 @@ const processParsingSequential = async (
|
|||
const info = result.methods.find((m) => m.name === nodeName && m.line === defLine);
|
||||
if (info) {
|
||||
enriched = true;
|
||||
arityForId = arityForIdFromInfo(info);
|
||||
methodProps = buildMethodProps(info);
|
||||
}
|
||||
}
|
||||
|
|
@ -465,36 +437,22 @@ const processParsingSequential = async (
|
|||
});
|
||||
if (info) {
|
||||
enriched = true;
|
||||
arityForId = arityForIdFromInfo(info);
|
||||
methodProps = buildMethodProps(info);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to generic extractMethodSignature
|
||||
if (!enriched) {
|
||||
const sig = extractMethodSignature(definitionNode);
|
||||
methodProps = {
|
||||
parameterCount: sig.parameterCount,
|
||||
...(sig.requiredParameterCount !== undefined
|
||||
? { requiredParameterCount: sig.requiredParameterCount }
|
||||
: {}),
|
||||
...(sig.parameterTypes ? { parameterTypes: sig.parameterTypes } : {}),
|
||||
returnType: sig.returnType,
|
||||
};
|
||||
}
|
||||
|
||||
// Language-specific return type fallback (e.g. Ruby YARD @return [Type])
|
||||
// Also upgrades uninformative AST types like PHP `array` with PHPDoc `@return User[]`
|
||||
const rt = methodProps.returnType as string | undefined;
|
||||
if (!rt || rt === 'array' || rt === 'iterable') {
|
||||
const tc = provider.typeConfig;
|
||||
if (tc?.extractReturnType) {
|
||||
const docReturn = tc.extractReturnType(definitionNode);
|
||||
if (docReturn) methodProps.returnType = docReturn;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Append #<paramCount> to Method/Constructor IDs to disambiguate overloads.
|
||||
// Functions are not suffixed — they don't overload by name in the same scope.
|
||||
const needsAritySuffix = nodeLabel === 'Method' || nodeLabel === 'Constructor';
|
||||
const arityTag = needsAritySuffix && arityForId !== undefined ? `#${arityForId}` : '';
|
||||
const nodeId = generateId(nodeLabel, `${file.path}:${qualifiedName}${arityTag}`);
|
||||
const frameworkHint = definitionNode
|
||||
? detectFrameworkFromAST(language, (definitionNode.text || '').slice(0, 300))
|
||||
: null;
|
||||
|
||||
const node: GraphNode = {
|
||||
id: nodeId,
|
||||
label: nodeLabel as NodeLabel,
|
||||
|
|
|
|||
|
|
@ -1103,7 +1103,7 @@ async function runChunkedParseAndResolve(
|
|||
* Post-parse graph analysis: MRO, community detection, process extraction.
|
||||
*
|
||||
* @reads graph (all nodes and relationships from parse + resolve phases)
|
||||
* @writes graph (Community nodes, Process nodes, MEMBER_OF edges, STEP_IN_PROCESS edges, OVERRIDES edges)
|
||||
* @writes graph (Community nodes, Process nodes, MEMBER_OF edges, STEP_IN_PROCESS edges, METHOD_OVERRIDES edges)
|
||||
*/
|
||||
async function runGraphAnalysisPhases(
|
||||
graph: ReturnType<typeof createKnowledgeGraph>,
|
||||
|
|
@ -1126,7 +1126,7 @@ async function runGraphAnalysisPhases(
|
|||
const mroResult = computeMRO(graph);
|
||||
if (isDev && mroResult.entries.length > 0) {
|
||||
console.log(
|
||||
`🔀 MRO: ${mroResult.entries.length} classes analyzed, ${mroResult.ambiguityCount} ambiguities found, ${mroResult.overrideEdges} OVERRIDES edges`,
|
||||
`🔀 MRO: ${mroResult.entries.length} classes analyzed, ${mroResult.ambiguityCount} ambiguities, ${mroResult.overrideEdges} METHOD_OVERRIDES, ${mroResult.methodImplementsEdges} METHOD_IMPLEMENTS`,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import {
|
||||
type SyntaxNode,
|
||||
FUNCTION_NODE_TYPES,
|
||||
extractFunctionName,
|
||||
CLASS_CONTAINER_TYPES,
|
||||
genericFuncName,
|
||||
} from './utils/ast-helpers.js';
|
||||
import { CALL_EXPRESSION_TYPES } from './utils/call-analysis.js';
|
||||
import { SupportedLanguages } from 'gitnexus-shared';
|
||||
|
|
@ -132,6 +132,7 @@ const lookupInEnv = (
|
|||
callNode: SyntaxNode,
|
||||
patternOverrides?: PatternOverrides,
|
||||
enclosingFunctionFinder?: (n: SyntaxNode) => { funcName: string; label: NodeLabel } | null,
|
||||
extractFunctionNameHook?: (n: SyntaxNode) => { funcName: string | null; label: NodeLabel } | null,
|
||||
): string | undefined => {
|
||||
// Self/this receiver: resolve to enclosing class name via AST walk
|
||||
if (varName === 'self' || varName === 'this' || varName === '$this') {
|
||||
|
|
@ -145,7 +146,11 @@ const lookupInEnv = (
|
|||
}
|
||||
|
||||
// Determine the enclosing function scope for the call
|
||||
const scopeKey = findEnclosingScopeKey(callNode, enclosingFunctionFinder);
|
||||
const scopeKey = findEnclosingScopeKey(
|
||||
callNode,
|
||||
enclosingFunctionFinder,
|
||||
extractFunctionNameHook,
|
||||
);
|
||||
|
||||
// Check position-indexed pattern overrides first (e.g., Kotlin when/is smart casts).
|
||||
// These take priority over flat scopeEnv because they represent per-branch narrowing.
|
||||
|
|
@ -361,11 +366,12 @@ const extractParentClassFromNode = (classNode: SyntaxNode): string | undefined =
|
|||
const findEnclosingScopeKey = (
|
||||
node: SyntaxNode,
|
||||
enclosingFunctionFinder?: (n: SyntaxNode) => { funcName: string; label: NodeLabel } | null,
|
||||
extractFunctionNameHook?: (n: SyntaxNode) => { funcName: string | null; label: NodeLabel } | null,
|
||||
): string | undefined => {
|
||||
let current = node.parent;
|
||||
while (current) {
|
||||
if (FUNCTION_NODE_TYPES.has(current.type)) {
|
||||
const { funcName } = extractFunctionName(current);
|
||||
const funcName = extractFunctionNameHook?.(current)?.funcName ?? genericFuncName(current);
|
||||
if (funcName) return `${funcName}@${current.startIndex}`;
|
||||
}
|
||||
// Language-specific hook (e.g., Dart function_body → sibling function_signature)
|
||||
|
|
@ -621,7 +627,15 @@ const resolveMethodReturnType = (
|
|||
parentMap?: ReadonlyMap<string, readonly string[]>,
|
||||
): string | undefined => {
|
||||
if (!symbolTable) return undefined;
|
||||
const receiverType = scopeEnv.get(receiver);
|
||||
let receiverType = scopeEnv.get(receiver);
|
||||
// When substituteThisReceiver replaced $this/self with the enclosing class name,
|
||||
// the receiver IS the type — look it up directly as a class name.
|
||||
if (!receiverType) {
|
||||
const lookup =
|
||||
getClassDefs ??
|
||||
((name: string) => symbolTable.lookupFuzzy(name).filter((d) => CLASS_LIKE_TYPES.has(d.type)));
|
||||
if (lookup(receiver).length > 0) receiverType = receiver;
|
||||
}
|
||||
if (!receiverType) return undefined;
|
||||
const lookup =
|
||||
getClassDefs ??
|
||||
|
|
@ -765,6 +779,11 @@ export interface BuildTypeEnvOptions {
|
|||
enclosingFunctionFinder?: (
|
||||
ancestorNode: SyntaxNode,
|
||||
) => { funcName: string; label: NodeLabel } | null;
|
||||
/** Language-specific function name extraction from an AST node.
|
||||
* Replaces the generic name-field lookup for languages with non-standard
|
||||
* AST structures (C/C++ declarator unwrapping, Swift init/deinit, etc.).
|
||||
* When null is returned or not provided, falls back to node.childForFieldName('name')?.text. */
|
||||
extractFunctionName?: (node: SyntaxNode) => { funcName: string | null; label: NodeLabel } | null;
|
||||
}
|
||||
|
||||
/** Seed cross-file type bindings into the file scope.
|
||||
|
|
@ -794,6 +813,7 @@ export const buildTypeEnv = (
|
|||
|
||||
const symbolTable = options?.symbolTable;
|
||||
const parentMap = options?.parentMap;
|
||||
const extractFuncNameHook = options?.extractFunctionName;
|
||||
const env: TypeEnv = new Map();
|
||||
const patternOverrides: PatternOverrides = new Map();
|
||||
// Phase P: maps `scope\0varName` → constructor type when a declaration has BOTH
|
||||
|
|
@ -1057,7 +1077,7 @@ export const buildTypeEnv = (
|
|||
// Detect scope boundaries (function/method definitions)
|
||||
let scope = currentScope;
|
||||
if (FUNCTION_NODE_TYPES.has(node.type)) {
|
||||
const { funcName } = extractFunctionName(node);
|
||||
const funcName = extractFuncNameHook?.(node)?.funcName ?? genericFuncName(node);
|
||||
if (funcName) scope = `${funcName}@${node.startIndex}`;
|
||||
}
|
||||
|
||||
|
|
@ -1206,7 +1226,14 @@ export const buildTypeEnv = (
|
|||
|
||||
return {
|
||||
lookup: (varName, callNode) =>
|
||||
lookupInEnv(env, varName, callNode, patternOverrides, options?.enclosingFunctionFinder),
|
||||
lookupInEnv(
|
||||
env,
|
||||
varName,
|
||||
callNode,
|
||||
patternOverrides,
|
||||
options?.enclosingFunctionFinder,
|
||||
extractFuncNameHook,
|
||||
),
|
||||
constructorBindings: bindings,
|
||||
fileScope: () => env.get(FILE_SCOPE) ?? EMPTY_FILE_SCOPE,
|
||||
allScopes: () => env as ReadonlyMap<string, ReadonlyMap<string, string>>,
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import type {
|
|||
InitializerExtractor,
|
||||
ClassNameLookup,
|
||||
ConstructorBindingScanner,
|
||||
ReturnTypeExtractor,
|
||||
PendingAssignmentExtractor,
|
||||
ForLoopExtractor,
|
||||
} from './types.js';
|
||||
|
|
@ -337,60 +336,6 @@ const scanConstructorBinding: ConstructorBindingScanner = (node) => {
|
|||
return undefined;
|
||||
};
|
||||
|
||||
/** Regex to extract PHPDoc @return annotations: `@return User` */
|
||||
const PHPDOC_RETURN_RE = /@return\s+(\S+)/;
|
||||
|
||||
/**
|
||||
* Normalize a PHPDoc return type for storage in the SymbolTable.
|
||||
* Unlike normalizePhpType (which strips User[] → User for scopeEnv), this preserves
|
||||
* array notation so lookupRawReturnType can extract element types for for-loop resolution.
|
||||
* \App\Models\User[] → User[]
|
||||
* ?User → User
|
||||
* Collection<User> → Collection<User> (preserved for extractElementTypeFromString)
|
||||
*/
|
||||
const normalizePhpReturnType = (raw: string): string | undefined => {
|
||||
// Strip nullable prefix: ?User[] → User[]
|
||||
let type = raw.startsWith('?') ? raw.slice(1) : raw;
|
||||
// Strip union with null/false/void: User[]|null → User[]
|
||||
const parts = type
|
||||
.split('|')
|
||||
.filter((p) => p !== 'null' && p !== 'false' && p !== 'void' && p !== 'mixed');
|
||||
if (parts.length !== 1) return undefined;
|
||||
type = parts[0];
|
||||
// Strip namespace: \App\Models\User[] → User[]
|
||||
const segments = type.split('\\');
|
||||
type = segments[segments.length - 1];
|
||||
// Skip uninformative types
|
||||
if (
|
||||
type === 'mixed' ||
|
||||
type === 'void' ||
|
||||
type === 'self' ||
|
||||
type === 'static' ||
|
||||
type === 'object' ||
|
||||
type === 'array'
|
||||
)
|
||||
return undefined;
|
||||
if (/^\w+(\[\])?$/.test(type) || /^\w+\s*</.test(type)) return type;
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Extract return type from PHPDoc `@return Type` annotation preceding a method.
|
||||
* Walks backwards through preceding siblings looking for comment nodes.
|
||||
* Preserves array notation (e.g., User[]) for for-loop element type extraction.
|
||||
*/
|
||||
const extractReturnType: ReturnTypeExtractor = (node) => {
|
||||
let sibling = node.previousSibling;
|
||||
while (sibling) {
|
||||
if (sibling.type === 'comment') {
|
||||
const match = PHPDOC_RETURN_RE.exec(sibling.text);
|
||||
if (match) return normalizePhpReturnType(match[1]);
|
||||
} else if (sibling.isNamed && !SKIP_NODE_TYPES.has(sibling.type)) break;
|
||||
sibling = sibling.previousSibling;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/** PHP: $alias = $user → assignment_expression with variable_name left/right.
|
||||
* PHP TypeEnv stores variables WITH $ prefix ($user → User), so we keep $ in lhs/rhs. */
|
||||
const extractPendingAssignment: PendingAssignmentExtractor = (node, scopeEnv) => {
|
||||
|
|
@ -605,7 +550,6 @@ export const typeConfig: LanguageTypeConfig = {
|
|||
extractParameter,
|
||||
extractInitializer,
|
||||
scanConstructorBinding,
|
||||
extractReturnType,
|
||||
extractForLoopBinding,
|
||||
extractPendingAssignment,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import type {
|
|||
TypeBindingExtractor,
|
||||
InitializerExtractor,
|
||||
ConstructorBindingScanner,
|
||||
ReturnTypeExtractor,
|
||||
PendingAssignmentExtractor,
|
||||
ForLoopExtractor,
|
||||
} from './types.js';
|
||||
|
|
@ -43,9 +42,6 @@ const YARD_PARAM_RE = /@param\s+(\w+)\s+\[([^\]]+)\]/g;
|
|||
/** Alternate YARD order: `@param [Type] name` */
|
||||
const YARD_PARAM_ALT_RE = /@param\s+\[([^\]]+)\]\s+(\w+)/g;
|
||||
|
||||
/** Regex to extract @return annotations: `@return [Type]` */
|
||||
const YARD_RETURN_RE = /@return\s+\[([^\]]+)\]/;
|
||||
|
||||
/**
|
||||
* Extract the simple type name from a YARD type string.
|
||||
* Handles:
|
||||
|
|
@ -229,35 +225,6 @@ const extractInitializer: InitializerExtractor = (node, env, classNames): void =
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Extract return type from YARD `@return [Type]` annotation preceding a method.
|
||||
* Reuses the same comment-walking strategy as collectYardParams: try direct
|
||||
* siblings first, fall back to parent (body_statement) siblings for class methods.
|
||||
*/
|
||||
const extractReturnType: ReturnTypeExtractor = (node) => {
|
||||
const search = (startNode: SyntaxNode): string | undefined => {
|
||||
let sibling = startNode.previousSibling;
|
||||
while (sibling) {
|
||||
if (sibling.type === 'comment') {
|
||||
const match = YARD_RETURN_RE.exec(sibling.text);
|
||||
if (match) return extractYardTypeName(match[1]);
|
||||
} else if (sibling.isNamed) {
|
||||
break;
|
||||
}
|
||||
sibling = sibling.previousSibling;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const result = search(node);
|
||||
if (result) return result;
|
||||
|
||||
if (node.parent?.type === 'body_statement') {
|
||||
return search(node.parent);
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Ruby constructor binding scanner: captures both `user = User.new` and
|
||||
* plain call assignments like `user = get_user()`.
|
||||
|
|
@ -452,7 +419,6 @@ export const typeConfig: LanguageTypeConfig = {
|
|||
extractParameter,
|
||||
extractInitializer,
|
||||
scanConstructorBinding,
|
||||
extractReturnType,
|
||||
extractForLoopBinding,
|
||||
extractPendingAssignment,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -30,11 +30,6 @@ export type ConstructorBindingScanner = (
|
|||
node: SyntaxNode,
|
||||
) => { varName: string; calleeName: string; receiverClassName?: string } | undefined;
|
||||
|
||||
/** Extracts a return type string from a method/function definition node.
|
||||
* Used for languages where return types are expressed in comments (e.g. YARD @return [Type])
|
||||
* rather than in AST fields. Returns undefined if no return type can be determined. */
|
||||
export type ReturnTypeExtractor = (node: SyntaxNode) => string | undefined;
|
||||
|
||||
/** Infer the type name of a literal AST node for overload disambiguation.
|
||||
* Returns the canonical type name (e.g. 'int', 'String', 'boolean') or undefined
|
||||
* for non-literal nodes. Only used when resolveCallTarget has multiple candidates
|
||||
|
|
@ -170,9 +165,6 @@ export interface LanguageTypeConfig {
|
|||
* Called on every AST node during buildTypeEnv walk; returns undefined for non-matches.
|
||||
* The callee binding is unverified — the caller must confirm against the SymbolTable. */
|
||||
scanConstructorBinding?: ConstructorBindingScanner;
|
||||
/** Extract return type from comment-based annotations (e.g. YARD @return [Type]).
|
||||
* Called as fallback when extractMethodSignature finds no AST-based return type. */
|
||||
extractReturnType?: ReturnTypeExtractor;
|
||||
/** Extract loop variable → type binding from a for-each AST node. */
|
||||
extractForLoopBinding?: ForLoopExtractor;
|
||||
/** Extract pending assignment for Tier 2 propagation.
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import type {
|
|||
InitializerExtractor,
|
||||
ClassNameLookup,
|
||||
ConstructorBindingScanner,
|
||||
ReturnTypeExtractor,
|
||||
PendingAssignmentExtractor,
|
||||
PendingAssignment,
|
||||
ForLoopExtractor,
|
||||
|
|
@ -198,44 +197,6 @@ const scanConstructorBinding: ConstructorBindingScanner = (node) => {
|
|||
return { varName: nameNode.text, calleeName };
|
||||
};
|
||||
|
||||
/** Regex to extract @returns or @return from JSDoc comments: `@returns {Type}` */
|
||||
const JSDOC_RETURN_RE = /@returns?\s*\{([^}]+)\}/;
|
||||
|
||||
/**
|
||||
* Minimal sanitization for JSDoc return types — preserves generic wrappers
|
||||
* (e.g. `Promise<User>`) so that extractReturnTypeName in call-processor
|
||||
* can apply WRAPPER_GENERICS unwrapping. Unlike normalizeJsDocType (which
|
||||
* strips generics), this only strips JSDoc-specific syntax markers.
|
||||
*/
|
||||
const sanitizeReturnType = (raw: string): string | undefined => {
|
||||
let type = raw.trim();
|
||||
// Strip JSDoc nullable/non-nullable prefixes: ?User → User, !User → User
|
||||
if (type.startsWith('?') || type.startsWith('!')) type = type.slice(1);
|
||||
// Strip module: prefix — module:models.User → models.User
|
||||
if (type.startsWith('module:')) type = type.slice(7);
|
||||
// Reject unions (ambiguous)
|
||||
if (type.includes('|')) return undefined;
|
||||
if (!type) return undefined;
|
||||
return type;
|
||||
};
|
||||
|
||||
/**
|
||||
* Extract return type from JSDoc `@returns {Type}` or `@return {Type}` annotation
|
||||
* preceding a function/method definition. Walks backwards through preceding siblings
|
||||
* looking for comment nodes containing the annotation.
|
||||
*/
|
||||
const extractReturnType: ReturnTypeExtractor = (node) => {
|
||||
let sibling = node.previousSibling;
|
||||
while (sibling) {
|
||||
if (sibling.type === 'comment') {
|
||||
const match = JSDOC_RETURN_RE.exec(sibling.text);
|
||||
if (match) return sanitizeReturnType(match[1]);
|
||||
} else if (sibling.isNamed && sibling.type !== 'decorator') break;
|
||||
sibling = sibling.previousSibling;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const FOR_LOOP_NODE_TYPES: ReadonlySet<string> = new Set(['for_in_statement']);
|
||||
|
||||
/** TS function/method node types that carry a parameters list. */
|
||||
|
|
@ -742,7 +703,6 @@ export const typeConfig: LanguageTypeConfig = {
|
|||
extractParameter,
|
||||
extractInitializer,
|
||||
scanConstructorBinding,
|
||||
extractReturnType,
|
||||
extractForLoopBinding,
|
||||
extractPendingAssignment,
|
||||
extractPatternBinding,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import type Parser from 'tree-sitter';
|
|||
import type { NodeLabel } from 'gitnexus-shared';
|
||||
import type { LanguageProvider } from '../language-provider.js';
|
||||
import { generateId } from '../../../lib/utils.js';
|
||||
import { extractSimpleTypeName } from '../type-extractors/shared.js';
|
||||
|
||||
/** Tree-sitter AST node. Re-exported for use across ingestion modules. */
|
||||
export type SyntaxNode = Parser.SyntaxNode;
|
||||
|
|
@ -48,7 +47,13 @@ export const getDefinitionNodeFromCaptures = (
|
|||
|
||||
/**
|
||||
* Node types that represent function/method definitions across languages.
|
||||
* Used to find the enclosing function for a call site.
|
||||
* Used by parent-walk in call-processor, parse-worker, and type-env to detect
|
||||
* enclosing function scope boundaries.
|
||||
*
|
||||
* INVARIANT: This set MUST be a superset of every language's
|
||||
* MethodExtractionConfig.methodNodeTypes. When adding a new node type to a
|
||||
* MethodExtractor config, add it here too — otherwise enclosing-function
|
||||
* resolution will silently miss that node type during parent-walks.
|
||||
*/
|
||||
export const FUNCTION_NODE_TYPES = new Set([
|
||||
// TypeScript/JavaScript
|
||||
|
|
@ -91,18 +96,6 @@ export const FUNCTION_NODE_TYPES = new Set([
|
|||
'method_signature',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Node types for standard function declarations that need C/C++ declarator handling.
|
||||
* Used by extractFunctionName to determine how to extract the function name.
|
||||
*/
|
||||
export const FUNCTION_DECLARATION_TYPES = new Set([
|
||||
'function_declaration',
|
||||
'function_definition',
|
||||
'async_function_declaration',
|
||||
'generator_function_declaration',
|
||||
'function_item',
|
||||
]);
|
||||
|
||||
/**
|
||||
* AST node types that represent a class-like container (for HAS_METHOD edge extraction).
|
||||
*
|
||||
|
|
@ -165,20 +158,6 @@ export const CONTAINER_TYPE_TO_LABEL: Record<string, string> = {
|
|||
companion_object: 'Class',
|
||||
};
|
||||
|
||||
/** Check if a Kotlin function_declaration capture is inside a class_body (i.e., a method).
|
||||
* Kotlin grammar uses function_declaration for both top-level functions and class methods.
|
||||
* Returns true when the captured definition node has a class_body ancestor. */
|
||||
export function isKotlinClassMethod(
|
||||
captureNode: { parent?: SyntaxNode | null } | null | undefined,
|
||||
): boolean {
|
||||
let ancestor = captureNode?.parent;
|
||||
while (ancestor) {
|
||||
if (ancestor.type === 'class_body') return true;
|
||||
ancestor = ancestor.parent;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the graph node label from a tree-sitter capture map.
|
||||
* Handles language-specific reclassification via the provider's labelOverride hook
|
||||
|
|
@ -337,7 +316,17 @@ export const findEnclosingClassInfo = (
|
|||
c.type === 'constant',
|
||||
);
|
||||
if (nameNode) {
|
||||
const label = CONTAINER_TYPE_TO_LABEL[current.type] || 'Class';
|
||||
let label = CONTAINER_TYPE_TO_LABEL[current.type] || 'Class';
|
||||
// Kotlin: class_declaration with an anonymous "interface" keyword child
|
||||
// is actually an interface, not a class. Refine the label to match the
|
||||
// node ID generated from the tree-sitter query capture (@definition.interface).
|
||||
if (
|
||||
current.type === 'class_declaration' &&
|
||||
label === 'Class' &&
|
||||
current.children?.some((c: SyntaxNode) => c.type === 'interface')
|
||||
) {
|
||||
label = 'Interface';
|
||||
}
|
||||
return {
|
||||
classId: generateId(label, `${filePath}:${nameNode.text}`),
|
||||
className: nameNode.text,
|
||||
|
|
@ -375,553 +364,49 @@ export const findSiblingChild = (
|
|||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Extract function name and label from a function_definition or similar AST node.
|
||||
* Handles C/C++ qualified_identifier (ClassName::MethodName) and other language patterns.
|
||||
*/
|
||||
export const extractFunctionName = (
|
||||
node: SyntaxNode,
|
||||
): { funcName: string | null; label: NodeLabel } => {
|
||||
let funcName: string | null = null;
|
||||
let label: NodeLabel = 'Function';
|
||||
|
||||
// Swift init/deinit
|
||||
if (node.type === 'init_declaration' || node.type === 'deinit_declaration') {
|
||||
return {
|
||||
funcName: node.type === 'init_declaration' ? 'init' : 'deinit',
|
||||
label: 'Constructor',
|
||||
};
|
||||
/** Generic name extraction from a function-like AST node.
|
||||
* Tries `node.childForFieldName('name')?.text`, then scans children for
|
||||
* `identifier` / `property_identifier` / `simple_identifier`. */
|
||||
export const genericFuncName = (node: SyntaxNode): string | null => {
|
||||
const nameField = node.childForFieldName?.('name');
|
||||
if (nameField) return nameField.text;
|
||||
for (let i = 0; i < node.childCount; i++) {
|
||||
const c = node.child(i);
|
||||
if (
|
||||
c?.type === 'identifier' ||
|
||||
c?.type === 'property_identifier' ||
|
||||
c?.type === 'simple_identifier'
|
||||
)
|
||||
return c.text;
|
||||
}
|
||||
|
||||
if (FUNCTION_DECLARATION_TYPES.has(node.type)) {
|
||||
// C/C++: function_definition -> [pointer_declarator ->] function_declarator -> qualified_identifier/identifier
|
||||
// Unwrap pointer_declarator / reference_declarator wrappers to reach function_declarator
|
||||
let declarator = node.childForFieldName?.('declarator');
|
||||
if (!declarator) {
|
||||
for (let i = 0; i < node.childCount; i++) {
|
||||
const c = node.child(i);
|
||||
if (c?.type === 'function_declarator') {
|
||||
declarator = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
while (
|
||||
declarator &&
|
||||
(declarator.type === 'pointer_declarator' || declarator.type === 'reference_declarator')
|
||||
) {
|
||||
let nextDeclarator = declarator.childForFieldName?.('declarator');
|
||||
if (!nextDeclarator) {
|
||||
for (let i = 0; i < declarator.childCount; i++) {
|
||||
const c = declarator.child(i);
|
||||
if (
|
||||
c?.type === 'function_declarator' ||
|
||||
c?.type === 'pointer_declarator' ||
|
||||
c?.type === 'reference_declarator'
|
||||
) {
|
||||
nextDeclarator = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
declarator = nextDeclarator;
|
||||
}
|
||||
if (declarator) {
|
||||
let innerDeclarator = declarator.childForFieldName?.('declarator');
|
||||
if (!innerDeclarator) {
|
||||
for (let i = 0; i < declarator.childCount; i++) {
|
||||
const c = declarator.child(i);
|
||||
if (
|
||||
c?.type === 'qualified_identifier' ||
|
||||
c?.type === 'identifier' ||
|
||||
c?.type === 'field_identifier' ||
|
||||
c?.type === 'parenthesized_declarator'
|
||||
) {
|
||||
innerDeclarator = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (innerDeclarator?.type === 'qualified_identifier') {
|
||||
let nameNode = innerDeclarator.childForFieldName?.('name');
|
||||
if (!nameNode) {
|
||||
for (let i = 0; i < innerDeclarator.childCount; i++) {
|
||||
const c = innerDeclarator.child(i);
|
||||
if (c?.type === 'identifier') {
|
||||
nameNode = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (nameNode?.text) {
|
||||
funcName = nameNode.text;
|
||||
label = 'Method';
|
||||
}
|
||||
} else if (
|
||||
innerDeclarator?.type === 'identifier' ||
|
||||
innerDeclarator?.type === 'field_identifier'
|
||||
) {
|
||||
// field_identifier is used for method names inside C++ class bodies
|
||||
funcName = innerDeclarator.text;
|
||||
if (innerDeclarator.type === 'field_identifier') label = 'Method';
|
||||
} else if (innerDeclarator?.type === 'parenthesized_declarator') {
|
||||
let nestedId: SyntaxNode | null = null;
|
||||
for (let i = 0; i < innerDeclarator.childCount; i++) {
|
||||
const c = innerDeclarator.child(i);
|
||||
if (c?.type === 'qualified_identifier' || c?.type === 'identifier') {
|
||||
nestedId = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (nestedId?.type === 'qualified_identifier') {
|
||||
let nameNode = nestedId.childForFieldName?.('name');
|
||||
if (!nameNode) {
|
||||
for (let i = 0; i < nestedId.childCount; i++) {
|
||||
const c = nestedId.child(i);
|
||||
if (c?.type === 'identifier') {
|
||||
nameNode = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (nameNode?.text) {
|
||||
funcName = nameNode.text;
|
||||
label = 'Method';
|
||||
}
|
||||
} else if (nestedId?.type === 'identifier') {
|
||||
funcName = nestedId.text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback for other languages (Kotlin uses simple_identifier, Swift uses simple_identifier)
|
||||
if (!funcName) {
|
||||
let nameNode = node.childForFieldName?.('name');
|
||||
if (!nameNode) {
|
||||
for (let i = 0; i < node.childCount; i++) {
|
||||
const c = node.child(i);
|
||||
if (
|
||||
c?.type === 'identifier' ||
|
||||
c?.type === 'property_identifier' ||
|
||||
c?.type === 'simple_identifier'
|
||||
) {
|
||||
nameNode = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
funcName = nameNode?.text;
|
||||
}
|
||||
} else if (node.type === 'impl_item') {
|
||||
let funcItem: SyntaxNode | null = null;
|
||||
for (let i = 0; i < node.childCount; i++) {
|
||||
const c = node.child(i);
|
||||
if (c?.type === 'function_item') {
|
||||
funcItem = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (funcItem) {
|
||||
let nameNode = funcItem.childForFieldName?.('name');
|
||||
if (!nameNode) {
|
||||
for (let i = 0; i < funcItem.childCount; i++) {
|
||||
const c = funcItem.child(i);
|
||||
if (c?.type === 'identifier') {
|
||||
nameNode = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
funcName = nameNode?.text;
|
||||
label = 'Method';
|
||||
}
|
||||
} else if (node.type === 'method_definition') {
|
||||
let nameNode = node.childForFieldName?.('name');
|
||||
if (!nameNode) {
|
||||
for (let i = 0; i < node.childCount; i++) {
|
||||
const c = node.child(i);
|
||||
if (c?.type === 'property_identifier') {
|
||||
nameNode = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
funcName = nameNode?.text;
|
||||
label = 'Method';
|
||||
} else if (node.type === 'method_declaration' || node.type === 'constructor_declaration') {
|
||||
let nameNode = node.childForFieldName?.('name');
|
||||
if (!nameNode) {
|
||||
for (let i = 0; i < node.childCount; i++) {
|
||||
const c = node.child(i);
|
||||
if (c?.type === 'identifier') {
|
||||
nameNode = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
funcName = nameNode?.text;
|
||||
label = 'Method';
|
||||
} else if (node.type === 'arrow_function' || node.type === 'function_expression') {
|
||||
const parent = node.parent;
|
||||
if (parent?.type === 'variable_declarator') {
|
||||
let nameNode = parent.childForFieldName?.('name');
|
||||
if (!nameNode) {
|
||||
for (let i = 0; i < parent.childCount; i++) {
|
||||
const c = parent.child(i);
|
||||
if (c?.type === 'identifier') {
|
||||
nameNode = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
funcName = nameNode?.text;
|
||||
}
|
||||
} else if (node.type === 'method' || node.type === 'singleton_method') {
|
||||
let nameNode = node.childForFieldName?.('name');
|
||||
if (!nameNode) {
|
||||
for (let i = 0; i < node.childCount; i++) {
|
||||
const c = node.child(i);
|
||||
if (c?.type === 'identifier') {
|
||||
nameNode = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
funcName = nameNode?.text;
|
||||
label = 'Method';
|
||||
} else if (node.type === 'function_signature') {
|
||||
// Dart: top-level function signatures
|
||||
let nameNode = node.childForFieldName?.('name');
|
||||
if (!nameNode) {
|
||||
for (let i = 0; i < node.childCount; i++) {
|
||||
const c = node.child(i);
|
||||
if (c?.type === 'identifier') {
|
||||
nameNode = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
funcName = nameNode?.text ?? null;
|
||||
} else if (node.type === 'method_signature') {
|
||||
// Dart: method_signature wraps function_signature
|
||||
let funcSig: SyntaxNode | null = null;
|
||||
for (let i = 0; i < node.childCount; i++) {
|
||||
const c = node.child(i);
|
||||
if (c?.type === 'function_signature') {
|
||||
funcSig = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (funcSig) {
|
||||
let nameNode = funcSig.childForFieldName?.('name');
|
||||
if (!nameNode) {
|
||||
for (let i = 0; i < funcSig.childCount; i++) {
|
||||
const c = funcSig.child(i);
|
||||
if (c?.type === 'identifier') {
|
||||
nameNode = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
funcName = nameNode?.text ?? null;
|
||||
}
|
||||
label = 'Method';
|
||||
}
|
||||
|
||||
return { funcName, label };
|
||||
return null;
|
||||
};
|
||||
|
||||
export interface MethodSignature {
|
||||
parameterCount: number | undefined;
|
||||
/** Number of required (non-optional, non-default) parameters.
|
||||
* Only set when fewer than parameterCount — enables range-based arity filtering.
|
||||
* undefined means all parameters are required (or metadata unavailable). */
|
||||
requiredParameterCount: number | undefined;
|
||||
/** Per-parameter type names extracted via extractSimpleTypeName.
|
||||
* Only populated for languages with method overloading (Java, Kotlin, C#, C++).
|
||||
* undefined (not []) when no types are extractable — avoids empty array allocations. */
|
||||
parameterTypes: string[] | undefined;
|
||||
returnType: string | undefined;
|
||||
}
|
||||
/** AST node types that represent a method definition (for `inferFunctionLabel`). */
|
||||
export const METHOD_LABEL_NODE_TYPES = new Set([
|
||||
'method_definition',
|
||||
'method_declaration',
|
||||
'method',
|
||||
'singleton_method',
|
||||
]);
|
||||
|
||||
/** Argument list node types shared between extractMethodSignature and countCallArguments. */
|
||||
/** AST node types that represent a constructor definition (for `inferFunctionLabel`). */
|
||||
export const CONSTRUCTOR_LABEL_NODE_TYPES = new Set([
|
||||
'constructor_declaration',
|
||||
'compact_constructor_declaration',
|
||||
]);
|
||||
|
||||
/** Infer node label from AST node type for function-like nodes without a provider hook. */
|
||||
export const inferFunctionLabel = (nodeType: string): NodeLabel =>
|
||||
METHOD_LABEL_NODE_TYPES.has(nodeType)
|
||||
? 'Method'
|
||||
: CONSTRUCTOR_LABEL_NODE_TYPES.has(nodeType)
|
||||
? 'Constructor'
|
||||
: 'Function';
|
||||
|
||||
/** Argument list node types shared between countCallArguments and call-resolution helpers. */
|
||||
export const CALL_ARGUMENT_LIST_TYPES = new Set(['arguments', 'argument_list', 'value_arguments']);
|
||||
|
||||
/**
|
||||
* Extract parameter count and return type text from an AST method/function node.
|
||||
* Works across languages by looking for common AST patterns.
|
||||
*/
|
||||
export const extractMethodSignature = (node: SyntaxNode | null | undefined): MethodSignature => {
|
||||
let parameterCount: number | undefined = 0;
|
||||
let requiredCount = 0;
|
||||
let returnType: string | undefined;
|
||||
let isVariadic = false;
|
||||
const paramTypes: string[] = [];
|
||||
|
||||
if (!node)
|
||||
return {
|
||||
parameterCount,
|
||||
requiredParameterCount: undefined,
|
||||
parameterTypes: undefined,
|
||||
returnType,
|
||||
};
|
||||
|
||||
const paramListTypes = new Set([
|
||||
'formal_parameters',
|
||||
'parameters',
|
||||
'parameter_list',
|
||||
'function_parameters',
|
||||
'method_parameters',
|
||||
'function_value_parameters',
|
||||
'formal_parameter_list', // Dart
|
||||
]);
|
||||
|
||||
// Node types that indicate variadic/rest parameters
|
||||
const VARIADIC_PARAM_TYPES = new Set([
|
||||
'variadic_parameter_declaration', // Go: ...string
|
||||
'variadic_parameter', // Rust: extern "C" fn(...)
|
||||
'spread_parameter', // Java: Object... args
|
||||
'list_splat_pattern', // Python: *args
|
||||
'dictionary_splat_pattern', // Python: **kwargs
|
||||
]);
|
||||
|
||||
/** AST node types that represent parameters with default values. */
|
||||
const OPTIONAL_PARAM_TYPES = new Set([
|
||||
'optional_parameter', // TypeScript, Ruby: (x?: number), (x: number = 5), def f(x = 5)
|
||||
'default_parameter', // Python: def f(x=5)
|
||||
'typed_default_parameter', // Python: def f(x: int = 5)
|
||||
'optional_parameter_declaration', // C++: void f(int x = 5)
|
||||
]);
|
||||
|
||||
/** Check if a parameter node has a default value (handles Kotlin, C#, Swift, PHP
|
||||
* where defaults are expressed as child nodes rather than distinct node types). */
|
||||
const hasDefaultValue = (paramNode: SyntaxNode): boolean => {
|
||||
if (OPTIONAL_PARAM_TYPES.has(paramNode.type)) return true;
|
||||
// C#, Swift, PHP: check for '=' token or equals_value_clause child
|
||||
for (let i = 0; i < paramNode.childCount; i++) {
|
||||
const c = paramNode.child(i);
|
||||
if (!c) continue;
|
||||
if (c.type === '=' || c.type === 'equals_value_clause') return true;
|
||||
}
|
||||
// Kotlin: default values are siblings of the parameter node, not children.
|
||||
// The AST is: parameter, =, <literal> — all at function_value_parameters level.
|
||||
// Check if the immediately following sibling is '=' (default value separator).
|
||||
const sib = paramNode.nextSibling;
|
||||
if (sib && sib.type === '=') return true;
|
||||
return false;
|
||||
};
|
||||
|
||||
const findParameterList = (current: SyntaxNode): SyntaxNode | null => {
|
||||
for (const child of current.children) {
|
||||
if (paramListTypes.has(child.type)) return child;
|
||||
}
|
||||
for (const child of current.children) {
|
||||
const nested = findParameterList(child);
|
||||
if (nested) return nested;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const parameterList = paramListTypes.has(node.type)
|
||||
? node // node itself IS the parameter list (e.g. C# primary constructors)
|
||||
: (node.childForFieldName?.('parameters') ?? findParameterList(node));
|
||||
|
||||
if (parameterList && paramListTypes.has(parameterList.type)) {
|
||||
for (const param of parameterList.namedChildren) {
|
||||
if (param.type === 'comment') continue;
|
||||
if (
|
||||
param.text === 'self' ||
|
||||
param.text === '&self' ||
|
||||
param.text === '&mut self' ||
|
||||
param.type === 'self_parameter'
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
// TypeScript: `this` parameter is a compile-time type constraint, not a real param
|
||||
// e.g., handle(this: void, event: Event) — only count 'event'
|
||||
if (param.type === 'required_parameter') {
|
||||
const patternNode = param.childForFieldName('pattern');
|
||||
if (patternNode?.type === 'this') continue;
|
||||
}
|
||||
// Kotlin: default values are siblings of the parameter node inside
|
||||
// function_value_parameters, so they appear as named children (e.g.
|
||||
// string_literal, integer_literal, boolean_literal, call_expression).
|
||||
// Skip any named child that isn't a parameter-like or modifier node.
|
||||
if (
|
||||
param.type.endsWith('_literal') ||
|
||||
param.type === 'call_expression' ||
|
||||
param.type === 'navigation_expression' ||
|
||||
param.type === 'prefix_expression' ||
|
||||
param.type === 'parenthesized_expression'
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
// Check for variadic parameter types
|
||||
if (VARIADIC_PARAM_TYPES.has(param.type)) {
|
||||
isVariadic = true;
|
||||
continue;
|
||||
}
|
||||
// TypeScript/JavaScript: rest parameter — required_parameter containing rest_pattern
|
||||
if (param.type === 'required_parameter' || param.type === 'optional_parameter') {
|
||||
for (const child of param.children) {
|
||||
if (child.type === 'rest_pattern') {
|
||||
isVariadic = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (isVariadic) continue;
|
||||
}
|
||||
// Kotlin: vararg modifier on a regular parameter
|
||||
if (param.type === 'parameter' || param.type === 'formal_parameter') {
|
||||
const prev = param.previousSibling;
|
||||
if (prev?.type === 'parameter_modifiers' && prev.text.includes('vararg')) {
|
||||
isVariadic = true;
|
||||
}
|
||||
}
|
||||
// Extract parameter type name for overload disambiguation.
|
||||
// Works for Java (formal_parameter), Kotlin (parameter), C# (parameter),
|
||||
// C++ (parameter_declaration). Uses childForFieldName('type') which is the
|
||||
// standard tree-sitter field for typed parameters across these languages.
|
||||
// Kotlin uses positional children instead of 'type' field — fall back to
|
||||
// searching for user_type/nullable_type/predefined_type children.
|
||||
const paramTypeNode = param.childForFieldName('type');
|
||||
if (paramTypeNode) {
|
||||
const typeName = extractSimpleTypeName(paramTypeNode);
|
||||
paramTypes.push(typeName ?? 'unknown');
|
||||
} else {
|
||||
// Kotlin: parameter → [simple_identifier, user_type|nullable_type]
|
||||
let found = false;
|
||||
for (const child of param.namedChildren) {
|
||||
if (
|
||||
child.type === 'user_type' ||
|
||||
child.type === 'nullable_type' ||
|
||||
child.type === 'type_identifier' ||
|
||||
child.type === 'predefined_type'
|
||||
) {
|
||||
const typeName = extractSimpleTypeName(child);
|
||||
paramTypes.push(typeName ?? 'unknown');
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) paramTypes.push('unknown');
|
||||
}
|
||||
if (!hasDefaultValue(param)) requiredCount++;
|
||||
parameterCount++;
|
||||
}
|
||||
// C/C++: bare `...` token in parameter list (not a named child — check all children)
|
||||
if (!isVariadic) {
|
||||
for (const child of parameterList.children) {
|
||||
if (!child.isNamed && child.text === '...') {
|
||||
isVariadic = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Swift fallback: tree-sitter-swift places `parameter` nodes as direct children of
|
||||
// function_declaration without a wrapping parameters/function_parameters list node.
|
||||
// When no parameter list was found, count direct `parameter` children on the node.
|
||||
if (!parameterList && parameterCount === 0) {
|
||||
for (const child of node.namedChildren) {
|
||||
if (child.type === 'parameter') {
|
||||
if (!hasDefaultValue(child)) requiredCount++;
|
||||
parameterCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return type extraction — language-specific field names
|
||||
// Go: 'result' field is either a type_identifier or parameter_list (multi-return)
|
||||
const goResult = node.childForFieldName?.('result');
|
||||
if (goResult) {
|
||||
if (goResult.type === 'parameter_list') {
|
||||
// Multi-return: extract first parameter's type only (e.g. (*User, error) → *User)
|
||||
const firstParam = goResult.firstNamedChild;
|
||||
if (firstParam?.type === 'parameter_declaration') {
|
||||
const typeNode = firstParam.childForFieldName('type');
|
||||
if (typeNode) returnType = typeNode.text;
|
||||
} else if (firstParam) {
|
||||
// Unnamed return types: (string, error) — first child is a bare type node
|
||||
returnType = firstParam.text;
|
||||
}
|
||||
} else {
|
||||
returnType = goResult.text;
|
||||
}
|
||||
}
|
||||
|
||||
// Rust: 'return_type' field — the value IS the type node (e.g. primitive_type, type_identifier).
|
||||
// Skip if the node is a type_annotation (TS/Python), which is handled by the generic loop below.
|
||||
if (!returnType) {
|
||||
const rustReturn = node.childForFieldName?.('return_type');
|
||||
if (rustReturn && rustReturn.type !== 'type_annotation') {
|
||||
returnType = rustReturn.text;
|
||||
}
|
||||
}
|
||||
|
||||
// C/C++: 'type' field on function_definition
|
||||
if (!returnType) {
|
||||
const cppType = node.childForFieldName?.('type');
|
||||
if (cppType && cppType.text !== 'void') {
|
||||
returnType = cppType.text;
|
||||
}
|
||||
}
|
||||
|
||||
// C#: 'returns' field on method_declaration
|
||||
if (!returnType) {
|
||||
const csReturn = node.childForFieldName?.('returns');
|
||||
if (csReturn && csReturn.text !== 'void') {
|
||||
returnType = csReturn.text;
|
||||
}
|
||||
}
|
||||
|
||||
// TS/Rust/Python/C#/Kotlin: type_annotation or return_type child
|
||||
if (!returnType) {
|
||||
for (const child of node.children) {
|
||||
if (child.type === 'type_annotation' || child.type === 'return_type') {
|
||||
const typeNode = child.children.find((c) => c.isNamed);
|
||||
if (typeNode) returnType = typeNode.text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Kotlin: fun getUser(): User — return type is a bare user_type child of
|
||||
// function_declaration. The Kotlin grammar does NOT wrap it in type_annotation
|
||||
// or return_type; it appears as a direct child after function_value_parameters.
|
||||
// Note: Kotlin uses function_value_parameters (not a field), so we find it by type.
|
||||
if (!returnType) {
|
||||
let paramsEnd = -1;
|
||||
for (let i = 0; i < node.childCount; i++) {
|
||||
const child = node.child(i);
|
||||
if (!child) continue;
|
||||
if (child.type === 'function_value_parameters' || child.type === 'value_parameters') {
|
||||
paramsEnd = child.endIndex;
|
||||
}
|
||||
if (paramsEnd >= 0 && child.type === 'user_type' && child.startIndex > paramsEnd) {
|
||||
returnType = child.text;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isVariadic) parameterCount = undefined;
|
||||
|
||||
// Only include parameterTypes when at least one type was successfully extracted.
|
||||
// Use undefined (not []) to avoid empty array allocations for untyped parameters.
|
||||
const hasTypes = paramTypes.length > 0 && paramTypes.some((t) => t !== 'unknown');
|
||||
// Only set requiredParameterCount when it differs from total — saves memory on the common case.
|
||||
const requiredParameterCount =
|
||||
!isVariadic && requiredCount < (parameterCount ?? 0) ? requiredCount : undefined;
|
||||
return {
|
||||
parameterCount,
|
||||
requiredParameterCount,
|
||||
parameterTypes: hasTypes ? paramTypes : undefined,
|
||||
returnType,
|
||||
};
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Generic AST traversal helpers (shared by parse-worker + php-helpers)
|
||||
// ============================================================================
|
||||
|
|
@ -945,18 +430,6 @@ export function extractStringContent(node: SyntaxNode | null | undefined): strin
|
|||
return null;
|
||||
}
|
||||
|
||||
/** Check if a C/C++ function_definition is inside a class or struct body.
|
||||
* Used by the C/C++ labelOverride to skip duplicate function captures
|
||||
* that are already covered by definition.method queries. */
|
||||
export function isCppInsideClassOrStruct(functionNode: SyntaxNode): boolean {
|
||||
let ancestor: SyntaxNode | null = functionNode?.parent ?? null;
|
||||
while (ancestor) {
|
||||
if (ancestor.type === 'class_specifier' || ancestor.type === 'struct_specifier') return true;
|
||||
ancestor = ancestor.parent;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Find the first direct named child of a tree-sitter node matching the given type. */
|
||||
export function findChild(node: SyntaxNode, type: string): SyntaxNode | null {
|
||||
for (let i = 0; i < node.namedChildCount; i++) {
|
||||
|
|
|
|||
38
gitnexus/src/core/ingestion/utils/method-props.ts
Normal file
38
gitnexus/src/core/ingestion/utils/method-props.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import type { MethodInfo } from '../method-types.js';
|
||||
|
||||
/**
|
||||
* Compute arity for ID-generation purposes.
|
||||
* Returns `undefined` when any parameter is variadic (arity is indeterminate).
|
||||
*/
|
||||
export function arityForIdFromInfo(info: MethodInfo): number | undefined {
|
||||
return info.parameters.some((p) => p.isVariadic) ? undefined : info.parameters.length;
|
||||
}
|
||||
|
||||
/** Convert MethodInfo from methodExtractor into flat properties for a graph node. */
|
||||
export function buildMethodProps(info: MethodInfo): Record<string, unknown> {
|
||||
const types: string[] = [];
|
||||
let optionalCount = 0;
|
||||
let hasVariadic = false;
|
||||
for (const p of info.parameters) {
|
||||
if (p.type !== null) types.push(p.type);
|
||||
if (p.isOptional) optionalCount++;
|
||||
if (p.isVariadic) hasVariadic = true;
|
||||
}
|
||||
return {
|
||||
parameterCount: hasVariadic ? undefined : info.parameters.length,
|
||||
...(!hasVariadic && optionalCount > 0
|
||||
? { requiredParameterCount: info.parameters.length - optionalCount }
|
||||
: {}),
|
||||
...(types.length > 0 ? { parameterTypes: types } : {}),
|
||||
returnType: info.returnType ?? undefined,
|
||||
visibility: info.visibility,
|
||||
isStatic: info.isStatic,
|
||||
isAbstract: info.isAbstract,
|
||||
isFinal: info.isFinal,
|
||||
...(info.isVirtual ? { isVirtual: info.isVirtual } : {}),
|
||||
...(info.isOverride ? { isOverride: info.isOverride } : {}),
|
||||
...(info.isAsync ? { isAsync: info.isAsync } : {}),
|
||||
...(info.isPartial ? { isPartial: info.isPartial } : {}),
|
||||
...(info.annotations.length > 0 ? { annotations: info.annotations } : {}),
|
||||
};
|
||||
}
|
||||
|
|
@ -41,14 +41,15 @@ try {
|
|||
import { getLanguageFromFilename } from 'gitnexus-shared';
|
||||
import {
|
||||
FUNCTION_NODE_TYPES,
|
||||
extractFunctionName,
|
||||
getDefinitionNodeFromCaptures,
|
||||
findEnclosingClassInfo,
|
||||
type EnclosingClassInfo,
|
||||
getLabelFromCaptures,
|
||||
extractMethodSignature,
|
||||
findDescendant,
|
||||
extractStringContent,
|
||||
genericFuncName,
|
||||
inferFunctionLabel,
|
||||
CLASS_CONTAINER_TYPES,
|
||||
type SyntaxNode,
|
||||
} from '../utils/ast-helpers.js';
|
||||
import {
|
||||
|
|
@ -75,7 +76,8 @@ import type { NamedBinding } from '../named-bindings/types.js';
|
|||
import type { NodeLabel } from 'gitnexus-shared';
|
||||
import type { FieldInfo, FieldExtractorContext } from '../field-types.js';
|
||||
import type { MethodInfo, MethodExtractorContext } from '../method-types.js';
|
||||
import { CLASS_CONTAINER_TYPES } from '../utils/ast-helpers.js';
|
||||
import { buildMethodProps, arityForIdFromInfo } from '../utils/method-props.js';
|
||||
import type { LanguageProvider } from '../language-provider.js';
|
||||
|
||||
// ============================================================================
|
||||
// Types for serializable results
|
||||
|
|
@ -94,14 +96,8 @@ interface ParsedNode {
|
|||
astFrameworkMultiplier?: number;
|
||||
astFrameworkReason?: string;
|
||||
description?: string;
|
||||
parameterCount?: number;
|
||||
requiredParameterCount?: number;
|
||||
returnType?: string;
|
||||
// Field/property metadata (populated by FieldExtractor)
|
||||
declaredType?: string;
|
||||
visibility?: string;
|
||||
isStatic?: boolean;
|
||||
isReadonly?: boolean;
|
||||
// Method/field metadata — extensible via buildMethodProps spread
|
||||
[key: string]: unknown;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -511,8 +507,6 @@ function getMethodInfo(
|
|||
// Enclosing function detection (for call extraction) — cached
|
||||
// ============================================================================
|
||||
|
||||
import type { LanguageProvider } from '../language-provider.js';
|
||||
|
||||
/** Walk up AST to find enclosing function, return its generateId or null for top-level.
|
||||
* Applies provider.labelOverride so the label matches the definition phase (single source of truth). */
|
||||
const findEnclosingFunctionId = (
|
||||
|
|
@ -526,7 +520,9 @@ const findEnclosingFunctionId = (
|
|||
let current = node.parent;
|
||||
while (current) {
|
||||
if (FUNCTION_NODE_TYPES.has(current.type)) {
|
||||
const { funcName, label } = extractFunctionName(current);
|
||||
const efnResult = provider.methodExtractor?.extractFunctionName?.(current);
|
||||
const funcName = efnResult?.funcName ?? genericFuncName(current);
|
||||
const label = efnResult?.label ?? inferFunctionLabel(current.type);
|
||||
if (funcName) {
|
||||
// Apply labelOverride so label matches definition phase (e.g., Kotlin Function→Method).
|
||||
// null means "skip as definition" — keep original label for scope identification.
|
||||
|
|
@ -538,7 +534,28 @@ const findEnclosingFunctionId = (
|
|||
// Qualify with enclosing class to match definition-phase node IDs
|
||||
const classInfo = cachedFindEnclosingClassInfo(current, filePath);
|
||||
const qualifiedName = classInfo ? `${classInfo.className}.${funcName}` : funcName;
|
||||
const result = generateId(finalLabel, `${filePath}:${qualifiedName}`);
|
||||
// Include #<arity> suffix to match definition-phase Method/Constructor IDs.
|
||||
// Use the same MethodExtractor (getMethodInfo) as the definition phase.
|
||||
let arity: number | undefined;
|
||||
if (finalLabel === 'Method' || finalLabel === 'Constructor') {
|
||||
const classNode =
|
||||
findEnclosingClassNode(current) ?? findClassNodeByQualifiedName(current);
|
||||
if (classNode) {
|
||||
const methodMap = getMethodInfo(classNode, provider, {
|
||||
filePath,
|
||||
language: getLanguageFromFilename(filePath),
|
||||
});
|
||||
const defLine = current.startPosition.row + 1;
|
||||
const info = methodMap?.get(`${funcName}:${defLine}`);
|
||||
if (info) {
|
||||
arity = info.parameters.some((p) => p.isVariadic)
|
||||
? undefined
|
||||
: info.parameters.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
const arityTag = arity !== undefined ? `#${arity}` : '';
|
||||
const result = generateId(finalLabel, `${filePath}:${qualifiedName}${arityTag}`);
|
||||
functionIdCache.set(node, result);
|
||||
return result;
|
||||
}
|
||||
|
|
@ -562,7 +579,28 @@ const findEnclosingFunctionId = (
|
|||
const qualifiedName = classInfo
|
||||
? `${classInfo.className}.${customResult.funcName}`
|
||||
: customResult.funcName;
|
||||
const result = generateId(finalLabel, `${filePath}:${qualifiedName}`);
|
||||
// Include #<arity> suffix to match definition-phase Method/Constructor IDs.
|
||||
const sigNode = current.previousSibling ?? current;
|
||||
let arity2: number | undefined;
|
||||
if (finalLabel === 'Method' || finalLabel === 'Constructor') {
|
||||
const classNode2 =
|
||||
findEnclosingClassNode(sigNode) ?? findClassNodeByQualifiedName(sigNode);
|
||||
if (classNode2) {
|
||||
const methodMap2 = getMethodInfo(classNode2, provider, {
|
||||
filePath,
|
||||
language: getLanguageFromFilename(filePath),
|
||||
});
|
||||
const defLine2 = sigNode.startPosition.row + 1;
|
||||
const info2 = methodMap2?.get(`${customResult.funcName}:${defLine2}`);
|
||||
if (info2) {
|
||||
arity2 = info2.parameters.some((p) => p.isVariadic)
|
||||
? undefined
|
||||
: info2.parameters.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
const arityTag2 = arity2 !== undefined ? `#${arity2}` : '';
|
||||
const result = generateId(finalLabel, `${filePath}:${qualifiedName}${arityTag2}`);
|
||||
functionIdCache.set(node, result);
|
||||
return result;
|
||||
}
|
||||
|
|
@ -1312,6 +1350,7 @@ const processFileGroup = (
|
|||
const typeEnv = buildTypeEnv(tree, language, {
|
||||
parentMap,
|
||||
enclosingFunctionFinder: provider?.enclosingFunctionFinder,
|
||||
extractFunctionName: provider?.methodExtractor?.extractFunctionName,
|
||||
});
|
||||
const callRouter = provider.callRouter;
|
||||
|
||||
|
|
@ -1775,7 +1814,57 @@ const processFileGroup = (
|
|||
const qualifiedName = enclosingClassInfo
|
||||
? `${enclosingClassInfo.className}.${nodeName}`
|
||||
: nodeName;
|
||||
const nodeId = generateId(nodeLabel, `${file.path}:${qualifiedName}`);
|
||||
|
||||
// Extract method metadata BEFORE generating node ID — parameterCount is needed
|
||||
// to disambiguate overloaded methods via #<arity> suffix in the ID.
|
||||
let declaredType: string | undefined;
|
||||
let methodProps: Record<string, unknown> = {};
|
||||
let arityForId: number | undefined; // raw param count for ID, even for variadic
|
||||
if (nodeLabel === 'Function' || nodeLabel === 'Method' || nodeLabel === 'Constructor') {
|
||||
// Use MethodExtractor for method metadata — provides parameterCount, parameterTypes,
|
||||
// returnType, isAbstract/isFinal/annotations, visibility, and more.
|
||||
let enrichedByMethodExtractor = false;
|
||||
if (provider.methodExtractor && definitionNode) {
|
||||
const classNode =
|
||||
findEnclosingClassNode(definitionNode) ?? findClassNodeByQualifiedName(definitionNode);
|
||||
if (classNode) {
|
||||
const methodMap = getMethodInfo(classNode, provider, {
|
||||
filePath: file.path,
|
||||
language,
|
||||
});
|
||||
const defLine = definitionNode.startPosition.row + 1;
|
||||
const info = methodMap?.get(`${nodeName}:${defLine}`);
|
||||
if (info) {
|
||||
enrichedByMethodExtractor = true;
|
||||
arityForId = arityForIdFromInfo(info);
|
||||
methodProps = buildMethodProps(info);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For top-level methods (e.g. Go method_declaration), try extractFromNode
|
||||
if (
|
||||
!enrichedByMethodExtractor &&
|
||||
provider.methodExtractor?.extractFromNode &&
|
||||
definitionNode
|
||||
) {
|
||||
const info = provider.methodExtractor.extractFromNode(definitionNode, {
|
||||
filePath: file.path,
|
||||
language,
|
||||
});
|
||||
if (info) {
|
||||
enrichedByMethodExtractor = true;
|
||||
arityForId = arityForIdFromInfo(info);
|
||||
methodProps = buildMethodProps(info);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Append #<paramCount> to Method/Constructor IDs to disambiguate overloads.
|
||||
// Functions are not suffixed — they don't overload by name in the same scope.
|
||||
const needsAritySuffix = nodeLabel === 'Method' || nodeLabel === 'Constructor';
|
||||
const arityTag = needsAritySuffix && arityForId !== undefined ? `#${arityForId}` : '';
|
||||
const nodeId = generateId(nodeLabel, `${file.path}:${qualifiedName}${arityTag}`);
|
||||
|
||||
const description = provider.descriptionExtractor?.(nodeLabel, nodeName, captureMap);
|
||||
|
||||
|
|
@ -1817,124 +1906,8 @@ const processFileGroup = (
|
|||
}
|
||||
}
|
||||
|
||||
let parameterCount: number | undefined;
|
||||
let requiredParameterCount: number | undefined;
|
||||
let parameterTypes: string[] | undefined;
|
||||
let returnType: string | undefined;
|
||||
let declaredType: string | undefined;
|
||||
let visibility: string | undefined;
|
||||
let isStatic: boolean | undefined;
|
||||
let isReadonly: boolean | undefined;
|
||||
let isAbstract: boolean | undefined;
|
||||
let isFinal: boolean | undefined;
|
||||
let isVirtual: boolean | undefined;
|
||||
let isOverride: boolean | undefined;
|
||||
let isAsync: boolean | undefined;
|
||||
let isPartial: boolean | undefined;
|
||||
let annotations: string[] | undefined;
|
||||
if (nodeLabel === 'Function' || nodeLabel === 'Method' || nodeLabel === 'Constructor') {
|
||||
// Try MethodExtractor first — it provides everything extractMethodSignature does, plus
|
||||
// isAbstract/isFinal/annotations. Only fall back to extractMethodSignature when no
|
||||
// MethodExtractor is available or the method isn't inside a class body.
|
||||
let enrichedByMethodExtractor = false;
|
||||
if (provider.methodExtractor && definitionNode) {
|
||||
const classNode =
|
||||
findEnclosingClassNode(definitionNode) ?? findClassNodeByQualifiedName(definitionNode);
|
||||
if (classNode) {
|
||||
const methodMap = getMethodInfo(classNode, provider, {
|
||||
filePath: file.path,
|
||||
language,
|
||||
});
|
||||
const defLine = definitionNode.startPosition.row + 1;
|
||||
const info = methodMap?.get(`${nodeName}:${defLine}`);
|
||||
if (info) {
|
||||
enrichedByMethodExtractor = true;
|
||||
const hasVariadic = info.parameters.some((p) => p.isVariadic);
|
||||
parameterCount = hasVariadic ? undefined : info.parameters.length;
|
||||
const types: string[] = [];
|
||||
let optionalCount = 0;
|
||||
for (const p of info.parameters) {
|
||||
if (p.type !== null) types.push(p.type);
|
||||
if (p.isOptional) optionalCount++;
|
||||
}
|
||||
parameterTypes = types.length > 0 ? types : undefined;
|
||||
requiredParameterCount =
|
||||
!hasVariadic && optionalCount > 0
|
||||
? info.parameters.length - optionalCount
|
||||
: undefined;
|
||||
returnType = info.returnType ?? undefined;
|
||||
visibility = info.visibility;
|
||||
isStatic = info.isStatic;
|
||||
isAbstract = info.isAbstract;
|
||||
isFinal = info.isFinal;
|
||||
if (info.isVirtual) isVirtual = info.isVirtual;
|
||||
if (info.isOverride) isOverride = info.isOverride;
|
||||
if (info.isAsync) isAsync = info.isAsync;
|
||||
if (info.isPartial) isPartial = info.isPartial;
|
||||
if (info.annotations.length > 0) annotations = info.annotations;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For top-level methods (e.g. Go method_declaration), try extractFromNode
|
||||
if (
|
||||
!enrichedByMethodExtractor &&
|
||||
provider.methodExtractor?.extractFromNode &&
|
||||
definitionNode
|
||||
) {
|
||||
const info = provider.methodExtractor.extractFromNode(definitionNode, {
|
||||
filePath: file.path,
|
||||
language,
|
||||
});
|
||||
if (info) {
|
||||
enrichedByMethodExtractor = true;
|
||||
const hasVariadic = info.parameters.some((p) => p.isVariadic);
|
||||
parameterCount = hasVariadic ? undefined : info.parameters.length;
|
||||
const types: string[] = [];
|
||||
let optionalCount = 0;
|
||||
for (const p of info.parameters) {
|
||||
if (p.type !== null) types.push(p.type);
|
||||
if (p.isOptional) optionalCount++;
|
||||
}
|
||||
parameterTypes = types.length > 0 ? types : undefined;
|
||||
requiredParameterCount =
|
||||
!hasVariadic && optionalCount > 0
|
||||
? info.parameters.length - optionalCount
|
||||
: undefined;
|
||||
returnType = info.returnType ?? undefined;
|
||||
visibility = info.visibility;
|
||||
isStatic = info.isStatic;
|
||||
isAbstract = info.isAbstract;
|
||||
isFinal = info.isFinal;
|
||||
if (info.isVirtual) isVirtual = info.isVirtual;
|
||||
if (info.isOverride) isOverride = info.isOverride;
|
||||
if (info.isAsync) isAsync = info.isAsync;
|
||||
if (info.isPartial) isPartial = info.isPartial;
|
||||
if (info.annotations.length > 0) annotations = info.annotations;
|
||||
}
|
||||
}
|
||||
|
||||
if (!enrichedByMethodExtractor) {
|
||||
const sig = extractMethodSignature(definitionNode);
|
||||
parameterCount = sig.parameterCount;
|
||||
requiredParameterCount = sig.requiredParameterCount;
|
||||
parameterTypes = sig.parameterTypes;
|
||||
returnType = sig.returnType;
|
||||
}
|
||||
|
||||
// Language-specific return type fallback (e.g. Ruby YARD @return [Type])
|
||||
// Also upgrades uninformative AST types like PHP `array` with PHPDoc `@return User[]`
|
||||
if (
|
||||
(!returnType || returnType === 'array' || returnType === 'iterable') &&
|
||||
definitionNode
|
||||
) {
|
||||
const tc = provider.typeConfig;
|
||||
if (tc?.extractReturnType) {
|
||||
const docReturn = tc.extractReturnType(definitionNode);
|
||||
if (docReturn) returnType = docReturn;
|
||||
}
|
||||
}
|
||||
} else if (nodeLabel === 'Property' && definitionNode) {
|
||||
// Property metadata extraction (not needed before nodeId — Properties don't overload)
|
||||
if (nodeLabel === 'Property' && definitionNode) {
|
||||
// FieldExtractor is the single source of truth when available
|
||||
if (provider.fieldExtractor && typeEnv) {
|
||||
const classNode = findEnclosingClassNode(definitionNode);
|
||||
|
|
@ -1948,9 +1921,9 @@ const processFileGroup = (
|
|||
const info = fieldMap?.get(nodeName);
|
||||
if (info) {
|
||||
declaredType = info.type ?? undefined;
|
||||
visibility = info.visibility;
|
||||
isStatic = info.isStatic;
|
||||
isReadonly = info.isReadonly;
|
||||
methodProps.visibility = info.visibility;
|
||||
methodProps.isStatic = info.isStatic;
|
||||
methodProps.isReadonly = info.isReadonly;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1976,21 +1949,8 @@ const processFileGroup = (
|
|||
}
|
||||
: {}),
|
||||
...(description !== undefined ? { description } : {}),
|
||||
...(parameterCount !== undefined ? { parameterCount } : {}),
|
||||
...(requiredParameterCount !== undefined ? { requiredParameterCount } : {}),
|
||||
...(parameterTypes !== undefined ? { parameterTypes } : {}),
|
||||
...(returnType !== undefined ? { returnType } : {}),
|
||||
...methodProps,
|
||||
...(declaredType !== undefined ? { declaredType } : {}),
|
||||
...(visibility !== undefined ? { visibility } : {}),
|
||||
...(isStatic !== undefined ? { isStatic } : {}),
|
||||
...(isReadonly !== undefined ? { isReadonly } : {}),
|
||||
...(isAbstract !== undefined ? { isAbstract } : {}),
|
||||
...(isFinal !== undefined ? { isFinal } : {}),
|
||||
...(isVirtual !== undefined ? { isVirtual } : {}),
|
||||
...(isOverride !== undefined ? { isOverride } : {}),
|
||||
...(isAsync !== undefined ? { isAsync } : {}),
|
||||
...(isPartial !== undefined ? { isPartial } : {}),
|
||||
...(annotations !== undefined ? { annotations } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -2001,22 +1961,30 @@ const processFileGroup = (
|
|||
name: nodeName,
|
||||
nodeId,
|
||||
type: nodeLabel,
|
||||
...(parameterCount !== undefined ? { parameterCount } : {}),
|
||||
...(requiredParameterCount !== undefined ? { requiredParameterCount } : {}),
|
||||
...(parameterTypes !== undefined ? { parameterTypes } : {}),
|
||||
...(returnType !== undefined ? { returnType } : {}),
|
||||
parameterCount: methodProps.parameterCount as number | undefined,
|
||||
requiredParameterCount: methodProps.requiredParameterCount as number | undefined,
|
||||
parameterTypes: methodProps.parameterTypes as string[] | undefined,
|
||||
returnType: methodProps.returnType as string | undefined,
|
||||
...(declaredType !== undefined ? { declaredType } : {}),
|
||||
...(enclosingClassId ? { ownerId: enclosingClassId } : {}),
|
||||
...(visibility !== undefined ? { visibility } : {}),
|
||||
...(isStatic !== undefined ? { isStatic } : {}),
|
||||
...(isReadonly !== undefined ? { isReadonly } : {}),
|
||||
...(isAbstract !== undefined ? { isAbstract } : {}),
|
||||
...(isFinal !== undefined ? { isFinal } : {}),
|
||||
...(isVirtual !== undefined ? { isVirtual } : {}),
|
||||
...(isOverride !== undefined ? { isOverride } : {}),
|
||||
...(isAsync !== undefined ? { isAsync } : {}),
|
||||
...(isPartial !== undefined ? { isPartial } : {}),
|
||||
...(annotations !== undefined ? { annotations } : {}),
|
||||
visibility: methodProps.visibility as string | undefined,
|
||||
isStatic: methodProps.isStatic as boolean | undefined,
|
||||
isReadonly: methodProps.isReadonly as boolean | undefined,
|
||||
isAbstract: methodProps.isAbstract as boolean | undefined,
|
||||
isFinal: methodProps.isFinal as boolean | undefined,
|
||||
...(methodProps.isVirtual !== undefined
|
||||
? { isVirtual: methodProps.isVirtual as boolean }
|
||||
: {}),
|
||||
...(methodProps.isOverride !== undefined
|
||||
? { isOverride: methodProps.isOverride as boolean }
|
||||
: {}),
|
||||
...(methodProps.isAsync !== undefined ? { isAsync: methodProps.isAsync as boolean } : {}),
|
||||
...(methodProps.isPartial !== undefined
|
||||
? { isPartial: methodProps.isPartial as boolean }
|
||||
: {}),
|
||||
...(methodProps.annotations !== undefined
|
||||
? { annotations: methodProps.annotations as string[] }
|
||||
: {}),
|
||||
});
|
||||
|
||||
const fileId = generateId('File', file.path);
|
||||
|
|
|
|||
|
|
@ -96,7 +96,9 @@ export const VALID_RELATION_TYPES = new Set([
|
|||
'IMPLEMENTS',
|
||||
'HAS_METHOD',
|
||||
'HAS_PROPERTY',
|
||||
'OVERRIDES',
|
||||
'METHOD_OVERRIDES',
|
||||
'OVERRIDES', // Legacy alias — dual-read for pre-rename indexes
|
||||
'METHOD_IMPLEMENTS',
|
||||
'ACCESSES',
|
||||
'HANDLES_ROUTE',
|
||||
'FETCHES',
|
||||
|
|
@ -117,7 +119,8 @@ export const VALID_RELATION_TYPES = new Set([
|
|||
* CALLS / IMPORTS – direct, strongly-typed references → 0.9
|
||||
* EXTENDS – class hierarchy, statically verifiable → 0.85
|
||||
* IMPLEMENTS – interface contract, statically verifiable → 0.85
|
||||
* OVERRIDES – method override, statically verifiable → 0.85
|
||||
* METHOD_OVERRIDES – method override, statically verifiable → 0.85
|
||||
* METHOD_IMPLEMENTS – interface method implementation, statically verifiable → 0.85
|
||||
* HAS_METHOD – structural containment → 0.95
|
||||
* HAS_PROPERTY – structural containment → 0.95
|
||||
* ACCESSES – field read/write, may be indirect → 0.8
|
||||
|
|
@ -129,7 +132,8 @@ export const IMPACT_RELATION_CONFIDENCE: Readonly<Record<string, number>> = {
|
|||
IMPORTS: 0.9,
|
||||
EXTENDS: 0.85,
|
||||
IMPLEMENTS: 0.85,
|
||||
OVERRIDES: 0.85,
|
||||
METHOD_OVERRIDES: 0.85,
|
||||
METHOD_IMPLEMENTS: 0.85,
|
||||
HAS_METHOD: 0.95,
|
||||
HAS_PROPERTY: 0.95,
|
||||
ACCESSES: 0.8,
|
||||
|
|
@ -1200,7 +1204,7 @@ export class LocalBackend {
|
|||
repo.id,
|
||||
`
|
||||
MATCH (caller)-[r:CodeRelation]->(n {id: $symId})
|
||||
WHERE r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS', 'HAS_METHOD', 'HAS_PROPERTY', 'OVERRIDES', 'ACCESSES']
|
||||
WHERE r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS', 'HAS_METHOD', 'HAS_PROPERTY', 'METHOD_OVERRIDES', 'OVERRIDES', 'METHOD_IMPLEMENTS', 'ACCESSES']
|
||||
RETURN r.type AS relType, caller.id AS uid, caller.name AS name, caller.filePath AS filePath, labels(caller)[0] AS kind
|
||||
LIMIT 30
|
||||
`,
|
||||
|
|
@ -1290,7 +1294,7 @@ export class LocalBackend {
|
|||
repo.id,
|
||||
`
|
||||
MATCH (n {id: $symId})-[r:CodeRelation]->(target)
|
||||
WHERE r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS', 'HAS_METHOD', 'HAS_PROPERTY', 'OVERRIDES', 'ACCESSES']
|
||||
WHERE r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS', 'HAS_METHOD', 'HAS_PROPERTY', 'METHOD_OVERRIDES', 'OVERRIDES', 'METHOD_IMPLEMENTS', 'ACCESSES']
|
||||
RETURN r.type AS relType, target.id AS uid, target.name AS name, target.filePath AS filePath, labels(target)[0] AS kind
|
||||
LIMIT 30
|
||||
`,
|
||||
|
|
@ -1909,12 +1913,34 @@ export class LocalBackend {
|
|||
|
||||
const { target, direction } = params;
|
||||
const maxDepth = params.maxDepth || 3;
|
||||
// Map legacy relation type names before filtering (backward compat for OVERRIDES → METHOD_OVERRIDES)
|
||||
const mappedRelTypes = params.relationTypes?.flatMap((t: string) =>
|
||||
t === 'OVERRIDES' ? ['OVERRIDES', 'METHOD_OVERRIDES'] : [t],
|
||||
);
|
||||
const rawRelTypes =
|
||||
params.relationTypes && params.relationTypes.length > 0
|
||||
? params.relationTypes.filter((t) => VALID_RELATION_TYPES.has(t))
|
||||
: ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS'];
|
||||
mappedRelTypes && mappedRelTypes.length > 0
|
||||
? mappedRelTypes.filter((t: string) => VALID_RELATION_TYPES.has(t))
|
||||
: [
|
||||
'CALLS',
|
||||
'IMPORTS',
|
||||
'EXTENDS',
|
||||
'IMPLEMENTS',
|
||||
'METHOD_OVERRIDES',
|
||||
'OVERRIDES',
|
||||
'METHOD_IMPLEMENTS',
|
||||
];
|
||||
const relationTypes =
|
||||
rawRelTypes.length > 0 ? rawRelTypes : ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS'];
|
||||
rawRelTypes.length > 0
|
||||
? rawRelTypes
|
||||
: [
|
||||
'CALLS',
|
||||
'IMPORTS',
|
||||
'EXTENDS',
|
||||
'IMPLEMENTS',
|
||||
'METHOD_OVERRIDES',
|
||||
'OVERRIDES',
|
||||
'METHOD_IMPLEMENTS',
|
||||
];
|
||||
const includeTests = params.includeTests ?? false;
|
||||
const minConfidence = params.minConfidence ?? 0;
|
||||
|
||||
|
|
@ -2457,12 +2483,34 @@ export class LocalBackend {
|
|||
const symType =
|
||||
typeof labelRaw === 'string' && labelRaw.trim().length > 0 ? labelRaw.trim() : '';
|
||||
|
||||
// Map legacy relation type names (backward compat for OVERRIDES → METHOD_OVERRIDES)
|
||||
const mappedRelTypes = opts.relationTypes?.flatMap((t: string) =>
|
||||
t === 'OVERRIDES' ? ['OVERRIDES', 'METHOD_OVERRIDES'] : [t],
|
||||
);
|
||||
const rawRelTypes =
|
||||
opts.relationTypes && opts.relationTypes.length > 0
|
||||
? opts.relationTypes.filter((t) => VALID_RELATION_TYPES.has(t))
|
||||
: ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS'];
|
||||
mappedRelTypes && mappedRelTypes.length > 0
|
||||
? mappedRelTypes.filter((t: string) => VALID_RELATION_TYPES.has(t))
|
||||
: [
|
||||
'CALLS',
|
||||
'IMPORTS',
|
||||
'EXTENDS',
|
||||
'IMPLEMENTS',
|
||||
'METHOD_OVERRIDES',
|
||||
'OVERRIDES',
|
||||
'METHOD_IMPLEMENTS',
|
||||
];
|
||||
const relationTypes =
|
||||
rawRelTypes.length > 0 ? rawRelTypes : ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS'];
|
||||
rawRelTypes.length > 0
|
||||
? rawRelTypes
|
||||
: [
|
||||
'CALLS',
|
||||
'IMPORTS',
|
||||
'EXTENDS',
|
||||
'IMPLEMENTS',
|
||||
'METHOD_OVERRIDES',
|
||||
'OVERRIDES',
|
||||
'METHOD_IMPLEMENTS',
|
||||
];
|
||||
|
||||
try {
|
||||
return await this._runImpactBFS(repo, sym, symType, dir, {
|
||||
|
|
|
|||
|
|
@ -353,7 +353,8 @@ relationships:
|
|||
- HAS_METHOD: Class/Struct/Interface owns a Method
|
||||
- HAS_PROPERTY: Class/Struct/Interface owns a Property (field)
|
||||
- ACCESSES: Function/Method reads or writes a Property (reason: 'read' or 'write')
|
||||
- OVERRIDES: Method overrides another Method (MRO)
|
||||
- METHOD_OVERRIDES: Method overrides another Method (MRO)
|
||||
- METHOD_IMPLEMENTS: ConcreteMethod implements InterfaceMethod (matched by name + parameterTypes)
|
||||
- MEMBER_OF: Symbol belongs to community
|
||||
- STEP_IN_PROCESS: Symbol is step N in process
|
||||
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ SCHEMA:
|
|||
- Nodes: File, Folder, Function, Class, Interface, Method, CodeElement, Community, Process, Route, Tool
|
||||
- Multi-language nodes (use backticks): \`Struct\`, \`Enum\`, \`Trait\`, \`Impl\`, etc.
|
||||
- All edges via single CodeRelation table with 'type' property
|
||||
- Edge types: CONTAINS, DEFINES, CALLS, IMPORTS, EXTENDS, IMPLEMENTS, HAS_METHOD, HAS_PROPERTY, ACCESSES, OVERRIDES, MEMBER_OF, STEP_IN_PROCESS, HANDLES_ROUTE, FETCHES, HANDLES_TOOL, ENTRY_POINT_OF
|
||||
- Edge types: CONTAINS, DEFINES, CALLS, IMPORTS, EXTENDS, IMPLEMENTS, HAS_METHOD, HAS_PROPERTY, ACCESSES, METHOD_OVERRIDES, METHOD_IMPLEMENTS, MEMBER_OF, STEP_IN_PROCESS, HANDLES_ROUTE, FETCHES, HANDLES_TOOL, ENTRY_POINT_OF
|
||||
- Edge properties: type (STRING), confidence (DOUBLE), reason (STRING), step (INT32)
|
||||
|
||||
EXAMPLES:
|
||||
|
|
@ -122,7 +122,7 @@ EXAMPLES:
|
|||
MATCH (f:Function)-[r:CodeRelation {type: 'ACCESSES', reason: 'write'}]->(p:Property) WHERE p.name = "address" RETURN f.name, f.filePath
|
||||
|
||||
• Find method overrides (MRO resolution):
|
||||
MATCH (winner:Method)-[r:CodeRelation {type: 'OVERRIDES'}]->(loser:Method) RETURN winner.name, winner.filePath, loser.filePath, r.reason
|
||||
MATCH (winner:Method)-[r:CodeRelation {type: 'METHOD_OVERRIDES'}]->(loser:Method) RETURN winner.name, winner.filePath, loser.filePath, r.reason
|
||||
|
||||
• Detect diamond inheritance:
|
||||
MATCH (d:Class)-[:CodeRelation {type: 'EXTENDS'}]->(b1), (d)-[:CodeRelation {type: 'EXTENDS'}]->(b2), (b1)-[:CodeRelation {type: 'EXTENDS'}]->(a), (b2)-[:CodeRelation {type: 'EXTENDS'}]->(a) WHERE b1 <> b2 RETURN d.name, b1.name, b2.name, a.name
|
||||
|
|
@ -265,7 +265,7 @@ Depth groups:
|
|||
|
||||
TIP: Default traversal uses CALLS/IMPORTS/EXTENDS/IMPLEMENTS. For class members, include HAS_METHOD and HAS_PROPERTY in relationTypes. For field access analysis, include ACCESSES in relationTypes.
|
||||
|
||||
EdgeType: CALLS, IMPORTS, EXTENDS, IMPLEMENTS, HAS_METHOD, HAS_PROPERTY, OVERRIDES, ACCESSES
|
||||
EdgeType: CALLS, IMPORTS, EXTENDS, IMPLEMENTS, HAS_METHOD, HAS_PROPERTY, METHOD_OVERRIDES, METHOD_IMPLEMENTS, ACCESSES
|
||||
Confidence: 1.0 = certain, <0.8 = fuzzy match`,
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
|
|
@ -284,7 +284,7 @@ Confidence: 1.0 = certain, <0.8 = fuzzy match`,
|
|||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description:
|
||||
'Filter: CALLS, IMPORTS, EXTENDS, IMPLEMENTS, HAS_METHOD, HAS_PROPERTY, OVERRIDES, ACCESSES (default: usage-based, ACCESSES excluded by default)',
|
||||
'Filter: CALLS, IMPORTS, EXTENDS, IMPLEMENTS, HAS_METHOD, HAS_PROPERTY, METHOD_OVERRIDES, METHOD_IMPLEMENTS, ACCESSES (default: usage-based, ACCESSES excluded by default)',
|
||||
},
|
||||
includeTests: { type: 'boolean', description: 'Include test files (default: false)' },
|
||||
minConfidence: { type: 'number', description: 'Minimum confidence 0-1 (default: 0.7)' },
|
||||
|
|
|
|||
7
gitnexus/test/fixtures/lang-resolution/csharp-interface-dispatch/App.cs
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/csharp-interface-dispatch/App.cs
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
public class App {
|
||||
public static void Main() {
|
||||
IRepository repo = new SqlRepository();
|
||||
repo.Find(1);
|
||||
repo.Save("test");
|
||||
}
|
||||
}
|
||||
4
gitnexus/test/fixtures/lang-resolution/csharp-interface-dispatch/IRepository.cs
vendored
Normal file
4
gitnexus/test/fixtures/lang-resolution/csharp-interface-dispatch/IRepository.cs
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
public interface IRepository {
|
||||
string Find(int id);
|
||||
bool Save(string entity);
|
||||
}
|
||||
9
gitnexus/test/fixtures/lang-resolution/csharp-interface-dispatch/SqlRepository.cs
vendored
Normal file
9
gitnexus/test/fixtures/lang-resolution/csharp-interface-dispatch/SqlRepository.cs
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
public class SqlRepository : IRepository {
|
||||
public string Find(int id) {
|
||||
return "found";
|
||||
}
|
||||
|
||||
public bool Save(string entity) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
8
gitnexus/test/fixtures/lang-resolution/csharp-overload-dispatch/App.cs
vendored
Normal file
8
gitnexus/test/fixtures/lang-resolution/csharp-overload-dispatch/App.cs
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
public class App {
|
||||
public void Run() {
|
||||
var repo = new SqlRepository();
|
||||
repo.Find(42);
|
||||
repo.Find("alice", true);
|
||||
repo.Save("test");
|
||||
}
|
||||
}
|
||||
5
gitnexus/test/fixtures/lang-resolution/csharp-overload-dispatch/IRepository.cs
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/csharp-overload-dispatch/IRepository.cs
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
public interface IRepository {
|
||||
string Find(int id);
|
||||
string Find(string name, bool exact);
|
||||
void Save(string data);
|
||||
}
|
||||
11
gitnexus/test/fixtures/lang-resolution/csharp-overload-dispatch/SqlRepository.cs
vendored
Normal file
11
gitnexus/test/fixtures/lang-resolution/csharp-overload-dispatch/SqlRepository.cs
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
public class SqlRepository : IRepository {
|
||||
public string Find(int id) {
|
||||
return "found-by-id";
|
||||
}
|
||||
public string Find(string name, bool exact) {
|
||||
return "found-by-name";
|
||||
}
|
||||
public void Save(string data) {
|
||||
Console.WriteLine(data);
|
||||
}
|
||||
}
|
||||
7
gitnexus/test/fixtures/lang-resolution/dart-interface-dispatch/app.dart
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/dart-interface-dispatch/app.dart
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import 'sql_repository.dart';
|
||||
|
||||
void main() {
|
||||
final repo = SqlRepository();
|
||||
repo.find(1);
|
||||
repo.save("test");
|
||||
}
|
||||
4
gitnexus/test/fixtures/lang-resolution/dart-interface-dispatch/repository.dart
vendored
Normal file
4
gitnexus/test/fixtures/lang-resolution/dart-interface-dispatch/repository.dart
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
abstract class Repository {
|
||||
String find(int id);
|
||||
bool save(String entity);
|
||||
}
|
||||
13
gitnexus/test/fixtures/lang-resolution/dart-interface-dispatch/sql_repository.dart
vendored
Normal file
13
gitnexus/test/fixtures/lang-resolution/dart-interface-dispatch/sql_repository.dart
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import 'repository.dart';
|
||||
|
||||
class SqlRepository implements Repository {
|
||||
@override
|
||||
String find(int id) {
|
||||
return "found";
|
||||
}
|
||||
|
||||
@override
|
||||
bool save(String entity) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
8
gitnexus/test/fixtures/lang-resolution/java-overload-dispatch/App.java
vendored
Normal file
8
gitnexus/test/fixtures/lang-resolution/java-overload-dispatch/App.java
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
public class App {
|
||||
public void run() {
|
||||
SqlRepository repo = new SqlRepository();
|
||||
repo.find(42);
|
||||
repo.find("alice", true);
|
||||
repo.save("test");
|
||||
}
|
||||
}
|
||||
5
gitnexus/test/fixtures/lang-resolution/java-overload-dispatch/Repository.java
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/java-overload-dispatch/Repository.java
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
public interface Repository {
|
||||
String find(int id);
|
||||
String find(String name, boolean exact);
|
||||
void save(String data);
|
||||
}
|
||||
13
gitnexus/test/fixtures/lang-resolution/java-overload-dispatch/SqlRepository.java
vendored
Normal file
13
gitnexus/test/fixtures/lang-resolution/java-overload-dispatch/SqlRepository.java
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
public class SqlRepository implements Repository {
|
||||
public String find(int id) {
|
||||
return "found-by-id";
|
||||
}
|
||||
|
||||
public String find(String name, boolean exact) {
|
||||
return "found-by-name";
|
||||
}
|
||||
|
||||
public void save(String data) {
|
||||
System.out.println(data);
|
||||
}
|
||||
}
|
||||
5
gitnexus/test/fixtures/lang-resolution/kotlin-interface-dispatch/App.kt
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/kotlin-interface-dispatch/App.kt
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
fun main() {
|
||||
val repo: Repository = SqlRepository()
|
||||
repo.find(1)
|
||||
repo.save("test")
|
||||
}
|
||||
4
gitnexus/test/fixtures/lang-resolution/kotlin-interface-dispatch/Repository.kt
vendored
Normal file
4
gitnexus/test/fixtures/lang-resolution/kotlin-interface-dispatch/Repository.kt
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
interface Repository {
|
||||
fun find(id: Int): String
|
||||
fun save(entity: String): Boolean
|
||||
}
|
||||
9
gitnexus/test/fixtures/lang-resolution/kotlin-interface-dispatch/SqlRepository.kt
vendored
Normal file
9
gitnexus/test/fixtures/lang-resolution/kotlin-interface-dispatch/SqlRepository.kt
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
class SqlRepository : Repository {
|
||||
override fun find(id: Int): String {
|
||||
return "found"
|
||||
}
|
||||
|
||||
override fun save(entity: String): Boolean {
|
||||
return true
|
||||
}
|
||||
}
|
||||
6
gitnexus/test/fixtures/lang-resolution/kotlin-overload-dispatch/App.kt
vendored
Normal file
6
gitnexus/test/fixtures/lang-resolution/kotlin-overload-dispatch/App.kt
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
fun main() {
|
||||
val repo = SqlRepository()
|
||||
repo.find(42)
|
||||
repo.find("alice", true)
|
||||
repo.save("test")
|
||||
}
|
||||
5
gitnexus/test/fixtures/lang-resolution/kotlin-overload-dispatch/Repository.kt
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/kotlin-overload-dispatch/Repository.kt
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
interface Repository {
|
||||
fun find(id: Int): String
|
||||
fun find(name: String, exact: Boolean): String
|
||||
fun save(data: String)
|
||||
}
|
||||
5
gitnexus/test/fixtures/lang-resolution/kotlin-overload-dispatch/SqlRepository.kt
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/kotlin-overload-dispatch/SqlRepository.kt
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
class SqlRepository : Repository {
|
||||
override fun find(id: Int): String = "found-by-id"
|
||||
override fun find(name: String, exact: Boolean): String = "found-by-name"
|
||||
override fun save(data: String) { println(data) }
|
||||
}
|
||||
4
gitnexus/test/fixtures/lang-resolution/swift-overload-dispatch/App.swift
vendored
Normal file
4
gitnexus/test/fixtures/lang-resolution/swift-overload-dispatch/App.swift
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
let repo = SqlRepository()
|
||||
repo.find(id: 42)
|
||||
repo.find(name: "alice", exact: true)
|
||||
repo.save(data: "test")
|
||||
5
gitnexus/test/fixtures/lang-resolution/swift-overload-dispatch/Repository.swift
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/swift-overload-dispatch/Repository.swift
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
protocol Repository {
|
||||
func find(id: Int) -> String
|
||||
func find(name: String, exact: Bool) -> String
|
||||
func save(data: String)
|
||||
}
|
||||
5
gitnexus/test/fixtures/lang-resolution/swift-overload-dispatch/SqlRepository.swift
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/swift-overload-dispatch/SqlRepository.swift
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
class SqlRepository: Repository {
|
||||
func find(id: Int) -> String { return "found-by-id" }
|
||||
func find(name: String, exact: Bool) -> String { return "found-by-name" }
|
||||
func save(data: String) { print(data) }
|
||||
}
|
||||
6
gitnexus/test/fixtures/lang-resolution/ts-overload-dispatch/app.ts
vendored
Normal file
6
gitnexus/test/fixtures/lang-resolution/ts-overload-dispatch/app.ts
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { SqlRepository } from './sql-repository';
|
||||
|
||||
const repo = new SqlRepository();
|
||||
repo.find(42);
|
||||
repo.find('alice');
|
||||
repo.save('test');
|
||||
5
gitnexus/test/fixtures/lang-resolution/ts-overload-dispatch/repository.ts
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/ts-overload-dispatch/repository.ts
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
export interface IRepository {
|
||||
find(id: number): string;
|
||||
find(name: string): string;
|
||||
save(data: string): void;
|
||||
}
|
||||
12
gitnexus/test/fixtures/lang-resolution/ts-overload-dispatch/sql-repository.ts
vendored
Normal file
12
gitnexus/test/fixtures/lang-resolution/ts-overload-dispatch/sql-repository.ts
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { IRepository } from './repository';
|
||||
|
||||
export class SqlRepository implements IRepository {
|
||||
find(id: number): string;
|
||||
find(name: string): string;
|
||||
find(arg: number | string): string {
|
||||
return typeof arg === 'number' ? 'found-by-id' : 'found-by-name';
|
||||
}
|
||||
save(data: string): void {
|
||||
console.log(data);
|
||||
}
|
||||
}
|
||||
5
gitnexus/test/fixtures/lang-resolution/typescript-interface-dispatch/app.ts
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/typescript-interface-dispatch/app.ts
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { SqlRepository } from './sql-repository';
|
||||
|
||||
const repo = new SqlRepository();
|
||||
repo.find(1);
|
||||
repo.save("test");
|
||||
4
gitnexus/test/fixtures/lang-resolution/typescript-interface-dispatch/repository.ts
vendored
Normal file
4
gitnexus/test/fixtures/lang-resolution/typescript-interface-dispatch/repository.ts
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export interface IRepository {
|
||||
find(id: number): string;
|
||||
save(entity: string): boolean;
|
||||
}
|
||||
11
gitnexus/test/fixtures/lang-resolution/typescript-interface-dispatch/sql-repository.ts
vendored
Normal file
11
gitnexus/test/fixtures/lang-resolution/typescript-interface-dispatch/sql-repository.ts
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { IRepository } from './repository';
|
||||
|
||||
export class SqlRepository implements IRepository {
|
||||
find(id: number): string {
|
||||
return "found";
|
||||
}
|
||||
|
||||
save(entity: string): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
2
gitnexus/test/fixtures/local-backend-seed.ts
vendored
2
gitnexus/test/fixtures/local-backend-seed.ts
vendored
|
|
@ -34,7 +34,7 @@ export const LOCAL_BACKEND_SEED_DATA = [
|
|||
CREATE (c)-[:CodeRelation {type: 'HAS_METHOD', confidence: 1.0, reason: 'class-method', step: 0}]->(m)`,
|
||||
// OVERRIDES: AuthService.authenticate -> BaseService.authenticate
|
||||
`MATCH (a:Method), (b:Method) WHERE a.id = 'method:AuthService.authenticate' AND b.id = 'method:BaseService.authenticate'
|
||||
CREATE (a)-[:CodeRelation {type: 'OVERRIDES', confidence: 1.0, reason: 'mro-resolution', step: 0}]->(b)`,
|
||||
CREATE (a)-[:CodeRelation {type: 'METHOD_OVERRIDES', confidence: 1.0, reason: 'mro-resolution', step: 0}]->(b)`,
|
||||
// HAS_METHOD: BaseService -> authenticate
|
||||
`MATCH (c:Class), (m:Method) WHERE c.id = 'class:BaseService' AND m.id = 'method:BaseService.authenticate'
|
||||
CREATE (c)-[:CodeRelation {type: 'HAS_METHOD', confidence: 1.0, reason: 'class-method', step: 0}]->(m)`,
|
||||
|
|
|
|||
|
|
@ -144,7 +144,7 @@ withTestLbugDB(
|
|||
const result = await backend.callTool('impact', {
|
||||
target: 'authenticate',
|
||||
direction: 'downstream',
|
||||
relationTypes: ['OVERRIDES'],
|
||||
relationTypes: ['METHOD_OVERRIDES'],
|
||||
});
|
||||
expect(result).not.toHaveProperty('error');
|
||||
// AuthService.authenticate overrides BaseService.authenticate
|
||||
|
|
@ -154,6 +154,22 @@ withTestLbugDB(
|
|||
expect(names).toContain('authenticate');
|
||||
});
|
||||
|
||||
it('expands legacy OVERRIDES to include METHOD_OVERRIDES (dual-read)', async () => {
|
||||
// Pass the LEGACY alias 'OVERRIDES' — impactByUid should flatMap-expand
|
||||
// it to ['OVERRIDES', 'METHOD_OVERRIDES'] so the METHOD_OVERRIDES edge
|
||||
// between BaseService.authenticate and AuthService.authenticate is found.
|
||||
const result = await backend.callTool('impact', {
|
||||
target: 'authenticate',
|
||||
direction: 'downstream',
|
||||
relationTypes: ['OVERRIDES'],
|
||||
});
|
||||
expect(result).not.toHaveProperty('error');
|
||||
expect(result.impactedCount).toBeGreaterThanOrEqual(1);
|
||||
const d1 = result.byDepth[1] || result.byDepth['1'] || [];
|
||||
const names = d1.map((d: any) => d.name);
|
||||
expect(names).toContain('authenticate');
|
||||
});
|
||||
|
||||
it('does not return HAS_METHOD results when filtering by CALLS only', async () => {
|
||||
const result = await backend.callTool('impact', {
|
||||
target: 'AuthService',
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ withTestLbugDB(
|
|||
'EXTENDS',
|
||||
'IMPLEMENTS',
|
||||
'HAS_METHOD',
|
||||
'OVERRIDES',
|
||||
'METHOD_OVERRIDES',
|
||||
'ACCESSES',
|
||||
];
|
||||
const invalidTypes = ['CONTAINS', 'STEP_IN_PROCESS', 'MEMBER_OF', 'DROP_TABLE'];
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ describe('C++ diamond inheritance', () => {
|
|||
});
|
||||
|
||||
it('no OVERRIDES edges target Property nodes', () => {
|
||||
const overrides = getRelationships(result, 'OVERRIDES');
|
||||
const overrides = getRelationships(result, 'METHOD_OVERRIDES');
|
||||
for (const edge of overrides) {
|
||||
const target = result.graph.getNode(edge.rel.targetId);
|
||||
expect(target).toBeDefined();
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ describe('C# heritage resolution', () => {
|
|||
});
|
||||
|
||||
it('no OVERRIDES edges target Property nodes', () => {
|
||||
const overrides = getRelationships(result, 'OVERRIDES');
|
||||
const overrides = getRelationships(result, 'METHOD_OVERRIDES');
|
||||
for (const edge of overrides) {
|
||||
const target = result.graph.getNode(edge.rel.targetId);
|
||||
expect(target).toBeDefined();
|
||||
|
|
@ -1722,3 +1722,109 @@ describe('C# method enrichment', () => {
|
|||
expect(classifyCall).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Interface dispatch: METHOD_IMPLEMENTS edges
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('C# interface dispatch (METHOD_IMPLEMENTS)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(path.join(FIXTURES, 'csharp-interface-dispatch'), () => {});
|
||||
}, 60000);
|
||||
|
||||
it('detects IRepository interface and SqlRepository class', () => {
|
||||
const classes = getNodesByLabel(result, 'Class');
|
||||
const ifaces = getNodesByLabel(result, 'Interface');
|
||||
expect(classes).toContain('SqlRepository');
|
||||
expect(ifaces).toContain('IRepository');
|
||||
});
|
||||
|
||||
it('emits IMPLEMENTS edge SqlRepository → IRepository', () => {
|
||||
const impl = getRelationships(result, 'IMPLEMENTS');
|
||||
const edge = impl.find((e) => e.source === 'SqlRepository' && e.target === 'IRepository');
|
||||
expect(edge).toBeDefined();
|
||||
});
|
||||
|
||||
it('emits METHOD_IMPLEMENTS edges for Find and Save', () => {
|
||||
const mi = getRelationships(result, 'METHOD_IMPLEMENTS');
|
||||
const findEdge = mi.find(
|
||||
(e) =>
|
||||
e.source === 'Find' &&
|
||||
e.target === 'Find' &&
|
||||
e.sourceFilePath.includes('SqlRepository') &&
|
||||
e.targetFilePath.includes('IRepository'),
|
||||
);
|
||||
const saveEdge = mi.find(
|
||||
(e) =>
|
||||
e.source === 'Save' &&
|
||||
e.target === 'Save' &&
|
||||
e.sourceFilePath.includes('SqlRepository') &&
|
||||
e.targetFilePath.includes('IRepository'),
|
||||
);
|
||||
expect(findEdge).toBeDefined();
|
||||
expect(saveEdge).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Overloaded method disambiguation: METHOD_IMPLEMENTS with overloads
|
||||
// IRepository declares Find(int), Find(string), Save(string).
|
||||
// SqlRepository implements all three.
|
||||
// Overloaded methods (same name, different params) collapse into a single
|
||||
// graph node (generateId drops startLine), so Find appears once per file.
|
||||
// METHOD_IMPLEMENTS still emits one edge per unique (source, target) pair.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('C# overloaded method disambiguation (METHOD_IMPLEMENTS)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(path.join(FIXTURES, 'csharp-overload-dispatch'), () => {});
|
||||
}, 60000);
|
||||
|
||||
it('detects 2 distinct Find Method nodes on SqlRepository (different arities)', () => {
|
||||
const methods = getNodesByLabelFull(result, 'Method');
|
||||
const findOnSql = methods.filter(
|
||||
(m) => m.name === 'Find' && m.properties.filePath?.includes('SqlRepository'),
|
||||
);
|
||||
expect(findOnSql.length).toBe(2);
|
||||
});
|
||||
|
||||
it('emits METHOD_IMPLEMENTS edges for both Find overloads', () => {
|
||||
const mi = getRelationships(result, 'METHOD_IMPLEMENTS');
|
||||
const findEdges = mi.filter(
|
||||
(e) =>
|
||||
e.source === 'Find' &&
|
||||
e.target === 'Find' &&
|
||||
e.sourceFilePath.includes('SqlRepository') &&
|
||||
e.targetFilePath.includes('IRepository'),
|
||||
);
|
||||
expect(findEdges.length).toBe(2);
|
||||
});
|
||||
|
||||
it('emits METHOD_IMPLEMENTS for Save -> IRepository.Save', () => {
|
||||
const mi = getRelationships(result, 'METHOD_IMPLEMENTS');
|
||||
const saveEdge = mi.find(
|
||||
(e) =>
|
||||
e.source === 'Save' &&
|
||||
e.target === 'Save' &&
|
||||
e.sourceFilePath.includes('SqlRepository') &&
|
||||
e.targetFilePath.includes('IRepository'),
|
||||
);
|
||||
expect(saveEdge).toBeDefined();
|
||||
});
|
||||
|
||||
it('emits exactly 3 METHOD_IMPLEMENTS edges', () => {
|
||||
const mi = getRelationships(result, 'METHOD_IMPLEMENTS');
|
||||
expect(mi.length).toBe(3);
|
||||
});
|
||||
|
||||
it('detects SqlRepository class and IRepository interface', () => {
|
||||
const classes = getNodesByLabel(result, 'Class');
|
||||
const ifaces = getNodesByLabel(result, 'Interface');
|
||||
expect(classes).toContain('SqlRepository');
|
||||
expect(ifaces).toContain('IRepository');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -429,3 +429,48 @@ describe.skipIf(!dartAvailable)('Dart async method detection', () => {
|
|||
expect(formatName!.properties.returnType).toBe('String');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Interface dispatch: METHOD_IMPLEMENTS edges from concrete → abstract methods
|
||||
// abstract Repository with find/save, SqlRepository implements them
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe.skipIf(!dartAvailable)('Dart interface dispatch (METHOD_IMPLEMENTS)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(path.join(FIXTURES, 'dart-interface-dispatch'), () => {});
|
||||
}, 60000);
|
||||
|
||||
it('detects Repository class and SqlRepository class', () => {
|
||||
const classes = getNodesByLabel(result, 'Class');
|
||||
expect(classes).toContain('Repository');
|
||||
expect(classes).toContain('SqlRepository');
|
||||
});
|
||||
|
||||
it('emits IMPLEMENTS edge SqlRepository → Repository', () => {
|
||||
const impl = getRelationships(result, 'IMPLEMENTS');
|
||||
const edge = impl.find((e) => e.source === 'SqlRepository' && e.target === 'Repository');
|
||||
expect(edge).toBeDefined();
|
||||
});
|
||||
|
||||
it('emits METHOD_IMPLEMENTS edges for find and save', () => {
|
||||
const mi = getRelationships(result, 'METHOD_IMPLEMENTS');
|
||||
const findEdge = mi.find(
|
||||
(e) =>
|
||||
e.source === 'find' &&
|
||||
e.target === 'find' &&
|
||||
e.sourceFilePath.includes('sql_repository') &&
|
||||
e.targetFilePath.includes('repository'),
|
||||
);
|
||||
const saveEdge = mi.find(
|
||||
(e) =>
|
||||
e.source === 'save' &&
|
||||
e.target === 'save' &&
|
||||
e.sourceFilePath.includes('sql_repository') &&
|
||||
e.targetFilePath.includes('repository'),
|
||||
);
|
||||
expect(findEdge).toBeDefined();
|
||||
expect(saveEdge).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ describe('Go package import & call resolution', () => {
|
|||
});
|
||||
|
||||
it('no OVERRIDES edges target Property nodes', () => {
|
||||
const overrides = getRelationships(result, 'OVERRIDES');
|
||||
const overrides = getRelationships(result, 'METHOD_OVERRIDES');
|
||||
for (const edge of overrides) {
|
||||
const target = result.graph.getNode(edge.rel.targetId);
|
||||
expect(target).toBeDefined();
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ describe('Java heritage resolution', () => {
|
|||
});
|
||||
|
||||
it('no OVERRIDES edges target Property nodes', () => {
|
||||
const overrides = getRelationships(result, 'OVERRIDES');
|
||||
const overrides = getRelationships(result, 'METHOD_OVERRIDES');
|
||||
for (const edge of overrides) {
|
||||
const target = result.graph.getNode(edge.rel.targetId);
|
||||
expect(target).toBeDefined();
|
||||
|
|
@ -385,6 +385,42 @@ describe('Java variadic call resolution', () => {
|
|||
expect(logCall!.source).toBe('run');
|
||||
expect(logCall!.targetFilePath).toBe('com/example/util/Logger.java');
|
||||
});
|
||||
|
||||
it('CALLS edges from within variadic method have valid sourceId (no ID mismatch)', () => {
|
||||
// Collect all CALLS edges whose source is in Logger.java
|
||||
const danglingSourceIds: string[] = [];
|
||||
for (const rel of result.graph.iterRelationships()) {
|
||||
if (rel.type !== 'CALLS') continue;
|
||||
const sourceNode = result.graph.getNode(rel.sourceId);
|
||||
if (!sourceNode) {
|
||||
danglingSourceIds.push(rel.sourceId);
|
||||
continue;
|
||||
}
|
||||
// Specifically flag Logger.java sources that don't resolve
|
||||
if (
|
||||
sourceNode.properties.filePath === 'com/example/util/Logger.java' &&
|
||||
!result.graph.getNode(rel.sourceId)
|
||||
) {
|
||||
danglingSourceIds.push(rel.sourceId);
|
||||
}
|
||||
}
|
||||
|
||||
// No CALLS edge should have a dangling (unresolvable) sourceId.
|
||||
// This catches the bug where definition creates Method:...record#N but
|
||||
// findEnclosingFunctionId generates Method:...record (no suffix),
|
||||
// producing CALLS edges whose sourceId doesn't match any graph node.
|
||||
expect(danglingSourceIds).toEqual([]);
|
||||
|
||||
// Additionally verify that ALL relationships (not just CALLS) have
|
||||
// resolvable sourceIds — a stronger invariant.
|
||||
const allDangling: string[] = [];
|
||||
for (const rel of result.graph.iterRelationships()) {
|
||||
if (!result.graph.getNode(rel.sourceId)) {
|
||||
allDangling.push(`${rel.type}:${rel.sourceId}`);
|
||||
}
|
||||
}
|
||||
expect(allDangling).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -1651,3 +1687,165 @@ describe('Java method enrichment', () => {
|
|||
expect(classifyCall).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Java interface dispatch (METHOD_IMPLEMENTS)
|
||||
// Action interface: execute(), priority()
|
||||
// LogEvent implements Action, SendEmail implements Action
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Java interface dispatch (METHOD_IMPLEMENTS)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(path.join(FIXTURES, 'java-interface-dispatch'), () => {});
|
||||
}, 60000);
|
||||
|
||||
it('emits METHOD_IMPLEMENTS edges from LogEvent.execute → Action.execute', () => {
|
||||
const mi = getRelationships(result, 'METHOD_IMPLEMENTS');
|
||||
const edge = mi.find(
|
||||
(e) =>
|
||||
e.source === 'execute' &&
|
||||
e.target === 'execute' &&
|
||||
e.sourceFilePath.includes('LogEvent') &&
|
||||
e.targetFilePath.includes('Action'),
|
||||
);
|
||||
expect(edge).toBeDefined();
|
||||
});
|
||||
|
||||
it('emits METHOD_IMPLEMENTS edges from SendEmail.execute → Action.execute', () => {
|
||||
const mi = getRelationships(result, 'METHOD_IMPLEMENTS');
|
||||
const edge = mi.find(
|
||||
(e) =>
|
||||
e.source === 'execute' &&
|
||||
e.target === 'execute' &&
|
||||
e.sourceFilePath.includes('SendEmail') &&
|
||||
e.targetFilePath.includes('Action'),
|
||||
);
|
||||
expect(edge).toBeDefined();
|
||||
});
|
||||
|
||||
it('emits METHOD_IMPLEMENTS for priority() in both implementors', () => {
|
||||
const mi = getRelationships(result, 'METHOD_IMPLEMENTS');
|
||||
const priorityEdges = mi.filter(
|
||||
(e) =>
|
||||
e.source === 'priority' && e.target === 'priority' && e.targetFilePath.includes('Action'),
|
||||
);
|
||||
expect(priorityEdges.length).toBe(2);
|
||||
const sourceFiles = priorityEdges.map((e) => e.sourceFilePath).sort();
|
||||
expect(sourceFiles.some((f) => f.includes('LogEvent'))).toBe(true);
|
||||
expect(sourceFiles.some((f) => f.includes('SendEmail'))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Java overloaded method disambiguation (METHOD_IMPLEMENTS with arity)
|
||||
// Repository interface: find(int), find(String, boolean), save(String)
|
||||
// SqlRepository implements Repository with matching overloads
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Java overloaded method disambiguation (METHOD_IMPLEMENTS)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(path.join(FIXTURES, 'java-overload-dispatch'), () => {});
|
||||
}, 60000);
|
||||
|
||||
it('detects distinct Method nodes for overloaded find methods on SqlRepository', () => {
|
||||
const methods = getNodesByLabelFull(result, 'Method');
|
||||
const findMethods = methods.filter(
|
||||
(m) => m.name === 'find' && m.properties.filePath?.includes('SqlRepository'),
|
||||
);
|
||||
expect(findMethods.length).toBe(2);
|
||||
const paramCounts = findMethods.map((m) => m.properties.parameterCount).sort();
|
||||
expect(paramCounts).toEqual([1, 2]);
|
||||
});
|
||||
|
||||
it('detects distinct Method nodes for overloaded find methods on Repository interface', () => {
|
||||
const methods = getNodesByLabelFull(result, 'Method');
|
||||
const findMethods = methods.filter(
|
||||
(m) =>
|
||||
m.name === 'find' &&
|
||||
m.properties.filePath?.includes('Repository') &&
|
||||
!m.properties.filePath?.includes('SqlRepository'),
|
||||
);
|
||||
expect(findMethods.length).toBe(2);
|
||||
const paramCounts = findMethods.map((m) => m.properties.parameterCount).sort();
|
||||
expect(paramCounts).toEqual([1, 2]);
|
||||
});
|
||||
|
||||
it('emits METHOD_IMPLEMENTS for find(int) → Repository.find(int)', () => {
|
||||
const mi = getRelationships(result, 'METHOD_IMPLEMENTS');
|
||||
const edge = mi.find(
|
||||
(e) =>
|
||||
e.source === 'find' &&
|
||||
e.target === 'find' &&
|
||||
e.sourceFilePath.includes('SqlRepository') &&
|
||||
e.targetFilePath.includes('Repository'),
|
||||
);
|
||||
expect(edge).toBeDefined();
|
||||
// Verify at least one find→find edge has arity 1 on source side
|
||||
const findEdges = mi.filter(
|
||||
(e) =>
|
||||
e.source === 'find' &&
|
||||
e.target === 'find' &&
|
||||
e.sourceFilePath.includes('SqlRepository') &&
|
||||
e.targetFilePath.includes('Repository'),
|
||||
);
|
||||
const sourceNodes = findEdges.map((e) => {
|
||||
const methods = getNodesByLabelFull(result, 'Method');
|
||||
return methods.find(
|
||||
(m) =>
|
||||
m.name === 'find' &&
|
||||
m.properties.filePath?.includes('SqlRepository') &&
|
||||
m.properties.parameterCount === 1,
|
||||
);
|
||||
});
|
||||
expect(sourceNodes.some((n) => n !== undefined)).toBe(true);
|
||||
});
|
||||
|
||||
it('emits METHOD_IMPLEMENTS for find(String, boolean) → Repository.find(String, boolean)', () => {
|
||||
const mi = getRelationships(result, 'METHOD_IMPLEMENTS');
|
||||
const findEdges = mi.filter(
|
||||
(e) =>
|
||||
e.source === 'find' &&
|
||||
e.target === 'find' &&
|
||||
e.sourceFilePath.includes('SqlRepository') &&
|
||||
e.targetFilePath.includes('Repository'),
|
||||
);
|
||||
// There should be two find→find edges (one per overload)
|
||||
expect(findEdges.length).toBe(2);
|
||||
});
|
||||
|
||||
it('emits METHOD_IMPLEMENTS for save(String) → Repository.save(String)', () => {
|
||||
const mi = getRelationships(result, 'METHOD_IMPLEMENTS');
|
||||
const edge = mi.find(
|
||||
(e) =>
|
||||
e.source === 'save' &&
|
||||
e.target === 'save' &&
|
||||
e.sourceFilePath.includes('SqlRepository') &&
|
||||
e.targetFilePath.includes('Repository'),
|
||||
);
|
||||
expect(edge).toBeDefined();
|
||||
});
|
||||
|
||||
it('emits exactly 3 METHOD_IMPLEMENTS edges', () => {
|
||||
const mi = getRelationships(result, 'METHOD_IMPLEMENTS');
|
||||
const edges = mi.filter(
|
||||
(e) => e.sourceFilePath.includes('SqlRepository') && e.targetFilePath.includes('Repository'),
|
||||
);
|
||||
expect(edges.length).toBe(3);
|
||||
});
|
||||
|
||||
it('emits CALLS edges from run() to both find overloads', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const findCalls = calls.filter(
|
||||
(c) =>
|
||||
c.source === 'run' &&
|
||||
c.target === 'find' &&
|
||||
c.sourceFilePath.includes('App') &&
|
||||
c.targetFilePath.includes('SqlRepository'),
|
||||
);
|
||||
expect(findCalls.length).toBe(2);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ describe('Kotlin heritage resolution', () => {
|
|||
});
|
||||
|
||||
it('no OVERRIDES edges target Property nodes', () => {
|
||||
const overrides = getRelationships(result, 'OVERRIDES');
|
||||
const overrides = getRelationships(result, 'METHOD_OVERRIDES');
|
||||
for (const edge of overrides) {
|
||||
const target = result.graph.getNode(edge.rel.targetId);
|
||||
expect(target).toBeDefined();
|
||||
|
|
@ -1811,3 +1811,100 @@ describe('Kotlin method enrichment', () => {
|
|||
expect(classifyCall).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Interface dispatch: METHOD_IMPLEMENTS edges from concrete → interface methods
|
||||
// Repository interface with find/save, SqlRepository implements them
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Kotlin interface dispatch (METHOD_IMPLEMENTS)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(path.join(FIXTURES, 'kotlin-interface-dispatch'), () => {});
|
||||
}, 60000);
|
||||
|
||||
it('detects Repository interface and SqlRepository class', () => {
|
||||
const classes = getNodesByLabel(result, 'Class');
|
||||
const ifaces = getNodesByLabel(result, 'Interface');
|
||||
expect(classes).toContain('SqlRepository');
|
||||
expect(ifaces).toContain('Repository');
|
||||
});
|
||||
|
||||
it('emits IMPLEMENTS edge SqlRepository → Repository', () => {
|
||||
const impl = getRelationships(result, 'IMPLEMENTS');
|
||||
const edge = impl.find((e) => e.source === 'SqlRepository' && e.target === 'Repository');
|
||||
expect(edge).toBeDefined();
|
||||
});
|
||||
|
||||
it('emits METHOD_IMPLEMENTS edges for find and save', () => {
|
||||
const mi = getRelationships(result, 'METHOD_IMPLEMENTS');
|
||||
const findEdge = mi.find(
|
||||
(e) =>
|
||||
e.source === 'find' &&
|
||||
e.target === 'find' &&
|
||||
e.sourceFilePath.includes('SqlRepository') &&
|
||||
e.targetFilePath.includes('Repository'),
|
||||
);
|
||||
const saveEdge = mi.find(
|
||||
(e) =>
|
||||
e.source === 'save' &&
|
||||
e.target === 'save' &&
|
||||
e.sourceFilePath.includes('SqlRepository') &&
|
||||
e.targetFilePath.includes('Repository'),
|
||||
);
|
||||
expect(findEdge).toBeDefined();
|
||||
expect(saveEdge).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Overloaded method disambiguation: interface with overloaded find + save,
|
||||
// concrete class implements all three. Verifies METHOD_IMPLEMENTS edges
|
||||
// correctly distinguish between overloaded signatures.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Kotlin overloaded method disambiguation', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(path.join(FIXTURES, 'kotlin-overload-dispatch'), () => {});
|
||||
}, 60000);
|
||||
|
||||
it('detects 2 distinct find Method nodes on SqlRepository', () => {
|
||||
const methods = getNodesByLabelFull(result, 'Method');
|
||||
const sqlRepoFinds = methods.filter(
|
||||
(m) => m.name === 'find' && m.properties.filePath?.includes('SqlRepository'),
|
||||
);
|
||||
expect(sqlRepoFinds.length).toBe(2);
|
||||
});
|
||||
|
||||
it('emits METHOD_IMPLEMENTS edges for both find overloads', () => {
|
||||
const mi = getRelationships(result, 'METHOD_IMPLEMENTS');
|
||||
const findEdges = mi.filter(
|
||||
(e) =>
|
||||
e.source === 'find' &&
|
||||
e.target === 'find' &&
|
||||
e.sourceFilePath.includes('SqlRepository') &&
|
||||
e.targetFilePath.includes('Repository'),
|
||||
);
|
||||
expect(findEdges.length).toBe(2);
|
||||
});
|
||||
|
||||
it('emits METHOD_IMPLEMENTS edge for save', () => {
|
||||
const mi = getRelationships(result, 'METHOD_IMPLEMENTS');
|
||||
const saveEdge = mi.find(
|
||||
(e) =>
|
||||
e.source === 'save' &&
|
||||
e.target === 'save' &&
|
||||
e.sourceFilePath.includes('SqlRepository') &&
|
||||
e.targetFilePath.includes('Repository'),
|
||||
);
|
||||
expect(saveEdge).toBeDefined();
|
||||
});
|
||||
|
||||
it('emits exactly 3 METHOD_IMPLEMENTS edges total', () => {
|
||||
const mi = getRelationships(result, 'METHOD_IMPLEMENTS');
|
||||
expect(mi.length).toBe(3);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -128,7 +128,7 @@ describe('PHP heritage & import resolution', () => {
|
|||
// --- Property OVERRIDES exclusion ---
|
||||
|
||||
it('does not emit OVERRIDES for property name collisions ($status in both traits)', () => {
|
||||
const overrides = getRelationships(result, 'OVERRIDES');
|
||||
const overrides = getRelationships(result, 'METHOD_OVERRIDES');
|
||||
// OVERRIDES should only target Method nodes, never Property nodes
|
||||
for (const edge of overrides) {
|
||||
const target = result.graph.getNode(edge.rel.targetId);
|
||||
|
|
@ -140,7 +140,7 @@ describe('PHP heritage & import resolution', () => {
|
|||
// --- MRO: OVERRIDES edge ---
|
||||
|
||||
it('emits OVERRIDES edge for User overriding log (inherited from BaseModel)', () => {
|
||||
const overrides = getRelationships(result, 'OVERRIDES');
|
||||
const overrides = getRelationships(result, 'METHOD_OVERRIDES');
|
||||
expect(overrides.length).toBe(1);
|
||||
const logOverride = overrides.find((e) => e.source === 'User' && e.target === 'log');
|
||||
expect(logOverride).toBeDefined();
|
||||
|
|
@ -1769,4 +1769,14 @@ describe('PHP abstract dispatch', () => {
|
|||
expect(params).toContain('int');
|
||||
}
|
||||
});
|
||||
|
||||
it('emits METHOD_IMPLEMENTS edges from SqlRepository methods → Repository interface methods', () => {
|
||||
const mi = getRelationships(result, 'METHOD_IMPLEMENTS');
|
||||
const edges = mi.filter(
|
||||
(e) => e.sourceFilePath.includes('SqlRepository') && e.targetFilePath.includes('Repository'),
|
||||
);
|
||||
expect(edges.length).toBe(2);
|
||||
const names = edges.map((e) => e.source).sort();
|
||||
expect(names).toEqual(['find', 'save']);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ describe('Python relative import & heritage resolution', () => {
|
|||
});
|
||||
|
||||
it('no OVERRIDES edges target Property nodes', () => {
|
||||
const overrides = getRelationships(result, 'OVERRIDES');
|
||||
const overrides = getRelationships(result, 'METHOD_OVERRIDES');
|
||||
for (const edge of overrides) {
|
||||
const target = result.graph.getNode(edge.rel.targetId);
|
||||
expect(target).toBeDefined();
|
||||
|
|
@ -2100,4 +2100,14 @@ describe('Python abstract dispatch', () => {
|
|||
expect(params).toContain('int');
|
||||
}
|
||||
});
|
||||
|
||||
it('does not emit METHOD_IMPLEMENTS for abstract-class inheritance (only interface/trait parents)', () => {
|
||||
// Python ABC is modelled as a Class with EXTENDS (not Interface with IMPLEMENTS),
|
||||
// so the MRO processor does not emit METHOD_IMPLEMENTS edges here.
|
||||
const mi = getRelationships(result, 'METHOD_IMPLEMENTS');
|
||||
const edges = mi.filter(
|
||||
(e) => e.sourceFilePath.includes('impl.py') && e.targetFilePath.includes('base.py'),
|
||||
);
|
||||
expect(edges.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -194,7 +194,7 @@ describe('Ruby require_relative, heritage & property resolution', () => {
|
|||
// --- No OVERRIDES edges target Property nodes ---
|
||||
|
||||
it('no OVERRIDES edges target Property nodes', () => {
|
||||
const overrides = getRelationships(result, 'OVERRIDES');
|
||||
const overrides = getRelationships(result, 'METHOD_OVERRIDES');
|
||||
for (const edge of overrides) {
|
||||
const target = result.graph.getNode(edge.rel.targetId);
|
||||
expect(target).toBeDefined();
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ describe('Rust trait implementation resolution', () => {
|
|||
});
|
||||
|
||||
it('no OVERRIDES edges target Property nodes', () => {
|
||||
const overrides = getRelationships(result, 'OVERRIDES');
|
||||
const overrides = getRelationships(result, 'METHOD_OVERRIDES');
|
||||
for (const edge of overrides) {
|
||||
const target = result.graph.getNode(edge.rel.targetId);
|
||||
expect(target).toBeDefined();
|
||||
|
|
@ -1847,4 +1847,13 @@ describe('Rust abstract dispatch (Repository trait)', () => {
|
|||
expect(saveCall).toBeDefined();
|
||||
expect(countCall).toBeDefined();
|
||||
});
|
||||
|
||||
it('emits METHOD_IMPLEMENTS edges from SqlRepo impl methods → Repository trait methods', () => {
|
||||
const mi = getRelationships(result, 'METHOD_IMPLEMENTS');
|
||||
// find and save are required trait methods; count has a default impl so no METHOD_IMPLEMENTS
|
||||
const libEdges = mi.filter((e) => e.sourceFilePath.includes('lib.rs'));
|
||||
expect(libEdges.length).toBe(2);
|
||||
const names = libEdges.map((e) => e.source).sort();
|
||||
expect(names).toEqual(['find', 'save']);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -799,4 +799,69 @@ describe.skipIf(!swiftAvailable)('Swift abstract dispatch', () => {
|
|||
expect(sqlFind).toBeDefined();
|
||||
expect(sqlFind!.properties.returnType).toBe('String');
|
||||
});
|
||||
|
||||
it('emits METHOD_IMPLEMENTS edges from SqlRepository methods → Repository protocol methods', () => {
|
||||
const mi = getRelationships(result, 'METHOD_IMPLEMENTS');
|
||||
const edges = mi.filter((e) => e.sourceFilePath.includes('Repository.swift'));
|
||||
expect(edges.length).toBe(2);
|
||||
const names = edges.map((e) => e.source).sort();
|
||||
expect(names).toEqual(['find', 'save']);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Overloaded method disambiguation: protocol with overloaded find + save,
|
||||
// concrete class implements all three. Verifies METHOD_IMPLEMENTS edges
|
||||
// correctly distinguish between overloaded signatures.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe.skipIf(!swiftAvailable)('Swift overloaded method disambiguation', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(path.join(FIXTURES, 'swift-overload-dispatch'), () => {});
|
||||
}, 60000);
|
||||
|
||||
it('detects 2 distinct find Method nodes on SqlRepository', () => {
|
||||
const methods = getNodesByLabelFull(result, 'Method');
|
||||
const sqlRepoFinds = methods.filter(
|
||||
(m) => m.name === 'find' && m.properties.filePath?.includes('SqlRepository'),
|
||||
);
|
||||
// Swift class methods may be emitted as Function nodes
|
||||
const functions = getNodesByLabelFull(result, 'Function');
|
||||
const sqlRepoFindFns = functions.filter(
|
||||
(m) => m.name === 'find' && m.properties.filePath?.includes('SqlRepository'),
|
||||
);
|
||||
const totalFinds = sqlRepoFinds.length + sqlRepoFindFns.length;
|
||||
expect(totalFinds).toBe(2);
|
||||
});
|
||||
|
||||
it('emits METHOD_IMPLEMENTS edges for both find overloads', () => {
|
||||
const mi = getRelationships(result, 'METHOD_IMPLEMENTS');
|
||||
const findEdges = mi.filter(
|
||||
(e) =>
|
||||
e.source === 'find' &&
|
||||
e.target === 'find' &&
|
||||
e.sourceFilePath.includes('SqlRepository') &&
|
||||
e.targetFilePath.includes('Repository'),
|
||||
);
|
||||
expect(findEdges.length).toBe(2);
|
||||
});
|
||||
|
||||
it('emits METHOD_IMPLEMENTS edge for save', () => {
|
||||
const mi = getRelationships(result, 'METHOD_IMPLEMENTS');
|
||||
const saveEdge = mi.find(
|
||||
(e) =>
|
||||
e.source === 'save' &&
|
||||
e.target === 'save' &&
|
||||
e.sourceFilePath.includes('SqlRepository') &&
|
||||
e.targetFilePath.includes('Repository'),
|
||||
);
|
||||
expect(saveEdge).toBeDefined();
|
||||
});
|
||||
|
||||
it('emits exactly 3 METHOD_IMPLEMENTS edges total', () => {
|
||||
const mi = getRelationships(result, 'METHOD_IMPLEMENTS');
|
||||
expect(mi.length).toBe(3);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ describe('TypeScript heritage resolution', () => {
|
|||
});
|
||||
|
||||
it('no OVERRIDES edges target Property nodes', () => {
|
||||
const overrides = getRelationships(result, 'OVERRIDES');
|
||||
const overrides = getRelationships(result, 'METHOD_OVERRIDES');
|
||||
for (const edge of overrides) {
|
||||
const target = result.graph.getNode(edge.rel.targetId);
|
||||
expect(target).toBeDefined();
|
||||
|
|
@ -886,7 +886,7 @@ describe('TypeScript return type inference via explicit function return type', (
|
|||
});
|
||||
|
||||
it('resolves user.save() to User#save via return type of getUser(): User', () => {
|
||||
// TS has explicit return types in the source, so extractMethodSignature captures
|
||||
// TS has explicit return types in the source, so the method extractor captures
|
||||
// the return type. The TS extractInitializer handles `const user = getUser()`
|
||||
// via the variable_declarator path, enabling save() to resolve to User#save.
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
|
|
@ -2395,3 +2395,104 @@ describe('TypeScript method enrichment', () => {
|
|||
expect(classifyCall).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Interface dispatch: METHOD_IMPLEMENTS edges
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('TypeScript interface dispatch (METHOD_IMPLEMENTS)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'typescript-interface-dispatch'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('detects IRepository interface and SqlRepository class', () => {
|
||||
const classes = getNodesByLabel(result, 'Class');
|
||||
const ifaces = getNodesByLabel(result, 'Interface');
|
||||
expect(classes).toContain('SqlRepository');
|
||||
expect(ifaces).toContain('IRepository');
|
||||
});
|
||||
|
||||
it('emits IMPLEMENTS edge SqlRepository → IRepository', () => {
|
||||
const impl = getRelationships(result, 'IMPLEMENTS');
|
||||
const edge = impl.find((e) => e.source === 'SqlRepository' && e.target === 'IRepository');
|
||||
expect(edge).toBeDefined();
|
||||
});
|
||||
|
||||
it('emits METHOD_IMPLEMENTS edges for find and save', () => {
|
||||
const mi = getRelationships(result, 'METHOD_IMPLEMENTS');
|
||||
const findEdge = mi.find(
|
||||
(e) =>
|
||||
e.source === 'find' &&
|
||||
e.target === 'find' &&
|
||||
e.sourceFilePath.includes('sql-repository') &&
|
||||
e.targetFilePath.includes('repository'),
|
||||
);
|
||||
const saveEdge = mi.find(
|
||||
(e) =>
|
||||
e.source === 'save' &&
|
||||
e.target === 'save' &&
|
||||
e.sourceFilePath.includes('sql-repository') &&
|
||||
e.targetFilePath.includes('repository'),
|
||||
);
|
||||
expect(findEdge).toBeDefined();
|
||||
expect(saveEdge).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Overloaded method disambiguation: interface with overloaded find + save,
|
||||
// concrete class implements all three. TypeScript overloads collapse to one
|
||||
// implementation signature — expect the implementation body, not individual
|
||||
// overload signatures.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('TypeScript overloaded method disambiguation', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(path.join(FIXTURES, 'ts-overload-dispatch'), () => {});
|
||||
}, 60000);
|
||||
|
||||
it('emits METHOD_IMPLEMENTS edge for find', () => {
|
||||
const mi = getRelationships(result, 'METHOD_IMPLEMENTS');
|
||||
const findEdge = mi.find(
|
||||
(e) =>
|
||||
e.source === 'find' &&
|
||||
e.target === 'find' &&
|
||||
e.sourceFilePath.includes('sql-repository') &&
|
||||
e.targetFilePath.includes('repository'),
|
||||
);
|
||||
expect(findEdge).toBeDefined();
|
||||
});
|
||||
|
||||
it('emits METHOD_IMPLEMENTS edge for save', () => {
|
||||
const mi = getRelationships(result, 'METHOD_IMPLEMENTS');
|
||||
const saveEdge = mi.find(
|
||||
(e) =>
|
||||
e.source === 'save' &&
|
||||
e.target === 'save' &&
|
||||
e.sourceFilePath.includes('sql-repository') &&
|
||||
e.targetFilePath.includes('repository'),
|
||||
);
|
||||
expect(saveEdge).toBeDefined();
|
||||
});
|
||||
|
||||
it('TypeScript overloads collapse — find has one implementation METHOD_IMPLEMENTS edge', () => {
|
||||
const mi = getRelationships(result, 'METHOD_IMPLEMENTS');
|
||||
// TypeScript overloads collapse to one implementation signature,
|
||||
// so we expect a single METHOD_IMPLEMENTS edge for find (not two)
|
||||
const findEdges = mi.filter(
|
||||
(e) =>
|
||||
e.source === 'find' &&
|
||||
e.target === 'find' &&
|
||||
e.sourceFilePath.includes('sql-repository') &&
|
||||
e.targetFilePath.includes('repository'),
|
||||
);
|
||||
expect(findEdges.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -34,8 +34,12 @@ describe('IMPACT_RELATION_CONFIDENCE', () => {
|
|||
expect(IMPACT_RELATION_CONFIDENCE['IMPLEMENTS']).toBe(0.85);
|
||||
});
|
||||
|
||||
it('OVERRIDES has confidence 0.85 (statically verifiable override)', () => {
|
||||
expect(IMPACT_RELATION_CONFIDENCE['OVERRIDES']).toBe(0.85);
|
||||
it('METHOD_OVERRIDES has confidence 0.85 (statically verifiable override)', () => {
|
||||
expect(IMPACT_RELATION_CONFIDENCE['METHOD_OVERRIDES']).toBe(0.85);
|
||||
});
|
||||
|
||||
it('METHOD_IMPLEMENTS has confidence 0.85 (statically verifiable implementation)', () => {
|
||||
expect(IMPACT_RELATION_CONFIDENCE['METHOD_IMPLEMENTS']).toBe(0.85);
|
||||
});
|
||||
|
||||
it('HAS_METHOD has confidence 0.95 (structural containment)', () => {
|
||||
|
|
@ -76,7 +80,8 @@ describe('confidenceForRelType', () => {
|
|||
expect(confidenceForRelType('IMPORTS')).toBe(0.9);
|
||||
expect(confidenceForRelType('EXTENDS')).toBe(0.85);
|
||||
expect(confidenceForRelType('IMPLEMENTS')).toBe(0.85);
|
||||
expect(confidenceForRelType('OVERRIDES')).toBe(0.85);
|
||||
expect(confidenceForRelType('METHOD_OVERRIDES')).toBe(0.85);
|
||||
expect(confidenceForRelType('METHOD_IMPLEMENTS')).toBe(0.85);
|
||||
expect(confidenceForRelType('HAS_METHOD')).toBe(0.95);
|
||||
expect(confidenceForRelType('HAS_PROPERTY')).toBe(0.95);
|
||||
expect(confidenceForRelType('ACCESSES')).toBe(0.8);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { getLanguageFromFilename, SupportedLanguages } from 'gitnexus-shared';
|
||||
import { getProvider } from '../../src/core/ingestion/languages/index.js';
|
||||
import { extractFunctionName } from '../../src/core/ingestion/utils/ast-helpers.js';
|
||||
import type { SyntaxNode } from '../../src/core/ingestion/utils/ast-helpers.js';
|
||||
import type { NodeLabel } from 'gitnexus-shared';
|
||||
import type { LanguageProvider } from '../../src/core/ingestion/language-provider.js';
|
||||
import {
|
||||
getTreeSitterBufferSize,
|
||||
TREE_SITTER_BUFFER_SIZE,
|
||||
|
|
@ -341,8 +343,23 @@ describe('isBuiltInOrNoise', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('extractFunctionName', () => {
|
||||
describe('extractFunctionName (via methodExtractor)', () => {
|
||||
const parser = new Parser();
|
||||
const cProvider = getProvider(SupportedLanguages.C);
|
||||
const cppProvider = getProvider(SupportedLanguages.CPlusPlus);
|
||||
const tsProvider = getProvider(SupportedLanguages.TypeScript);
|
||||
|
||||
/** Test helper: extracts function name using methodExtractor hook with generic fallback. */
|
||||
const extractFunctionName = (
|
||||
node: SyntaxNode | null,
|
||||
provider?: LanguageProvider,
|
||||
): { funcName: string | null; label: NodeLabel } => {
|
||||
if (!node) return { funcName: null, label: 'Function' };
|
||||
const result = provider?.methodExtractor?.extractFunctionName?.(node);
|
||||
if (result) return result;
|
||||
const funcName = node.childForFieldName?.('name')?.text ?? null;
|
||||
return { funcName, label: 'Function' };
|
||||
};
|
||||
|
||||
describe('C', () => {
|
||||
it('extracts function name from C function definition', () => {
|
||||
|
|
@ -351,7 +368,7 @@ describe('extractFunctionName', () => {
|
|||
const tree = parser.parse(code);
|
||||
const funcNode = tree.rootNode.child(0);
|
||||
|
||||
const result = extractFunctionName(funcNode);
|
||||
const result = extractFunctionName(funcNode, cProvider);
|
||||
|
||||
expect(result.funcName).toBe('main');
|
||||
expect(result.label).toBe('Function');
|
||||
|
|
@ -363,7 +380,7 @@ describe('extractFunctionName', () => {
|
|||
const tree = parser.parse(code);
|
||||
const funcNode = tree.rootNode.child(0);
|
||||
|
||||
const result = extractFunctionName(funcNode);
|
||||
const result = extractFunctionName(funcNode, cProvider);
|
||||
|
||||
expect(result.funcName).toBe('helper');
|
||||
expect(result.label).toBe('Function');
|
||||
|
|
@ -377,7 +394,7 @@ describe('extractFunctionName', () => {
|
|||
const tree = parser.parse(code);
|
||||
const funcNode = tree.rootNode.child(0);
|
||||
|
||||
const result = extractFunctionName(funcNode);
|
||||
const result = extractFunctionName(funcNode, cppProvider);
|
||||
|
||||
expect(result.funcName).toBe('OnEncryptData');
|
||||
expect(result.label).toBe('Method');
|
||||
|
|
@ -389,7 +406,7 @@ describe('extractFunctionName', () => {
|
|||
const tree = parser.parse(code);
|
||||
const funcNode = tree.rootNode.child(0);
|
||||
|
||||
const result = extractFunctionName(funcNode);
|
||||
const result = extractFunctionName(funcNode, cppProvider);
|
||||
|
||||
expect(result.funcName).toBe('OnDataOprEvent');
|
||||
expect(result.label).toBe('Method');
|
||||
|
|
@ -401,7 +418,7 @@ describe('extractFunctionName', () => {
|
|||
const tree = parser.parse(code);
|
||||
const funcNode = tree.rootNode.child(0);
|
||||
|
||||
const result = extractFunctionName(funcNode);
|
||||
const result = extractFunctionName(funcNode, cppProvider);
|
||||
|
||||
expect(result.funcName).toBe('standalone_function');
|
||||
expect(result.label).toBe('Function');
|
||||
|
|
@ -413,7 +430,7 @@ describe('extractFunctionName', () => {
|
|||
const tree = parser.parse(code);
|
||||
const funcNode = tree.rootNode.child(0);
|
||||
|
||||
const result = extractFunctionName(funcNode);
|
||||
const result = extractFunctionName(funcNode, cppProvider);
|
||||
|
||||
expect(result.funcName).toBe('handler');
|
||||
expect(result.label).toBe('Method');
|
||||
|
|
@ -427,7 +444,7 @@ describe('extractFunctionName', () => {
|
|||
const tree = parser.parse(code);
|
||||
const funcNode = tree.rootNode.child(0);
|
||||
|
||||
const result = extractFunctionName(funcNode);
|
||||
const result = extractFunctionName(funcNode, cProvider);
|
||||
|
||||
expect(result.funcName).toBe('get_data');
|
||||
expect(result.label).toBe('Function');
|
||||
|
|
@ -439,7 +456,7 @@ describe('extractFunctionName', () => {
|
|||
const tree = parser.parse(code);
|
||||
const funcNode = tree.rootNode.child(0);
|
||||
|
||||
const result = extractFunctionName(funcNode);
|
||||
const result = extractFunctionName(funcNode, cProvider);
|
||||
|
||||
expect(result.funcName).toBe('get_strings');
|
||||
expect(result.label).toBe('Function');
|
||||
|
|
@ -451,7 +468,7 @@ describe('extractFunctionName', () => {
|
|||
const tree = parser.parse(code);
|
||||
const funcNode = tree.rootNode.child(0);
|
||||
|
||||
const result = extractFunctionName(funcNode);
|
||||
const result = extractFunctionName(funcNode, cProvider);
|
||||
|
||||
expect(result.funcName).toBe('create_node');
|
||||
expect(result.label).toBe('Function');
|
||||
|
|
@ -465,7 +482,7 @@ describe('extractFunctionName', () => {
|
|||
const tree = parser.parse(code);
|
||||
const funcNode = tree.rootNode.child(0);
|
||||
|
||||
const result = extractFunctionName(funcNode);
|
||||
const result = extractFunctionName(funcNode, cppProvider);
|
||||
|
||||
expect(result.funcName).toBe('getData');
|
||||
expect(result.label).toBe('Method');
|
||||
|
|
@ -477,7 +494,7 @@ describe('extractFunctionName', () => {
|
|||
const tree = parser.parse(code);
|
||||
const funcNode = tree.rootNode.child(0);
|
||||
|
||||
const result = extractFunctionName(funcNode);
|
||||
const result = extractFunctionName(funcNode, cppProvider);
|
||||
|
||||
expect(result.funcName).toBe('get_name');
|
||||
expect(result.label).toBe('Function');
|
||||
|
|
@ -489,7 +506,7 @@ describe('extractFunctionName', () => {
|
|||
const tree = parser.parse(code);
|
||||
const funcNode = tree.rootNode.child(0);
|
||||
|
||||
const result = extractFunctionName(funcNode);
|
||||
const result = extractFunctionName(funcNode, cppProvider);
|
||||
|
||||
expect(result.funcName).toBe('at');
|
||||
expect(result.label).toBe('Method');
|
||||
|
|
@ -501,7 +518,7 @@ describe('extractFunctionName', () => {
|
|||
const tree = parser.parse(code);
|
||||
const funcNode = tree.rootNode.child(0);
|
||||
|
||||
const result = extractFunctionName(funcNode);
|
||||
const result = extractFunctionName(funcNode, cppProvider);
|
||||
|
||||
expect(result.funcName).toBe('getName');
|
||||
expect(result.label).toBe('Method');
|
||||
|
|
@ -515,7 +532,7 @@ describe('extractFunctionName', () => {
|
|||
const tree = parser.parse(code);
|
||||
const funcNode = tree.rootNode.child(0);
|
||||
|
||||
const result = extractFunctionName(funcNode);
|
||||
const result = extractFunctionName(funcNode, cppProvider);
|
||||
|
||||
// destructor_name includes the ~ prefix
|
||||
expect(result.funcName).toBe('~MyClass');
|
||||
|
|
@ -533,7 +550,7 @@ describe('extractFunctionName', () => {
|
|||
const declarator = varDecl!.namedChild(0);
|
||||
const arrowFunc = declarator!.namedChild(1);
|
||||
|
||||
const result = extractFunctionName(arrowFunc);
|
||||
const result = extractFunctionName(arrowFunc, tsProvider);
|
||||
|
||||
expect(result.funcName).toBe('myHandler');
|
||||
expect(result.label).toBe('Function');
|
||||
|
|
@ -548,7 +565,7 @@ describe('extractFunctionName', () => {
|
|||
const declarator = varDecl!.namedChild(0);
|
||||
const funcExpr = declarator!.namedChild(1);
|
||||
|
||||
const result = extractFunctionName(funcExpr);
|
||||
const result = extractFunctionName(funcExpr, tsProvider);
|
||||
|
||||
expect(result.funcName).toBe('processItem');
|
||||
expect(result.label).toBe('Function');
|
||||
|
|
|
|||
|
|
@ -1,523 +0,0 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { extractMethodSignature } from '../../src/core/ingestion/utils/ast-helpers.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 CPP from 'tree-sitter-cpp';
|
||||
import Go from 'tree-sitter-go';
|
||||
import Rust from 'tree-sitter-rust';
|
||||
|
||||
describe('extractMethodSignature', () => {
|
||||
const parser = new Parser();
|
||||
|
||||
it('returns zero params and no return type for null node', () => {
|
||||
const sig = extractMethodSignature(null);
|
||||
expect(sig.parameterCount).toBe(0);
|
||||
expect(sig.returnType).toBeUndefined();
|
||||
});
|
||||
|
||||
describe('TypeScript', () => {
|
||||
it('extracts params and return type from a typed method', () => {
|
||||
parser.setLanguage(TypeScript.typescript);
|
||||
const code = `class Foo {
|
||||
greet(name: string, age: number): boolean { return true; }
|
||||
}`;
|
||||
const tree = parser.parse(code);
|
||||
const classNode = tree.rootNode.child(0)!;
|
||||
const classBody = classNode.childForFieldName('body')!;
|
||||
const methodNode = classBody.namedChild(0)!;
|
||||
|
||||
const sig = extractMethodSignature(methodNode);
|
||||
expect(sig.parameterCount).toBe(2);
|
||||
expect(sig.returnType).toBe('boolean');
|
||||
});
|
||||
|
||||
it('extracts zero params from a method with no parameters', () => {
|
||||
parser.setLanguage(TypeScript.typescript);
|
||||
const code = `class Foo {
|
||||
run(): void {}
|
||||
}`;
|
||||
const tree = parser.parse(code);
|
||||
const classNode = tree.rootNode.child(0)!;
|
||||
const classBody = classNode.childForFieldName('body')!;
|
||||
const methodNode = classBody.namedChild(0)!;
|
||||
|
||||
const sig = extractMethodSignature(methodNode);
|
||||
expect(sig.parameterCount).toBe(0);
|
||||
expect(sig.returnType).toBe('void');
|
||||
});
|
||||
|
||||
it('extracts params without return type annotation', () => {
|
||||
parser.setLanguage(TypeScript.typescript);
|
||||
const code = `class Foo {
|
||||
process(x: number) { return x + 1; }
|
||||
}`;
|
||||
const tree = parser.parse(code);
|
||||
const classNode = tree.rootNode.child(0)!;
|
||||
const classBody = classNode.childForFieldName('body')!;
|
||||
const methodNode = classBody.namedChild(0)!;
|
||||
|
||||
const sig = extractMethodSignature(methodNode);
|
||||
expect(sig.parameterCount).toBe(1);
|
||||
expect(sig.returnType).toBeUndefined();
|
||||
});
|
||||
|
||||
it('skips TypeScript this-parameter (compile-time constraint)', () => {
|
||||
parser.setLanguage(TypeScript.typescript);
|
||||
const code = `class Handler {
|
||||
handle(this: void, event: Event): void {}
|
||||
}`;
|
||||
const tree = parser.parse(code);
|
||||
const classNode = tree.rootNode.child(0)!;
|
||||
const classBody = classNode.childForFieldName('body')!;
|
||||
const methodNode = classBody.namedChild(0)!;
|
||||
|
||||
const sig = extractMethodSignature(methodNode);
|
||||
// 'this' is not a real parameter — only 'event' should be counted
|
||||
expect(sig.parameterCount).toBe(1);
|
||||
});
|
||||
|
||||
it('skips this-parameter in top-level function', () => {
|
||||
parser.setLanguage(TypeScript.typescript);
|
||||
const code = `function onClick(this: HTMLElement, ev: MouseEvent): void {}`;
|
||||
const tree = parser.parse(code);
|
||||
const funcNode = tree.rootNode.child(0)!;
|
||||
|
||||
const sig = extractMethodSignature(funcNode);
|
||||
expect(sig.parameterCount).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Python', () => {
|
||||
it('skips self parameter', () => {
|
||||
parser.setLanguage(Python);
|
||||
const code = `class Foo:
|
||||
def bar(self, x, y):
|
||||
pass`;
|
||||
const tree = parser.parse(code);
|
||||
const classNode = tree.rootNode.child(0)!;
|
||||
const classBody = classNode.childForFieldName('body')!;
|
||||
const methodNode = classBody.namedChild(0)!;
|
||||
|
||||
const sig = extractMethodSignature(methodNode);
|
||||
expect(sig.parameterCount).toBe(2);
|
||||
expect(sig.returnType).toBeUndefined();
|
||||
});
|
||||
|
||||
it('handles method with only self', () => {
|
||||
parser.setLanguage(Python);
|
||||
const code = `class Foo:
|
||||
def noop(self):
|
||||
pass`;
|
||||
const tree = parser.parse(code);
|
||||
const classNode = tree.rootNode.child(0)!;
|
||||
const classBody = classNode.childForFieldName('body')!;
|
||||
const methodNode = classBody.namedChild(0)!;
|
||||
|
||||
const sig = extractMethodSignature(methodNode);
|
||||
expect(sig.parameterCount).toBe(0);
|
||||
});
|
||||
|
||||
it('handles Python return type annotation', () => {
|
||||
parser.setLanguage(Python);
|
||||
const code = `class Foo:
|
||||
def bar(self, x: int) -> bool:
|
||||
return True`;
|
||||
const tree = parser.parse(code);
|
||||
const classNode = tree.rootNode.child(0)!;
|
||||
const classBody = classNode.childForFieldName('body')!;
|
||||
const methodNode = classBody.namedChild(0)!;
|
||||
|
||||
const sig = extractMethodSignature(methodNode);
|
||||
expect(sig.parameterCount).toBe(1);
|
||||
// The important thing is parameterCount is correct; returnType may vary.
|
||||
});
|
||||
});
|
||||
|
||||
describe('Java', () => {
|
||||
it('extracts params from a Java method', () => {
|
||||
parser.setLanguage(Java);
|
||||
const code = `class Foo {
|
||||
public int add(int a, int b) { return a + b; }
|
||||
}`;
|
||||
const tree = parser.parse(code);
|
||||
const classNode = tree.rootNode.child(0)!;
|
||||
const classBody = classNode.childForFieldName('body')!;
|
||||
const methodNode = classBody.namedChild(0)!;
|
||||
|
||||
const sig = extractMethodSignature(methodNode);
|
||||
expect(sig.parameterCount).toBe(2);
|
||||
});
|
||||
|
||||
it('extracts zero params from no-arg Java method', () => {
|
||||
parser.setLanguage(Java);
|
||||
const code = `class Foo {
|
||||
public void run() {}
|
||||
}`;
|
||||
const tree = parser.parse(code);
|
||||
const classNode = tree.rootNode.child(0)!;
|
||||
const classBody = classNode.childForFieldName('body')!;
|
||||
const methodNode = classBody.namedChild(0)!;
|
||||
|
||||
const sig = extractMethodSignature(methodNode);
|
||||
expect(sig.parameterCount).toBe(0);
|
||||
});
|
||||
|
||||
it('extracts parameterTypes for Java overloaded methods', () => {
|
||||
parser.setLanguage(Java);
|
||||
const code = `class Svc {
|
||||
public User lookup(int id) { return null; }
|
||||
public User lookup(String name) { return null; }
|
||||
public void process(int code, String msg) {}
|
||||
}`;
|
||||
const tree = parser.parse(code);
|
||||
const classBody = tree.rootNode.child(0)!.childForFieldName('body')!;
|
||||
|
||||
const sig0 = extractMethodSignature(classBody.namedChild(0)!);
|
||||
expect(sig0.parameterCount).toBe(1);
|
||||
expect(sig0.parameterTypes).toEqual(['int']);
|
||||
|
||||
const sig1 = extractMethodSignature(classBody.namedChild(1)!);
|
||||
expect(sig1.parameterCount).toBe(1);
|
||||
expect(sig1.parameterTypes).toEqual(['String']);
|
||||
|
||||
const sig2 = extractMethodSignature(classBody.namedChild(2)!);
|
||||
expect(sig2.parameterCount).toBe(2);
|
||||
expect(sig2.parameterTypes).toEqual(['int', 'String']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Kotlin', () => {
|
||||
it('extracts params from a Kotlin function declaration', () => {
|
||||
parser.setLanguage(Kotlin);
|
||||
const code = `object OneArg {
|
||||
fun writeAudit(message: String): String {
|
||||
return message
|
||||
}
|
||||
}`;
|
||||
const tree = parser.parse(code);
|
||||
const objectNode = tree.rootNode.child(0)!;
|
||||
const classBody = objectNode.namedChild(1)!;
|
||||
const functionNode = classBody.namedChild(0)!;
|
||||
|
||||
const sig = extractMethodSignature(functionNode);
|
||||
expect(sig.parameterCount).toBe(1);
|
||||
});
|
||||
|
||||
it('extracts zero params from a no-arg Kotlin function', () => {
|
||||
parser.setLanguage(Kotlin);
|
||||
const code = `object ZeroArg {
|
||||
fun writeAudit(): String {
|
||||
return "zero"
|
||||
}
|
||||
}`;
|
||||
const tree = parser.parse(code);
|
||||
const objectNode = tree.rootNode.child(0)!;
|
||||
const classBody = objectNode.namedChild(1)!;
|
||||
const functionNode = classBody.namedChild(0)!;
|
||||
|
||||
const sig = extractMethodSignature(functionNode);
|
||||
expect(sig.parameterCount).toBe(0);
|
||||
});
|
||||
|
||||
it('extracts parameterTypes for Kotlin overloaded functions', () => {
|
||||
parser.setLanguage(Kotlin);
|
||||
const code = `class Svc {
|
||||
fun lookup(id: Int): User? { return null }
|
||||
fun lookup(name: String): User? { return null }
|
||||
}`;
|
||||
const tree = parser.parse(code);
|
||||
const classBody = tree.rootNode.child(0)!.namedChild(1)!;
|
||||
|
||||
const sig0 = extractMethodSignature(classBody.namedChild(0)!);
|
||||
expect(sig0.parameterCount).toBe(1);
|
||||
expect(sig0.parameterTypes).toEqual(['Int']);
|
||||
|
||||
const sig1 = extractMethodSignature(classBody.namedChild(1)!);
|
||||
expect(sig1.parameterCount).toBe(1);
|
||||
expect(sig1.parameterTypes).toEqual(['String']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('C++', () => {
|
||||
it('extracts params from a nested C++ declarator', () => {
|
||||
parser.setLanguage(CPP);
|
||||
const code = `inline const char* write_audit(const char* message) {
|
||||
return message;
|
||||
}`;
|
||||
const tree = parser.parse(code);
|
||||
const functionNode = tree.rootNode.namedChild(0)!;
|
||||
|
||||
const sig = extractMethodSignature(functionNode);
|
||||
expect(sig.parameterCount).toBe(1);
|
||||
});
|
||||
|
||||
it('extracts zero params from a no-arg C++ function', () => {
|
||||
parser.setLanguage(CPP);
|
||||
const code = `inline const char* write_audit() {
|
||||
return "zero";
|
||||
}`;
|
||||
const tree = parser.parse(code);
|
||||
const functionNode = tree.rootNode.namedChild(0)!;
|
||||
|
||||
const sig = extractMethodSignature(functionNode);
|
||||
expect(sig.parameterCount).toBe(0);
|
||||
});
|
||||
|
||||
it('extracts parameterTypes for C++ overloaded functions', () => {
|
||||
parser.setLanguage(CPP);
|
||||
const code = `User* lookup(int id) { return nullptr; }
|
||||
User* lookup(string name) { return nullptr; }`;
|
||||
const tree = parser.parse(code);
|
||||
|
||||
const sig0 = extractMethodSignature(tree.rootNode.namedChild(0)!);
|
||||
expect(sig0.parameterCount).toBe(1);
|
||||
expect(sig0.parameterTypes).toEqual(['int']);
|
||||
|
||||
const sig1 = extractMethodSignature(tree.rootNode.namedChild(1)!);
|
||||
expect(sig1.parameterCount).toBe(1);
|
||||
expect(sig1.parameterTypes).toEqual(['string']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('C#', () => {
|
||||
it('extracts params from a C# method', () => {
|
||||
parser.setLanguage(CSharp);
|
||||
const code = `class Foo {
|
||||
public bool Check(string name, int count) { return true; }
|
||||
}`;
|
||||
const tree = parser.parse(code);
|
||||
const classNode = tree.rootNode.child(0)!;
|
||||
const classBody = classNode.childForFieldName('body')!;
|
||||
const methodNode = classBody.namedChild(0)!;
|
||||
|
||||
const sig = extractMethodSignature(methodNode);
|
||||
expect(sig.parameterCount).toBe(2);
|
||||
});
|
||||
|
||||
it('extracts parameterTypes for C# overloaded methods', () => {
|
||||
parser.setLanguage(CSharp);
|
||||
const code = `class Svc {
|
||||
public User Lookup(int id) { return null; }
|
||||
public User Lookup(string name) { return null; }
|
||||
}`;
|
||||
const tree = parser.parse(code);
|
||||
const classBody = tree.rootNode.child(0)!.childForFieldName('body')!;
|
||||
|
||||
const sig0 = extractMethodSignature(classBody.namedChild(0)!);
|
||||
expect(sig0.parameterCount).toBe(1);
|
||||
expect(sig0.parameterTypes).toEqual(['int']);
|
||||
|
||||
const sig1 = extractMethodSignature(classBody.namedChild(1)!);
|
||||
expect(sig1.parameterCount).toBe(1);
|
||||
expect(sig1.parameterTypes).toEqual(['string']);
|
||||
});
|
||||
|
||||
it('handles C# method with no params', () => {
|
||||
parser.setLanguage(CSharp);
|
||||
const code = `class Foo {
|
||||
public void Execute() {}
|
||||
}`;
|
||||
const tree = parser.parse(code);
|
||||
const classNode = tree.rootNode.child(0)!;
|
||||
const classBody = classNode.childForFieldName('body')!;
|
||||
const methodNode = classBody.namedChild(0)!;
|
||||
|
||||
const sig = extractMethodSignature(methodNode);
|
||||
expect(sig.parameterCount).toBe(0);
|
||||
});
|
||||
|
||||
it('extracts return type from C# method', () => {
|
||||
parser.setLanguage(CSharp);
|
||||
const code = `class Svc {
|
||||
public User GetUser(string name) { return null; }
|
||||
}`;
|
||||
const tree = parser.parse(code);
|
||||
const classNode = tree.rootNode.child(0)!;
|
||||
const classBody = classNode.childForFieldName('body')!;
|
||||
const methodNode = classBody.namedChild(0)!;
|
||||
|
||||
const sig = extractMethodSignature(methodNode);
|
||||
expect(sig.returnType).toBe('User');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Go', () => {
|
||||
it('extracts params and single return type', () => {
|
||||
parser.setLanguage(Go);
|
||||
const code = `package main
|
||||
func add(a int, b int) int { return a + b }`;
|
||||
const tree = parser.parse(code);
|
||||
const funcNode = tree.rootNode.namedChildren.find((c) => c.type === 'function_declaration')!;
|
||||
|
||||
const sig = extractMethodSignature(funcNode);
|
||||
expect(sig.parameterCount).toBe(2);
|
||||
expect(sig.returnType).toBe('int');
|
||||
});
|
||||
|
||||
it('extracts multi-return type', () => {
|
||||
parser.setLanguage(Go);
|
||||
const code = `package main
|
||||
func parse(s string) (string, error) { return s, nil }`;
|
||||
const tree = parser.parse(code);
|
||||
const funcNode = tree.rootNode.namedChildren.find((c) => c.type === 'function_declaration')!;
|
||||
|
||||
const sig = extractMethodSignature(funcNode);
|
||||
expect(sig.parameterCount).toBe(1);
|
||||
expect(sig.returnType).toBe('string');
|
||||
});
|
||||
|
||||
it('handles no return type', () => {
|
||||
parser.setLanguage(Go);
|
||||
const code = `package main
|
||||
func doSomething(x int) { }`;
|
||||
const tree = parser.parse(code);
|
||||
const funcNode = tree.rootNode.namedChildren.find((c) => c.type === 'function_declaration')!;
|
||||
|
||||
const sig = extractMethodSignature(funcNode);
|
||||
expect(sig.parameterCount).toBe(1);
|
||||
expect(sig.returnType).toBeUndefined();
|
||||
});
|
||||
|
||||
it('marks variadic function with undefined parameterCount', () => {
|
||||
parser.setLanguage(Go);
|
||||
const code = `package main
|
||||
func log(args ...string) int { return 0 }`;
|
||||
const tree = parser.parse(code);
|
||||
const funcNode = tree.rootNode.namedChildren.find((c) => c.type === 'function_declaration')!;
|
||||
|
||||
const sig = extractMethodSignature(funcNode);
|
||||
expect(sig.parameterCount).toBeUndefined();
|
||||
expect(sig.returnType).toBe('int');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Rust', () => {
|
||||
it('extracts return type from function', () => {
|
||||
parser.setLanguage(Rust);
|
||||
const code = `fn add(a: i32, b: i32) -> i32 { a + b }`;
|
||||
const tree = parser.parse(code);
|
||||
const funcNode = tree.rootNode.namedChild(0)!;
|
||||
|
||||
const sig = extractMethodSignature(funcNode);
|
||||
expect(sig.parameterCount).toBe(2);
|
||||
expect(sig.returnType).toBe('i32');
|
||||
});
|
||||
});
|
||||
|
||||
describe('C++ return types', () => {
|
||||
it('extracts primitive return type', () => {
|
||||
parser.setLanguage(CPP);
|
||||
const code = `int add(int a, int b) { return a + b; }`;
|
||||
const tree = parser.parse(code);
|
||||
const funcNode = tree.rootNode.namedChild(0)!;
|
||||
|
||||
const sig = extractMethodSignature(funcNode);
|
||||
expect(sig.parameterCount).toBe(2);
|
||||
expect(sig.returnType).toBe('int');
|
||||
});
|
||||
|
||||
it('extracts qualified return type', () => {
|
||||
parser.setLanguage(CPP);
|
||||
const code = `std::string getName() { return ""; }`;
|
||||
const tree = parser.parse(code);
|
||||
const funcNode = tree.rootNode.namedChild(0)!;
|
||||
|
||||
const sig = extractMethodSignature(funcNode);
|
||||
expect(sig.parameterCount).toBe(0);
|
||||
expect(sig.returnType).toBe('std::string');
|
||||
});
|
||||
|
||||
it('returns undefined returnType for void', () => {
|
||||
parser.setLanguage(CPP);
|
||||
const code = `void doNothing() { }`;
|
||||
const tree = parser.parse(code);
|
||||
const funcNode = tree.rootNode.namedChild(0)!;
|
||||
|
||||
const sig = extractMethodSignature(funcNode);
|
||||
expect(sig.returnType).toBeUndefined();
|
||||
});
|
||||
|
||||
it('marks variadic function with undefined parameterCount', () => {
|
||||
parser.setLanguage(CPP);
|
||||
const code = `int printf(const char* fmt, ...) { return 0; }`;
|
||||
const tree = parser.parse(code);
|
||||
const funcNode = tree.rootNode.namedChild(0)!;
|
||||
|
||||
const sig = extractMethodSignature(funcNode);
|
||||
expect(sig.parameterCount).toBeUndefined();
|
||||
expect(sig.returnType).toBe('int');
|
||||
});
|
||||
});
|
||||
|
||||
describe('variadic params', () => {
|
||||
it('Java: marks varargs with undefined parameterCount', () => {
|
||||
parser.setLanguage(Java);
|
||||
const code = `class Foo {
|
||||
public void log(String fmt, Object... args) {}
|
||||
}`;
|
||||
const tree = parser.parse(code);
|
||||
const classNode = tree.rootNode.child(0)!;
|
||||
const classBody = classNode.childForFieldName('body')!;
|
||||
const methodNode = classBody.namedChild(0)!;
|
||||
|
||||
const sig = extractMethodSignature(methodNode);
|
||||
expect(sig.parameterCount).toBeUndefined();
|
||||
});
|
||||
|
||||
it('Python: marks *args with undefined parameterCount', () => {
|
||||
parser.setLanguage(Python);
|
||||
const code = `class Foo:
|
||||
def log(self, fmt, *args):
|
||||
pass`;
|
||||
const tree = parser.parse(code);
|
||||
const classNode = tree.rootNode.child(0)!;
|
||||
const classBody = classNode.childForFieldName('body')!;
|
||||
const methodNode = classBody.namedChild(0)!;
|
||||
|
||||
const sig = extractMethodSignature(methodNode);
|
||||
expect(sig.parameterCount).toBeUndefined();
|
||||
});
|
||||
|
||||
it('Python: marks **kwargs with undefined parameterCount', () => {
|
||||
parser.setLanguage(Python);
|
||||
const code = `class Foo:
|
||||
def config(self, **kwargs):
|
||||
pass`;
|
||||
const tree = parser.parse(code);
|
||||
const classNode = tree.rootNode.child(0)!;
|
||||
const classBody = classNode.childForFieldName('body')!;
|
||||
const methodNode = classBody.namedChild(0)!;
|
||||
|
||||
const sig = extractMethodSignature(methodNode);
|
||||
expect(sig.parameterCount).toBeUndefined();
|
||||
});
|
||||
|
||||
it('TypeScript: marks rest params with undefined parameterCount', () => {
|
||||
parser.setLanguage(TypeScript.typescript);
|
||||
const code = `function logEntry(...messages: string[]): void {}`;
|
||||
const tree = parser.parse(code);
|
||||
const funcNode = tree.rootNode.namedChild(0)!;
|
||||
|
||||
const sig = extractMethodSignature(funcNode);
|
||||
expect(sig.parameterCount).toBeUndefined();
|
||||
});
|
||||
|
||||
it('Kotlin: marks vararg with undefined parameterCount', () => {
|
||||
parser.setLanguage(Kotlin);
|
||||
const code = `object Foo {
|
||||
fun log(vararg args: String) {}
|
||||
}`;
|
||||
const tree = parser.parse(code);
|
||||
const objectNode = tree.rootNode.child(0)!;
|
||||
const classBody = objectNode.namedChild(1)!;
|
||||
const functionNode = classBody.namedChild(0)!;
|
||||
|
||||
const sig = extractMethodSignature(functionNode);
|
||||
expect(sig.parameterCount).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -106,7 +106,7 @@ describe('isWriteQuery', () => {
|
|||
|
||||
describe('VALID_RELATION_TYPES', () => {
|
||||
it('contains all expected relation types', () => {
|
||||
expect(VALID_RELATION_TYPES.size).toBe(13);
|
||||
expect(VALID_RELATION_TYPES.size).toBe(15);
|
||||
for (const t of [
|
||||
'CALLS',
|
||||
'IMPORTS',
|
||||
|
|
@ -114,7 +114,9 @@ describe('VALID_RELATION_TYPES', () => {
|
|||
'IMPLEMENTS',
|
||||
'HAS_METHOD',
|
||||
'HAS_PROPERTY',
|
||||
'METHOD_OVERRIDES',
|
||||
'OVERRIDES',
|
||||
'METHOD_IMPLEMENTS',
|
||||
'ACCESSES',
|
||||
'HANDLES_ROUTE',
|
||||
'FETCHES',
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue