mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-08 03:08:13 +00:00
fix: link object literal methods to exported bindings
This commit is contained in:
parent
803f0bed5f
commit
9a27e6cb50
4 changed files with 122 additions and 8 deletions
|
|
@ -14,6 +14,7 @@ import { isVerboseIngestionEnabled } from './utils/verbose.js';
|
|||
import {
|
||||
getDefinitionNodeFromCaptures,
|
||||
findEnclosingClassInfo,
|
||||
findObjectLiteralBindingInfo,
|
||||
getLabelFromCaptures,
|
||||
CLASS_CONTAINER_TYPES,
|
||||
type SyntaxNode,
|
||||
|
|
@ -531,6 +532,10 @@ const processParsingSequential = async (
|
|||
)
|
||||
: null;
|
||||
const enclosingClassId = enclosingClassInfo?.classId ?? null;
|
||||
const objectLiteralOwnerInfo =
|
||||
!enclosingClassId && nodeLabel === 'Method' && definitionNode
|
||||
? findObjectLiteralBindingInfo(definitionNode, file.path)
|
||||
: null;
|
||||
|
||||
// Qualify method/property IDs with enclosing class name to avoid collisions
|
||||
// e.g. "Method:animal.dart:Animal.speak" vs "Method:animal.dart:Dog.speak"
|
||||
|
|
@ -805,15 +810,18 @@ const processParsingSequential = async (
|
|||
graph.addRelationship(relationship);
|
||||
|
||||
// ── HAS_METHOD / HAS_PROPERTY: link member to enclosing class ──
|
||||
if (enclosingClassId) {
|
||||
const ownerIdForMemberEdge = enclosingClassId ?? objectLiteralOwnerInfo?.ownerId ?? null;
|
||||
if (ownerIdForMemberEdge) {
|
||||
const memberEdgeType = nodeLabel === 'Property' ? 'HAS_PROPERTY' : 'HAS_METHOD';
|
||||
graph.addRelationship({
|
||||
id: generateId(memberEdgeType, `${enclosingClassId}->${nodeId}`),
|
||||
sourceId: enclosingClassId,
|
||||
id: generateId(memberEdgeType, `${ownerIdForMemberEdge}->${nodeId}`),
|
||||
sourceId: ownerIdForMemberEdge,
|
||||
targetId: nodeId,
|
||||
type: memberEdgeType,
|
||||
confidence: 1.0,
|
||||
reason: '',
|
||||
reason: objectLiteralOwnerInfo
|
||||
? 'object literal method belongs to exported object binding'
|
||||
: '',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -411,6 +411,60 @@ export const findEnclosingClassInfo = (
|
|||
return null;
|
||||
};
|
||||
|
||||
/** Object literal binding info for TS/JS shorthand methods. */
|
||||
export interface ObjectLiteralBindingInfo {
|
||||
ownerId: string;
|
||||
ownerName: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the file-scope variable that owns an object literal method definition.
|
||||
*
|
||||
* Covers TypeScript/JavaScript shorthand object methods such as:
|
||||
*
|
||||
* export const service = { async load() {} };
|
||||
*
|
||||
* tree-sitter represents `load` as a `method_definition` inside an `object`,
|
||||
* not inside a class container. Without this fallback, ingestion emits a
|
||||
* top-level `Method` node but no edge from the exported `service` value to
|
||||
* that method, so impact queries cannot discover `service.load`.
|
||||
*/
|
||||
export const findObjectLiteralBindingInfo = (
|
||||
node: SyntaxNode,
|
||||
filePath: string,
|
||||
): ObjectLiteralBindingInfo | null => {
|
||||
let current: SyntaxNode | null = node;
|
||||
let sawObjectLiteral = false;
|
||||
|
||||
while (current) {
|
||||
if (current.type === 'object') sawObjectLiteral = true;
|
||||
|
||||
if (sawObjectLiteral && current.type === 'variable_declarator') {
|
||||
const nameNode = current.childForFieldName?.('name');
|
||||
if (!nameNode || nameNode.type !== 'identifier') return null;
|
||||
|
||||
const declaration = current.parent;
|
||||
const ownerLabel = declaration?.type === 'variable_declaration' ? 'Variable' : 'Const';
|
||||
return {
|
||||
ownerId: generateId(ownerLabel, `${filePath}:${nameNode.text}`),
|
||||
ownerName: nameNode.text,
|
||||
};
|
||||
}
|
||||
|
||||
// Stop at a function/class boundary before finding an owning object literal.
|
||||
if (
|
||||
current !== node &&
|
||||
(FUNCTION_NODE_TYPES.has(current.type) || CLASS_CONTAINER_TYPES.has(current.type))
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
current = current.parent;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
/** Convenience wrapper: returns just the class ID string (backward compat). */
|
||||
export const findEnclosingClassId = (node: SyntaxNode, filePath: string): string | null => {
|
||||
return findEnclosingClassInfo(node, filePath)?.classId ?? null;
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ import {
|
|||
FUNCTION_NODE_TYPES,
|
||||
getDefinitionNodeFromCaptures,
|
||||
findEnclosingClassInfo,
|
||||
findObjectLiteralBindingInfo,
|
||||
type EnclosingClassInfo,
|
||||
getLabelFromCaptures,
|
||||
findDescendant,
|
||||
|
|
@ -2059,6 +2060,10 @@ const processFileGroup = (
|
|||
)
|
||||
: null;
|
||||
const enclosingClassId = enclosingClassInfo?.classId ?? null;
|
||||
const objectLiteralOwnerInfo =
|
||||
!enclosingClassId && nodeLabel === 'Method' && definitionNode
|
||||
? findObjectLiteralBindingInfo(definitionNode, file.path)
|
||||
: null;
|
||||
|
||||
// Qualify method/property IDs with enclosing class name to avoid collisions
|
||||
const qualifiedName = enclosingClassInfo
|
||||
|
|
@ -2346,15 +2351,18 @@ const processFileGroup = (
|
|||
});
|
||||
|
||||
// ── HAS_METHOD / HAS_PROPERTY: link member to enclosing class ──
|
||||
if (enclosingClassId) {
|
||||
const ownerIdForMemberEdge = enclosingClassId ?? objectLiteralOwnerInfo?.ownerId ?? null;
|
||||
if (ownerIdForMemberEdge) {
|
||||
const memberEdgeType = nodeLabel === 'Property' ? 'HAS_PROPERTY' : 'HAS_METHOD';
|
||||
result.relationships.push({
|
||||
id: generateId(memberEdgeType, `${enclosingClassId}->${nodeId}`),
|
||||
sourceId: enclosingClassId,
|
||||
id: generateId(memberEdgeType, `${ownerIdForMemberEdge}->${nodeId}`),
|
||||
sourceId: ownerIdForMemberEdge,
|
||||
targetId: nodeId,
|
||||
type: memberEdgeType,
|
||||
confidence: 1.0,
|
||||
reason: '',
|
||||
reason: objectLiteralOwnerInfo
|
||||
? 'object literal method belongs to exported object binding'
|
||||
: '',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,3 +42,47 @@ describe('processParsing worker fallback', () => {
|
|||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('TypeScript object literal method exports', () => {
|
||||
it('links exported object literal shorthand methods back to the exported object', async () => {
|
||||
const graph = createKnowledgeGraph();
|
||||
|
||||
await processParsing(
|
||||
graph,
|
||||
[
|
||||
{
|
||||
path: 'src/foo.ts',
|
||||
content: `export const fooService = {
|
||||
async getUser(id: string) {
|
||||
return findUser(id);
|
||||
},
|
||||
saveUser(user: User) {
|
||||
return persist(user);
|
||||
},
|
||||
};
|
||||
`,
|
||||
},
|
||||
],
|
||||
createSymbolTable(),
|
||||
createASTCache(),
|
||||
createASTCache(),
|
||||
);
|
||||
|
||||
const service = graph.nodes.find(
|
||||
(node) => node.label === 'Const' && node.properties.name === 'fooService',
|
||||
);
|
||||
expect(service, 'exported object literal should be captured as a Const').toBeDefined();
|
||||
|
||||
const methodNames = new Set(
|
||||
graph.nodes.filter((node) => node.label === 'Method').map((node) => node.properties.name),
|
||||
);
|
||||
expect(methodNames).toEqual(new Set(['getUser', 'saveUser']));
|
||||
|
||||
const linkedMethodNames = graph.relationships
|
||||
.filter((rel) => rel.type === 'HAS_METHOD' && rel.sourceId === service!.id)
|
||||
.map((rel) => graph.getNode(rel.targetId)?.properties.name)
|
||||
.sort();
|
||||
|
||||
expect(linkedMethodNames).toEqual(['getUser', 'saveUser']);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue