@holmes-lab/holmes-kit 0.5.0 → 0.7.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.
@@ -17,7 +17,9 @@ function defUseOf(ast, cfg, fn, source) {
17
17
  const isPython = ast.lang === 'python';
18
18
  const isGo = ast.lang === 'go';
19
19
  const params = new Map();
20
- const bareArrow = ast.nodes[fn.nodeIndex].type === 'arrow_function'
20
+ const bareArrow = (ast.nodes[fn.nodeIndex].type === 'arrow_function'
21
+ // @implements A-SPEC-520.1 — Java's single-parameter lambda (`z -> …`) has the same shape.
22
+ || ast.nodes[fn.nodeIndex].type === 'lambda_expression')
21
23
  && !kids.of(fn.nodeIndex).some((k) => ast.nodes[k].type === 'formal_parameters');
22
24
  for (const k of kids.of(fn.nodeIndex)) {
23
25
  // Only formal_parameters — a declaration's NAME identifier is not a parameter (measured: the
@@ -27,7 +29,9 @@ function defUseOf(ast, cfg, fn, source) {
27
29
  // times on one declaration: receiver, parameters, NAMED RESULTS. All of them bind names at
28
30
  // ENTRY, so Go must not break after the first (the probe pinned the second-list shape).
29
31
  const isParamList = ast.nodes[k].type === 'formal_parameters' || ast.nodes[k].type === 'parameters'
30
- || ast.nodes[k].type === 'parameter_list';
32
+ || ast.nodes[k].type === 'parameter_list'
33
+ // @implements A-SPEC-522.1 — Rust: fn parameters and closure |x| parameters.
34
+ || ast.nodes[k].type === 'parameters' || ast.nodes[k].type === 'closure_parameters';
31
35
  if (!isParamList && !(bareArrow && ast.nodes[k].type === 'identifier'))
32
36
  continue;
33
37
  const collect = (n) => {
@@ -55,6 +59,21 @@ function defUseOf(ast, cfg, fn, source) {
55
59
  for (const b of cfg.blocks)
56
60
  for (const s of b.stmts)
57
61
  stmtSet.add(s);
62
+ // @implements A-SPEC-522.1 — walk a Rust PATTERN: lowercase-initial identifiers bind (defs),
63
+ // capitalized ones are constructors/types (skipped). Everything else recurses.
64
+ const walkRustPattern = (n, stmt) => {
65
+ const ty = ast.nodes[n].type;
66
+ if (ty === 'identifier') {
67
+ const t = text(n);
68
+ if (/^[a-z_]/.test(t) && t !== '_')
69
+ add(defs, stmt, t);
70
+ return;
71
+ }
72
+ if (ty.includes('literal') || ty === 'type_identifier')
73
+ return;
74
+ for (const c of kids.of(n))
75
+ walkRustPattern(c, stmt);
76
+ };
58
77
  const classify = (stmt) => {
59
78
  const walk = (n, defCtx) => {
60
79
  const ty = ast.nodes[n].type;
@@ -62,7 +81,7 @@ function defUseOf(ast, cfg, fn, source) {
62
81
  return; // nested function boundary
63
82
  if (n !== stmt && stmtSet.has(n))
64
83
  return; // nested statements are their own rows
65
- if (ty === 'type_annotation' || ty === 'type_arguments' || ty === 'comment')
84
+ if (ty === 'type_annotation' || ty === 'type_arguments' || ty.includes('comment'))
66
85
  return;
67
86
  if (ty === 'identifier' || ty === 'shorthand_property_identifier_pattern') {
68
87
  if (defCtx)
@@ -78,6 +97,164 @@ function defUseOf(ast, cfg, fn, source) {
78
97
  if (ty === 'field_identifier' || ty === 'package_identifier' || ty === 'label_name')
79
98
  return;
80
99
  switch (ty) {
100
+ // @implements A-SPEC-523.1 — C++'s binding shapes.
101
+ case 'init_declarator': {
102
+ // `int y = g()` / `T& r = s` — the declarator's identifier (through reference/pointer
103
+ // wrappers) is the def; the initializer is a use.
104
+ const named = kids.of(n).filter((c) => ast.nodes[c].named);
105
+ const firstId = (m) => {
106
+ if (ast.nodes[m].type === 'identifier')
107
+ return m;
108
+ for (const c of kids.of(m)) {
109
+ const r = firstId(c);
110
+ if (r !== undefined)
111
+ return r;
112
+ }
113
+ return undefined;
114
+ };
115
+ if (named[0] !== undefined) {
116
+ const id = firstId(named[0]);
117
+ if (id !== undefined)
118
+ add(defs, stmt, text(id));
119
+ }
120
+ for (const r of named.slice(1))
121
+ walk(r, false);
122
+ return;
123
+ }
124
+ case 'for_range_loop': {
125
+ // `for (auto& s : xs)` — the declarator's identifier binds; the range is a use.
126
+ const named = kids.of(n).filter((c) => ast.nodes[c].named);
127
+ const body = named.find((c) => ast.nodes[c].type === 'compound_statement');
128
+ for (const c of named) {
129
+ if (c === body)
130
+ continue;
131
+ const ty = ast.nodes[c].type;
132
+ if (ty.endsWith('_declarator') || ty === 'identifier') {
133
+ const firstId = (m) => {
134
+ if (ast.nodes[m].type === 'identifier')
135
+ return m;
136
+ for (const k of kids.of(m)) {
137
+ const r = firstId(k);
138
+ if (r !== undefined)
139
+ return r;
140
+ }
141
+ return undefined;
142
+ };
143
+ // the LAST such child before the body is the RANGE when it is a bare identifier —
144
+ // walk order below fixes that: declarator-shaped nodes def, the final expr uses.
145
+ if (ty === 'identifier' && c === named.filter((x) => x !== body).at(-1)) {
146
+ walk(c, false);
147
+ continue;
148
+ }
149
+ const id = firstId(c);
150
+ if (id !== undefined)
151
+ add(defs, stmt, text(id));
152
+ }
153
+ else if (!ty.includes('type') && ty !== 'placeholder_type_specifier') {
154
+ walk(c, false);
155
+ }
156
+ }
157
+ return;
158
+ }
159
+ // @implements A-SPEC-522.1 — Rust's binding shapes. Pattern identifiers bind by the
160
+ // LOWERCASE-INITIAL convention (rustc warns on violations): `Some(x)` binds x, skips the
161
+ // constructor Some — a documented heuristic, not a guess about arbitrary code.
162
+ case 'let_declaration': {
163
+ const named = kids.of(n).filter((c) => ast.nodes[c].named);
164
+ if (named[0] !== undefined)
165
+ walkRustPattern(named[0], stmt);
166
+ for (const r of named.slice(1))
167
+ walk(r, false);
168
+ return;
169
+ }
170
+ case 'compound_assignment_expr': {
171
+ const [lhs, ...rest] = kids.of(n).filter((c) => ast.nodes[c].named);
172
+ if (lhs !== undefined && ast.nodes[lhs].type === 'identifier') {
173
+ add(defs, stmt, text(lhs));
174
+ add(uses, stmt, text(lhs));
175
+ }
176
+ else if (lhs !== undefined)
177
+ walk(lhs, false);
178
+ for (const r of rest)
179
+ walk(r, false);
180
+ return;
181
+ }
182
+ case 'for_expression': {
183
+ // `for PAT in ITER { .. }` — the pattern binds fresh; iterable is a use.
184
+ const named = kids.of(n).filter((c) => ast.nodes[c].named);
185
+ if (named[0] !== undefined)
186
+ walkRustPattern(named[0], stmt);
187
+ for (const r of named.slice(1))
188
+ walk(r, false);
189
+ return;
190
+ }
191
+ case 'let_condition': {
192
+ // `while let Some(x) = e` / `if let ...` — pattern binds at the header point.
193
+ const named = kids.of(n).filter((c) => ast.nodes[c].named);
194
+ if (named[0] !== undefined)
195
+ walkRustPattern(named[0], stmt);
196
+ for (const r of named.slice(1))
197
+ walk(r, false);
198
+ return;
199
+ }
200
+ case 'scoped_identifier': {
201
+ // a::b::c — path segments are module/type names, not local reads.
202
+ return;
203
+ }
204
+ // @implements A-SPEC-520.1 — Java's shapes. A field's name and a method's name are plain
205
+ // `identifier` nodes in this grammar, so an unguarded walk would read `this.x = x` as TWO
206
+ // uses of x and every call as a use of its method name — with a same-named local, that is
207
+ // a fabricated reaching edge, not just noise.
208
+ // @implements A-SPEC-521.1 — C#: the LAST named child of a member access is the member's
209
+ // NAME (`this` is unnamed, so `this.x` has exactly one — the name). Walking it would read
210
+ // `this.x = x` as two uses of x. Delegates held in FIELDS lose their use through this
211
+ // (declared); a bare local delegate call (`a()`) keeps its use — the callee is walked.
212
+ case 'member_access_expression': {
213
+ const named = kids.of(n).filter((c) => ast.nodes[c].named);
214
+ for (let ix = 0; ix < named.length - 1; ix++)
215
+ walk(named[ix], false);
216
+ return;
217
+ }
218
+ case 'field_access': {
219
+ const named = kids.of(n).filter((c) => ast.nodes[c].named);
220
+ if (named.length > 0)
221
+ walk(named[0], false); // the object; the field name is skipped
222
+ return;
223
+ }
224
+ case 'method_invocation': {
225
+ const named = kids.of(n).filter((c) => ast.nodes[c].named);
226
+ const argsAt = named.findIndex((c) => ast.nodes[c].type === 'argument_list');
227
+ named.forEach((c, ix) => {
228
+ if (ix === (argsAt > 0 ? argsAt - 1 : -1) && ast.nodes[c].type === 'identifier')
229
+ return; // the name
230
+ if (ast.nodes[c].type === 'type_arguments')
231
+ return;
232
+ walk(c, false);
233
+ });
234
+ return;
235
+ }
236
+ case 'enhanced_for_statement':
237
+ case 'foreach_statement': {
238
+ // `for (T s : xs)` — the binding is a def; the iterable and body-side reads are uses.
239
+ const named = kids.of(n).filter((c) => ast.nodes[c].named);
240
+ for (const c of named) {
241
+ if (ast.nodes[c].type === 'identifier')
242
+ add(defs, stmt, text(c));
243
+ else
244
+ walk(c, false);
245
+ }
246
+ return;
247
+ }
248
+ case 'resource': {
249
+ // try-with-resources: `var r = open()` — r defs at the resource point, the init is a use.
250
+ for (const c of kids.of(n)) {
251
+ if (ast.nodes[c].type === 'identifier')
252
+ add(defs, stmt, text(c));
253
+ else
254
+ walk(c, false);
255
+ }
256
+ return;
257
+ }
81
258
  // @implements A-SPEC-519.1 — Go's binding shapes, written from the probed grammar.
82
259
  case 'short_var_declaration': {
83
260
  // `a, b := e1, e2` — every LHS identifier is a def; the RHS list is uses.
@@ -285,13 +462,82 @@ function defUseOf(ast, cfg, fn, source) {
285
462
  };
286
463
  for (const s of stmtSet)
287
464
  classify(s);
465
+ // @implements A-SPEC-522.1 — Rust match patterns bind at the MATCH HEADER point (the same
466
+ // posture as C#'s declaration pattern, for the same measured reason: a def on the point that
467
+ // uses it can never reach). Guards (`n if g(n)`) read at the header too.
468
+ if (ast.lang === 'rust') {
469
+ ast.nodes.forEach((n, i) => {
470
+ if (n.type !== 'match_expression' || !stmtSet.has(i))
471
+ return;
472
+ const mb = kids.of(i).find((k) => ast.nodes[k].type === 'match_block');
473
+ for (const arm of mb === undefined ? [] : kids.of(mb)) {
474
+ if (ast.nodes[arm].type !== 'match_arm')
475
+ continue;
476
+ const pat = kids.of(arm).find((k) => ast.nodes[k].type === 'match_pattern');
477
+ if (pat !== undefined) {
478
+ walkRustPattern(pat, i);
479
+ // a guard inside the pattern (`n if g(n)`) reads its names at dispatch time
480
+ for (const g of kids.of(pat)) {
481
+ if (ast.nodes[g].type !== 'match_pattern' && ast.nodes[g].named) {
482
+ const collect = (m) => {
483
+ if (ast.nodes[m].type === 'identifier' && /^[a-z_]/.test(text(m)))
484
+ add(uses, i, text(m));
485
+ for (const c of kids.of(m))
486
+ collect(c);
487
+ };
488
+ collect(g);
489
+ }
490
+ }
491
+ }
492
+ }
493
+ });
494
+ }
495
+ // @implements A-SPEC-521.1 — a C# declaration pattern (`case string f:`) binds at the SWITCH
496
+ // HEADER point, the same posture as Go's type-switch binding. Attaching it to the section's
497
+ // first statement was the first design and measurably wrong: reaching definitions read a use
498
+ // against the statement's IN set, so a def on the same point can never reach `case string f:
499
+ // Use(f);` — and that first statement usually IS the use. (Design corrected mid-slice, re-sealed.)
500
+ ast.nodes.forEach((n, i) => {
501
+ if (n.type !== 'switch_section')
502
+ return;
503
+ const pat = kids.of(i).find((k) => ast.nodes[k].type === 'declaration_pattern');
504
+ if (pat === undefined)
505
+ return;
506
+ const bind = kids.of(pat).filter((k) => ast.nodes[k].named).at(-1);
507
+ if (bind === undefined || ast.nodes[bind].type !== 'identifier')
508
+ return;
509
+ let sw = ast.nodes[i].parent; // switch_body
510
+ while (sw >= 0 && ast.nodes[sw].type !== 'switch_statement')
511
+ sw = ast.nodes[sw].parent;
512
+ if (sw >= 0 && stmtSet.has(sw))
513
+ add(defs, sw, text(bind));
514
+ });
288
515
  // catch parameters: a def at the head of the handler body's first statement.
289
516
  ast.nodes.forEach((n, i) => {
290
517
  if (n.type !== 'catch_clause')
291
518
  return;
292
519
  const named = kids.of(i).filter((k) => ast.nodes[k].named);
293
- const param = named.find((k) => ast.nodes[k].type === 'identifier');
294
- const block = named.find((k) => ast.nodes[k].type === 'statement_block');
520
+ // @implements A-SPEC-520.1 Java wraps the parameter in catch_formal_parameter; TS puts the
521
+ // identifier directly under the clause. One lookup serves both.
522
+ const formal = named.find((k) => ast.nodes[k].type === 'catch_formal_parameter');
523
+ // @implements A-SPEC-523.1 — C++ wraps it deeper still: parameter_list > parameter_declaration.
524
+ const plist = named.find((k) => ast.nodes[k].type === 'parameter_list');
525
+ const firstIdIn = (m) => {
526
+ if (ast.nodes[m].type === 'identifier')
527
+ return m;
528
+ for (const c of kids.of(m)) {
529
+ const r = firstIdIn(c);
530
+ if (r !== undefined)
531
+ return r;
532
+ }
533
+ return undefined;
534
+ };
535
+ const param = named.find((k) => ast.nodes[k].type === 'identifier')
536
+ ?? (formal !== undefined
537
+ ? kids.of(formal).find((k) => ast.nodes[k].type === 'identifier')
538
+ : undefined)
539
+ ?? (plist !== undefined ? firstIdIn(plist) : undefined);
540
+ const block = named.find((k) => ['statement_block', 'block', 'compound_statement'].includes(ast.nodes[k].type));
295
541
  if (param === undefined || block === undefined)
296
542
  return;
297
543
  const first = kids.of(block).find((k) => stmtSet.has(k));
@@ -55,3 +55,20 @@ export type LanguageMatrix = Record<Layer, Record<string, Cell>>;
55
55
  export declare function languageMatrix(): LanguageMatrix;
56
56
  /** Render docs/language-support.md — the table is a PRODUCT of the code, not a claim beside it. */
57
57
  export declare function renderLanguageSupport(): string;
58
+ /**
59
+ * The corpus evidence behind each language's grades — TRANSCRIBED from the activation log's
60
+ * measurements (sources: S-510 for ts/py, S-519.1..S-523.1 for the five), so the README can cite
61
+ * numbers without re-running corpora at test time. If a re-run moves a number, this table moves
62
+ * with it in the same commit — the pin in the test makes forgetting that a red suite.
63
+ */
64
+ export declare const CORPUS_EVIDENCE: Record<MatrixLanguage, {
65
+ fns: string;
66
+ corpus: string;
67
+ note?: string;
68
+ }>;
69
+ /**
70
+ * The README's language table — a RENDER, like docs/language-support.md, byte-pinned by a test.
71
+ * Glyphs are derived from the live matrix; the evidence column is CORPUS_EVIDENCE. No slogans:
72
+ * a grade appears next to the measurement that earned it.
73
+ */
74
+ export declare function renderReadmeLanguageTable(): string;
@@ -1,8 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.MATRIX_LANGUAGES = exports.LAYERS = void 0;
3
+ exports.CORPUS_EVIDENCE = exports.MATRIX_LANGUAGES = exports.LAYERS = void 0;
4
4
  exports.languageMatrix = languageMatrix;
5
5
  exports.renderLanguageSupport = renderLanguageSupport;
6
+ exports.renderReadmeLanguageTable = renderReadmeLanguageTable;
6
7
  // @implements A-SPEC-510.6
7
8
  /**
8
9
  * The layer × language support matrix — what "officially supported" MEANS, made checkable.
@@ -57,9 +58,15 @@ function relationsCell(ext) {
57
58
  function astCell(astLang) {
58
59
  // The TS grammar family covers tsx/js too — AST_LANGUAGES is the authority on which wasm applies.
59
60
  const covered = AST_LANG_SET.has(astLang) || (astLang === 'typescript' && AST_LANG_SET.has('tsx'));
60
- return covered
61
- ? { support: 'full', basis: 'vendored tree-sitter grammar (L1 AST persisted)' }
62
- : { support: 'none', basis: 'no vendored grammar for the L1 substrate' };
61
+ if (!covered)
62
+ return { support: 'none', basis: 'no vendored grammar for the L1 substrate' };
63
+ // @implements A-SPEC-523.1 the C++ cell carries its measured limit IN the table: 25-34% of
64
+ // template/macro-heavy real files refuse at L1 (nlohmann/fmt/leveldb), and every layer above
65
+ // speaks only about what L1 accepts. Same glyph, weaker evidence — the reader sees it here.
66
+ if (astLang === 'cpp') {
67
+ return { support: 'full', basis: 'vendored tree-sitter grammar (L1 AST persisted) — limit: template/macro-heavy real files can refuse at L1 (measured 25-34% on three corpora); layers above speak only for what parses' };
68
+ }
69
+ return { support: 'full', basis: 'vendored tree-sitter grammar (L1 AST persisted)' };
63
70
  }
64
71
  function cfgFamilyCell(astLang, layer) {
65
72
  const covered = cfg_1.CFG_LANGUAGES.has(astLang) || (astLang === 'typescript' && cfg_1.CFG_LANGUAGES.has('tsx'));
@@ -147,3 +154,44 @@ function renderLanguageSupport() {
147
154
  '',
148
155
  ].join('\n');
149
156
  }
157
+ // @implements A-SPEC-526.1
158
+ /**
159
+ * The corpus evidence behind each language's grades — TRANSCRIBED from the activation log's
160
+ * measurements (sources: S-510 for ts/py, S-519.1..S-523.1 for the five), so the README can cite
161
+ * numbers without re-running corpora at test time. If a re-run moves a number, this table moves
162
+ * with it in the same commit — the pin in the test makes forgetting that a red suite.
163
+ */
164
+ exports.CORPUS_EVIDENCE = {
165
+ typescript: { fns: '10,929', corpus: 'this repository' },
166
+ python: { fns: '1,209', corpus: 'jarvis' },
167
+ csharp: { fns: '9,184', corpus: 'Newtonsoft.Json + RestSharp', note: '4.4% of files refuse at L1 (preprocessor across syntax)' },
168
+ java: { fns: '8,376', corpus: 'gson + junit4' },
169
+ go: { fns: '2,637', corpus: 'gin + cobra' },
170
+ rust: { fns: '5,247', corpus: 'ripgrep + serde' },
171
+ cpp: { fns: '1,762', corpus: 'nlohmann + fmt + leveldb', note: '32.1% of template/macro-heavy files refuse at L1 — layers above speak only for what parses' },
172
+ };
173
+ // @implements A-SPEC-526.1
174
+ /**
175
+ * The README's language table — a RENDER, like docs/language-support.md, byte-pinned by a test.
176
+ * Glyphs are derived from the live matrix; the evidence column is CORPUS_EVIDENCE. No slogans:
177
+ * a grade appears next to the measurement that earned it.
178
+ */
179
+ function renderReadmeLanguageTable() {
180
+ const m = languageMatrix();
181
+ const glyph = (s) => (s === 'full' ? '●' : s === 'partial' ? '◐' : '○');
182
+ const lines = [];
183
+ lines.push('| Language | relations | ast | cfg | ddg | cdg | taint | Corpus evidence |');
184
+ lines.push('| :--- | :---: | :---: | :---: | :---: | :---: | :---: | :--- |');
185
+ for (const { lang, label } of exports.MATRIX_LANGUAGES) {
186
+ const ev = exports.CORPUS_EVIDENCE[lang];
187
+ const cells = ['relations', 'ast', 'cfg', 'ddg', 'cdg', 'taint']
188
+ .map((layer) => glyph(m[layer][lang].support)).join(' | ');
189
+ const note = ev.note ? ` — ${ev.note}` : '';
190
+ lines.push(`| **${label}** | ${cells} | ${ev.fns} functions (${ev.corpus}), zero invariant violations${note} |`);
191
+ }
192
+ lines.push('');
193
+ lines.push('Every grade is a measurement, not a goal: a cell moves only when a real corpus proves it.');
194
+ lines.push('Full per-cell bases (and every stated limit) live in [docs/language-support.md](docs/language-support.md), which is generated from the same derivation and byte-pinned by the suite — as is this table.');
195
+ lines.push('');
196
+ return lines.join('\n');
197
+ }
@@ -84,6 +84,17 @@ const CALLS_ONLY = { symbols: true, relations: ['calls'], graphResolved: ['calls
84
84
  const CALLS_AND_INHERITS = {
85
85
  symbols: true, relations: ['calls', 'inherits'], graphResolved: ['calls', 'inherits'],
86
86
  };
87
+ // @implements A-SPEC-525.1
88
+ /**
89
+ * All three relations, extracted AND graph-resolved. The five non-TS/Python languages moved here
90
+ * when their imports were measured ARRIVING (A-SPEC-286's column): go 389, rust 64, java 2,688,
91
+ * csharp 316, cpp 482 resolved edges over the ten corpora. Rates differ by language semantics —
92
+ * C# resolves only where namespace happens to mirror the folder layout, Rust skips use-trees as
93
+ * ambiguous — and the unresolved remainder is external/ambiguous by design, never guessed.
94
+ */
95
+ const FULL_RELATIONS = {
96
+ symbols: true, relations: ['calls', 'imports', 'inherits'], graphResolved: ['calls', 'imports', 'inherits'],
97
+ };
87
98
  exports.LANGUAGE_CAPABILITY = {
88
99
  '.ts': TS_FAMILY, '.mts': TS_FAMILY, '.cts': TS_FAMILY, '.tsx': TS_FAMILY,
89
100
  '.js': TS_FAMILY, '.mjs': TS_FAMILY, '.jsx': TS_FAMILY,
@@ -93,12 +104,13 @@ exports.LANGUAGE_CAPABILITY = {
93
104
  '.py': { symbols: true, relations: ['calls', 'imports', 'inherits'], graphResolved: ['calls', 'imports', 'inherits'] },
94
105
  // @implements A-SPEC-511.1 — inheritance recovered: Java extends/implements, C# base_list,
95
106
  // Go struct embedding (interface satisfaction is a declared permanent gap), Rust `impl T for S`.
96
- '.java': CALLS_AND_INHERITS, '.cs': CALLS_AND_INHERITS, '.go': CALLS_AND_INHERITS, '.rs': CALLS_AND_INHERITS,
107
+ // @implements A-SPEC-525.1 imports measured arriving for all five (see FULL_RELATIONS).
108
+ '.java': FULL_RELATIONS, '.cs': FULL_RELATIONS, '.go': FULL_RELATIONS, '.rs': FULL_RELATIONS,
97
109
  // @implements A-SPEC-287 — C++ used to extract calls that never became graph edges, because its
98
110
  // edge scope omitted the class node the symbol walk uses. Fixed; it now resolves like the others.
99
111
  // @implements A-SPEC-511.1 — base_class_clause recovered (access specifiers skipped).
100
- '.cpp': CALLS_AND_INHERITS, '.cc': CALLS_AND_INHERITS, '.cxx': CALLS_AND_INHERITS,
101
- '.hpp': CALLS_AND_INHERITS, '.hh': CALLS_AND_INHERITS, '.h': CALLS_AND_INHERITS,
112
+ '.cpp': FULL_RELATIONS, '.cc': FULL_RELATIONS, '.cxx': FULL_RELATIONS,
113
+ '.hpp': FULL_RELATIONS, '.hh': FULL_RELATIONS, '.h': FULL_RELATIONS,
102
114
  };
103
115
  function capabilityFor(ext) {
104
116
  return exports.LANGUAGE_CAPABILITY[ext.toLowerCase()];
@@ -538,7 +538,9 @@ const EDGE_CONFIG = {
538
538
  // syntactic fact. Interface satisfaction is implicit and structural (no `implements` keyword), so
539
539
  // recovering it needs a type checker — a DECLARED PERMANENT LIMIT, never a guess.
540
540
  go: { callTypes: ['call_expression'], calleeField: 'function', scopeTypes: ['function_declaration', 'method_declaration'],
541
- inherits: { declTypes: ['type_spec'], mode: 'go-embedding' } },
541
+ inherits: { declTypes: ['type_spec'], mode: 'go-embedding' },
542
+ // @implements A-SPEC-525.1 — import_spec's interpreted_string_literal, quotes stripped.
543
+ imports: { nodeType: 'import_spec', mode: 'go-string' } },
542
544
  // @implements A-SPEC-300
543
545
  // `impl_item`/`trait_item` must open a qualifying scope, exactly as the symbol walk does: a method
544
546
  // in `impl Greet for En` is the symbol `En.hello`, but its edges came out qualified as bare
@@ -547,18 +549,27 @@ const EDGE_CONFIG = {
547
549
  // @implements A-SPEC-511.1 — `impl Trait for Type` ⇒ Type inherits Trait (implementer → abstract).
548
550
  // An inherent `impl Type { … }` carries no trait field and is NOT inheritance: no edge.
549
551
  rust: { callTypes: ['call_expression'], calleeField: 'function', scopeTypes: ['function_item', 'impl_item', 'trait_item'],
550
- inherits: { declTypes: ['impl_item'], mode: 'rust-impl' } },
552
+ inherits: { declTypes: ['impl_item'], mode: 'rust-impl' },
553
+ // @implements A-SPEC-525.1 — use_declaration's scoped path; use-trees (braces) are
554
+ // ambiguous fan-outs and are skipped rather than half-read.
555
+ imports: { nodeType: 'use_declaration', mode: 'rust-use' } },
551
556
  // @implements A-SPEC-511.1 — Java separates the two syntactically, and BOTH are inheritance edges:
552
557
  // `superclass` (extends) and `super_interfaces` (implements), plus an interface's own extends.
553
558
  java: { callTypes: ['method_invocation'], calleeField: 'name', scopeTypes: ['class_declaration', 'method_declaration'],
554
559
  inherits: { declTypes: ['class_declaration', 'interface_declaration', 'record_declaration', 'enum_declaration'],
555
- fields: ['superclass', 'super_interfaces', 'interfaces', 'extends_interfaces'] } },
560
+ fields: ['superclass', 'super_interfaces', 'interfaces', 'extends_interfaces'] },
561
+ // @implements A-SPEC-525.1 — import_declaration's scoped_identifier; a wildcard
562
+ // (asterisk sibling) is ambiguous and skipped.
563
+ imports: { nodeType: 'import_declaration', mode: 'java-scoped' } },
556
564
  // @implements A-SPEC-511.1 — C# puts the base class AND the interfaces in ONE `base_list`, with no
557
565
  // syntactic marker telling them apart. Splitting them would be a guess, so BOTH become `inherits`
558
566
  // (a deliberate asymmetry with the TS walk, which emits extends only — H-SPEC-511 decision 1).
559
567
  csharp: { callTypes: ['invocation_expression'], calleeField: 'function', scopeTypes: ['class_declaration', 'method_declaration'],
560
568
  inherits: { declTypes: ['class_declaration', 'interface_declaration', 'struct_declaration', 'record_declaration'],
561
- childTypes: ['base_list'] } },
569
+ childTypes: ['base_list'] },
570
+ // @implements A-SPEC-525.1 — using_directive's qualified_name (a NAMESPACE, which the
571
+ // resolver may fail to map to a file — that failure is measured, not hidden).
572
+ imports: { nodeType: 'using_directive', mode: 'csharp-using' } },
562
573
  // @implements A-SPEC-287
563
574
  // C++ listed only `function_definition`, so a member function's edges came out qualified as bare
564
575
  // `run` while the symbol walk (which treats class_specifier/struct_specifier as scopes) emitted
@@ -568,7 +579,9 @@ const EDGE_CONFIG = {
568
579
  // @implements A-SPEC-511.1 — C++ bases live in `base_class_clause`; access specifiers
569
580
  // (public/private/protected/virtual) are skipped and only the type name is taken.
570
581
  cpp: { callTypes: ['call_expression'], calleeField: 'function', scopeTypes: ['class_specifier', 'struct_specifier', 'function_definition'],
571
- inherits: { declTypes: ['class_specifier', 'struct_specifier'], childTypes: ['base_class_clause'] } },
582
+ inherits: { declTypes: ['class_specifier', 'struct_specifier'], childTypes: ['base_class_clause'] },
583
+ // @implements A-SPEC-525.1 — quoted includes only; <system> headers are external.
584
+ imports: { nodeType: 'preproc_include', mode: 'cpp-include' } },
572
585
  };
573
586
  // The callee's bare name: a plain identifier is itself; a member/selector/scoped/field node
574
587
  // (`o.m`, `self.c`, `mod::f`, `this.C`) unwraps to its LAST identifier segment — the method name.
@@ -764,6 +777,49 @@ function walkEdges(tree, cfg) {
764
777
  }
765
778
  }
766
779
  };
780
+ // @implements A-SPEC-525.1 — imports, table-driven like everything else in this walk. Each
781
+ // mode reads ONE probed shape; anything ambiguous (use-trees, wildcards, system headers) emits
782
+ // nothing rather than half of something.
783
+ const imp = cfg.imports;
784
+ const emitImport = (node) => {
785
+ if (imp.mode === 'go-string') {
786
+ const lit = node.namedChildren.find((c) => c.type === 'interpreted_string_literal');
787
+ if (lit)
788
+ out.push({ from: '<module>', to: lit.text.replace(/["']/g, ''), rel: 'imports' });
789
+ return;
790
+ }
791
+ if (imp.mode === 'rust-use') {
792
+ const arg = node.namedChildren.find((c) => ['scoped_identifier', 'identifier', 'use_as_clause', 'scoped_use_list', 'use_wildcard'].includes(c.type));
793
+ if (!arg)
794
+ return;
795
+ if (arg.type === 'scoped_use_list' || arg.type === 'use_wildcard')
796
+ return; // ambiguous fan-out
797
+ const pathNode = arg.type === 'use_as_clause' ? arg.namedChildren[0] : arg;
798
+ if (pathNode)
799
+ out.push({ from: '<module>', to: pathNode.text, rel: 'imports' });
800
+ return;
801
+ }
802
+ if (imp.mode === 'java-scoped') {
803
+ if (node.children.some((c) => c.type === 'asterisk'))
804
+ return; // wildcard
805
+ const scoped = node.namedChildren.find((c) => ['scoped_identifier', 'identifier'].includes(c.type));
806
+ if (scoped)
807
+ out.push({ from: '<module>', to: scoped.text, rel: 'imports' });
808
+ return;
809
+ }
810
+ if (imp.mode === 'csharp-using') {
811
+ const q = node.namedChildren.find((c) => ['qualified_name', 'identifier'].includes(c.type));
812
+ if (q)
813
+ out.push({ from: '<module>', to: q.text, rel: 'imports' });
814
+ return;
815
+ }
816
+ if (imp.mode === 'cpp-include') {
817
+ const lit = node.namedChildren.find((c) => c.type === 'string_literal');
818
+ if (lit)
819
+ out.push({ from: '<module>', to: lit.text.replace(/["']/g, ''), rel: 'imports' });
820
+ return; // <system>: external
821
+ }
822
+ };
767
823
  const visit = (node) => {
768
824
  if (callTypes.has(node.type)) {
769
825
  const callee = calleeNameOf(node.childForFieldName(cfg.calleeField));
@@ -772,6 +828,8 @@ function walkEdges(tree, cfg) {
772
828
  }
773
829
  if (inh && declTypes.has(node.type))
774
830
  emitInherits(node);
831
+ if (imp && node.type === imp.nodeType)
832
+ emitImport(node);
775
833
  for (let i = 0; i < node.childCount; i++)
776
834
  visit(node.child(i));
777
835
  };
@@ -1708,13 +1708,21 @@ function makeRawHandlers(store, opts) {
1708
1708
  const { vocabularyFor } = require('../rtm/taint-vocabulary');
1709
1709
  const parser = new TreeSitterTsParser();
1710
1710
  const factSet = [];
1711
- const pythonFiles = [];
1711
+ // @implements A-SPEC-524.1 — every non-TS language with a CFG takes the flow-sensitive
1712
+ // lane, each judged with ITS OWN vocabulary. The TS family keeps the facts+reaching-defs
1713
+ // lane (its interprocedural propagation lives there).
1714
+ const FLOW_EXT = [
1715
+ [/\.py$/i, 'python'], [/\.go$/i, 'go'], [/\.rs$/i, 'rust'],
1716
+ [/\.java$/i, 'java'], [/\.cs$/i, 'csharp'], [/\.(cpp|cc|cxx|hpp|h)$/i, 'cpp'],
1717
+ ];
1718
+ const flowLangFiles = new Map();
1712
1719
  for (const f of scanned) {
1713
1720
  const vocab = vocabularyFor(f.sourcePath);
1714
1721
  if (!vocab)
1715
1722
  continue;
1716
- if (/\.py$/i.test(f.sourcePath)) {
1717
- pythonFiles.push(f.sourcePath);
1723
+ const flowLang = FLOW_EXT.find(([re]) => re.test(f.sourcePath))?.[1];
1724
+ if (flowLang !== undefined) {
1725
+ flowLangFiles.set(flowLang, [...(flowLangFiles.get(flowLang) ?? []), f.sourcePath]);
1718
1726
  continue;
1719
1727
  }
1720
1728
  let src;
@@ -1740,36 +1748,40 @@ function makeRawHandlers(store, opts) {
1740
1748
  kept.push(...out.kept.map((f) => ({ ...f, lane: 'facts+reaching-defs' })));
1741
1749
  refuted += out.removed.length;
1742
1750
  }
1743
- // @implements A-SPEC-513.1 Python takes the flow-sensitive lane directly: its facts
1744
- // extractor has no Python walk, but its CFG/DDG do (S-510.7), and the judgement above
1745
- // them is language-agnostic.
1746
- const pyVocab = vocabularyFor('x.py');
1747
- if (pythonFiles.length > 0) {
1751
+ // @implements A-SPEC-513.1 / A-SPEC-524.1 the flow-sensitive lane, generalized from
1752
+ // Python-only to every language whose CFG/DDG landed in P2. The judgement engine is
1753
+ // language-agnostic; only the vocabulary is selected per language.
1754
+ const flowLangsAnalysed = [];
1755
+ if (flowLangFiles.size > 0) {
1748
1756
  const { parseAst } = require('../cpg/foundation/ast-store');
1749
1757
  const { cfgOf, functionsIn } = require('../cpg/foundation/cfg');
1750
1758
  const { ddgOf } = require('../cpg/foundation/ddg');
1751
1759
  const { flowSensitiveTaint } = require('../rtm/flow-sensitive-taint');
1752
- for (const rel of pythonFiles) {
1753
- let src;
1754
- try {
1755
- src = fs.readFileSync(path.join(root, rel), 'utf8');
1756
- }
1757
- catch {
1758
- continue;
1759
- }
1760
- const ast = await parseAst(src, rel);
1761
- if (!ast || ast.errorCount > 0)
1762
- continue;
1763
- for (const fn of functionsIn(ast)) {
1764
- const fcfg = cfgOf(ast, fn, src);
1765
- if ('unsupported' in fcfg)
1760
+ for (const [flowLang, files] of flowLangFiles) {
1761
+ flowLangsAnalysed.push(flowLang);
1762
+ for (const rel of files) {
1763
+ let src;
1764
+ try {
1765
+ src = fs.readFileSync(path.join(root, rel), 'utf8');
1766
+ }
1767
+ catch {
1766
1768
  continue;
1767
- const fddg = ddgOf(ast, fcfg, fn, src);
1768
- if ('unsupported' in fddg)
1769
+ }
1770
+ const vocab = vocabularyFor(rel);
1771
+ const ast = await parseAst(src, rel);
1772
+ if (!ast || ast.errorCount > 0)
1769
1773
  continue;
1770
- const r = flowSensitiveTaint({ ast, cfg: fcfg, ddg: fddg, source: src, fnName: rel, config: pyVocab });
1771
- for (const f of r.findings)
1772
- kept.push({ ...f, file: rel, lane: 'flow-sensitive' });
1774
+ for (const fn of functionsIn(ast)) {
1775
+ const fcfg = cfgOf(ast, fn, src);
1776
+ if ('unsupported' in fcfg)
1777
+ continue;
1778
+ const fddg = ddgOf(ast, fcfg, fn, src);
1779
+ if ('unsupported' in fddg)
1780
+ continue;
1781
+ const r = flowSensitiveTaint({ ast, cfg: fcfg, ddg: fddg, source: src, fnName: rel, config: vocab });
1782
+ for (const f of r.findings)
1783
+ kept.push({ ...f, file: rel, lane: 'flow-sensitive' });
1784
+ }
1773
1785
  }
1774
1786
  }
1775
1787
  }
@@ -1783,7 +1795,7 @@ function makeRawHandlers(store, opts) {
1783
1795
  converged: raw.converged,
1784
1796
  truncated: raw.truncated,
1785
1797
  /** Which languages this run could actually judge, so a zero is readable. */
1786
- languagesAnalysed: [...new Set([factSet.length > 0 ? 'typescript' : null, pythonFiles.length > 0 ? 'python' : null].filter(Boolean))],
1798
+ languagesAnalysed: [...new Set([factSet.length > 0 ? 'typescript' : null, ...flowLangsAnalysed].filter(Boolean))],
1787
1799
  },
1788
1800
  };
1789
1801
  }