@holmes-lab/holmes-kit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/LICENSE +21 -0
- package/README.md +102 -0
- package/bin/holmes-hook-antigravity.js +31 -0
- package/bin/holmes-kit.js +23 -0
- package/bin/holmes-mcp.js +34 -0
- package/bin/holmes-stop-antigravity.js +29 -0
- package/dist/.build-id +1 -0
- package/dist/holmes/cli/agents.js +168 -0
- package/dist/holmes/cli/doctor.js +625 -0
- package/dist/holmes/cli/gitignore-merge.js +84 -0
- package/dist/holmes/cli/governed-precondition.js +157 -0
- package/dist/holmes/cli/index.js +384 -0
- package/dist/holmes/cli/init.js +462 -0
- package/dist/holmes/cli/playbook-skills.js +711 -0
- package/dist/holmes/cli/roles-readme.js +134 -0
- package/dist/holmes/cli/settings-merge.js +122 -0
- package/dist/holmes/config/config.js +70 -0
- package/dist/holmes/context/bundler.js +114 -0
- package/dist/holmes/context/render.js +29 -0
- package/dist/holmes/context/tiers.js +110 -0
- package/dist/holmes/context/tokens.js +8 -0
- package/dist/holmes/cpg/cpg-scanner.js +213 -0
- package/dist/holmes/cpg/hash-cache.js +86 -0
- package/dist/holmes/cpg/language-parser-walk.js +917 -0
- package/dist/holmes/cpg/language-parser-worker.js +81 -0
- package/dist/holmes/cpg/language-parser.js +234 -0
- package/dist/holmes/cpg/scan-cache.js +108 -0
- package/dist/holmes/cpg/source-path.js +44 -0
- package/dist/holmes/cpg/test-files.js +84 -0
- package/dist/holmes/governance/constitution-debt.js +73 -0
- package/dist/holmes/governance/constitution-report.js +25 -0
- package/dist/holmes/governance/constitution.js +129 -0
- package/dist/holmes/governance/identity.js +30 -0
- package/dist/holmes/governance/ledger-lock.js +165 -0
- package/dist/holmes/governance/ledger-store.conformance.js +90 -0
- package/dist/holmes/governance/ledger-store.js +106 -0
- package/dist/holmes/governance/progress-ledger.js +83 -0
- package/dist/holmes/governance/provenance-chain.js +365 -0
- package/dist/holmes/governance/provenance-ledger.js +0 -0
- package/dist/holmes/governance/provenance-schema.js +47 -0
- package/dist/holmes/governance/replica-id.js +106 -0
- package/dist/holmes/governance/role-policy.js +137 -0
- package/dist/holmes/governance/trust-score.js +43 -0
- package/dist/holmes/guardrail/anchors.js +31 -0
- package/dist/holmes/guardrail/blind-spots.js +38 -0
- package/dist/holmes/guardrail/decision-ledger.js +107 -0
- package/dist/holmes/guardrail/executable-artifact.js +129 -0
- package/dist/holmes/guardrail/governance-history.js +101 -0
- package/dist/holmes/guardrail/phase.js +169 -0
- package/dist/holmes/guardrail/risk-classifier.js +450 -0
- package/dist/holmes/guardrail/risk-gate.js +160 -0
- package/dist/holmes/guardrail/risk-types.js +6 -0
- package/dist/holmes/guardrail/tspec-state.js +392 -0
- package/dist/holmes/guardrail/write-target.js +224 -0
- package/dist/holmes/hooks/adapters/antigravity.js +194 -0
- package/dist/holmes/hooks/pre-tool-use.js +1262 -0
- package/dist/holmes/hooks/stop.js +416 -0
- package/dist/holmes/mcp/basis.js +162 -0
- package/dist/holmes/mcp/handlers.js +1831 -0
- package/dist/holmes/mcp/server.js +71 -0
- package/dist/holmes/mcp/stdio-client.js +165 -0
- package/dist/holmes/mcp/supervisor.js +178 -0
- package/dist/holmes/mcp/tool-schemas.js +394 -0
- package/dist/holmes/mcp/validate-args.js +281 -0
- package/dist/holmes/messages/registry.js +50 -0
- package/dist/holmes/project/baseline.js +210 -0
- package/dist/holmes/project/change-source.js +233 -0
- package/dist/holmes/project/ignore.js +145 -0
- package/dist/holmes/project/root.js +113 -0
- package/dist/holmes/reverse/anchor.js +162 -0
- package/dist/holmes/reverse/cluster.js +187 -0
- package/dist/holmes/reverse/draft.js +151 -0
- package/dist/holmes/reverse/dynamic-wiring.js +47 -0
- package/dist/holmes/reverse/scan.js +194 -0
- package/dist/holmes/reverse/surface.js +154 -0
- package/dist/holmes/reverse/test-map.js +263 -0
- package/dist/holmes/review/coverage.js +33 -0
- package/dist/holmes/review/findings.js +123 -0
- package/dist/holmes/review/package.js +40 -0
- package/dist/holmes/review/review-targets.js +92 -0
- package/dist/holmes/review/scope.js +57 -0
- package/dist/holmes/review/test-evidence.js +77 -0
- package/dist/holmes/review/test-runner.js +572 -0
- package/dist/holmes/rtm/dataflow-taint.js +262 -0
- package/dist/holmes/rtm/gap-analyzer.js +27 -0
- package/dist/holmes/rtm/git-changes.js +72 -0
- package/dist/holmes/rtm/incremental.js +45 -0
- package/dist/holmes/rtm/localize.js +100 -0
- package/dist/holmes/rtm/rtm-builder.js +191 -0
- package/dist/holmes/rtm/rtm-check.js +89 -0
- package/dist/holmes/rtm/rtm-graph.js +232 -0
- package/dist/holmes/rtm/taint.js +92 -0
- package/dist/holmes/rtm/test-scope.js +336 -0
- package/dist/holmes/spec/approval-blockers.js +204 -0
- package/dist/holmes/spec/breaking-change.js +89 -0
- package/dist/holmes/spec/legacy-format.js +87 -0
- package/dist/holmes/spec/spec-digest.js +71 -0
- package/dist/holmes/spec/spec-parser.js +106 -0
- package/dist/holmes/spec/spec-store.conformance.js +118 -0
- package/dist/holmes/spec/spec-store.js +331 -0
- package/dist/holmes/spec/spec-types.js +177 -0
- package/dist/holmes/spec/validator.js +280 -0
- package/package.json +76 -0
- package/playbooks/adopt/PLAYBOOK.md +125 -0
- package/playbooks/author-slice/PLAYBOOK.md +119 -0
- package/playbooks/promote-slice/PLAYBOOK.md +134 -0
|
@@ -0,0 +1,917 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Single source of truth for the per-language symbol/edge tree-walking
|
|
3
|
+
// logic. Called from BOTH the inline (production) parse path and the
|
|
4
|
+
// jest-only worker_thread path in language-parser.ts / language-parser-worker.js,
|
|
5
|
+
// so the two paths are guaranteed to produce byte-identical results — there
|
|
6
|
+
// is no second copy of this logic anywhere to drift out of sync.
|
|
7
|
+
// @implements A-SPEC-121.6
|
|
8
|
+
// Table-driven per-language node-type config that drives the single shared
|
|
9
|
+
// lexical symbol walk below (walkSymbols). Adding a new language is meant to
|
|
10
|
+
// be exactly one entry here — the walk itself should not need to change.
|
|
11
|
+
//
|
|
12
|
+
// Fields:
|
|
13
|
+
// classTypes node types that open a class scope; the class's name
|
|
14
|
+
// is read via childForFieldName('name'); entering one
|
|
15
|
+
// sets `enclosingClass` to that name for its children.
|
|
16
|
+
// funcTypes function/method node types whose "is this a method"
|
|
17
|
+
// status depends on lexical context (see
|
|
18
|
+
// funcIsMethodInClass) rather than the node type
|
|
19
|
+
// itself.
|
|
20
|
+
// methodTypes node types that are ALWAYS a method when lexically
|
|
21
|
+
// inside a class (e.g. TypeScript's method_definition,
|
|
22
|
+
// which is also used for object-literal methods —
|
|
23
|
+
// those have no enclosing class and are skipped so we
|
|
24
|
+
// don't emit spurious unqualified method symbols).
|
|
25
|
+
// funcIsMethodInClass when true, a funcTypes node lexically inside a class
|
|
26
|
+
// becomes a method (qualified `Class.name`); when
|
|
27
|
+
// false (as for TypeScript, which has a dedicated
|
|
28
|
+
// methodTypes node type instead), a funcTypes node is
|
|
29
|
+
// always a plain function regardless of context.
|
|
30
|
+
// This flag also controls the nested-def reset rule:
|
|
31
|
+
// recursing into a funcTypes node resets
|
|
32
|
+
// enclosingClass to null for its children ONLY when
|
|
33
|
+
// funcIsMethodInClass is true — i.e. only for
|
|
34
|
+
// languages (like Python) where a nested def's
|
|
35
|
+
// method-ness is context-sensitive, so a def nested
|
|
36
|
+
// inside another def must not be mistaken for a method
|
|
37
|
+
// of the outer class. Languages with a dedicated
|
|
38
|
+
// methodTypes node (TypeScript) don't reset here,
|
|
39
|
+
// matching their original (pre-refactor) behavior.
|
|
40
|
+
const LANG_CONFIG = {
|
|
41
|
+
typescript: {
|
|
42
|
+
classTypes: ['class_declaration'],
|
|
43
|
+
funcTypes: ['function_declaration'],
|
|
44
|
+
methodTypes: ['method_definition'],
|
|
45
|
+
funcIsMethodInClass: false,
|
|
46
|
+
},
|
|
47
|
+
python: {
|
|
48
|
+
classTypes: ['class_definition'],
|
|
49
|
+
funcTypes: ['function_definition'],
|
|
50
|
+
methodTypes: [],
|
|
51
|
+
funcIsMethodInClass: true,
|
|
52
|
+
},
|
|
53
|
+
// @implements A-SPEC-121.6
|
|
54
|
+
// C# is lexical, like TypeScript: class/struct/interface/record all open a
|
|
55
|
+
// class-like scope (all have a `name` field), and method_declaration is
|
|
56
|
+
// ALWAYS a method node type — never a plain function, never context-
|
|
57
|
+
// sensitive — so funcTypes stays empty and funcIsMethodInClass is false,
|
|
58
|
+
// mirroring the typescript shape rather than python's.
|
|
59
|
+
csharp: {
|
|
60
|
+
classTypes: ['class_declaration', 'struct_declaration', 'interface_declaration', 'record_declaration'],
|
|
61
|
+
funcTypes: [],
|
|
62
|
+
methodTypes: ['method_declaration'],
|
|
63
|
+
funcIsMethodInClass: false,
|
|
64
|
+
},
|
|
65
|
+
// @implements A-SPEC-121.6
|
|
66
|
+
// Java is lexical, like C#: class/interface/enum/record all open a
|
|
67
|
+
// class-like scope (all have a `name` field), and method_declaration is
|
|
68
|
+
// ALWAYS a method node type — never a plain function, never context-
|
|
69
|
+
// sensitive — so funcTypes stays empty and funcIsMethodInClass is false,
|
|
70
|
+
// mirroring the csharp shape.
|
|
71
|
+
java: {
|
|
72
|
+
classTypes: ['class_declaration', 'interface_declaration', 'enum_declaration', 'record_declaration'],
|
|
73
|
+
funcTypes: [],
|
|
74
|
+
methodTypes: ['method_declaration'],
|
|
75
|
+
funcIsMethodInClass: false,
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
// tsx shares the exact same node-type vocabulary as typescript.
|
|
79
|
+
LANG_CONFIG.tsx = LANG_CONFIG.typescript;
|
|
80
|
+
// One shared lexical walk driven entirely by LANG_CONFIG. Reproduces, node
|
|
81
|
+
// for node, both the original TypeScript walk (method_definition is always a
|
|
82
|
+
// method node, skipped when it has no enclosing class — the object-literal-
|
|
83
|
+
// method case) and the original Python walk (function_definition is a method
|
|
84
|
+
// only when lexically inside a class, and recursing into ANY def resets
|
|
85
|
+
// enclosingClass to null for its children, so a def nested inside a def is
|
|
86
|
+
// never mistaken for a method of the outer class).
|
|
87
|
+
function walkSymbols(tree, config) {
|
|
88
|
+
const out = [];
|
|
89
|
+
const visit = (node, enclosingClass) => {
|
|
90
|
+
const type = node.type;
|
|
91
|
+
if (config.classTypes.includes(type)) {
|
|
92
|
+
const nameNode = node.childForFieldName('name');
|
|
93
|
+
const name = nameNode ? nameNode.text : undefined;
|
|
94
|
+
if (name) {
|
|
95
|
+
out.push({
|
|
96
|
+
name,
|
|
97
|
+
kind: 'class',
|
|
98
|
+
qualifiedName: name,
|
|
99
|
+
startLine: node.startPosition.row + 1,
|
|
100
|
+
endLine: node.endPosition.row + 1,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
const childClass = name || enclosingClass;
|
|
104
|
+
for (let i = 0; i < node.childCount; i++)
|
|
105
|
+
visit(node.child(i), childClass);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (config.funcTypes.includes(type)) {
|
|
109
|
+
const nameNode = node.childForFieldName('name');
|
|
110
|
+
const name = nameNode ? nameNode.text : undefined;
|
|
111
|
+
if (name) {
|
|
112
|
+
const isMethod = config.funcIsMethodInClass && !!enclosingClass;
|
|
113
|
+
const kind = isMethod ? 'method' : 'function';
|
|
114
|
+
const qualifiedName = isMethod ? `${enclosingClass}.${name}` : name;
|
|
115
|
+
out.push({
|
|
116
|
+
name,
|
|
117
|
+
kind,
|
|
118
|
+
qualifiedName,
|
|
119
|
+
startLine: node.startPosition.row + 1,
|
|
120
|
+
endLine: node.endPosition.row + 1,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
// Nested-def reset rule (see funcIsMethodInClass doc above): only
|
|
124
|
+
// context-sensitive languages (Python) reset enclosingClass to null
|
|
125
|
+
// here; languages with a dedicated methodTypes node (TypeScript) carry
|
|
126
|
+
// enclosingClass through unchanged, matching original behavior.
|
|
127
|
+
const childEnclosing = config.funcIsMethodInClass ? null : enclosingClass;
|
|
128
|
+
for (let i = 0; i < node.childCount; i++)
|
|
129
|
+
visit(node.child(i), childEnclosing);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (config.methodTypes.includes(type)) {
|
|
133
|
+
const nameNode = node.childForFieldName('name');
|
|
134
|
+
if (nameNode) {
|
|
135
|
+
const name = nameNode.text;
|
|
136
|
+
// Only emit method symbols for genuine class members
|
|
137
|
+
// (enclosingClass !== null). Skip object-literal methods, which have
|
|
138
|
+
// no enclosing class, to avoid spurious unqualified method symbols.
|
|
139
|
+
if (enclosingClass) {
|
|
140
|
+
out.push({
|
|
141
|
+
name,
|
|
142
|
+
kind: 'method',
|
|
143
|
+
qualifiedName: `${enclosingClass}.${name}`,
|
|
144
|
+
startLine: node.startPosition.row + 1,
|
|
145
|
+
endLine: node.endPosition.row + 1,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
for (let i = 0; i < node.childCount; i++)
|
|
150
|
+
visit(node.child(i), enclosingClass);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
for (let i = 0; i < node.childCount; i++)
|
|
154
|
+
visit(node.child(i), enclosingClass);
|
|
155
|
+
};
|
|
156
|
+
visit(tree.rootNode, null);
|
|
157
|
+
return out;
|
|
158
|
+
}
|
|
159
|
+
// @implements A-SPEC-121.6
|
|
160
|
+
// Go is NOT lexical the way TypeScript/Python/C#/Java are: Go has no class
|
|
161
|
+
// construct at all, and a method's qualifying "owner" is its RECEIVER TYPE
|
|
162
|
+
// (`func (t T) M(){}` -> `T.M`), not lexical nesting inside a class-like
|
|
163
|
+
// scope. Reusing walkSymbols'/LANG_CONFIG's enclosingClass model would be
|
|
164
|
+
// actively wrong for Go (there is nothing to lexically nest into), so Go
|
|
165
|
+
// gets its own dedicated walker instead of a LANG_CONFIG table entry.
|
|
166
|
+
function walkGoSymbols(tree) {
|
|
167
|
+
const out = [];
|
|
168
|
+
// Descend through a receiver's (possibly pointer, possibly generic) type to
|
|
169
|
+
// the underlying type name, e.g. `(t T)` -> 'T', `(p *T)` -> 'T',
|
|
170
|
+
// `(t T[V])` -> 'T', `(p *T[V])` -> 'T' (Go 1.18+ generic receivers). The
|
|
171
|
+
// qualifying name is always the bare type name — never '*T', never 'T[V]' —
|
|
172
|
+
// so an RTM `T.Method` lookup resolves regardless of pointer/generic form.
|
|
173
|
+
const receiverTypeName = (methodDeclNode) => {
|
|
174
|
+
const receiver = methodDeclNode.childForFieldName('receiver');
|
|
175
|
+
if (!receiver)
|
|
176
|
+
return undefined;
|
|
177
|
+
const paramDecl = receiver.namedChild(0);
|
|
178
|
+
if (!paramDecl)
|
|
179
|
+
return undefined;
|
|
180
|
+
let typeNode = paramDecl.childForFieldName('type');
|
|
181
|
+
if (!typeNode)
|
|
182
|
+
return undefined;
|
|
183
|
+
if (typeNode.type === 'pointer_type')
|
|
184
|
+
typeNode = typeNode.namedChild(0);
|
|
185
|
+
// generic_type is `BaseType[TypeArgs]`; its first named child is the base
|
|
186
|
+
// type_identifier. Unwrap it so `T[V]` qualifies as `T`.
|
|
187
|
+
if (typeNode && typeNode.type === 'generic_type')
|
|
188
|
+
typeNode = typeNode.namedChild(0);
|
|
189
|
+
return typeNode ? typeNode.text : undefined;
|
|
190
|
+
};
|
|
191
|
+
const visit = (node) => {
|
|
192
|
+
const type = node.type;
|
|
193
|
+
if (type === 'function_declaration') {
|
|
194
|
+
const nameNode = node.childForFieldName('name');
|
|
195
|
+
if (nameNode) {
|
|
196
|
+
out.push({
|
|
197
|
+
name: nameNode.text,
|
|
198
|
+
kind: 'function',
|
|
199
|
+
qualifiedName: nameNode.text,
|
|
200
|
+
startLine: node.startPosition.row + 1,
|
|
201
|
+
endLine: node.endPosition.row + 1,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
else if (type === 'type_declaration') {
|
|
206
|
+
// A type_declaration can group multiple type_spec nodes (`type ( A
|
|
207
|
+
// struct{}; B struct{} )`); each spec is emitted with its OWN line
|
|
208
|
+
// range, not the enclosing type_declaration's.
|
|
209
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
210
|
+
const spec = node.namedChild(i);
|
|
211
|
+
// `type_spec` = a normal named type (`type T struct{}`, `type C float64`);
|
|
212
|
+
// `type_alias` = the `=` alias form (`type Alias = int`). Both name a
|
|
213
|
+
// type and are emitted as a class-kind symbol; only these two node
|
|
214
|
+
// types carry a type name here.
|
|
215
|
+
if (spec.type !== 'type_spec' && spec.type !== 'type_alias')
|
|
216
|
+
continue;
|
|
217
|
+
const nameNode = spec.childForFieldName('name');
|
|
218
|
+
if (nameNode) {
|
|
219
|
+
out.push({
|
|
220
|
+
name: nameNode.text,
|
|
221
|
+
kind: 'class',
|
|
222
|
+
qualifiedName: nameNode.text,
|
|
223
|
+
startLine: spec.startPosition.row + 1,
|
|
224
|
+
endLine: spec.endPosition.row + 1,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
else if (type === 'method_declaration') {
|
|
230
|
+
const nameNode = node.childForFieldName('name');
|
|
231
|
+
const recvType = receiverTypeName(node);
|
|
232
|
+
if (nameNode && recvType) {
|
|
233
|
+
out.push({
|
|
234
|
+
name: nameNode.text,
|
|
235
|
+
kind: 'method',
|
|
236
|
+
qualifiedName: `${recvType}.${nameNode.text}`,
|
|
237
|
+
startLine: node.startPosition.row + 1,
|
|
238
|
+
endLine: node.endPosition.row + 1,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
for (let i = 0; i < node.childCount; i++)
|
|
243
|
+
visit(node.child(i));
|
|
244
|
+
};
|
|
245
|
+
visit(tree.rootNode);
|
|
246
|
+
return out;
|
|
247
|
+
}
|
|
248
|
+
// @implements A-SPEC-121.6
|
|
249
|
+
// Rust, like Go, is NOT lexical the way TypeScript/Python/C#/Java are — but
|
|
250
|
+
// unlike Go, it uses the IMPL-BLOCK method model: a method is a plain
|
|
251
|
+
// `function_item` (the SAME node type as a top-level function) that happens
|
|
252
|
+
// to be lexically nested inside an `impl_item`, and it is qualified by the
|
|
253
|
+
// impl's TARGET TYPE (`impl S { fn m(&self){} }` -> `S.m`), not by a receiver
|
|
254
|
+
// parameter the way Go methods are. `impl_item` itself never emits a class
|
|
255
|
+
// symbol — it only opens a qualifying scope for the function_items nested
|
|
256
|
+
// inside it — so this needs its own dedicated walker rather than a
|
|
257
|
+
// LANG_CONFIG entry (walkSymbols' enclosingClass model assumes the "class"
|
|
258
|
+
// node itself is the thing being named, which is exactly backwards for
|
|
259
|
+
// impl_item: the impl names a type it is NOT a definition of).
|
|
260
|
+
function walkRustSymbols(tree) {
|
|
261
|
+
const out = [];
|
|
262
|
+
// The `type` field of an impl_item names the type being implemented for
|
|
263
|
+
// (present on both `impl S {...}` and `impl T for S {...}` — the `trait`
|
|
264
|
+
// field, `T`, is deliberately ignored for qualification purposes: methods
|
|
265
|
+
// in a trait impl are qualified by the IMPLEMENTING type, not the trait).
|
|
266
|
+
// A generic impl's type field is a `generic_type` (`G<X>`) whose first
|
|
267
|
+
// named child is the base `type_identifier`; unwrap it so `G<X>` qualifies
|
|
268
|
+
// as bare `G`, mirroring walkGoSymbols' receiver generic_type unwrap.
|
|
269
|
+
const implTargetTypeName = (implNode) => {
|
|
270
|
+
let typeNode = implNode.childForFieldName('type');
|
|
271
|
+
if (!typeNode)
|
|
272
|
+
return undefined;
|
|
273
|
+
while (typeNode && typeNode.type !== 'type_identifier' && typeNode.namedChildCount > 0) {
|
|
274
|
+
// Unwrap to the bare type_identifier. For a module-path-qualified target
|
|
275
|
+
// (`scoped_type_identifier`, e.g. `a::S` / `crate::Foo`) the actual type
|
|
276
|
+
// is the LAST named child (`S`/`Foo`), not the first (which is the path
|
|
277
|
+
// segment `a`/`crate`). For `generic_type` (`G<X>` -> `G`),
|
|
278
|
+
// `reference_type` (`&S` -> `S`) and other wrappers the base is the first
|
|
279
|
+
// named child. Taking the first child on a scoped_type_identifier would
|
|
280
|
+
// drop qualification entirely (return undefined -> method mis-emitted as
|
|
281
|
+
// a bare unqualified function).
|
|
282
|
+
typeNode =
|
|
283
|
+
typeNode.type === 'scoped_type_identifier'
|
|
284
|
+
? typeNode.namedChild(typeNode.namedChildCount - 1)
|
|
285
|
+
: typeNode.namedChild(0);
|
|
286
|
+
}
|
|
287
|
+
return typeNode && typeNode.type === 'type_identifier' ? typeNode.text : undefined;
|
|
288
|
+
};
|
|
289
|
+
const visit = (node, enclosingImplType) => {
|
|
290
|
+
const type = node.type;
|
|
291
|
+
if (type === 'struct_item' || type === 'enum_item' || type === 'trait_item') {
|
|
292
|
+
const nameNode = node.childForFieldName('name');
|
|
293
|
+
const name = nameNode ? nameNode.text : undefined;
|
|
294
|
+
if (name) {
|
|
295
|
+
out.push({
|
|
296
|
+
name,
|
|
297
|
+
kind: 'class',
|
|
298
|
+
qualifiedName: name,
|
|
299
|
+
startLine: node.startPosition.row + 1,
|
|
300
|
+
endLine: node.endPosition.row + 1,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
// A trait's DEFAULT-body methods (`trait T{ fn d(&self){} }`) are
|
|
304
|
+
// function_item nodes lexically nested directly inside the trait, with
|
|
305
|
+
// no enclosing impl_item — so trait_item must ALSO open a qualifying
|
|
306
|
+
// scope for its children, exactly like impl_item, or a default method
|
|
307
|
+
// would wrongly surface as an unqualified top-level function. Harmless
|
|
308
|
+
// for struct_item/enum_item, which never have function_item children
|
|
309
|
+
// directly (Rust has no inline-method syntax on structs/enums — only
|
|
310
|
+
// via a separate impl block, which sets its own qualifier regardless).
|
|
311
|
+
const childImplType = name || enclosingImplType;
|
|
312
|
+
for (let i = 0; i < node.childCount; i++)
|
|
313
|
+
visit(node.child(i), childImplType);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
if (type === 'impl_item') {
|
|
317
|
+
// impl_item does NOT emit a class symbol of its own — it only opens a
|
|
318
|
+
// qualifying scope (the target type name) for its function_item
|
|
319
|
+
// children.
|
|
320
|
+
const implType = implTargetTypeName(node);
|
|
321
|
+
for (let i = 0; i < node.childCount; i++)
|
|
322
|
+
visit(node.child(i), implType);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
if (type === 'function_item') {
|
|
326
|
+
const nameNode = node.childForFieldName('name');
|
|
327
|
+
const name = nameNode ? nameNode.text : undefined;
|
|
328
|
+
if (name) {
|
|
329
|
+
const isMethod = !!enclosingImplType;
|
|
330
|
+
out.push({
|
|
331
|
+
name,
|
|
332
|
+
kind: isMethod ? 'method' : 'function',
|
|
333
|
+
qualifiedName: isMethod ? `${enclosingImplType}.${name}` : name,
|
|
334
|
+
startLine: node.startPosition.row + 1,
|
|
335
|
+
endLine: node.endPosition.row + 1,
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
// A function nested inside this function_item's body (a local fn) is
|
|
339
|
+
// never itself a method, regardless of whether THIS function_item was
|
|
340
|
+
// one — reset enclosingImplType to undefined for its children.
|
|
341
|
+
for (let i = 0; i < node.childCount; i++)
|
|
342
|
+
visit(node.child(i), undefined);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
for (let i = 0; i < node.childCount; i++)
|
|
346
|
+
visit(node.child(i), enclosingImplType);
|
|
347
|
+
};
|
|
348
|
+
visit(tree.rootNode, undefined);
|
|
349
|
+
return out;
|
|
350
|
+
}
|
|
351
|
+
// @implements A-SPEC-121.6
|
|
352
|
+
// C++ IS lexical (member functions live inside class_specifier/struct_specifier,
|
|
353
|
+
// like TypeScript/Python/C#/Java) — but unlike those, the function's NAME is
|
|
354
|
+
// never a direct `name` field on the definition node itself. A
|
|
355
|
+
// `function_definition`'s name lives inside a `declarator` chain
|
|
356
|
+
// (function_declarator -> identifier/field_identifier/qualified_identifier),
|
|
357
|
+
// so walkSymbols' `childForFieldName('name')` model does not apply and C++
|
|
358
|
+
// needs its own dedicated walker rather than a LANG_CONFIG entry.
|
|
359
|
+
function walkCppSymbols(tree) {
|
|
360
|
+
const out = [];
|
|
361
|
+
// Descend through a function_declarator's `declarator` field to the
|
|
362
|
+
// underlying name node, unwrapping pointer/reference-returning-function
|
|
363
|
+
// wrappers (`int* foo()` / `int& foo()` parse the declarator as
|
|
364
|
+
// pointer_declarator/reference_declarator wrapping the function_declarator,
|
|
365
|
+
// or vice versa depending on nesting) until an identifier/field_identifier/
|
|
366
|
+
// qualified_identifier is reached, or giving up (undefined) if none is found.
|
|
367
|
+
const resolveDeclaratorName = (declNode) => {
|
|
368
|
+
let n = declNode;
|
|
369
|
+
while (n) {
|
|
370
|
+
if (n.type === 'identifier' || n.type === 'field_identifier' || n.type === 'qualified_identifier') {
|
|
371
|
+
return n;
|
|
372
|
+
}
|
|
373
|
+
if (n.type === 'function_declarator' || n.type === 'pointer_declarator' || n.type === 'reference_declarator') {
|
|
374
|
+
// `reference_declarator` (a reference-return function/method like
|
|
375
|
+
// `int& bar()` or `C& operator=(...)`) has NO named `declarator` field
|
|
376
|
+
// in tree-sitter-cpp — its wrapped declarator is its first named child.
|
|
377
|
+
// Falling back to namedChild(0) recovers the name instead of silently
|
|
378
|
+
// dropping the whole symbol. function_declarator/pointer_declarator do
|
|
379
|
+
// expose the `declarator` field, so this fallback only fires when the
|
|
380
|
+
// field is absent.
|
|
381
|
+
const next = n.childForFieldName('declarator') || n.namedChild(0);
|
|
382
|
+
if (!next || next === n)
|
|
383
|
+
return undefined;
|
|
384
|
+
n = next;
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
return undefined;
|
|
388
|
+
}
|
|
389
|
+
return undefined;
|
|
390
|
+
};
|
|
391
|
+
const visit = (node, enclosingClass) => {
|
|
392
|
+
const type = node.type;
|
|
393
|
+
if (type === 'class_specifier' || type === 'struct_specifier') {
|
|
394
|
+
const nameNode = node.childForFieldName('name');
|
|
395
|
+
const name = nameNode ? nameNode.text : undefined;
|
|
396
|
+
if (name) {
|
|
397
|
+
out.push({
|
|
398
|
+
name,
|
|
399
|
+
kind: 'class',
|
|
400
|
+
qualifiedName: name,
|
|
401
|
+
startLine: node.startPosition.row + 1,
|
|
402
|
+
endLine: node.endPosition.row + 1,
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
const childClass = name || enclosingClass;
|
|
406
|
+
for (let i = 0; i < node.childCount; i++)
|
|
407
|
+
visit(node.child(i), childClass);
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
if (type === 'function_definition') {
|
|
411
|
+
// The declarator field of a function_definition is a function_declarator
|
|
412
|
+
// (possibly wrapped in pointer_declarator/reference_declarator for a
|
|
413
|
+
// pointer/reference return type, e.g. `int* foo(){}` / `int& foo(){}`);
|
|
414
|
+
// resolveDeclaratorName unwraps those wrappers uniformly, drilling down
|
|
415
|
+
// to the innermost identifier/field_identifier/qualified_identifier.
|
|
416
|
+
const declarator = node.childForFieldName('declarator');
|
|
417
|
+
const nameNode = declarator ? resolveDeclaratorName(declarator) : undefined;
|
|
418
|
+
if (nameNode) {
|
|
419
|
+
const startLine = node.startPosition.row + 1;
|
|
420
|
+
const endLine = node.endPosition.row + 1;
|
|
421
|
+
if (nameNode.type === 'qualified_identifier') {
|
|
422
|
+
// Out-of-line member definition, e.g. `void C::outOfLine(){}` or a
|
|
423
|
+
// multi-segment `A::B::m` — the qualifying CLASS is always the
|
|
424
|
+
// segment immediately before the LAST '::'; the method name is the
|
|
425
|
+
// final segment.
|
|
426
|
+
const segments = nameNode.text.split('::');
|
|
427
|
+
const name = segments[segments.length - 1];
|
|
428
|
+
const cls = segments.length > 1 ? segments[segments.length - 2] : undefined;
|
|
429
|
+
if (name && cls) {
|
|
430
|
+
out.push({
|
|
431
|
+
name,
|
|
432
|
+
kind: 'method',
|
|
433
|
+
qualifiedName: `${cls}.${name}`,
|
|
434
|
+
startLine,
|
|
435
|
+
endLine,
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
else {
|
|
440
|
+
// identifier -> plain function, unless lexically inside a class
|
|
441
|
+
// (which shouldn't normally happen for a bare identifier declarator,
|
|
442
|
+
// but is handled defensively); field_identifier -> always a member
|
|
443
|
+
// function (only appears inside a class/struct body).
|
|
444
|
+
const name = nameNode.text;
|
|
445
|
+
const isMethod = nameNode.type === 'field_identifier' || !!enclosingClass;
|
|
446
|
+
const kind = isMethod ? 'method' : 'function';
|
|
447
|
+
const qualifiedName = isMethod && enclosingClass ? `${enclosingClass}.${name}` : name;
|
|
448
|
+
out.push({ name, kind, qualifiedName, startLine, endLine });
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
// A function nested inside this function's body is never itself a
|
|
452
|
+
// method of the outer enclosing class merely by lexical accident, but
|
|
453
|
+
// C++ has no local function/class definitions inside a function body
|
|
454
|
+
// in the way Python does — recurse with enclosingClass unchanged is
|
|
455
|
+
// harmless here since function bodies can't contain further
|
|
456
|
+
// function_definition/class_specifier nodes at the top level we care
|
|
457
|
+
// about. Recurse into children (e.g. lambda bodies) preserving context.
|
|
458
|
+
for (let i = 0; i < node.childCount; i++)
|
|
459
|
+
visit(node.child(i), enclosingClass);
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
// namespace_definition is deliberately NOT a class-like scope: a
|
|
463
|
+
// function_definition lexically inside a namespace is still a plain
|
|
464
|
+
// top-level FUNCTION (not qualified by the namespace) — so no special
|
|
465
|
+
// handling is needed here; it just falls through to the generic recurse
|
|
466
|
+
// below with enclosingClass unchanged (namespaces don't open a class scope).
|
|
467
|
+
for (let i = 0; i < node.childCount; i++)
|
|
468
|
+
visit(node.child(i), enclosingClass);
|
|
469
|
+
};
|
|
470
|
+
visit(tree.rootNode, undefined);
|
|
471
|
+
return out;
|
|
472
|
+
}
|
|
473
|
+
function extractSymbolsFromTree(tree, lang = 'typescript') {
|
|
474
|
+
if (lang === 'go')
|
|
475
|
+
return walkGoSymbols(tree);
|
|
476
|
+
if (lang === 'rust')
|
|
477
|
+
return walkRustSymbols(tree);
|
|
478
|
+
if (lang === 'cpp')
|
|
479
|
+
return walkCppSymbols(tree);
|
|
480
|
+
const config = LANG_CONFIG[lang] || LANG_CONFIG.typescript;
|
|
481
|
+
return walkSymbols(tree, config);
|
|
482
|
+
}
|
|
483
|
+
// @implements A-SPEC-136
|
|
484
|
+
// Per-language call-edge extraction table, measured against the installed grammars. Adding a language
|
|
485
|
+
// is one row here — the same table-driven shape symbol extraction already uses (LANG_CONFIG). The
|
|
486
|
+
// shared walkEdges below reads it. `callTypes` is the call-node type; `calleeField` is the field
|
|
487
|
+
// holding the callee (an identifier, or a member/selector node that walkEdges unwraps to its last
|
|
488
|
+
// identifier segment); `scopeTypes` are the class/function nodes whose `name` builds the enclosing
|
|
489
|
+
// qualified path (so a method's edges resolve to `Class.method`, matching the symbol walk).
|
|
490
|
+
const EDGE_CONFIG = {
|
|
491
|
+
go: { callTypes: ['call_expression'], calleeField: 'function', scopeTypes: ['function_declaration', 'method_declaration'] },
|
|
492
|
+
rust: { callTypes: ['call_expression'], calleeField: 'function', scopeTypes: ['function_item'] },
|
|
493
|
+
java: { callTypes: ['method_invocation'], calleeField: 'name', scopeTypes: ['class_declaration', 'method_declaration'] },
|
|
494
|
+
csharp: { callTypes: ['invocation_expression'], calleeField: 'function', scopeTypes: ['class_declaration', 'method_declaration'] },
|
|
495
|
+
cpp: { callTypes: ['call_expression'], calleeField: 'function', scopeTypes: ['function_definition'] },
|
|
496
|
+
};
|
|
497
|
+
// The callee's bare name: a plain identifier is itself; a member/selector/scoped/field node
|
|
498
|
+
// (`o.m`, `self.c`, `mod::f`, `this.C`) unwraps to its LAST identifier segment — the method name.
|
|
499
|
+
// The receiver's type is never inferred (precision over recall); addCallEdges decides ambiguity.
|
|
500
|
+
function calleeNameOf(node) {
|
|
501
|
+
if (!node)
|
|
502
|
+
return null;
|
|
503
|
+
if (node.type === 'identifier' || node.type === 'field_identifier')
|
|
504
|
+
return node.text;
|
|
505
|
+
// member/selector/scoped access: the last named child that is an identifier is the method name.
|
|
506
|
+
let last = null;
|
|
507
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
508
|
+
const c = node.namedChild(i);
|
|
509
|
+
if (c.type === 'identifier' || c.type === 'field_identifier')
|
|
510
|
+
last = c.text;
|
|
511
|
+
}
|
|
512
|
+
return last;
|
|
513
|
+
}
|
|
514
|
+
// @implements A-SPEC-136
|
|
515
|
+
// The shared table-driven edge walk. `from` is the enclosing definition's QUALIFIED name (so it
|
|
516
|
+
// matches the symbol node id and can be resolved), `to` is the bare callee name. A call the config
|
|
517
|
+
// cannot read emits nothing — never a guessed edge.
|
|
518
|
+
function walkEdges(tree, cfg) {
|
|
519
|
+
const out = [];
|
|
520
|
+
const scope = new Set(cfg.scopeTypes);
|
|
521
|
+
const callTypes = new Set(cfg.callTypes);
|
|
522
|
+
// A scope node's name. Most languages expose a `name` field; C++ nests it inside the declarator
|
|
523
|
+
// chain (function_definition → function_declarator → identifier), so fall back to descending
|
|
524
|
+
// `declarator` fields to the first identifier — otherwise a C++ function's edges would qualify to
|
|
525
|
+
// `<module>` and never resolve to its bare symbol name.
|
|
526
|
+
const scopeName = (n) => {
|
|
527
|
+
const nm = n.childForFieldName('name');
|
|
528
|
+
if (nm)
|
|
529
|
+
return nm.text;
|
|
530
|
+
let d = n.childForFieldName('declarator');
|
|
531
|
+
for (let hops = 0; d && hops < 5; hops++) {
|
|
532
|
+
if (d.type === 'identifier' || d.type === 'field_identifier')
|
|
533
|
+
return d.text;
|
|
534
|
+
if (d.type === 'qualified_identifier' || d.type === 'scoped_identifier')
|
|
535
|
+
return calleeNameOf(d); // last segment
|
|
536
|
+
d = d.childForFieldName('declarator');
|
|
537
|
+
}
|
|
538
|
+
return null;
|
|
539
|
+
};
|
|
540
|
+
const enclosing = (node) => {
|
|
541
|
+
const parts = [];
|
|
542
|
+
let n = node.parent;
|
|
543
|
+
while (n) {
|
|
544
|
+
if (scope.has(n.type)) {
|
|
545
|
+
const nm = scopeName(n);
|
|
546
|
+
if (nm)
|
|
547
|
+
parts.unshift(nm);
|
|
548
|
+
}
|
|
549
|
+
n = n.parent;
|
|
550
|
+
}
|
|
551
|
+
return parts.length ? parts.join('.') : '<module>';
|
|
552
|
+
};
|
|
553
|
+
const visit = (node) => {
|
|
554
|
+
if (callTypes.has(node.type)) {
|
|
555
|
+
const callee = calleeNameOf(node.childForFieldName(cfg.calleeField));
|
|
556
|
+
if (callee)
|
|
557
|
+
out.push({ from: enclosing(node), to: callee, rel: 'calls' });
|
|
558
|
+
}
|
|
559
|
+
for (let i = 0; i < node.childCount; i++)
|
|
560
|
+
visit(node.child(i));
|
|
561
|
+
};
|
|
562
|
+
visit(tree.rootNode);
|
|
563
|
+
return out;
|
|
564
|
+
}
|
|
565
|
+
function extractEdgesFromTree(tree, lang = 'typescript') {
|
|
566
|
+
// @implements A-SPEC-136
|
|
567
|
+
// Call edges ARE wired end-to-end: CpgScanner.scan populates ScannedFile.edges from here,
|
|
568
|
+
// addCallEdges resolves bare callee names to graph edges (precision-over-recall), and impactedBy
|
|
569
|
+
// traverses `callersOf`. TS/JS/Python have bespoke walks (kept for their import handling); the
|
|
570
|
+
// other five are table-driven (EDGE_CONFIG + walkEdges). See the CodeEdge comment in
|
|
571
|
+
// language-parser.ts.
|
|
572
|
+
if (lang === 'python')
|
|
573
|
+
return walkPythonEdges(tree);
|
|
574
|
+
if (EDGE_CONFIG[lang])
|
|
575
|
+
return walkEdges(tree, EDGE_CONFIG[lang]);
|
|
576
|
+
// TS/JS family: the original walk below (its import_statement handling is TS-grammar-specific).
|
|
577
|
+
if (lang !== 'typescript' && lang !== 'tsx')
|
|
578
|
+
return [];
|
|
579
|
+
const out = [];
|
|
580
|
+
// QUALIFIED, to match extractSymbolsFromTree: a method inside `class Foo` is `Foo.method`, which is
|
|
581
|
+
// the name the symbol walk emitted and therefore the only name a graph node can be resolved from.
|
|
582
|
+
// Returning the bare method name here would make every method's outgoing edges unresolvable.
|
|
583
|
+
const enclosingFn = (node) => {
|
|
584
|
+
const parts = [];
|
|
585
|
+
let n = node.parent;
|
|
586
|
+
while (n) {
|
|
587
|
+
if (n.type === 'function_declaration' || n.type === 'method_definition' || n.type === 'class_declaration') {
|
|
588
|
+
const nm = n.childForFieldName('name');
|
|
589
|
+
if (nm)
|
|
590
|
+
parts.unshift(nm.text);
|
|
591
|
+
}
|
|
592
|
+
n = n.parent;
|
|
593
|
+
}
|
|
594
|
+
return parts.length ? parts.join('.') : '<module>';
|
|
595
|
+
};
|
|
596
|
+
const visit = (node) => {
|
|
597
|
+
if (node.type === 'import_statement') {
|
|
598
|
+
const src = node.childForFieldName('source') ?? (node.namedChildren && node.namedChildren.find((c) => c.type === 'string'));
|
|
599
|
+
if (src)
|
|
600
|
+
out.push({ from: '<module>', to: src.text.replace(/['"]/g, ''), rel: 'imports' });
|
|
601
|
+
}
|
|
602
|
+
else if (node.type === 'call_expression') {
|
|
603
|
+
const fn = node.childForFieldName('function');
|
|
604
|
+
if (fn && (fn.type === 'identifier' || fn.type === 'member_expression')) {
|
|
605
|
+
const property = fn.childForFieldName && fn.childForFieldName('property');
|
|
606
|
+
const callee = fn.type === 'identifier' ? fn.text : (property ? property.text : fn.text);
|
|
607
|
+
out.push({ from: enclosingFn(node), to: callee, rel: 'calls' });
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
for (let i = 0; i < node.childCount; i++)
|
|
611
|
+
visit(node.child(i));
|
|
612
|
+
};
|
|
613
|
+
visit(tree.rootNode);
|
|
614
|
+
return out;
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Python call/import edges.
|
|
618
|
+
*
|
|
619
|
+
* Mirrors the TS walk's contract exactly: `from` is the enclosing definition's QUALIFIED name (so it
|
|
620
|
+
* matches what extractSymbolsFromTree emitted for the same node and can be resolved to a graph node
|
|
621
|
+
* id), `to` is the bare callee name for calls and the module path for imports.
|
|
622
|
+
*
|
|
623
|
+
* PRECISION OVER RECALL, per the target architecture's D-CPG principle. Only forms whose target is
|
|
624
|
+
* syntactically evident are emitted: a direct `f()` or `obj.m()` call, and an `import x` /
|
|
625
|
+
* `from x import y`. Dynamic dispatch (`getattr(o, name)()`), `importlib.import_module(var)` and
|
|
626
|
+
* string-keyed registries produce NOTHING rather than a guessed edge — a wrong edge is worse than a
|
|
627
|
+
* missing one, because the missing one shows up as an unanchored gap while the wrong one silently
|
|
628
|
+
* corrupts every impact answer that traverses it.
|
|
629
|
+
*/
|
|
630
|
+
function walkPythonEdges(tree) {
|
|
631
|
+
const out = [];
|
|
632
|
+
// Qualified enclosing name: `Class.method` for a def inside a class, matching the symbol walk.
|
|
633
|
+
const enclosing = (node) => {
|
|
634
|
+
const parts = [];
|
|
635
|
+
let n = node.parent;
|
|
636
|
+
while (n) {
|
|
637
|
+
if (n.type === 'function_definition' || n.type === 'class_definition') {
|
|
638
|
+
const nm = n.childForFieldName('name');
|
|
639
|
+
if (nm)
|
|
640
|
+
parts.unshift(nm.text);
|
|
641
|
+
}
|
|
642
|
+
n = n.parent;
|
|
643
|
+
}
|
|
644
|
+
return parts.length ? parts.join('.') : '<module>';
|
|
645
|
+
};
|
|
646
|
+
const moduleName = (n) => {
|
|
647
|
+
if (!n)
|
|
648
|
+
return '';
|
|
649
|
+
if (n.type === 'aliased_import') {
|
|
650
|
+
const nm = n.childForFieldName('name');
|
|
651
|
+
return nm ? nm.text : '';
|
|
652
|
+
}
|
|
653
|
+
return n.text;
|
|
654
|
+
};
|
|
655
|
+
const visit = (node) => {
|
|
656
|
+
if (node.type === 'import_statement') {
|
|
657
|
+
for (const c of node.namedChildren ?? []) {
|
|
658
|
+
const m = moduleName(c);
|
|
659
|
+
if (m)
|
|
660
|
+
out.push({ from: '<module>', to: m, rel: 'imports' });
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
else if (node.type === 'import_from_statement') {
|
|
664
|
+
const m = moduleName(node.childForFieldName('module_name'));
|
|
665
|
+
if (m)
|
|
666
|
+
out.push({ from: '<module>', to: m, rel: 'imports' });
|
|
667
|
+
}
|
|
668
|
+
else if (node.type === 'call') {
|
|
669
|
+
const fn = node.childForFieldName('function');
|
|
670
|
+
if (fn && (fn.type === 'identifier' || fn.type === 'attribute')) {
|
|
671
|
+
// `obj.method()` -> `method`, matching the TS walk which takes the member expression's
|
|
672
|
+
// property. The receiver is not resolvable without type inference, which is out of scope.
|
|
673
|
+
const attr = fn.childForFieldName && fn.childForFieldName('attribute');
|
|
674
|
+
const callee = fn.type === 'identifier' ? fn.text : (attr ? attr.text : fn.text);
|
|
675
|
+
if (callee)
|
|
676
|
+
out.push({ from: enclosing(node), to: callee, rel: 'calls' });
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
for (let i = 0; i < node.childCount; i++)
|
|
680
|
+
visit(node.child(i));
|
|
681
|
+
};
|
|
682
|
+
visit(tree.rootNode);
|
|
683
|
+
return out;
|
|
684
|
+
}
|
|
685
|
+
// @implements A-SPEC-131
|
|
686
|
+
// The ONE parse form both the inline and worker paths use. node-tree-sitter's parse(string) throws
|
|
687
|
+
// 'Invalid argument' above 32,767 chars (measured: handlers.ts at 37,289 chars — silently dropped
|
|
688
|
+
// from the graph for the project's entire history). An explicit bufferSize removes the cliff.
|
|
689
|
+
//
|
|
690
|
+
// MEASURED CONTRACT (adversarial-review sweep, 2026-08-06): the requirement is
|
|
691
|
+
// bufferSize >= the input's UTF-16 LENGTH — not its UTF-8 byte length. handlers.ts (37,289 units)
|
|
692
|
+
// fails at 32,768 and parses at 65,536; a 200K-unit Korean-heavy source fails at 65,536 regardless
|
|
693
|
+
// of its ~2.9x UTF-8 expansion; a 600K-unit source parses with bufferSize below its byte length.
|
|
694
|
+
// So `length * 2` over-provisions by design and NO input can re-introduce the cliff. Verified
|
|
695
|
+
// tree-identical to string parsing on small input, and error-free on the previously-throwing file.
|
|
696
|
+
function parseWithBuffer(parser, code) {
|
|
697
|
+
return parser.parse(code, undefined, { bufferSize: Math.max(1024 * 1024, code.length * 2 + 16) });
|
|
698
|
+
}
|
|
699
|
+
// @implements A-SPEC-140.1
|
|
700
|
+
// Data-flow FACTS — the four kinds a def-use fixpoint needs, and nothing more. This layer performs
|
|
701
|
+
// no analysis: it reports what the tree literally says, tagged with the enclosing function's
|
|
702
|
+
// qualified name so the facts join the call graph produced by extractEdgesFromTree.
|
|
703
|
+
//
|
|
704
|
+
// The identity scheme is deliberately the SAME as the TS/JS edge walk's `enclosingFn` — a fact whose
|
|
705
|
+
// `fn` did not match an edge's `from` would make every inter-procedural hop silently unresolvable,
|
|
706
|
+
// which reads as "clean" rather than "not analysed". A test pins the two against each other.
|
|
707
|
+
const DF_SCOPE_TYPES = new Set(['function_declaration', 'method_definition', 'class_declaration']);
|
|
708
|
+
const DF_FUNC_TYPES = new Set([
|
|
709
|
+
'function_declaration', 'method_definition', 'function_expression', 'arrow_function', 'function',
|
|
710
|
+
]);
|
|
711
|
+
const EXPR_MAX = 200;
|
|
712
|
+
// An expression as STRUCTURE, not text: `{ text, refs, callees }`.
|
|
713
|
+
//
|
|
714
|
+
// Text alone forces the propagation pass to ask "does this mention a tainted variable?" lexically,
|
|
715
|
+
// and two ordinary lines break that: `const note = "raw user input";` falsely taints `note` because
|
|
716
|
+
// the literal contains the word, and `exec(sanitize(raw))` looks tainted because the argument text
|
|
717
|
+
// contains `raw` even though it was sanitized inline. Both are precisely the false positives REQ-140
|
|
718
|
+
// exists to remove, so the structure is recovered HERE, from the tree, once.
|
|
719
|
+
//
|
|
720
|
+
// `refs` are the identifiers READ: a property name after a dot is not a read (`req.body.cmd` reads
|
|
721
|
+
// only `req`), and a callee is not a read. `callees` are the invoked names, by the same last-segment
|
|
722
|
+
// rule the edge walk uses. Both are deduped and in first-appearance order, which makes them
|
|
723
|
+
// deterministic and diffable.
|
|
724
|
+
function exprOf(node) {
|
|
725
|
+
const refs = [], callees = [];
|
|
726
|
+
const seenRef = new Set(), seenCallee = new Set();
|
|
727
|
+
if (!node)
|
|
728
|
+
return { text: '', refs, callees };
|
|
729
|
+
const addRef = (name) => { if (name && !seenRef.has(name)) {
|
|
730
|
+
seenRef.add(name);
|
|
731
|
+
refs.push(name);
|
|
732
|
+
} };
|
|
733
|
+
const addCallee = (name) => { if (name && !seenCallee.has(name)) {
|
|
734
|
+
seenCallee.add(name);
|
|
735
|
+
callees.push(name);
|
|
736
|
+
} };
|
|
737
|
+
const visit = (n) => {
|
|
738
|
+
if (n.type === 'call_expression') {
|
|
739
|
+
const fnNode = n.childForFieldName('function');
|
|
740
|
+
if (fnNode) {
|
|
741
|
+
if (fnNode.type === 'identifier') {
|
|
742
|
+
addCallee(fnNode.text);
|
|
743
|
+
}
|
|
744
|
+
else if (fnNode.type === 'member_expression') {
|
|
745
|
+
const prop = fnNode.childForFieldName('property');
|
|
746
|
+
addCallee(prop ? prop.text : null);
|
|
747
|
+
// The RECEIVER is genuinely read (`o.m(x)` reads `o`), so descend into the object side
|
|
748
|
+
// only — descending into the whole member_expression would re-add the method name.
|
|
749
|
+
const obj = fnNode.childForFieldName('object');
|
|
750
|
+
if (obj)
|
|
751
|
+
visit(obj);
|
|
752
|
+
}
|
|
753
|
+
else {
|
|
754
|
+
visit(fnNode); // computed/complex callee: no name to record, but its parts are read
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
const args = n.childForFieldName('arguments');
|
|
758
|
+
if (args)
|
|
759
|
+
for (let i = 0; i < args.childCount; i++)
|
|
760
|
+
visit(args.child(i));
|
|
761
|
+
return; // children handled explicitly above
|
|
762
|
+
}
|
|
763
|
+
if (n.type === 'member_expression') {
|
|
764
|
+
const obj = n.childForFieldName('object');
|
|
765
|
+
if (obj)
|
|
766
|
+
visit(obj);
|
|
767
|
+
// The `property` side is a name in the object, not a variable in scope — never a read.
|
|
768
|
+
// A COMPUTED index (`o[k]`) is a real read of `k`, and the grammar gives it as a
|
|
769
|
+
// subscript_expression, which falls through to the generic recursion below instead.
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
if (n.type === 'identifier' || n.type === 'shorthand_property_identifier')
|
|
773
|
+
addRef(n.text);
|
|
774
|
+
for (let i = 0; i < n.childCount; i++)
|
|
775
|
+
visit(n.child(i));
|
|
776
|
+
};
|
|
777
|
+
visit(node);
|
|
778
|
+
return { text: node.text.trim().slice(0, EXPR_MAX), refs, callees };
|
|
779
|
+
}
|
|
780
|
+
// Which languages have a data-flow extractor. A language absent here yields `undefined` — NOT an
|
|
781
|
+
// empty fact set, which a caller would be entitled to read as "analysed and found nothing".
|
|
782
|
+
const DATAFLOW_LANGS = new Set(['typescript', 'tsx']);
|
|
783
|
+
function extractDataFlowFromTree(tree, lang = 'typescript') {
|
|
784
|
+
if (!DATAFLOW_LANGS.has(lang))
|
|
785
|
+
return undefined;
|
|
786
|
+
const defs = [], calls = [], params = [], returns = [];
|
|
787
|
+
// Qualified enclosing name, identical in construction to the TS edge walk's enclosingFn.
|
|
788
|
+
const enclosing = (node) => {
|
|
789
|
+
const parts = [];
|
|
790
|
+
let n = node.parent;
|
|
791
|
+
while (n) {
|
|
792
|
+
if (DF_SCOPE_TYPES.has(n.type)) {
|
|
793
|
+
const nm = n.childForFieldName('name');
|
|
794
|
+
if (nm)
|
|
795
|
+
parts.unshift(nm.text);
|
|
796
|
+
}
|
|
797
|
+
n = n.parent;
|
|
798
|
+
}
|
|
799
|
+
return parts.length ? parts.join('.') : '<module>';
|
|
800
|
+
};
|
|
801
|
+
// A function node's own qualified name, or null when it has none — `enclosing` walks from the
|
|
802
|
+
// PARENT, so a function's parameters (which live inside the function node) must be attributed to
|
|
803
|
+
// the function itself.
|
|
804
|
+
//
|
|
805
|
+
// An ANONYMOUS function (arrow, function expression) yields null and therefore emits NO parameter
|
|
806
|
+
// facts. Measured why: attributing them to the enclosing scope collapsed three distinct top-level
|
|
807
|
+
// arrows onto `{fn:'<module>', index:0}` with three different names — not a missing fact but a
|
|
808
|
+
// meaningless one, and `<module>` has no parameters. Losing the hop is a disclosed false negative;
|
|
809
|
+
// inventing an owner is the kind of wrong fact this walk exists to avoid.
|
|
810
|
+
//
|
|
811
|
+
// NAMED LIMIT, MEASURED. Counting function nodes: 88.6% of this repo's own src (2452/2767) and
|
|
812
|
+
// 83.1% of `reference/`'s OSS TS (4093/4927) are anonymous. Most of that is inline callbacks,
|
|
813
|
+
// which need no parameter binding — the subset that actually costs coverage is a function BOUND
|
|
814
|
+
// TO A NAME (`const f = (a) => {}`), and that is 248 in src and 14 in reference. So in this
|
|
815
|
+
// repository roughly 44% of name-callable functions (248 of 563) contribute no parameter facts.
|
|
816
|
+
//
|
|
817
|
+
// Naming such a function from its declarator would need the CALL walk to agree — a call inside
|
|
818
|
+
// that arrow is attributed to `<module>` today, so taint entering `f` still could not reach
|
|
819
|
+
// anything `f` calls. A half-fix would raise the fact count while leaving the hop broken, which
|
|
820
|
+
// reads as coverage that does not exist. The real fix is one identity-scheme change shared by
|
|
821
|
+
// both walks (A-SPEC-136 territory, and it would improve impact analysis too, which suffers the
|
|
822
|
+
// same collapse). The two schemes staying identical is the invariant that lets facts join the
|
|
823
|
+
// graph at all, so they move together or not at all.
|
|
824
|
+
const selfName = (fnNode) => {
|
|
825
|
+
const nm = fnNode.childForFieldName('name');
|
|
826
|
+
if (!nm)
|
|
827
|
+
return null;
|
|
828
|
+
const outer = enclosing(fnNode);
|
|
829
|
+
return outer === '<module>' ? nm.text : `${outer}.${nm.text}`;
|
|
830
|
+
};
|
|
831
|
+
const visit = (node) => {
|
|
832
|
+
if (node.type === 'variable_declarator') {
|
|
833
|
+
const nm = node.childForFieldName('name');
|
|
834
|
+
const val = node.childForFieldName('value');
|
|
835
|
+
// Only a bare identifier target: a destructuring pattern binds several names through a
|
|
836
|
+
// structure this layer does not model, so it yields no def rather than a guessed one.
|
|
837
|
+
if (nm && nm.type === 'identifier' && val) {
|
|
838
|
+
defs.push({ fn: enclosing(node), name: nm.text, expr: exprOf(val), line: node.startPosition.row + 1 });
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
else if (node.type === 'assignment_expression') {
|
|
842
|
+
const left = node.childForFieldName('left');
|
|
843
|
+
const right = node.childForFieldName('right');
|
|
844
|
+
if (left && left.type === 'identifier' && right) {
|
|
845
|
+
defs.push({ fn: enclosing(node), name: left.text, expr: exprOf(right), line: node.startPosition.row + 1 });
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
else if (node.type === 'call_expression') {
|
|
849
|
+
const fn = node.childForFieldName('function');
|
|
850
|
+
if (fn && (fn.type === 'identifier' || fn.type === 'member_expression')) {
|
|
851
|
+
const property = fn.childForFieldName && fn.childForFieldName('property');
|
|
852
|
+
const callee = fn.type === 'identifier' ? fn.text : (property ? property.text : null);
|
|
853
|
+
if (callee) {
|
|
854
|
+
// ORDERED argument texts. Position is what binds an argument to a parameter, so this list
|
|
855
|
+
// is never sorted, deduped, or filtered — a dropped element would shift every later
|
|
856
|
+
// binding onto the wrong slot.
|
|
857
|
+
const argsNode = node.childForFieldName('arguments');
|
|
858
|
+
const args = [];
|
|
859
|
+
if (argsNode) {
|
|
860
|
+
for (let i = 0; i < argsNode.namedChildCount; i++) {
|
|
861
|
+
const a = argsNode.namedChild(i);
|
|
862
|
+
if (a.type === 'comment')
|
|
863
|
+
continue;
|
|
864
|
+
args.push(exprOf(a));
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
calls.push({ fn: enclosing(node), callee, args, line: node.startPosition.row + 1 });
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
else if (DF_FUNC_TYPES.has(node.type)) {
|
|
872
|
+
const ps = node.childForFieldName('parameters');
|
|
873
|
+
const owner = selfName(node);
|
|
874
|
+
if (ps && owner) {
|
|
875
|
+
for (let i = 0; i < ps.namedChildCount; i++) {
|
|
876
|
+
const p = ps.namedChild(i);
|
|
877
|
+
if (p.type === 'comment')
|
|
878
|
+
continue;
|
|
879
|
+
// The parameter's bare identifier. `x`, `x: T`, `x = d` and `x: T = d` all expose one;
|
|
880
|
+
// a destructuring or rest pattern does not, and is SKIPPED — but its POSITION still
|
|
881
|
+
// consumes an index, or every later parameter would bind one slot too early.
|
|
882
|
+
const name = paramIdentifier(p);
|
|
883
|
+
if (name)
|
|
884
|
+
params.push({ fn: owner, index: i, name });
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
else if (node.type === 'return_statement') {
|
|
889
|
+
// A bare `return;` has no named child and carries no value — no fact.
|
|
890
|
+
const val = node.namedChildCount > 0 ? node.namedChild(0) : null;
|
|
891
|
+
if (val && val.type !== 'comment') {
|
|
892
|
+
returns.push({ fn: enclosing(node), expr: exprOf(val), line: node.startPosition.row + 1 });
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
for (let i = 0; i < node.childCount; i++)
|
|
896
|
+
visit(node.child(i));
|
|
897
|
+
};
|
|
898
|
+
visit(tree.rootNode);
|
|
899
|
+
return { defs, calls, params, returns };
|
|
900
|
+
}
|
|
901
|
+
// The bare identifier a parameter binds, or null when the parameter binds through a pattern this
|
|
902
|
+
// layer does not model. Never returns the pattern's text: naming `{a, b}` as `a` would bind taint to
|
|
903
|
+
// a variable that does not exist at that position.
|
|
904
|
+
function paramIdentifier(p) {
|
|
905
|
+
if (p.type === 'identifier')
|
|
906
|
+
return p.text;
|
|
907
|
+
// required_parameter / optional_parameter (TS) wrap the binding in a `pattern` field.
|
|
908
|
+
const pat = p.childForFieldName && p.childForFieldName('pattern');
|
|
909
|
+
if (pat && pat.type === 'identifier')
|
|
910
|
+
return pat.text;
|
|
911
|
+
// `x = default` in plain JS: assignment_pattern with an identifier on the left.
|
|
912
|
+
const left = p.childForFieldName && p.childForFieldName('left');
|
|
913
|
+
if (left && left.type === 'identifier')
|
|
914
|
+
return left.text;
|
|
915
|
+
return null;
|
|
916
|
+
}
|
|
917
|
+
module.exports = { LANG_CONFIG, DATAFLOW_LANGS, extractSymbolsFromTree, extractEdgesFromTree, extractDataFlowFromTree, walkGoSymbols, walkRustSymbols, walkPythonEdges, parseWithBuffer };
|