explainer-video 0.0.0-stage → 0.4.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/.claude-plugin/plugin.json +12 -0
- package/LICENSE +21 -0
- package/README.md +152 -2
- package/cordis.patch.yml +7 -0
- package/lib/index.js +35 -0
- package/package.json +56 -4
- package/plugin.json +13 -0
- package/skills/coursework-author/.synced-from +5 -0
- package/skills/coursework-author/SKILL.md +209 -0
- package/skills/craft-codeflow-flows/SKILL.md +116 -0
- package/skills/craft-codeflow-mine/SKILL.md +207 -0
- package/skills/craft-codeflow-plan/SKILL.md +65 -0
- package/skills/craft-codeflow-scene/SKILL.md +168 -0
- package/skills/craft-video-align/SKILL.md +109 -0
- package/skills/craft-video-assemble/SKILL.md +69 -0
- package/skills/craft-video-audio/SKILL.md +59 -0
- package/skills/craft-video-enrich/SKILL.md +46 -0
- package/skills/craft-video-plan/SKILL.md +57 -0
- package/skills/craft-video-publish/SKILL.md +79 -0
- package/skills/craft-video-render/SKILL.md +68 -0
- package/skills/craft-video-scene/SKILL.md +112 -0
- package/skills/craft-video-script/SKILL.md +80 -0
- package/skills/explainer-video-from-codebase/FLOWS.md +577 -0
- package/skills/explainer-video-from-codebase/SKILL.md +659 -0
- package/skills/explainer-video-from-codebase/tools/apply-flows.mjs +531 -0
- package/skills/explainer-video-from-codebase/tools/approve-flows.mjs +56 -0
- package/skills/explainer-video-from-codebase/tools/codemap-md.mjs +116 -0
- package/skills/explainer-video-from-codebase/tools/config/dotenv.mjs +13 -0
- package/skills/explainer-video-from-codebase/tools/config/json.mjs +14 -0
- package/skills/explainer-video-from-codebase/tools/config/redact.mjs +16 -0
- package/skills/explainer-video-from-codebase/tools/config/toml-lite.mjs +13 -0
- package/skills/explainer-video-from-codebase/tools/config/yaml-lite.mjs +18 -0
- package/skills/explainer-video-from-codebase/tools/excerpt-check.mjs +186 -0
- package/skills/explainer-video-from-codebase/tools/excerpt.mjs +116 -0
- package/skills/explainer-video-from-codebase/tools/flow-diagram.mjs +168 -0
- package/skills/explainer-video-from-codebase/tools/flow-plan.mjs +129 -0
- package/skills/explainer-video-from-codebase/tools/flows-md.mjs +87 -0
- package/skills/explainer-video-from-codebase/tools/lang/c.mjs +684 -0
- package/skills/explainer-video-from-codebase/tools/lang/go.mjs +673 -0
- package/skills/explainer-video-from-codebase/tools/lang/python-config.mjs +143 -0
- package/skills/explainer-video-from-codebase/tools/lang/python-deps.mjs +62 -0
- package/skills/explainer-video-from-codebase/tools/lang/python-stdlib.mjs +24 -0
- package/skills/explainer-video-from-codebase/tools/lang/python.mjs +609 -0
- package/skills/explainer-video-from-codebase/tools/lang/typescript.mjs +963 -0
- package/skills/explainer-video-from-codebase/tools/lib/decompose.mjs +102 -0
- package/skills/explainer-video-from-codebase/tools/lib/groups.mjs +77 -0
- package/skills/explainer-video-from-codebase/tools/lib/observe-coverage.mjs +68 -0
- package/skills/explainer-video-from-codebase/tools/lib/observe-v8.mjs +232 -0
- package/skills/explainer-video-from-codebase/tools/lib/observe.mjs +162 -0
- package/skills/explainer-video-from-codebase/tools/lib/redact.mjs +28 -0
- package/skills/explainer-video-from-codebase/tools/lib/sandbox.mjs +97 -0
- package/skills/explainer-video-from-codebase/tools/lib/test-links.mjs +28 -0
- package/skills/explainer-video-from-codebase/tools/lib/toml.mjs +129 -0
- package/skills/explainer-video-from-codebase/tools/lib/trace-c.mjs +95 -0
- package/skills/explainer-video-from-codebase/tools/lib/trace-go.mjs +101 -0
- package/skills/explainer-video-from-codebase/tools/lib/trace-js.mjs +136 -0
- package/skills/explainer-video-from-codebase/tools/lib/tree-sitter.mjs +19 -0
- package/skills/explainer-video-from-codebase/tools/lib/walk.mjs +138 -0
- package/skills/explainer-video-from-codebase/tools/mine.mjs +409 -0
- package/skills/explainer-video-from-codebase/tools/propose.mjs +127 -0
- package/skills/explainer-video-from-codebase/tools/rank.mjs +119 -0
- package/skills/explainer-video-from-codebase/tools/ranking-md.mjs +76 -0
- package/skills/explainer-video-from-codebase/tools/rules/c-handlers.mjs +53 -0
- package/skills/explainer-video-from-codebase/tools/rules/celery-taskiq.mjs +48 -0
- package/skills/explainer-video-from-codebase/tools/rules/click-typer.mjs +6 -0
- package/skills/explainer-video-from-codebase/tools/rules/express.mjs +72 -0
- package/skills/explainer-video-from-codebase/tools/rules/fastapi.mjs +99 -0
- package/skills/explainer-video-from-codebase/tools/rules/go-http.mjs +173 -0
- package/skills/explainer-video-from-codebase/tools/rules/go-test.mjs +108 -0
- package/skills/explainer-video-from-codebase/tools/rules/grpc.mjs +57 -0
- package/skills/explainer-video-from-codebase/tools/rules/gtest.mjs +97 -0
- package/skills/explainer-video-from-codebase/tools/rules/haystack.mjs +55 -0
- package/skills/explainer-video-from-codebase/tools/rules/jest-vitest.mjs +100 -0
- package/skills/explainer-video-from-codebase/tools/rules/js-http-client.mjs +111 -0
- package/skills/explainer-video-from-codebase/tools/rules/nextjs.mjs +53 -0
- package/skills/explainer-video-from-codebase/tools/rules/process-spawn.mjs +49 -0
- package/skills/explainer-video-from-codebase/tools/rules/py-http-client.mjs +26 -0
- package/skills/explainer-video-from-codebase/tools/rules/pytest.mjs +107 -0
- package/skills/explainer-video-from-codebase/tools/rules/react-router.mjs +17 -0
- package/skills/explainer-video-from-codebase/tools/rules/registry.mjs +31 -0
- package/skills/explainer-video-from-codebase/tools/trace/c/fmtrace-sh +14 -0
- package/skills/explainer-video-from-codebase/tools/trace/js/fmtrace-net.cjs +33 -0
- package/skills/explainer-video-from-codebase/tools/trace/js/fmtrace-srcmap.cjs +41 -0
- package/skills/explainer-video-from-codebase/tools/trace/python/fmtrace_plugin.py +87 -0
- package/skills/explainer-video-from-codebase/tools/trace/python/sitecustomize.py +330 -0
- package/skills/explainer-video-from-codebase/tools/trace/sandbox/linux-bwrap.sh +73 -0
- package/skills/explainer-video-from-codebase/tools/trace/sandbox/macos-nonet.sb +41 -0
- package/skills/explainer-video-from-codebase/tools/trace.mjs +577 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/tree-sitter-c.txt +21 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/tree-sitter-cpp.txt +21 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/tree-sitter-go.txt +21 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/tree-sitter-javascript.txt +21 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/tree-sitter-python.txt +21 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/tree-sitter-typescript.txt +21 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/web-tree-sitter.txt +21 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/MANIFEST.json +172 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-c.wasm +0 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-cpp.wasm +0 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-go.wasm +0 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-javascript.wasm +0 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-python.wasm +0 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-tsx.wasm +0 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-typescript.wasm +0 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/web-tree-sitter.js +4081 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/web-tree-sitter.wasm +0 -0
- package/skills/explainer-video-from-coursework/ARCHITECTURE.md +59 -0
- package/skills/explainer-video-from-coursework/PIPELINE.md +343 -0
- package/skills/explainer-video-from-coursework/ROADMAP.md +725 -0
- package/skills/explainer-video-from-coursework/SERIES.md +232 -0
- package/skills/explainer-video-from-coursework/SETUP.md +105 -0
- package/skills/explainer-video-from-coursework/SKILL.md +858 -0
- package/skills/explainer-video-from-coursework/scenes/_pt-chart.js +12 -0
- package/skills/explainer-video-from-coursework/scenes/_pt-equation.js +10 -0
- package/skills/explainer-video-from-coursework/scenes/_pt-hubspoke.js +7 -0
- package/skills/explainer-video-from-coursework/scenes/_pt-loopflow.js +7 -0
- package/skills/explainer-video-from-coursework/scenes/_pt-sequence.js +12 -0
- package/skills/explainer-video-from-coursework/scenes/_pt-timeline.js +11 -0
- package/skills/explainer-video-from-coursework/src/scene-code.html +127 -0
- package/skills/explainer-video-from-coursework/src/scene-code.js +379 -0
- package/skills/explainer-video-from-coursework/src/scene-core.js +1250 -0
- package/skills/explainer-video-from-coursework/src/scene-diagram.html +50 -0
- package/skills/explainer-video-from-coursework/src/scene-svg.html +50 -0
- package/skills/explainer-video-from-coursework/src/timeline.js +297 -0
- package/skills/explainer-video-from-coursework/src/timing-chart.js +160 -0
- package/skills/explainer-video-from-coursework/tools/align.mjs +419 -0
- package/skills/explainer-video-from-coursework/tools/apply-enrichment.mjs +206 -0
- package/skills/explainer-video-from-coursework/tools/assemble.mjs +223 -0
- package/skills/explainer-video-from-coursework/tools/build-audio.mjs +234 -0
- package/skills/explainer-video-from-coursework/tools/chromium.mjs +73 -0
- package/skills/explainer-video-from-coursework/tools/debug-page.mjs +75 -0
- package/skills/explainer-video-from-coursework/tools/env.mjs +30 -0
- package/skills/explainer-video-from-coursework/tools/frame-dedup.mjs +131 -0
- package/skills/explainer-video-from-coursework/tools/key-source.mjs +151 -0
- package/skills/explainer-video-from-coursework/tools/model.mjs +21 -0
- package/skills/explainer-video-from-coursework/tools/naming.mjs +38 -0
- package/skills/explainer-video-from-coursework/tools/onset-audit.mjs +51 -0
- package/skills/explainer-video-from-coursework/tools/pacing-check.mjs +350 -0
- package/skills/explainer-video-from-coursework/tools/planner.mjs +616 -0
- package/skills/explainer-video-from-coursework/tools/publish.mjs +230 -0
- package/skills/explainer-video-from-coursework/tools/render.mjs +205 -0
- package/skills/explainer-video-from-coursework/tools/resolve-beats.mjs +239 -0
- package/skills/explainer-video-from-coursework/tools/script-check.mjs +174 -0
- package/skills/explainer-video-from-coursework/tools/stamp.mjs +71 -0
- package/skills/explainer-video-from-coursework/tools/timing-report.mjs +299 -0
- package/skills/explainer-video-from-coursework/tools/transcribe-gemini.mjs +96 -0
- package/skills/explainer-video-from-coursework/tools/tts-gemini.mjs +394 -0
- package/skills/explainer-video-from-coursework/tools/verify-audio.mjs +88 -0
- package/skills/explainer-video-from-coursework/tools/workdir.mjs +124 -0
- package/skills/explainer-video-from-coursework/vendor/plotly-basic.min.js +8 -0
- package/skills/explainer-video-from-coursework/vendor/plotly-basic.min.js.LICENSE.txt +16 -0
|
@@ -0,0 +1,684 @@
|
|
|
1
|
+
/* The C and C++ adapter: one visitor for both grammars (a C file is read by tree-sitter-c, a C++ file and a header
|
|
2
|
+
that holds C++ by tree-sitter-cpp), then name, member and function-pointer resolution into the same typed edges the
|
|
3
|
+
other adapters write. Rule packs (../rules/c-handlers.mjs, gtest.mjs) read the same facts and add links on top.
|
|
4
|
+
|
|
5
|
+
Ids: a module is the file's repo-relative path (`src/cart.c`); a symbol is `<file>:<name>` with namespaces and
|
|
6
|
+
classes joined by `.` (`<file>:shop.Cart.add`), in the file that defines it (an out-of-line `Cart::add` is in its
|
|
7
|
+
.cpp file); a class or struct is `<file>:<ns>.<Name>` where its body is. A gtest `TEST(Suite, Name)` is
|
|
8
|
+
`<file>:Suite.Name`, a Catch2 `TEST_CASE("title")` `<file>:title`.
|
|
9
|
+
|
|
10
|
+
Resolution rules (design rev3 §3.14, C and C++ rows):
|
|
11
|
+
- C linkage is global: a bare call names a function defined in the same file (a `static` one included), else the
|
|
12
|
+
one non-static definition of that name in the repo, preferring the files the caller's includes name (a header and
|
|
13
|
+
the source file beside it); a `static` one elsewhere is seen only from the files whose includes reach it; a quoted `#include` resolves beside the including file (as graphify does), else to
|
|
14
|
+
the one repo file whose path ends with it (an `-I` directory); a system include is external;
|
|
15
|
+
- C++ names: the enclosing class's members (an implicit `this->`), then the enclosing namespaces outward, `using
|
|
16
|
+
namespace` directives, then the global namespace; `ns::f()` and `K::m()` are qualified;
|
|
17
|
+
- members: `obj.m()` / `obj->m()` where obj is typed by a declaration (`K obj(…)`, `K* p`, `auto x = new K` /
|
|
18
|
+
`std::make_unique<K>` / `K(…)` / a call's declared return type), a parameter, a field of the enclosing class
|
|
19
|
+
(`store_->get()`), or `this`; `std::unique_ptr<K>` / `shared_ptr<K>` / references unwrap to K; a value built
|
|
20
|
+
here is its concrete class (`static`), a declared one is read through the class hierarchy: a `static` edge to
|
|
21
|
+
the method plus a `virtual` edge to each override in a subclass (a pure virtual `= 0` method is abstract);
|
|
22
|
+
- constructors: `new K`, `K obj(…)`, `K obj{…}`, `K(…)` and make_unique / make_shared give `ctor` edges to the class
|
|
23
|
+
and to its constructor;
|
|
24
|
+
- function pointers: a struct field or variable that holds functions (`.run = cmd_run`, `ops->open = f`) dispatches
|
|
25
|
+
a call through it (`c->run(…)`) to every function assigned to that field anywhere (`dispatch`, c.fn-pointer);
|
|
26
|
+
- a function named as an argument (`qsort(…, cmp)`, `pthread_create(…, worker, …)`) runs from there (c.callback);
|
|
27
|
+
`signal(SIGINT, h)` / `sa.sa_handler = h` make h an entry, not a callback;
|
|
28
|
+
- a call under `#ifdef X` / `#if X` carries that build guard, one under an `if (getenv(…))` its config guard; a
|
|
29
|
+
call in a `catch` is labelled "on failure"; an ALL_CAPS call is a macro, counted as a builtin. */
|
|
30
|
+
import { posix } from 'node:path';
|
|
31
|
+
|
|
32
|
+
export const C_LANGS = new Set(['c', 'cpp']);
|
|
33
|
+
// a .h header holds C++ when it says so; otherwise it is read as C
|
|
34
|
+
export const headerLang = text => /^\s*(class|namespace|template\s*<)\b|^\s*(public|private|protected)\s*:|\bstd::|^\s*#\s*include\s*<(iostream|string|vector|memory|map|unordered_map|functional)>/m.test(text) ? 'cpp' : 'c';
|
|
35
|
+
|
|
36
|
+
export function newCModel() {
|
|
37
|
+
return {
|
|
38
|
+
symbols: new Map(), classes: new Map(), modules: new Map(),
|
|
39
|
+
rawCalls: [], handed: [], returns: [], mains: [], tables: [], fnFields: [], signals: [], envReads: [], refs: new Map(),
|
|
40
|
+
edges: [], callStats: { resolved: 0, unresolvedName: 0, unresolvedAttr: 0 },
|
|
41
|
+
unresolved: { inRepo: 0, external: 0, builtin: 0, local: 0 }, externalImports: new Map(), unresolvedIncludes: [],
|
|
42
|
+
defNodes: 0, defsVisited: 0, defScopes: 0,
|
|
43
|
+
groupOf: null, // id -> group across every language; set by mine.mjs before edges are added
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/* ---------------- plain-data IR ---------------- */
|
|
48
|
+
|
|
49
|
+
const SMART = /^(std::)?(unique_ptr|shared_ptr|weak_ptr|optional|reference_wrapper)$/;
|
|
50
|
+
// a type node -> { k: 'name', name: 'ns::K' } (pointers, references, const and smart pointers unwrapped) | { k: 'other' }
|
|
51
|
+
export function cType(t) {
|
|
52
|
+
if (!t) return { k: 'other' };
|
|
53
|
+
switch (t.type) {
|
|
54
|
+
case 'type_identifier': return { k: 'name', name: t.text };
|
|
55
|
+
case 'struct_specifier': case 'class_specifier': case 'union_specifier': { const n = t.childForFieldName('name'); return n ? { k: 'name', name: n.text } : { k: 'other' }; }
|
|
56
|
+
case 'qualified_identifier': {
|
|
57
|
+
const inner = t.childForFieldName('name');
|
|
58
|
+
if (inner?.type === 'template_type') { const r = templateType(inner, t.text.split('<')[0]); if (r) return r; }
|
|
59
|
+
return { k: 'name', name: t.text.replace(/<.*$/s, '') };
|
|
60
|
+
}
|
|
61
|
+
case 'template_type': return templateType(t, t.childForFieldName('name').text) || { k: 'name', name: t.childForFieldName('name').text };
|
|
62
|
+
case 'type_descriptor': return cType(t.childForFieldName('type'));
|
|
63
|
+
case 'primitive_type': case 'sized_type_specifier': return { k: 'other', prim: t.text };
|
|
64
|
+
default: return { k: 'other' };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function templateType(t, name) {
|
|
68
|
+
if (!SMART.test(name.replace(/^::/, ''))) return null;
|
|
69
|
+
const arg = t.childForFieldName('arguments')?.namedChildren[0];
|
|
70
|
+
return arg ? cType(arg) : null;
|
|
71
|
+
}
|
|
72
|
+
const unquote = n => n.namedChildren.filter(c => c.type === 'string_content' || c.type === 'escape_sequence').map(c => c.text).join('');
|
|
73
|
+
export const cStr = n => n?.type === 'string_literal' ? unquote(n) : n?.type === 'concatenated_string' ? n.namedChildren.map(cStr).join('') : null;
|
|
74
|
+
export function cIr(n, depth = 0) {
|
|
75
|
+
if (!n || depth > 10) return { k: 'unknown' };
|
|
76
|
+
switch (n.type) {
|
|
77
|
+
case 'identifier': return { k: 'id', name: n.text };
|
|
78
|
+
case 'qualified_identifier': return { k: 'id', name: n.text.replace(/<[^<>]*>/g, '') };
|
|
79
|
+
case 'this': return { k: 'this' };
|
|
80
|
+
case 'field_expression': return { k: 'sel', obj: cIr(n.childForFieldName('argument'), depth + 1), field: n.childForFieldName('field').text, arrow: n.childForFieldName('operator')?.text === '->' };
|
|
81
|
+
case 'call_expression': {
|
|
82
|
+
const f = n.childForFieldName('function');
|
|
83
|
+
const tf = f?.type === 'qualified_identifier' ? f.childForFieldName('name') : f;
|
|
84
|
+
if (tf?.type === 'template_function' && /^(make_unique|make_shared)$/.test(tf.childForFieldName('name').text))
|
|
85
|
+
return { k: 'new', type: cType(tf.childForFieldName('arguments')?.namedChildren[0]) };
|
|
86
|
+
return { k: 'call', fn: cIr(f, depth + 1) };
|
|
87
|
+
}
|
|
88
|
+
case 'template_function': return { k: 'id', name: n.childForFieldName('name').text };
|
|
89
|
+
case 'new_expression': return { k: 'new', type: cType(n.childForFieldName('type')) };
|
|
90
|
+
case 'pointer_expression': return cIr(n.childForFieldName('argument'), depth + 1);
|
|
91
|
+
case 'parenthesized_expression': return cIr(n.namedChildren[0], depth + 1);
|
|
92
|
+
case 'subscript_expression': return { k: 'idx', obj: cIr(n.childForFieldName('argument'), depth + 1) };
|
|
93
|
+
case 'cast_expression': return { k: 'new', type: cType(n.childForFieldName('type')), cast: true };
|
|
94
|
+
case 'string_literal': case 'concatenated_string': return { k: 'str', v: cStr(n) };
|
|
95
|
+
case 'lambda_expression': return { k: 'fn' };
|
|
96
|
+
default: return { k: 'unknown' };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/* ---------------- extraction ---------------- */
|
|
101
|
+
|
|
102
|
+
const TEST_MACRO = /^(TEST|TEST_F|TEST_P|TYPED_TEST|TYPED_TEST_P)$/;
|
|
103
|
+
const CATCH_MACRO = /^(TEST_CASE|SCENARIO|TEST_CASE_METHOD|TEMPLATE_TEST_CASE)$/;
|
|
104
|
+
const declName = d => { // the identifier a (possibly pointer / reference / array / function) declarator names
|
|
105
|
+
for (let x = d; x; x = x.childForFieldName('declarator') || (/^(reference_declarator|parenthesized_declarator)$/.test(x.type) ? x.namedChildren.at(-1) : null)) {
|
|
106
|
+
if (/^(identifier|field_identifier|type_identifier)$/.test(x.type)) return x.text;
|
|
107
|
+
if (x.type === 'qualified_identifier' || x.type === 'destructor_name' || x.type === 'operator_name') return x.text;
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
};
|
|
111
|
+
// a definition's name, or (macro-heavy code tree-sitter cannot read cleanly, a template specialisation) its declarator's text
|
|
112
|
+
const nameOf = d => declName(d) ?? (d ? d.text.replace(/<[^<>]*>/g, '').replace(/[^\w~]+/g, '_').replace(/^_+|_+$/g, '') || 'anonymous' : 'anonymous');
|
|
113
|
+
const fnDeclarator = d => { for (let x = d; x; x = x.childForFieldName('declarator') || (x.type === 'parenthesized_declarator' ? x.namedChildren[0] : null)) if (x.type === 'function_declarator') return x; return null; };
|
|
114
|
+
const isPtrDecl = d => { for (let x = d; x; x = x.childForFieldName('declarator')) if (/^(pointer_declarator|reference_declarator)$/.test(x.type)) return true; return false; };
|
|
115
|
+
|
|
116
|
+
export function extractCFile(cm, rel, root, group, text, lang) {
|
|
117
|
+
const module = rel;
|
|
118
|
+
const M = { file: rel, group, lang, includes: [], defs: new Map(), vars: new Map(), usingNs: [] };
|
|
119
|
+
cm.modules.set(module, M);
|
|
120
|
+
const lines = text.split('\n');
|
|
121
|
+
const spanText = (a, b) => lines.slice(a - 1, b).join('\n');
|
|
122
|
+
const MOD = `${module}:<module>`;
|
|
123
|
+
const quals = new Set();
|
|
124
|
+
const uniqueId = q => { let id = `${module}:${q}`, i = 2; while (cm.symbols.has(id)) id = `${module}:${q}#${i++}`; return id; };
|
|
125
|
+
const join = (...xs) => xs.filter(Boolean).join('.');
|
|
126
|
+
|
|
127
|
+
// definitions the self-check expects: function bodies and named (or typedef-named) class / struct / union bodies
|
|
128
|
+
const isTypeBody = n => n.childForFieldName('body') && (n.childForFieldName('name') || n.parent?.type === 'type_definition');
|
|
129
|
+
const counted = root.descendantsOfType(['function_definition', 'struct_specifier', 'class_specifier', 'union_specifier'])
|
|
130
|
+
.filter(n => n.type === 'function_definition' ? !!fnDeclarator(n.childForFieldName('declarator')) : isTypeBody(n));
|
|
131
|
+
cm.defNodes += counted.length;
|
|
132
|
+
|
|
133
|
+
for (const inc of root.descendantsOfType('preproc_include')) {
|
|
134
|
+
const p = inc.childForFieldName('path'), line = inc.startPosition.row + 1;
|
|
135
|
+
if (p?.type === 'system_lib_string') { const h = p.text.slice(1, -1); M.includes.push({ path: h, system: true, line }); if (!cm.externalImports.has(h)) cm.externalImports.set(h, rel); }
|
|
136
|
+
else if (p?.type === 'string_literal') M.includes.push({ path: unquote(p), system: false, line });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const params = list => (list?.namedChildren || []).filter(p => /parameter_declaration$/.test(p.type))
|
|
140
|
+
.map(p => ({ name: declName(p.childForFieldName('declarator')), type: cType(p.childForFieldName('type')) }));
|
|
141
|
+
|
|
142
|
+
function defineFn(node, name, ctx, { cls = null, clsQual = null, kind, counts = true, extra = {} } = {}) {
|
|
143
|
+
if (counts) cm.defsVisited++;
|
|
144
|
+
const q = ctx.fn ? `${ctx.qual}.<locals>.${name}` : join(...ctx.ns, clsQual, name);
|
|
145
|
+
const id = uniqueId(q);
|
|
146
|
+
quals.add(q);
|
|
147
|
+
const start = node.startPosition.row + 1, end = node.endPosition.row + 1;
|
|
148
|
+
const fd = fnDeclarator(node.childForFieldName('declarator'));
|
|
149
|
+
const ps = params(fd?.childForFieldName('parameters'));
|
|
150
|
+
const isStatic = node.namedChildren.some(c => c.type === 'storage_class_specifier' && c.text === 'static');
|
|
151
|
+
const sym = { id, group, kind: kind || (clsQual ? 'method' : 'function'), file: rel, start, end, name: join(clsQual, name), module, lang, cls,
|
|
152
|
+
decos: [...(isStatic ? ['static'] : []), ...(extra.decos || [])], params: ps.map(p => p.name).filter(Boolean), paramTypes: ps.map(p => p.type),
|
|
153
|
+
result: cType(node.childForFieldName('type')), ns: ctx.ns, clsQual, text: spanText(start, end), ...(extra.sym || {}) };
|
|
154
|
+
cm.symbols.set(id, sym);
|
|
155
|
+
if (!clsQual && !ctx.fn) M.defs.set(join(...ctx.ns, name), id);
|
|
156
|
+
// a TEST_F body runs as a member of its fixture class
|
|
157
|
+
const inner = { ...ctx, fn: id, qual: q, clsQual: extra.bodyCls || clsQual || ctx.clsQual, cls: extra.bodyCls ? null : cls || ctx.cls, types: new Map(), guards: ctx.guards || [], onFailure: null, refs: new Set() };
|
|
158
|
+
ps.forEach(p => { if (p.name) inner.types.set(p.name, { t: 'annot', type: p.type }); });
|
|
159
|
+
for (const fi of node.descendantsOfType('field_initializer').filter(x => x.parent?.parent?.id === node.id)) for (const a of fi.namedChildren) if (a.type === 'argument_list') visit(a, inner);
|
|
160
|
+
const body = node.childForFieldName('body');
|
|
161
|
+
if (body) visit(body, inner);
|
|
162
|
+
cm.refs.set(id, inner.refs);
|
|
163
|
+
return id;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function defineClass(node, ctx, nameOverride) {
|
|
167
|
+
cm.defsVisited++;
|
|
168
|
+
const name = nameOverride || node.childForFieldName('name').text.replace(/<.*$/s, '');
|
|
169
|
+
const q = join(...ctx.ns, ctx.clsQual, name);
|
|
170
|
+
const id = uniqueId(q); quals.add(q);
|
|
171
|
+
const start = node.startPosition.row + 1, end = node.endPosition.row + 1;
|
|
172
|
+
const bases = (node.namedChildren.find(c => c.type === 'base_class_clause')?.namedChildren || []).filter(c => c.type !== 'access_specifier').map(c => cType(c)).filter(t => t.k === 'name');
|
|
173
|
+
const k = { id, module, name, qual: q, ns: ctx.ns, file: rel, line: start, baseExprs: bases.map(b => b.name), baseTypes: bases, methods: new Map(), fields: new Map(),
|
|
174
|
+
fnPtrFields: new Set(), fieldOrder: [], virtuals: new Set(), pure: new Set(), decos: [], lang };
|
|
175
|
+
cm.classes.set(id, k);
|
|
176
|
+
cm.symbols.set(id, { id, group, kind: 'class', file: rel, start, end, name: join(ctx.clsQual, name), module, lang, decos: [], text: spanText(start, end) });
|
|
177
|
+
const cctx = { ...ctx, clsQual: join(ctx.clsQual, name), cls: id };
|
|
178
|
+
for (const m of node.childForFieldName('body').namedChildren) {
|
|
179
|
+
if (m.type === 'function_definition' || (m.type === 'template_declaration' && m.namedChildren.some(c => c.type === 'function_definition'))) {
|
|
180
|
+
const fdef = m.type === 'function_definition' ? m : m.namedChildren.find(c => c.type === 'function_definition');
|
|
181
|
+
const fd = fnDeclarator(fdef.childForFieldName('declarator')); if (!fd) continue;
|
|
182
|
+
const nm = nameOf(fd.childForFieldName('declarator'));
|
|
183
|
+
if (fdef.children.some(c => c.type === 'virtual') || fd.namedChildren.some(c => c.type === 'virtual_specifier')) k.virtuals.add(nm);
|
|
184
|
+
k.methods.set(nm, defineFn(fdef, nm, cctx, { cls: id, clsQual: cctx.clsQual }));
|
|
185
|
+
} else if (m.type === 'field_declaration') {
|
|
186
|
+
const fd = fnDeclarator(m.childForFieldName('declarator'));
|
|
187
|
+
if (fd) { // a method declared here; a pure virtual one (`= 0`) is abstract, and gets a symbol of its own
|
|
188
|
+
const nm = declName(fd.childForFieldName('declarator'));
|
|
189
|
+
if (!nm) continue;
|
|
190
|
+
const isVirtual = m.children.some(c => c.type === 'virtual') || fd.namedChildren.some(c => c.type === 'virtual_specifier');
|
|
191
|
+
if (isVirtual) k.virtuals.add(nm);
|
|
192
|
+
if (m.childForFieldName('default_value')?.text === '0') {
|
|
193
|
+
k.pure.add(nm);
|
|
194
|
+
const mid = uniqueId(join(...ctx.ns, cctx.clsQual, nm)), ms = m.startPosition.row + 1;
|
|
195
|
+
cm.symbols.set(mid, { id: mid, group, kind: 'method', file: rel, start: ms, end: m.endPosition.row + 1, name: join(cctx.clsQual, nm), module, lang, cls: id,
|
|
196
|
+
decos: ['abstractmethod'], params: [], paramTypes: [], result: cType(m.childForFieldName('type')), ns: ctx.ns, clsQual: cctx.clsQual, text: spanText(ms, m.endPosition.row + 1) });
|
|
197
|
+
k.methods.set(nm, mid);
|
|
198
|
+
}
|
|
199
|
+
// a C function-pointer field: `int (*run)(int, char **)`
|
|
200
|
+
if (fd.childForFieldName('declarator')?.type === 'parenthesized_declarator') { k.fnPtrFields.add(nm); k.fieldOrder.push(nm); k.fields.set(nm, { k: 'other', fnPtr: true }); }
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
for (const d of m.childrenForFieldName('declarator')) { const nm = declName(d); if (!nm) continue; k.fieldOrder.push(nm); k.fields.set(nm, cType(m.childForFieldName('type')));
|
|
204
|
+
if (/std::function|function</.test(m.childForFieldName('type')?.text || '')) k.fnPtrFields.add(nm); }
|
|
205
|
+
const inner = m.childForFieldName('type');
|
|
206
|
+
if (inner && /^(struct|class|union)_specifier$/.test(inner.type) && inner.childForFieldName('body') && inner.childForFieldName('name')) defineClass(inner, cctx);
|
|
207
|
+
} else if (m.type === 'declaration' && /^MOCK_(CONST_)?METHOD\d*$/.test(fnDeclarator(m.childForFieldName('declarator'))?.childForFieldName('declarator')?.text || '')) {
|
|
208
|
+
// gmock: MOCK_METHOD(ret, name, (args), (override)) — the mock overrides `name`
|
|
209
|
+
const fd = fnDeclarator(m.childForFieldName('declarator'));
|
|
210
|
+
const ps = fd.childForFieldName('parameters').namedChildren;
|
|
211
|
+
const macro = fd.childForFieldName('declarator').text;
|
|
212
|
+
const nm = (macro === 'MOCK_METHOD' ? ps[1] : ps[0])?.text.trim().replace(/^\w+\s+/, '');
|
|
213
|
+
if (!nm || !/^\w+$/.test(nm)) continue;
|
|
214
|
+
const mid = uniqueId(join(...ctx.ns, cctx.clsQual, nm)), ms = m.startPosition.row + 1;
|
|
215
|
+
cm.symbols.set(mid, { id: mid, group, kind: 'method', file: rel, start: ms, end: m.endPosition.row + 1, name: join(cctx.clsQual, nm), module, lang, cls: id,
|
|
216
|
+
decos: ['mock'], params: [], paramTypes: [], result: { k: 'other' }, ns: ctx.ns, clsQual: cctx.clsQual, text: spanText(ms, m.endPosition.row + 1) });
|
|
217
|
+
k.methods.set(nm, mid); k.virtuals.add(nm); k.mocked = true;
|
|
218
|
+
} else if (/^(struct|class|union)_specifier$/.test(m.type) && m.childForFieldName('body') && m.childForFieldName('name')) defineClass(m, cctx);
|
|
219
|
+
else visitClassMembers(m, cctx, k); // templates, friends, preprocessor blocks, what a parse error left
|
|
220
|
+
}
|
|
221
|
+
return id;
|
|
222
|
+
}
|
|
223
|
+
function visitClassMembers(node, cctx, k) {
|
|
224
|
+
for (const c of node.namedChildren) {
|
|
225
|
+
if (/^(struct|class|union)_specifier$/.test(c.type) && c.childForFieldName('body') && c.childForFieldName('name')) defineClass(c, cctx);
|
|
226
|
+
else if (c.type === 'function_definition') { const fd = fnDeclarator(c.childForFieldName('declarator')); if (fd) { const nm = nameOf(fd.childForFieldName('declarator')); k.methods.set(nm, defineFn(c, nm, cctx, { cls: k.id, clsQual: cctx.clsQual })); } }
|
|
227
|
+
else if (c.type === 'type_definition' && /^(struct|class|union)_specifier$/.test(c.childForFieldName('type')?.type) && c.childForFieldName('type').childForFieldName('body'))
|
|
228
|
+
defineClass(c.childForFieldName('type'), cctx, c.childForFieldName('type').childForFieldName('name')?.text || nameOf(c.childForFieldName('declarator')));
|
|
229
|
+
else visitClassMembers(c, cctx, k);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// a table row: string keys and function names, positionally or by designated field
|
|
234
|
+
const rowOf = il => il.namedChildren.map((e, i) => {
|
|
235
|
+
if (e.type === 'initializer_pair') { const v = e.childForFieldName('value'); return { field: e.childForFieldName('designator')?.text.replace(/^\./, '').replace(/^\[|\]$/g, ''), ...cell(v), i }; }
|
|
236
|
+
return { ...cell(e), i };
|
|
237
|
+
});
|
|
238
|
+
const cell = v => { const s = cStr(v); if (s != null) return { str: s };
|
|
239
|
+
const x = v?.type === 'pointer_expression' ? v.childForFieldName('argument') : v;
|
|
240
|
+
if (x?.type === 'identifier' || x?.type === 'qualified_identifier') return { ref: x.text };
|
|
241
|
+
if (x?.type === 'lambda_expression') return { lambda: x };
|
|
242
|
+
return { other: true }; };
|
|
243
|
+
|
|
244
|
+
function declaration(node, ctx) {
|
|
245
|
+
const type = node.childForFieldName('type'), tIr = cType(type);
|
|
246
|
+
if (type && /^(struct|class|union)_specifier$/.test(type.type) && type.childForFieldName('body') && type.childForFieldName('name')) defineClass(type, ctx);
|
|
247
|
+
for (const d of node.childrenForFieldName('declarator')) {
|
|
248
|
+
if (d.type === 'function_declarator') continue; // a prototype
|
|
249
|
+
const init = d.type === 'init_declarator' ? d : null;
|
|
250
|
+
const dn = init ? init.childForFieldName('declarator') : d;
|
|
251
|
+
const name = declName(dn), value = init?.childForFieldName('value'), line = node.startPosition.row + 1;
|
|
252
|
+
if (!name) { if (value) visit(value, ctx); continue; }
|
|
253
|
+
const isArray = dn?.type === 'array_declarator' || dn?.childForFieldName?.('declarator')?.type === 'array_declarator';
|
|
254
|
+
const auto = type?.type === 'placeholder_type_specifier';
|
|
255
|
+
let spec = null;
|
|
256
|
+
if (!auto && tIr.k === 'name') spec = { t: value?.type === 'argument_list' || value?.type === 'initializer_list' || (!value && !isPtrDecl(dn)) ? 'new' : 'annot', type: tIr };
|
|
257
|
+
else if (value && value.type !== 'initializer_list' && value.type !== 'argument_list') { const ir = cIr(value); spec = ir.k === 'new' ? { t: 'new', type: ir.type } : { t: 'expr', ir }; }
|
|
258
|
+
// `K obj(…)`, `K obj{…}`, `K obj;` construct a C++ object; a C struct declaration runs nothing
|
|
259
|
+
if (lang === 'cpp' && spec?.t === 'new' && !isPtrDecl(dn) && tIr.k === 'name' && !auto && (ctx.fn || value)) cm.rawCalls.push(ctorCall(tIr.name, ctx, line, value));
|
|
260
|
+
if (ctx.fn) { if (spec) ctx.types.set(name, spec); }
|
|
261
|
+
else if (spec) M.vars.set(name, { ...spec, module, line, ns: ctx.ns });
|
|
262
|
+
// a table: an array of rows, or a map initialised with { {"key", fn}, … }
|
|
263
|
+
if (value?.type === 'initializer_list' && (isArray || /map\b|map</.test(type?.text || ''))) {
|
|
264
|
+
const rows = value.namedChildren.filter(r => r.type === 'initializer_list').map(r => ({ line: r.startPosition.row + 1, cells: rowOf(r) }));
|
|
265
|
+
if (rows.some(r => r.cells.some(x => x.str != null) && r.cells.some(x => x.ref || x.lambda))) {
|
|
266
|
+
for (const r of rows) for (const x of r.cells) if (x.lambda) { const key = r.cells.find(y => y.str != null)?.str; x.fnId = defineFn(x.lambda, `${name}[${JSON.stringify(key ?? r.line).replace(/:/g, "-")}]`, ctx, { counts: false }); delete x.lambda; }
|
|
267
|
+
cm.tables.push({ name, var: ctx.fn ? `${ctx.fn}#${name}` : `${module}#${name}`, module, file: rel, line, elemType: type?.type === 'struct_specifier' ? type.childForFieldName('name')?.text : tIr.name, map: !isArray, rows, ns: ctx.ns });
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (value?.type === 'initializer_list') noteFnFields(value, tIr, ctx);
|
|
272
|
+
if (value) visit(value, ctx);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
// `struct ops o = { .open = f }`, and `{ f, g }` by field position: the functions each field holds
|
|
276
|
+
function noteFnFields(il, tIr, ctx) {
|
|
277
|
+
il.namedChildren.forEach((e, i) => {
|
|
278
|
+
if (e.type === 'initializer_list') { noteFnFields(e, tIr, ctx); return; }
|
|
279
|
+
const v = e.type === 'initializer_pair' ? e.childForFieldName('value') : e;
|
|
280
|
+
const c = cell(v);
|
|
281
|
+
if (c.ref) cm.fnFields.push({ field: e.type === 'initializer_pair' ? e.childForFieldName('designator')?.text.replace(/^\./, '') : null, index: i, type: tIr.name, ref: c.ref, module, line: e.startPosition.row + 1, ns: ctx.ns });
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
const ctorCall = (typeName, ctx, line, value) => ({ from: ctx.fn || MOD, module, file: rel, line, isNew: true, type: typeName, types: ctx.types, cls: ctx.cls, clsQual: ctx.clsQual, ns: ctx.ns,
|
|
285
|
+
guards: ctx.guards, onFailure: ctx.onFailure, args: [], value: value?.type || null });
|
|
286
|
+
|
|
287
|
+
const argInfo = a => ({ type: a.type, ir: cIr(a), text: a.text.length > 200 ? a.text.slice(0, 200) : a.text, str: cStr(a) });
|
|
288
|
+
function call(node, ctx) {
|
|
289
|
+
const fnNode = node.childForFieldName('function');
|
|
290
|
+
const args = (node.childForFieldName('arguments')?.namedChildren || []).filter(a => a.type !== 'comment');
|
|
291
|
+
const from = ctx.fn || MOD, line = node.startPosition.row + 1, expr = fnNode.text;
|
|
292
|
+
const ir = cIr(node);
|
|
293
|
+
if (ir.k === 'new') { cm.rawCalls.push(ctorCall(ir.type.name, ctx, line, null)); args.forEach(a => visit(a, ctx)); return; }
|
|
294
|
+
if (/^(getenv|secure_getenv|std::getenv)$/.test(expr) && cStr(args[0]) != null) cm.envReads.push({ key: cStr(args[0]), file: rel, line, group, via: 'getenv' });
|
|
295
|
+
if (/^(signal|std::signal)$/.test(expr) && args[1]) cm.signals.push({ sig: args[0]?.text, ref: args[1].text.replace(/^&/, ''), module, file: rel, line, ns: ctx.ns });
|
|
296
|
+
const attr = fnNode.type === 'field_expression' ? fnNode.childForFieldName('field').text : null;
|
|
297
|
+
cm.rawCalls.push({ from, module, file: rel, expr, line, attr, obj: attr ? cIr(fnNode.childForFieldName('argument')) : null, fn: attr ? null : cIr(fnNode),
|
|
298
|
+
fnType: fnNode.type, types: ctx.types, cls: ctx.cls, clsQual: ctx.clsQual, ns: ctx.ns, guards: ctx.guards, onFailure: ctx.onFailure, top: !ctx.fn, args: args.map(argInfo) });
|
|
299
|
+
if (/^(signal|std::signal)$/.test(expr)) { args.forEach(a => visit(a, ctx)); return; }
|
|
300
|
+
if (/^(EXPECT_CALL|ON_CALL)$/.test(expr)) { if (args[0]) visit(args[0], ctx); return; } // gmock: the second argument is a matcher, not a call
|
|
301
|
+
const index = cm.rawCalls.length - 1;
|
|
302
|
+
for (const [i, a] of args.entries()) {
|
|
303
|
+
const x = a.type === 'pointer_expression' && a.childForFieldName('operator')?.text === '&' ? a.childForFieldName('argument') : a;
|
|
304
|
+
if (x.type === 'identifier' || x.type === 'qualified_identifier') cm.handed.push({ from, ref: x.text, line, file: rel, callee: expr, argIndex: i, call: index, module, types: ctx.types, cls: ctx.cls, clsQual: ctx.clsQual, ns: ctx.ns });
|
|
305
|
+
}
|
|
306
|
+
if (attr) visit(fnNode.childForFieldName('argument'), ctx);
|
|
307
|
+
else if (!/^(identifier|qualified_identifier|template_function)$/.test(fnNode.type)) visit(fnNode, ctx);
|
|
308
|
+
for (const a of args) visit(a, ctx);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const condText = n => (n.childForFieldName('condition')?.text || '').replace(/^\(|\)$/g, '');
|
|
312
|
+
function visitSeq(children, ctx) { // statements in order: a Catch2 TEST_CASE is a call followed by its block
|
|
313
|
+
for (let i = 0; i < children.length; i++) {
|
|
314
|
+
const c = children[i];
|
|
315
|
+
const ce = c.type === 'expression_statement' ? c.namedChildren[0] : null;
|
|
316
|
+
const head = ce?.type === 'call_expression' ? ce.childForFieldName('function')?.text : null;
|
|
317
|
+
if (head && CATCH_MACRO.test(head) && children[i + 1]?.type === 'compound_statement' && !ctx.fn) {
|
|
318
|
+
const args = ce.childForFieldName('arguments').namedChildren;
|
|
319
|
+
const fixture = head === 'TEST_CASE_METHOD' ? args[0]?.text : null;
|
|
320
|
+
const title = cStr(args.find(a => a.type === 'string_literal')) || head;
|
|
321
|
+
const block = children[i + 1];
|
|
322
|
+
defineTestBlock(c, block, title.replace(/:/g, '-').replace(/\s+/g, ' ').trim(), ctx, { macro: head, fixture, tags: args.filter(a => a.type === 'string_literal').slice(1).map(cStr) });
|
|
323
|
+
i++;
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
visit(c, ctx);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
// a Catch2 test: its symbol spans the macro line and the block after it
|
|
330
|
+
function defineTestBlock(stmt, block, title, ctx, { macro, fixture, tags }) {
|
|
331
|
+
cm.defsVisited++; cm.defNodes++;
|
|
332
|
+
const q = title, id = uniqueId(q); quals.add(q);
|
|
333
|
+
const start = stmt.startPosition.row + 1, end = block.endPosition.row + 1;
|
|
334
|
+
cm.symbols.set(id, { id, group, kind: 'function', file: rel, start, end, name: title, module, lang, cls: null, decos: [], params: [], paramTypes: [], result: { k: 'other' },
|
|
335
|
+
ns: ctx.ns, clsQual: fixture, test: true, macro, ...(fixture ? { fixture } : {}), ...(tags.length ? { tags } : {}), text: spanText(start, end) });
|
|
336
|
+
const inner = { ...ctx, fn: id, qual: q, clsQual: fixture || ctx.clsQual, types: new Map(), guards: [], onFailure: null, refs: new Set() };
|
|
337
|
+
visit(block, inner);
|
|
338
|
+
cm.refs.set(id, inner.refs);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function visit(node, ctx) {
|
|
342
|
+
switch (node.type) {
|
|
343
|
+
case 'comment': return;
|
|
344
|
+
case 'translation_unit': case 'declaration_list': case 'compound_statement': visitSeq(node.namedChildren, ctx); return;
|
|
345
|
+
case 'namespace_definition': {
|
|
346
|
+
const n = node.childForFieldName('name'), parts = n ? n.text.split('::').filter(Boolean) : [];
|
|
347
|
+
visit(node.childForFieldName('body'), { ...ctx, ns: [...ctx.ns, ...parts] });
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
case 'using_declaration': { const m = /^using\s+namespace\s+([\w:]+)\s*;/.exec(node.text); if (m) M.usingNs.push(m[1].replace(/::/g, '.')); return; }
|
|
351
|
+
case 'function_definition': {
|
|
352
|
+
const fd = fnDeclarator(node.childForFieldName('declarator'));
|
|
353
|
+
if (!fd) break;
|
|
354
|
+
const dn = fd.childForFieldName('declarator');
|
|
355
|
+
// gtest: TEST(Suite, Name) / TEST_F(Fixture, Name) { … }
|
|
356
|
+
if (!ctx.fn && dn?.type === 'identifier' && TEST_MACRO.test(dn.text)) {
|
|
357
|
+
const ps = fd.childForFieldName('parameters').namedChildren.map(p => p.text.trim());
|
|
358
|
+
const nm = `${ps[0]}.${ps[1]}`;
|
|
359
|
+
const fixture = dn.text !== 'TEST' ? ps[0] : null;
|
|
360
|
+
defineFn(node, nm, ctx, { kind: 'function', extra: { bodyCls: fixture, sym: { test: true, macro: dn.text, ...(fixture ? { fixture, clsQual: fixture } : {}), suite: ps[0], case: ps[1] } } });
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
const full = nameOf(dn);
|
|
364
|
+
const parts = full.replace(/<[^<>]*>/g, '').split('::').filter(Boolean);
|
|
365
|
+
const name = parts.pop(), owner = parts.length ? parts.join('.') : null;
|
|
366
|
+
// an out-of-line member `K::m` (or `ns::K::m`): its class is resolved later by its qualified name
|
|
367
|
+
const id = defineFn(node, name, ctx, owner ? { clsQual: owner } : {});
|
|
368
|
+
if (!owner && name === 'main' && !ctx.fn) cm.mains.push(id);
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
case 'template_declaration': case 'linkage_specification': for (const c of node.namedChildren) visit(c, ctx); return;
|
|
372
|
+
case 'class_specifier': case 'struct_specifier': case 'union_specifier':
|
|
373
|
+
if (node.childForFieldName('body') && node.childForFieldName('name')) { defineClass(node, ctx); return; }
|
|
374
|
+
break;
|
|
375
|
+
case 'type_definition': { // typedef struct { … } name;
|
|
376
|
+
const t = node.childForFieldName('type');
|
|
377
|
+
if (t && /^(struct|class|union)_specifier$/.test(t.type) && t.childForFieldName('body')) { defineClass(t, ctx, t.childForFieldName('name')?.text || nameOf(node.childForFieldName('declarator'))); return; }
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
case 'declaration': declaration(node, ctx); return;
|
|
381
|
+
case 'field_declaration': declaration(node, ctx); return; // outside a class body: what a parse error left
|
|
382
|
+
case 'preproc_ifdef': case 'preproc_if': case 'preproc_elif': {
|
|
383
|
+
const nm = node.childForFieldName('name')?.text, c = node.childForFieldName('condition')?.text;
|
|
384
|
+
const neg = node.type === 'preproc_ifdef' && /^#\s*ifndef/.test(node.text);
|
|
385
|
+
const g = { text: nm ? `#if${neg ? 'n' : ''}def ${nm}` : `#if ${c}`, neg: false, build: true };
|
|
386
|
+
const inner = { ...ctx, guards: [...(ctx.guards || []), g] };
|
|
387
|
+
const kids = node.namedChildren.filter(x => x.type !== 'identifier' || x.id !== node.childForFieldName('name')?.id).filter(x => x.id !== node.childForFieldName('condition')?.id && x.id !== node.childForFieldName('alternative')?.id);
|
|
388
|
+
visitSeq(kids, inner);
|
|
389
|
+
const alt = node.childForFieldName('alternative');
|
|
390
|
+
if (alt) visit(alt, { ...ctx, guards: [...(ctx.guards || []), { ...g, neg: true }] });
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
case 'preproc_else': visitSeq(node.namedChildren, ctx); return;
|
|
394
|
+
case 'if_statement': {
|
|
395
|
+
const c = condText(node);
|
|
396
|
+
visit(node.childForFieldName('condition'), ctx);
|
|
397
|
+
const g0 = ctx.guards || [];
|
|
398
|
+
visit(node.childForFieldName('consequence'), { ...ctx, guards: [...g0, { text: c, neg: false }] });
|
|
399
|
+
const alt = node.childForFieldName('alternative');
|
|
400
|
+
if (alt) visit(alt, { ...ctx, guards: [...g0, { text: c, neg: true }] });
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
case 'catch_clause': for (const c of node.namedChildren) visit(c, { ...ctx, onFailure: 'catch' + (node.childForFieldName('parameters') ? ` ${node.childForFieldName('parameters').text}` : '') }); return;
|
|
404
|
+
case 'return_statement': { const v = node.namedChildren[0]; if (v && ctx.fn) { const ir = cIr(v); if (ir.k === 'new' || ir.k === 'call' || ir.k === 'id') cm.returns.push({ fn: ctx.fn, ir, guards: ctx.guards, line: node.startPosition.row + 1, module, types: ctx.types, cls: ctx.cls, clsQual: ctx.clsQual, ns: ctx.ns }); } break; }
|
|
405
|
+
case 'call_expression': call(node, ctx); return;
|
|
406
|
+
case 'new_expression': { const t = cType(node.childForFieldName('type')); if (t.k === 'name') cm.rawCalls.push(ctorCall(t.name, ctx, node.startPosition.row + 1, null)); break; }
|
|
407
|
+
case 'assignment_expression': {
|
|
408
|
+
const l = node.childForFieldName('left'), r = node.childForFieldName('right');
|
|
409
|
+
const rv = r?.type === 'pointer_expression' ? r.childForFieldName('argument') : r;
|
|
410
|
+
// `ops->open = f`, `sa.sa_handler = h`: a function a field holds; `handlers["x"] = f`: a table row
|
|
411
|
+
if (l?.type === 'field_expression' && (rv?.type === 'identifier' || rv?.type === 'qualified_identifier')) {
|
|
412
|
+
const field = l.childForFieldName('field').text;
|
|
413
|
+
if (field === 'sa_handler' || field === 'sa_sigaction') cm.signals.push({ sig: null, ref: rv.text, module, file: rel, line: node.startPosition.row + 1, ns: ctx.ns });
|
|
414
|
+
else cm.fnFields.push({ field, index: null, type: null, ref: rv.text, module, line: node.startPosition.row + 1, ns: ctx.ns, obj: cIr(l.childForFieldName('argument')), types: ctx.types, cls: ctx.cls, clsQual: ctx.clsQual });
|
|
415
|
+
}
|
|
416
|
+
// the index is `index` in C and a `subscript_argument_list` (`indices`) in C++
|
|
417
|
+
const keyOf = sub => { const i = sub.childForFieldName('index') || sub.childForFieldName('indices'); return cStr(i?.type === 'subscript_argument_list' ? i.namedChildren[0] : i); };
|
|
418
|
+
if (l?.type === 'subscript_expression' && keyOf(l) != null && (rv?.type === 'identifier' || rv?.type === 'qualified_identifier' || rv?.type === 'lambda_expression')) {
|
|
419
|
+
const tname = l.childForFieldName('argument').text, key = keyOf(l);
|
|
420
|
+
const fnId = rv.type === "lambda_expression" ? defineFn(rv, `${tname}[${JSON.stringify(key).replace(/:/g, "-")}]`, ctx, { counts: false }) : null;
|
|
421
|
+
cm.tables.push({ name: tname, var: null, module, file: rel, line: node.startPosition.row + 1, map: true, assigned: true, from: ctx.fn, ns: ctx.ns, types: ctx.types,
|
|
422
|
+
rows: [{ line: node.startPosition.row + 1, cells: [{ str: key, i: 0 }, fnId ? { fnId, i: 1 } : { ref: rv.text, i: 1 }] }] });
|
|
423
|
+
if (fnId) return;
|
|
424
|
+
}
|
|
425
|
+
if (ctx.fn && l?.type === 'identifier' && r) { const ir = cIr(r); if (ir.k !== 'unknown') ctx.types.set(l.text, ir.k === 'new' ? { t: 'new', type: ir.type } : { t: 'expr', ir }); }
|
|
426
|
+
break;
|
|
427
|
+
}
|
|
428
|
+
case 'lambda_expression': { const b = node.childForFieldName('body'); if (b) visit(b, ctx); return; }
|
|
429
|
+
case 'identifier': ctx.refs?.add(node.text); return;
|
|
430
|
+
}
|
|
431
|
+
for (const c of node.namedChildren) visit(c, ctx);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
visit(root, { fn: null, qual: null, ns: [], cls: null, clsQual: null, types: new Map(), guards: [], onFailure: null, refs: null });
|
|
435
|
+
cm.defScopes += quals.size;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/* ---------------- resolution ---------------- */
|
|
439
|
+
|
|
440
|
+
export function cResolverFor(cm, { files }) {
|
|
441
|
+
const { classes, symbols, modules } = cm;
|
|
442
|
+
const fileSet = new Set(files);
|
|
443
|
+
// a quoted include: beside the including file (graphify's rule), else the one repo file whose path ends with it
|
|
444
|
+
const bySuffix = new Map();
|
|
445
|
+
const resolveInclude = (spec, from) => {
|
|
446
|
+
const beside = posix.normalize(posix.join(posix.dirname(from), spec));
|
|
447
|
+
if (fileSet.has(beside)) return beside;
|
|
448
|
+
if (!bySuffix.has(spec)) { const hits = files.filter(f => f === spec || f.endsWith('/' + spec)); bySuffix.set(spec, hits.length === 1 ? hits[0] : null); }
|
|
449
|
+
return bySuffix.get(spec);
|
|
450
|
+
};
|
|
451
|
+
// the files a module's includes name, and the sources beside them (`cart.h` -> `cart.c` / `cart.cpp`)
|
|
452
|
+
const near = new Map(), included = new Map();
|
|
453
|
+
for (const M of modules.values()) {
|
|
454
|
+
const set = new Set([M.file]), inc1 = [];
|
|
455
|
+
included.set(M.file, inc1);
|
|
456
|
+
for (const inc of M.includes) if (!inc.system) {
|
|
457
|
+
const f = resolveInclude(inc.path, M.file);
|
|
458
|
+
if (!f) { cm.unresolvedIncludes.push({ from: M.file, include: inc.path, line: inc.line }); continue; }
|
|
459
|
+
inc1.push(f); set.add(f); const stem = f.replace(/\.(h|hh|hpp|hxx)$/, ''); for (const e of ['.c', '.cc', '.cpp', '.cxx']) if (fileSet.has(stem + e)) set.add(stem + e);
|
|
460
|
+
}
|
|
461
|
+
near.set(M.file, set);
|
|
462
|
+
}
|
|
463
|
+
// classes by qualified and short name
|
|
464
|
+
const classByQual = new Map(), classByName = new Map();
|
|
465
|
+
for (const k of classes.values()) { (classByQual.get(k.qual) || classByQual.set(k.qual, []).get(k.qual)).push(k.id); (classByName.get(k.name) || classByName.set(k.name, []).get(k.name)).push(k.id); }
|
|
466
|
+
// the files a module reaches by #include, directly or through another included file
|
|
467
|
+
const reached = new Map();
|
|
468
|
+
const reachOf = f => { if (!reached.has(f)) { const out = new Set([f]), todo = [f];
|
|
469
|
+
while (todo.length) for (const g of included.get(todo.pop()) || []) if (!out.has(g)) { out.add(g); todo.push(g); }
|
|
470
|
+
reached.set(f, out); } return reached.get(f); };
|
|
471
|
+
// the definition of a C function / class nearest the caller: its own file, then what it includes, then the only one;
|
|
472
|
+
// a `static` one only from its own file or a file the caller's includes reach
|
|
473
|
+
const visible = (id, module) => { const s = symbols.get(id);
|
|
474
|
+
return !s || !(s.decos || []).includes('static') || reachOf(module).has(s.file); };
|
|
475
|
+
const pick = (all, module) => { const ids = (all || []).filter(id => visible(id, module)); if (!ids.length) return null; if (ids.length === 1) return ids[0];
|
|
476
|
+
const own = ids.filter(id => symbols.get(id).file === module); if (own.length) return own[0];
|
|
477
|
+
const pub = ids.filter(id => !(symbols.get(id).decos || []).includes('static'));
|
|
478
|
+
const n = pub.filter(id => near.get(module)?.has(symbols.get(id).file)); if (n.length === 1) return n[0];
|
|
479
|
+
return pub.length === 1 ? pub[0] : null; };
|
|
480
|
+
const nsChain = ns => { const out = []; for (let i = ns.length; i >= 0; i--) out.push(ns.slice(0, i)); return out; };
|
|
481
|
+
function classNamed(name, sc) {
|
|
482
|
+
const q = name.replace(/^::/, '').replace(/::/g, '.').replace(/<.*$/s, '');
|
|
483
|
+
for (const ns of nsChain(sc.ns || [])) { const hit = pick(classByQual.get([...ns, q].join('.')), sc.module); if (hit) return hit; }
|
|
484
|
+
for (const u of modules.get(sc.module)?.usingNs || []) { const hit = pick(classByQual.get(`${u}.${q}`), sc.module); if (hit) return hit; }
|
|
485
|
+
return q.includes('.') ? null : pick(classByName.get(q), sc.module);
|
|
486
|
+
}
|
|
487
|
+
// out-of-line methods join their class; a qualifier that names no class names namespaces (`ns::f() { … }`); a class's
|
|
488
|
+
// bases resolve
|
|
489
|
+
for (const s of symbols.values()) if (s.clsQual && !s.cls && s.kind !== 'class') {
|
|
490
|
+
const k = classNamed(s.clsQual, { ns: s.ns, module: s.module });
|
|
491
|
+
if (k) { s.cls = k; s.kind = s.test ? 'function' : 'method'; if (!s.test) classes.get(k).methods.set(s.name.split('.').pop(), s.id); }
|
|
492
|
+
else if (!s.test) { s.ns = [...(s.ns || []), ...s.clsQual.split('.')]; s.name = s.name.split('.').pop(); s.kind = 'function'; s.clsQual = null; }
|
|
493
|
+
}
|
|
494
|
+
// functions by qualified name (C linkage: non-static ones are global)
|
|
495
|
+
const funcs = new Map();
|
|
496
|
+
for (const s of symbols.values()) if (s.kind === 'function' && !s.test && !s.id.includes('<locals>')) {
|
|
497
|
+
const q = [...(s.ns || []), s.name].join('.');
|
|
498
|
+
(funcs.get(q) || funcs.set(q, []).get(q)).push(s.id);
|
|
499
|
+
}
|
|
500
|
+
for (const k of classes.values()) k.bases = k.baseTypes.map(t => classNamed(t.name, { ns: k.ns, module: k.module })).filter(Boolean);
|
|
501
|
+
const subclasses = new Map(); for (const k of classes.values()) for (const b of k.bases) (subclasses.get(b) || subclasses.set(b, []).get(b)).push(k.id);
|
|
502
|
+
const mro = (id, seen = new Set()) => seen.has(id) ? [] : (seen.add(id), [id, ...(classes.get(id)?.bases || []).flatMap(b => mro(b, seen))]);
|
|
503
|
+
const lookup = (clsId, m) => { for (const c of mro(clsId)) { const x = classes.get(c)?.methods.get(m); if (x) return x; } return null; };
|
|
504
|
+
const isVirtual = (clsId, m) => mro(clsId).some(c => classes.get(c)?.virtuals.has(m));
|
|
505
|
+
const overrides = (clsId, m, out = [], seen = new Set()) => { for (const s of subclasses.get(clsId) || []) { if (seen.has(s)) continue; seen.add(s);
|
|
506
|
+
const x = classes.get(s).methods.get(m); if (x) out.push(x); overrides(s, m, out, seen); } return out; };
|
|
507
|
+
const fieldType = (clsId, f) => { for (const c of mro(clsId)) { const t = classes.get(c)?.fields.get(f); if (t) return { t, module: classes.get(c).module, ns: classes.get(c).ns }; } return null; };
|
|
508
|
+
const ctorOf = clsId => { const k = classes.get(clsId); return k ? k.methods.get(k.name) || null : null; };
|
|
509
|
+
|
|
510
|
+
// a function by name from a scope: the enclosing namespaces outward, using-directives, then C linkage
|
|
511
|
+
function funcNamed(name, sc) {
|
|
512
|
+
const q = name.replace(/^::/, '').replace(/::/g, '.');
|
|
513
|
+
for (const ns of nsChain(sc.ns || [])) { const hit = pick(funcs.get([...ns, q].join('.')), sc.module); if (hit) return hit; }
|
|
514
|
+
for (const u of modules.get(sc.module)?.usingNs || []) { const hit = pick(funcs.get(`${u}.${q}`), sc.module); if (hit) return hit; }
|
|
515
|
+
return null;
|
|
516
|
+
}
|
|
517
|
+
const scopeCls = sc => sc.cls || (sc.clsQual ? classNamed(sc.clsQual, sc) : null);
|
|
518
|
+
// the type of an expression IR -> { cls, how: 'new' | 'annot' } | { ext } | null
|
|
519
|
+
function typeOf(ir, sc, depth = 0) {
|
|
520
|
+
if (!ir || depth > 8) return null;
|
|
521
|
+
switch (ir.k) {
|
|
522
|
+
case 'new': { const c = ir.type?.k === 'name' ? classNamed(ir.type.name, sc) : null; return c ? { cls: c, how: ir.cast ? 'annot' : 'new' } : ir.type?.name ? { ext: ir.type.name } : null; }
|
|
523
|
+
case 'this': { const c = scopeCls(sc); return c ? { cls: c, how: 'self' } : null; }
|
|
524
|
+
case 'id': {
|
|
525
|
+
const spec = sc.types?.get(ir.name);
|
|
526
|
+
if (spec) return typeOfSpec(spec, sc, depth + 1);
|
|
527
|
+
const own = scopeCls(sc), f = own && fieldType(own, ir.name); // an implicit this->field
|
|
528
|
+
if (f) return typeOfType(f.t, { ...sc, ns: f.ns, module: f.module });
|
|
529
|
+
for (const M of [modules.get(sc.module)]) { const v = M?.vars.get(ir.name); if (v) return typeOfSpec(v, { ...sc, module: v.module, ns: v.ns }, depth + 1); }
|
|
530
|
+
const g = [...modules.values()].map(M => M.vars.get(ir.name)).find(v => v && v.module !== sc.module); // an extern global
|
|
531
|
+
return g ? typeOfSpec(g, { ...sc, module: g.module, ns: g.ns }, depth + 1) : null;
|
|
532
|
+
}
|
|
533
|
+
case 'sel': { const o = typeOf(ir.obj, sc, depth + 1); const f = o?.cls && fieldType(o.cls, ir.field); return f ? typeOfType(f.t, { ...sc, ns: f.ns, module: f.module }) : null; }
|
|
534
|
+
case 'call': { const t = calleeOf(ir.fn, sc, depth + 1); const s = t && symbols.get(t);
|
|
535
|
+
if (s?.result?.k === 'name') { const c = classNamed(s.result.name, { ns: s.ns, module: s.module }); if (c) { const fr = factoryReturns(s.id); return fr.length ? { cls: fr[0], how: fr.length === 1 ? 'new' : 'annot', narrow: fr, factory: s.id } : { cls: c, how: 'annot' }; } }
|
|
536
|
+
return null; }
|
|
537
|
+
default: return null;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
const typeOfType = (t, sc) => { if (t?.k !== 'name') return null; const c = classNamed(t.name, sc); return c ? { cls: c, how: 'annot' } : { ext: t.name }; };
|
|
541
|
+
function typeOfSpec(spec, sc, depth) {
|
|
542
|
+
if (spec.t === 'new') { const c = classNamed(spec.type.name, sc); return c ? { cls: c, how: 'new' } : { ext: spec.type.name }; }
|
|
543
|
+
if (spec.t === 'annot') return typeOfType(spec.type, sc);
|
|
544
|
+
if (spec.t === 'expr') return typeOf(spec.ir, sc, depth + 1);
|
|
545
|
+
return null;
|
|
546
|
+
}
|
|
547
|
+
// what a call's function IR names (a function or method id)
|
|
548
|
+
function calleeOf(fn, sc, depth = 0) {
|
|
549
|
+
if (!fn || depth > 8) return null;
|
|
550
|
+
if (fn.k === 'id') {
|
|
551
|
+
const name = fn.name;
|
|
552
|
+
if (name.includes('::')) { // ns::f, K::m (a static member, or an explicit base call)
|
|
553
|
+
const parts = name.replace(/^::/, '').split('::'), last = parts.pop(), k = classNamed(parts.join('::'), sc);
|
|
554
|
+
if (k) return lookup(k, last);
|
|
555
|
+
return funcNamed(name, sc);
|
|
556
|
+
}
|
|
557
|
+
const own = scopeCls(sc), m = own && lookup(own, name);
|
|
558
|
+
if (m) return m;
|
|
559
|
+
return funcNamed(name, sc);
|
|
560
|
+
}
|
|
561
|
+
if (fn.k === 'sel') { const o = typeOf(fn.obj, sc, depth + 1); return o?.cls ? lookup(o.cls, fn.field) : null; }
|
|
562
|
+
return null;
|
|
563
|
+
}
|
|
564
|
+
const retMemo = new Map(), returnsOf = new Map();
|
|
565
|
+
for (const r of cm.returns) (returnsOf.get(r.fn) || returnsOf.set(r.fn, []).get(r.fn)).push(r);
|
|
566
|
+
// the concrete classes a function returns (`return new Sqlite(…)`, `return std::make_unique<Mem>()`), when there are any
|
|
567
|
+
function factoryReturns(fnId, depth = 0) {
|
|
568
|
+
if (retMemo.has(fnId)) return retMemo.get(fnId);
|
|
569
|
+
if (depth > 3) return [];
|
|
570
|
+
retMemo.set(fnId, []);
|
|
571
|
+
const out = new Set();
|
|
572
|
+
for (const r of returnsOf.get(fnId) || []) {
|
|
573
|
+
if (r.ir.k === 'new' && r.ir.type?.k === 'name' && !r.ir.cast) { const c = classNamed(r.ir.type.name, r); if (c) out.add(c); }
|
|
574
|
+
else if (r.ir.k === 'call') { const t = calleeOf(r.ir.fn, r); if (t) for (const c of factoryReturns(t, depth + 1)) out.add(c); }
|
|
575
|
+
}
|
|
576
|
+
const res = [...out]; retMemo.set(fnId, res); return res;
|
|
577
|
+
}
|
|
578
|
+
// functions assigned to a field anywhere: by designated name, by position in a struct's field order, or by assignment
|
|
579
|
+
const fnByField = new Map();
|
|
580
|
+
for (const f of cm.fnFields) {
|
|
581
|
+
const target = funcNamed(f.ref, f) || null;
|
|
582
|
+
if (!target) continue;
|
|
583
|
+
let field = f.field;
|
|
584
|
+
if (!field && f.type != null && f.index != null) { const k = classNamed(f.type, f); field = k ? classes.get(k).fieldOrder[f.index] : null; }
|
|
585
|
+
if (!field) continue;
|
|
586
|
+
(fnByField.get(field) || fnByField.set(field, new Map()).get(field)).set(target, f);
|
|
587
|
+
}
|
|
588
|
+
return { resolveInclude, near, funcNamed, classNamed, typeOf, calleeOf, lookup, mro, overrides, isVirtual, ctorOf, fieldType, factoryReturns, fnByField, subclasses, scopeCls };
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
export function addCEdge(cm, from, to, kind, line, extra = {}) {
|
|
592
|
+
if (!from || !to || from === to) return;
|
|
593
|
+
const e = { from, to, kind, line, ...extra };
|
|
594
|
+
const gf = cm.groupOf(from), gt = cm.groupOf(to);
|
|
595
|
+
if (gf && gt && gf !== gt) e.groups = [gf, gt];
|
|
596
|
+
cm.edges.push(e);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
const MACRO = /^[A-Z][A-Z0-9_]*$/;
|
|
600
|
+
const CFG_GUARD = /\bgetenv\b|\b(cfg|config|conf|settings|opts|options)\s*(->|\.)|\b(debug|verbose)\b/i;
|
|
601
|
+
const NOT_RUN = /^(printf|fprintf|sprintf|snprintf|puts|strcmp|strncmp|strlen|memcpy|memset|free|malloc|calloc|realloc|sizeof|assert|std::\w+)$/;
|
|
602
|
+
|
|
603
|
+
/* resolve every recorded call into edges; `skipCalls`: calls a rule pack has claimed (a dispatch through a table) */
|
|
604
|
+
export function resolveCCalls(cm, R, { skipCalls = new Set() } = {}) {
|
|
605
|
+
const { classes, symbols } = cm;
|
|
606
|
+
const definedNames = new Set([...symbols.values()].map(s => s.name.split('.').pop()));
|
|
607
|
+
const miss = (name, external) => { if (name && MACRO.test(name)) cm.unresolved.builtin++; else if (external) cm.unresolved.external++;
|
|
608
|
+
else if (name && definedNames.has(name)) cm.unresolved.inRepo++; else cm.unresolved.external++; };
|
|
609
|
+
cm.rawCalls.forEach((c, i) => {
|
|
610
|
+
if (skipCalls.has(i)) return;
|
|
611
|
+
const cg = (c.guards || []).filter(g => g.build || CFG_GUARD.test(g.text));
|
|
612
|
+
const extra = { ...(cg.length ? { cfg: cg.map(g => (g.neg ? 'not ' : '') + g.text.replace(/\s+/g, ' ')) } : {}), ...(c.onFailure ? { onFailure: c.onFailure } : {}) };
|
|
613
|
+
const add = (to, kind, more = {}) => addCEdge(cm, c.from, to, kind, c.line, { ...more, ...extra });
|
|
614
|
+
if (c.isNew) {
|
|
615
|
+
const k = R.classNamed(c.type, c);
|
|
616
|
+
if (k) { add(k, 'ctor'); const init = R.ctorOf(k); if (init) add(init, 'ctor'); c.target = k; cm.callStats.resolved++; }
|
|
617
|
+
return; // a value of a library type (std::string s) is no call
|
|
618
|
+
}
|
|
619
|
+
if (!c.attr) {
|
|
620
|
+
const f = c.fn;
|
|
621
|
+
if (f.k === 'id' && MACRO.test(f.name.split('::').pop())) { cm.unresolved.builtin++; cm.callStats.unresolvedName++; return; }
|
|
622
|
+
if (f.k === 'new') return;
|
|
623
|
+
const own = R.scopeCls(c), m = f.k === 'id' && !f.name.includes('::') && own ? R.lookup(own, f.name) : null;
|
|
624
|
+
if (m) { // an implicit this->m(): through the receiver's class, like a self-call
|
|
625
|
+
const site = `${c.from}@${c.line}:${f.name}`;
|
|
626
|
+
add(m, 'static', { self: f.name, site });
|
|
627
|
+
if (R.isVirtual(own, f.name)) for (const o of R.overrides(own, f.name)) add(o, 'virtual', { self: f.name, site, evidence: `override of ${f.name} in subclass` });
|
|
628
|
+
c.target = m; cm.callStats.resolved++; return;
|
|
629
|
+
}
|
|
630
|
+
const t = R.calleeOf(f, c);
|
|
631
|
+
if (t) { add(t, 'static'); c.target = t; cm.callStats.resolved++; return; }
|
|
632
|
+
const k = f.k === 'id' ? R.classNamed(f.name, c) : null; // K(…): a temporary of an in-repo class
|
|
633
|
+
if (k) { add(k, 'ctor'); const init = R.ctorOf(k); if (init) add(init, 'ctor'); c.target = k; cm.callStats.resolved++; return; }
|
|
634
|
+
cm.callStats.unresolvedName++; miss(f.k === 'id' ? f.name.split('::').pop() : null, f.k === 'id' && /^(std|boost)::/.test(f.name));
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
const site = `${c.from}@${c.line}:${c.attr}`;
|
|
638
|
+
const o = R.typeOf(c.obj, c);
|
|
639
|
+
if (o?.cls) {
|
|
640
|
+
const target = R.lookup(o.cls, c.attr);
|
|
641
|
+
if (o.how === 'self') {
|
|
642
|
+
if (target) add(target, 'static', { self: c.attr, site });
|
|
643
|
+
const ov = R.isVirtual(o.cls, c.attr) ? R.overrides(o.cls, c.attr) : [];
|
|
644
|
+
for (const x of ov) add(x, 'virtual', { self: c.attr, site, evidence: `override of ${c.attr} in subclass` });
|
|
645
|
+
if (target || ov.length) { c.target = target; cm.callStats.resolved++; return; }
|
|
646
|
+
} else if (o.narrow?.length > 1) {
|
|
647
|
+
const ts = [...new Set(o.narrow.map(k => R.lookup(k, c.attr)).filter(Boolean))];
|
|
648
|
+
if (ts.length) { for (const t of ts) add(t, ts.length === 1 ? 'static' : 'dispatch', { site, narrowedBy: o.factory }); c.target = ts[0]; cm.callStats.resolved++; return; }
|
|
649
|
+
} else if (o.how === 'new') {
|
|
650
|
+
if (target) { add(target, 'static', { recvClass: o.cls, site }); c.target = target; cm.callStats.resolved++; return; }
|
|
651
|
+
} else {
|
|
652
|
+
const ov = R.isVirtual(o.cls, c.attr) ? R.overrides(o.cls, c.attr) : [];
|
|
653
|
+
if (target) add(target, 'static', { site });
|
|
654
|
+
for (const x of ov) add(x, 'virtual', { site, evidence: `override of ${c.attr} in subclass` });
|
|
655
|
+
if (target || ov.length) { c.target = target; cm.callStats.resolved++; return; }
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
// a call through a function-pointer field: every function assigned to that field
|
|
659
|
+
const held = R.fnByField.get(c.attr);
|
|
660
|
+
if (held?.size && !(o?.cls && R.lookup(o.cls, c.attr))) {
|
|
661
|
+
const ts = [...held.keys()];
|
|
662
|
+
for (const t of ts) add(t, ts.length === 1 ? 'static' : 'dispatch', { site, rule: 'c.fn-pointer', evidence: `${c.file}:${c.line} calls through .${c.attr}, which ${ts.length} function${ts.length > 1 ? 's are' : ' is'} assigned to` });
|
|
663
|
+
c.target = ts[0]; cm.callStats.resolved++; return;
|
|
664
|
+
}
|
|
665
|
+
cm.callStats.unresolvedAttr++;
|
|
666
|
+
miss(null, !!o?.ext || !o);
|
|
667
|
+
});
|
|
668
|
+
// a function named as an argument runs from where it was handed (not a signal handler: that is an entry)
|
|
669
|
+
for (const h of cm.handed) {
|
|
670
|
+
if (h.call != null && skipCalls.has(h.call)) continue;
|
|
671
|
+
if (NOT_RUN.test(h.callee) || MACRO.test(h.callee)) continue;
|
|
672
|
+
const t = R.funcNamed(h.ref, h);
|
|
673
|
+
const s = t && symbols.get(t);
|
|
674
|
+
if (!s || s.kind !== 'function') continue;
|
|
675
|
+
addCEdge(cm, h.from, t, 'static', h.line, { rule: 'c.callback', evidence: `${h.file}:${h.line} hands ${h.ref} to ${h.callee}` });
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/* `main` (not a test file's: that is the test runner, and tests are no entries), and signal handlers */
|
|
680
|
+
export function cEntries(cm, R) {
|
|
681
|
+
const out = cm.mains.filter(m => cm.symbols.get(m).group !== 'test').map(m => ({ kind: 'main', id: m, file: cm.symbols.get(m).file, line: cm.symbols.get(m).start, rule: 'c.main' }));
|
|
682
|
+
for (const s of cm.signals) { const id = R.funcNamed(s.ref, s); if (id) out.push({ kind: 'signal', id, key: s.sig || 'sigaction', file: s.file, line: s.line, rule: 'c.signal' }); }
|
|
683
|
+
return out;
|
|
684
|
+
}
|