mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-27 21:55:14 +08:00
Same-kind, same-name declarations on one line shared an ID and silently overwrote each other in SQLite. Use per-extraction collision tracking to append a UTF-16 column only to later distinct declarations. Apply the same rule across wasm, Rust language walkers, Liquid, and CFML while preserving file IDs and edge endpoints. Add persistence, call-edge, identity-vector, and Unicode parity regressions. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
e27d6da6b2
commit
86708bf519
@@ -1,6 +1,6 @@
|
||||
import type { Node as SyntaxNode } from 'web-tree-sitter';
|
||||
import { Node, Edge, ExtractionResult, ExtractionError, UnresolvedReference, Language } from '../types';
|
||||
import { generateNodeId } from './tree-sitter-helpers';
|
||||
import { generateNodeId, NodeIdAllocator } from './tree-sitter-helpers';
|
||||
import { TreeSitterExtractor } from './tree-sitter';
|
||||
import { getParser } from './grammars';
|
||||
|
||||
@@ -22,6 +22,7 @@ export class CfmlExtractor {
|
||||
private source: string;
|
||||
private language: Language;
|
||||
private nodes: Node[] = [];
|
||||
private nodeIds = new NodeIdAllocator();
|
||||
private edges: Edge[] = [];
|
||||
private unresolvedReferences: UnresolvedReference[] = [];
|
||||
private errors: ExtractionError[] = [];
|
||||
@@ -174,7 +175,7 @@ export class CfmlExtractor {
|
||||
*/
|
||||
private extractComponent(openTag: SyntaxNode, containerId: string | undefined): SyntaxNode {
|
||||
const name = this.tagAttr(openTag, 'name') ?? this.componentNameFromPath();
|
||||
const id = generateNodeId(this.filePath, 'class', name, openTag.startPosition.row + 1);
|
||||
const id = this.nodeIds.generate(this.filePath, 'class', name, openTag.startPosition.row + 1, openTag.startPosition.column);
|
||||
|
||||
const classNode: Node = {
|
||||
id,
|
||||
@@ -261,7 +262,7 @@ export class CfmlExtractor {
|
||||
if (!name) return;
|
||||
|
||||
const kind = parentClassId ? 'method' : 'function';
|
||||
const id = generateNodeId(this.filePath, kind, name, tag.startPosition.row + 1);
|
||||
const id = this.nodeIds.generate(this.filePath, kind, name, tag.startPosition.row + 1, tag.startPosition.column);
|
||||
const access = this.tagAttr(tag, 'access');
|
||||
const visibility = access === 'private' ? 'private'
|
||||
: access === 'package' ? 'internal'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Node, Edge, ExtractionResult, ExtractionError, UnresolvedReference } from '../types';
|
||||
import { generateNodeId } from './tree-sitter-helpers';
|
||||
import { generateNodeId, NodeIdAllocator } from './tree-sitter-helpers';
|
||||
|
||||
/**
|
||||
* LiquidExtractor - Extracts relationships from Liquid template files
|
||||
@@ -14,6 +14,7 @@ export class LiquidExtractor {
|
||||
private filePath: string;
|
||||
private source: string;
|
||||
private nodes: Node[] = [];
|
||||
private nodeIds = new NodeIdAllocator();
|
||||
private edges: Edge[] = [];
|
||||
private unresolvedReferences: UnresolvedReference[] = [];
|
||||
private errors: ExtractionError[] = [];
|
||||
@@ -217,7 +218,7 @@ export class LiquidExtractor {
|
||||
const line = this.getLineNumber(match.index);
|
||||
|
||||
// Create an import node for searchability
|
||||
const importNodeId = generateNodeId(this.filePath, 'import', snippetName!, line);
|
||||
const importNodeId = this.nodeIds.generate(this.filePath, 'import', snippetName!, line, match.index - this.getLineStart(line));
|
||||
const importNode: Node = {
|
||||
id: importNodeId,
|
||||
kind: 'import',
|
||||
@@ -242,7 +243,7 @@ export class LiquidExtractor {
|
||||
});
|
||||
|
||||
// Create a component node for the snippet reference
|
||||
const nodeId = generateNodeId(this.filePath, 'component', `${tagType}:${snippetName}`, line);
|
||||
const nodeId = this.nodeIds.generate(this.filePath, 'component', `${tagType}:${snippetName}`, line, match.index - this.getLineStart(line));
|
||||
|
||||
const node: Node = {
|
||||
id: nodeId,
|
||||
@@ -289,7 +290,7 @@ export class LiquidExtractor {
|
||||
const line = this.getLineNumber(match.index);
|
||||
|
||||
// Create an import node for searchability
|
||||
const importNodeId = generateNodeId(this.filePath, 'import', sectionName!, line);
|
||||
const importNodeId = this.nodeIds.generate(this.filePath, 'import', sectionName!, line, match.index - this.getLineStart(line));
|
||||
const importNode: Node = {
|
||||
id: importNodeId,
|
||||
kind: 'import',
|
||||
@@ -314,7 +315,7 @@ export class LiquidExtractor {
|
||||
});
|
||||
|
||||
// Create a component node for the section reference
|
||||
const nodeId = generateNodeId(this.filePath, 'component', `section:${sectionName}`, line);
|
||||
const nodeId = this.nodeIds.generate(this.filePath, 'component', `section:${sectionName}`, line, match.index - this.getLineStart(line));
|
||||
|
||||
const node: Node = {
|
||||
id: nodeId,
|
||||
@@ -378,7 +379,7 @@ export class LiquidExtractor {
|
||||
}
|
||||
|
||||
// Create a node for the schema
|
||||
const nodeId = generateNodeId(this.filePath, 'constant', `schema:${schemaName}`, startLine);
|
||||
const nodeId = this.nodeIds.generate(this.filePath, 'constant', `schema:${schemaName}`, startLine, match.index - this.getLineStart(startLine));
|
||||
|
||||
const node: Node = {
|
||||
id: nodeId,
|
||||
@@ -419,7 +420,7 @@ export class LiquidExtractor {
|
||||
const line = this.getLineNumber(match.index);
|
||||
|
||||
// Create a variable node
|
||||
const nodeId = generateNodeId(this.filePath, 'variable', variableName!, line);
|
||||
const nodeId = this.nodeIds.generate(this.filePath, 'variable', variableName!, line, match.index - this.getLineStart(line));
|
||||
|
||||
const node: Node = {
|
||||
id: nodeId,
|
||||
|
||||
@@ -29,6 +29,23 @@ export function generateNodeId(
|
||||
return `${kind}:${hash}`;
|
||||
}
|
||||
|
||||
/** Per-extraction identities: preserve legacy IDs unless distinct source positions collide. */
|
||||
export class NodeIdAllocator {
|
||||
private firstColumns = new Map<string, number>();
|
||||
|
||||
generate(filePath: string, kind: NodeKind, name: string, line: number, column: number): string {
|
||||
const id = generateNodeId(filePath, kind, name, line);
|
||||
const firstColumn = this.firstColumns.get(id);
|
||||
if (firstColumn === undefined) {
|
||||
this.firstColumns.set(id, column);
|
||||
return id;
|
||||
}
|
||||
// Columns are zero-based UTF-16 code units in both wasm and the kernel.
|
||||
// Revisiting the same declaration must still produce the same identity.
|
||||
return firstColumn === column ? id : `${id}:${column}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract text from a syntax node
|
||||
*/
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
UnresolvedReference,
|
||||
} from '../types';
|
||||
import { getParser, detectLanguage, isLanguageSupported, isFileLevelOnlyLanguage } from './grammars';
|
||||
import { generateNodeId, getNodeText, getChildByField, getPrecedingDocstring } from './tree-sitter-helpers';
|
||||
import { NodeIdAllocator, getNodeText, getChildByField, getPrecedingDocstring } from './tree-sitter-helpers';
|
||||
import { FN_REF_SPECS, captureFnRefCandidates, type FnRefSpec, type FnRefCandidate } from './function-ref';
|
||||
import { isGeneratedFile } from './generated-detection';
|
||||
import type { LanguageExtractor, ExtractorContext } from './tree-sitter-types';
|
||||
@@ -454,6 +454,7 @@ export class TreeSitterExtractor {
|
||||
private source: string;
|
||||
private tree: Tree | null = null;
|
||||
private nodes: Node[] = [];
|
||||
private nodeIds = new NodeIdAllocator();
|
||||
private edges: Edge[] = [];
|
||||
private unresolvedReferences: UnresolvedReference[] = [];
|
||||
// Value-reference edges (default ON; set CODEGRAPH_VALUE_REFS=0 to disable; see flushValueRefs).
|
||||
@@ -1412,7 +1413,7 @@ export class TreeSitterExtractor {
|
||||
return null;
|
||||
}
|
||||
|
||||
const id = generateNodeId(this.filePath, kind, name, node.startPosition.row + 1);
|
||||
const id = this.nodeIds.generate(this.filePath, kind, name, node.startPosition.row + 1, node.startPosition.column);
|
||||
|
||||
// Some grammars (e.g. Dart) model a function/method body as a *sibling* of
|
||||
// the signature node, so the declaration node's own range is just the
|
||||
|
||||
Reference in New Issue
Block a user