@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.
- package/CHANGELOG.md +65 -0
- package/README.md +15 -9
- package/dist/.build-id +1 -1
- package/dist/holmes/cpg/foundation/cfg.js +487 -4
- package/dist/holmes/cpg/foundation/ddg.js +251 -5
- package/dist/holmes/cpg/foundation/language-matrix.d.ts +17 -0
- package/dist/holmes/cpg/foundation/language-matrix.js +52 -4
- package/dist/holmes/cpg/language-capability.js +15 -3
- package/dist/holmes/cpg/language-parser-walk.js +63 -5
- package/dist/holmes/mcp/handlers.js +40 -28
- package/dist/holmes/rtm/flow-sensitive-taint.js +18 -2
- package/dist/holmes/rtm/rtm-builder.d.ts +6 -14
- package/dist/holmes/rtm/rtm-builder.js +117 -0
- package/dist/holmes/rtm/taint-vocabulary.d.ts +1 -1
- package/dist/holmes/rtm/taint-vocabulary.js +46 -0
- package/package.json +1 -1
|
@@ -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
|
|
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
|
-
|
|
294
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
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':
|
|
101
|
-
'.hpp':
|
|
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
|
-
|
|
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
|
-
|
|
1717
|
-
|
|
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
|
|
1744
|
-
//
|
|
1745
|
-
//
|
|
1746
|
-
const
|
|
1747
|
-
if (
|
|
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
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
src
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
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
|
-
|
|
1768
|
-
|
|
1769
|
+
}
|
|
1770
|
+
const vocab = vocabularyFor(rel);
|
|
1771
|
+
const ast = await parseAst(src, rel);
|
|
1772
|
+
if (!ast || ast.errorCount > 0)
|
|
1769
1773
|
continue;
|
|
1770
|
-
const
|
|
1771
|
-
|
|
1772
|
-
|
|
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,
|
|
1798
|
+
languagesAnalysed: [...new Set([factSet.length > 0 ? 'typescript' : null, ...flowLangsAnalysed].filter(Boolean))],
|
|
1787
1799
|
},
|
|
1788
1800
|
};
|
|
1789
1801
|
}
|