feat(extraction): same-file value-reference edges for impact analysis — 15 languages (#897)

Adds same-file value-reference edges (reader symbol → const/var it reads) so impact analysis catches a constant's same-file consumers, closing the 'change this table, break its readers' hole. 15 languages validated S/M/L on public OSS: TS/JS/tsx, Go, Python, Rust, Ruby, C, Java, C#, PHP, Scala, Kotlin, Swift, Dart, Pascal/Delphi (+ Svelte/Vue/Astro inherited). Edges-only — node count identical on/off; default ON, CODEGRAPH_VALUE_REFS=0 opts out.
This commit is contained in:
Colby Mchenry
2026-06-16 12:16:00 -05:00
committed by GitHub
parent 2f6316500d
commit f34f606342
11 changed files with 2081 additions and 47 deletions
+7
View File
@@ -110,6 +110,13 @@ export const cExtractor: LanguageExtractor = {
nameField: 'declarator',
bodyField: 'body',
paramsField: 'parameters',
// A `const`/`static const` file-scope declaration carries a `type_qualifier`
// child reading "const" — extract those as `constant`, plain globals as
// `variable`.
isConst: (node) =>
node.namedChildren.some(
(c: SyntaxNode) => c.type === 'type_qualifier' && c.text === 'const'
),
getReturnType: extractCppReturnType,
resolveTypeAliasKind: (node, _source) => {
// C typedef: `typedef enum { ... } name;` or `typedef struct { ... } name;`
+16
View File
@@ -121,6 +121,22 @@ export const csharpExtractor: LanguageExtractor = {
}
return false;
},
// `const` and `static readonly` fields are C# constants (`MaxItems`, lookup
// tables, shared config). Drives `constant` kind so value-reference edges
// target them; instance `readonly` / plain `static` fields stay `field`s.
isConst: (node) => {
let hasStatic = false;
let hasReadonly = false;
for (let i = 0; i < node.childCount; i++) {
const child = node.child(i);
if (child?.type !== 'modifier') continue;
const t = child.text;
if (t === 'const') return true;
if (t === 'static') hasStatic = true;
else if (t === 'readonly') hasReadonly = true;
}
return hasStatic && hasReadonly;
},
isAsync: (node) => {
for (let i = 0; i < node.childCount; i++) {
const child = node.child(i);
+22
View File
@@ -133,6 +133,28 @@ export const dartExtractor: LanguageExtractor = {
callTypes: [], // Dart calls use identifier+selector, handled via extractBareCall
variableTypes: [],
extraClassNodeTypes: ['mixin_declaration', 'extension_declaration'],
// A Dart `static_final_declaration` is exactly a top-level or class-`static`
// `const`/`final` — the shared-constant idiom — so extract it as `constant`
// for value-reference edges. Instance fields, `var`, and typed declarations
// use `initialized_identifier`, and method-locals use
// `initialized_variable_definition`; neither is this node, so there are no
// instance/local leaks to guard. The name is the first `identifier`; its
// parent scope (`file:` top-level / `class:` static member) comes from the
// node stack, both of which the value-reference target gate accepts.
visitNode: (node, ctx) => {
if (node.type === 'static_final_declaration') {
const nameNode = node.namedChildren.find((c: SyntaxNode) => c.type === 'identifier');
if (nameNode) {
const valueNode = nameNode.nextNamedSibling;
const initValue = valueNode ? getNodeText(valueNode, ctx.source).slice(0, 100) : undefined;
ctx.createNode('constant', getNodeText(nameNode, ctx.source), node, {
signature: initValue ? `= ${initValue}${initValue.length >= 100 ? '...' : ''}` : undefined,
});
}
return true;
}
return false;
},
resolveBody: (node, bodyField) => {
// Dart: function_body is a next sibling of function_signature/method_signature
if (node.type === 'function_signature' || node.type === 'method_signature') {
+13
View File
@@ -86,6 +86,19 @@ export const javaExtractor: LanguageExtractor = {
}
return false;
},
// A `static final` field is a Java constant (`MAX_ITEMS`, lookup tables,
// shared config). Drives `constant` kind so value-reference edges target it;
// instance / `final`-only / `static`-only fields stay mutable `field`s.
isConst: (node) => {
for (let i = 0; i < node.childCount; i++) {
const child = node.child(i);
if (child?.type === 'modifiers') {
const text = child.text;
return /\bstatic\b/.test(text) && /\bfinal\b/.test(text);
}
}
return false;
},
extractImport: (node, source) => {
const importText = source.substring(node.startIndex, node.endIndex).trim();
const scopedId = node.namedChildren.find((c: SyntaxNode) => c.type === 'scoped_identifier');
+45
View File
@@ -85,6 +85,51 @@ export const kotlinExtractor: LanguageExtractor = {
nameField: 'simple_identifier',
bodyField: 'function_body',
visitNode: (node, ctx) => {
// Kotlin properties (`val` / `var` / `const val`). The name nests as
// property_declaration → variable_declaration → simple_identifier, which the
// generic variable/field path can't read — so nothing was extracted before.
// Kind by enclosing scope: a singleton `object` / `companion object` (and a
// top-level property) holds *shared* values — `val`→`constant`,
// `var`→`variable` (the Scala-object rule; a `const val` is a `val`). A
// `class`/`interface`/`enum` instance `val`/`var` is per-instance state →
// `field` (never a value-ref target, like a Java instance `final`). A
// property inside a function body / `init` block / lambda is a local and is
// skipped entirely.
if (node.type === 'property_declaration') {
const varDecl = node.namedChildren.find((c) => c.type === 'variable_declaration');
const nameNode = varDecl?.namedChildren.find((c) => c.type === 'simple_identifier');
if (!nameNode) return false; // destructuring `val (a,b)` etc. — leave to default
const name = getNodeText(nameNode, ctx.source);
if (!name) return false;
// Walk to the nearest enclosing definition: a function body / init / lambda
// means it's a local; `object`/`companion object` is a constant scope; a
// `class_declaration` (covers class/interface/enum) is an instance scope.
let scope: 'local' | 'const' | 'instance' = 'const';
for (let p = node.parent; p; p = p.parent) {
const pt = p.type;
if (
pt === 'function_body' || pt === 'function_declaration' ||
pt === 'lambda_literal' || pt === 'anonymous_initializer' ||
pt === 'control_structure_body' || pt === 'getter' || pt === 'setter'
) { scope = 'local'; break; }
if (pt === 'companion_object' || pt === 'object_declaration') { scope = 'const'; break; }
if (pt === 'class_declaration') { scope = 'instance'; break; }
}
if (scope === 'local') return true; // a local — don't extract
const binding = node.namedChildren.find((c) => c.type === 'binding_pattern_kind');
const isVal = binding != null && getNodeText(binding, ctx.source) === 'val';
const kind = scope === 'instance' ? 'field' : isVal ? 'constant' : 'variable';
const typeNode = node.childForFieldName('type');
const sig = typeNode
? `${isVal ? 'val' : 'var'} ${name}: ${getNodeText(typeNode, ctx.source)}`
: undefined;
ctx.createNode(kind, name, node, { signature: sig });
return true;
}
// Handle Kotlin `fun interface` declarations.
// Tree-sitter-kotlin doesn't support `fun interface` syntax (Kotlin 1.4+).
// It produces two different misparse patterns:
+22 -11
View File
@@ -136,18 +136,29 @@ export const scalaExtractor: LanguageExtractor = {
const name = getValVarName(node, ctx.source);
if (!name) return false;
const isInClass = ctx.nodeStack.length > 0 &&
(() => {
const parentId = ctx.nodeStack[ctx.nodeStack.length - 1];
const parentNode = ctx.nodes.find((n) => n.id === parentId);
return parentNode != null && (
parentNode.kind === 'class' || parentNode.kind === 'trait' ||
parentNode.kind === 'interface' || parentNode.kind === 'struct' ||
parentNode.kind === 'enum' || parentNode.kind === 'module'
);
})();
// An `object` is a singleton: its `val`s are shared constants (the Scala
// idiom for `static final` — `object Config { val Timeout = 30 }`), so
// emit them as `constant`/`variable` like a top-level val, which lets
// value-reference edges target them. A `class`/`trait`/`enum`/`given` val
// is a per-instance immutable field. Both an `object` and a `class`
// extract as `class` kind, so the AST node type of the enclosing
// definition — not the parent node's kind — is what distinguishes them.
let enclosingDef: string | null = null;
for (let p = node.parent; p; p = p.parent) {
if (
p.type === 'class_definition' || p.type === 'trait_definition' ||
p.type === 'enum_definition' || p.type === 'given_definition' ||
p.type === 'object_definition'
) {
enclosingDef = p.type;
break;
}
}
const isInstanceField =
enclosingDef === 'class_definition' || enclosingDef === 'trait_definition' ||
enclosingDef === 'enum_definition' || enclosingDef === 'given_definition';
const kind = isInClass ? 'field' : (t === 'val_definition' ? 'constant' : 'variable');
const kind = isInstanceField ? 'field' : (t === 'val_definition' ? 'constant' : 'variable');
const typeNode = node.childForFieldName('type');
const sig = typeNode
? `${t === 'val_definition' ? 'val' : 'var'} ${name}: ${getNodeText(typeNode, ctx.source)}`