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.
Files changed (150) hide show
  1. package/.claude-plugin/plugin.json +12 -0
  2. package/LICENSE +21 -0
  3. package/README.md +152 -2
  4. package/cordis.patch.yml +7 -0
  5. package/lib/index.js +35 -0
  6. package/package.json +56 -4
  7. package/plugin.json +13 -0
  8. package/skills/coursework-author/.synced-from +5 -0
  9. package/skills/coursework-author/SKILL.md +209 -0
  10. package/skills/craft-codeflow-flows/SKILL.md +116 -0
  11. package/skills/craft-codeflow-mine/SKILL.md +207 -0
  12. package/skills/craft-codeflow-plan/SKILL.md +65 -0
  13. package/skills/craft-codeflow-scene/SKILL.md +168 -0
  14. package/skills/craft-video-align/SKILL.md +109 -0
  15. package/skills/craft-video-assemble/SKILL.md +69 -0
  16. package/skills/craft-video-audio/SKILL.md +59 -0
  17. package/skills/craft-video-enrich/SKILL.md +46 -0
  18. package/skills/craft-video-plan/SKILL.md +57 -0
  19. package/skills/craft-video-publish/SKILL.md +79 -0
  20. package/skills/craft-video-render/SKILL.md +68 -0
  21. package/skills/craft-video-scene/SKILL.md +112 -0
  22. package/skills/craft-video-script/SKILL.md +80 -0
  23. package/skills/explainer-video-from-codebase/FLOWS.md +577 -0
  24. package/skills/explainer-video-from-codebase/SKILL.md +659 -0
  25. package/skills/explainer-video-from-codebase/tools/apply-flows.mjs +531 -0
  26. package/skills/explainer-video-from-codebase/tools/approve-flows.mjs +56 -0
  27. package/skills/explainer-video-from-codebase/tools/codemap-md.mjs +116 -0
  28. package/skills/explainer-video-from-codebase/tools/config/dotenv.mjs +13 -0
  29. package/skills/explainer-video-from-codebase/tools/config/json.mjs +14 -0
  30. package/skills/explainer-video-from-codebase/tools/config/redact.mjs +16 -0
  31. package/skills/explainer-video-from-codebase/tools/config/toml-lite.mjs +13 -0
  32. package/skills/explainer-video-from-codebase/tools/config/yaml-lite.mjs +18 -0
  33. package/skills/explainer-video-from-codebase/tools/excerpt-check.mjs +186 -0
  34. package/skills/explainer-video-from-codebase/tools/excerpt.mjs +116 -0
  35. package/skills/explainer-video-from-codebase/tools/flow-diagram.mjs +168 -0
  36. package/skills/explainer-video-from-codebase/tools/flow-plan.mjs +129 -0
  37. package/skills/explainer-video-from-codebase/tools/flows-md.mjs +87 -0
  38. package/skills/explainer-video-from-codebase/tools/lang/c.mjs +684 -0
  39. package/skills/explainer-video-from-codebase/tools/lang/go.mjs +673 -0
  40. package/skills/explainer-video-from-codebase/tools/lang/python-config.mjs +143 -0
  41. package/skills/explainer-video-from-codebase/tools/lang/python-deps.mjs +62 -0
  42. package/skills/explainer-video-from-codebase/tools/lang/python-stdlib.mjs +24 -0
  43. package/skills/explainer-video-from-codebase/tools/lang/python.mjs +609 -0
  44. package/skills/explainer-video-from-codebase/tools/lang/typescript.mjs +963 -0
  45. package/skills/explainer-video-from-codebase/tools/lib/decompose.mjs +102 -0
  46. package/skills/explainer-video-from-codebase/tools/lib/groups.mjs +77 -0
  47. package/skills/explainer-video-from-codebase/tools/lib/observe-coverage.mjs +68 -0
  48. package/skills/explainer-video-from-codebase/tools/lib/observe-v8.mjs +232 -0
  49. package/skills/explainer-video-from-codebase/tools/lib/observe.mjs +162 -0
  50. package/skills/explainer-video-from-codebase/tools/lib/redact.mjs +28 -0
  51. package/skills/explainer-video-from-codebase/tools/lib/sandbox.mjs +97 -0
  52. package/skills/explainer-video-from-codebase/tools/lib/test-links.mjs +28 -0
  53. package/skills/explainer-video-from-codebase/tools/lib/toml.mjs +129 -0
  54. package/skills/explainer-video-from-codebase/tools/lib/trace-c.mjs +95 -0
  55. package/skills/explainer-video-from-codebase/tools/lib/trace-go.mjs +101 -0
  56. package/skills/explainer-video-from-codebase/tools/lib/trace-js.mjs +136 -0
  57. package/skills/explainer-video-from-codebase/tools/lib/tree-sitter.mjs +19 -0
  58. package/skills/explainer-video-from-codebase/tools/lib/walk.mjs +138 -0
  59. package/skills/explainer-video-from-codebase/tools/mine.mjs +409 -0
  60. package/skills/explainer-video-from-codebase/tools/propose.mjs +127 -0
  61. package/skills/explainer-video-from-codebase/tools/rank.mjs +119 -0
  62. package/skills/explainer-video-from-codebase/tools/ranking-md.mjs +76 -0
  63. package/skills/explainer-video-from-codebase/tools/rules/c-handlers.mjs +53 -0
  64. package/skills/explainer-video-from-codebase/tools/rules/celery-taskiq.mjs +48 -0
  65. package/skills/explainer-video-from-codebase/tools/rules/click-typer.mjs +6 -0
  66. package/skills/explainer-video-from-codebase/tools/rules/express.mjs +72 -0
  67. package/skills/explainer-video-from-codebase/tools/rules/fastapi.mjs +99 -0
  68. package/skills/explainer-video-from-codebase/tools/rules/go-http.mjs +173 -0
  69. package/skills/explainer-video-from-codebase/tools/rules/go-test.mjs +108 -0
  70. package/skills/explainer-video-from-codebase/tools/rules/grpc.mjs +57 -0
  71. package/skills/explainer-video-from-codebase/tools/rules/gtest.mjs +97 -0
  72. package/skills/explainer-video-from-codebase/tools/rules/haystack.mjs +55 -0
  73. package/skills/explainer-video-from-codebase/tools/rules/jest-vitest.mjs +100 -0
  74. package/skills/explainer-video-from-codebase/tools/rules/js-http-client.mjs +111 -0
  75. package/skills/explainer-video-from-codebase/tools/rules/nextjs.mjs +53 -0
  76. package/skills/explainer-video-from-codebase/tools/rules/process-spawn.mjs +49 -0
  77. package/skills/explainer-video-from-codebase/tools/rules/py-http-client.mjs +26 -0
  78. package/skills/explainer-video-from-codebase/tools/rules/pytest.mjs +107 -0
  79. package/skills/explainer-video-from-codebase/tools/rules/react-router.mjs +17 -0
  80. package/skills/explainer-video-from-codebase/tools/rules/registry.mjs +31 -0
  81. package/skills/explainer-video-from-codebase/tools/trace/c/fmtrace-sh +14 -0
  82. package/skills/explainer-video-from-codebase/tools/trace/js/fmtrace-net.cjs +33 -0
  83. package/skills/explainer-video-from-codebase/tools/trace/js/fmtrace-srcmap.cjs +41 -0
  84. package/skills/explainer-video-from-codebase/tools/trace/python/fmtrace_plugin.py +87 -0
  85. package/skills/explainer-video-from-codebase/tools/trace/python/sitecustomize.py +330 -0
  86. package/skills/explainer-video-from-codebase/tools/trace/sandbox/linux-bwrap.sh +73 -0
  87. package/skills/explainer-video-from-codebase/tools/trace/sandbox/macos-nonet.sb +41 -0
  88. package/skills/explainer-video-from-codebase/tools/trace.mjs +577 -0
  89. package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/tree-sitter-c.txt +21 -0
  90. package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/tree-sitter-cpp.txt +21 -0
  91. package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/tree-sitter-go.txt +21 -0
  92. package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/tree-sitter-javascript.txt +21 -0
  93. package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/tree-sitter-python.txt +21 -0
  94. package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/tree-sitter-typescript.txt +21 -0
  95. package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/web-tree-sitter.txt +21 -0
  96. package/skills/explainer-video-from-codebase/vendor/tree-sitter/MANIFEST.json +172 -0
  97. package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-c.wasm +0 -0
  98. package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-cpp.wasm +0 -0
  99. package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-go.wasm +0 -0
  100. package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-javascript.wasm +0 -0
  101. package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-python.wasm +0 -0
  102. package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-tsx.wasm +0 -0
  103. package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-typescript.wasm +0 -0
  104. package/skills/explainer-video-from-codebase/vendor/tree-sitter/web-tree-sitter.js +4081 -0
  105. package/skills/explainer-video-from-codebase/vendor/tree-sitter/web-tree-sitter.wasm +0 -0
  106. package/skills/explainer-video-from-coursework/ARCHITECTURE.md +59 -0
  107. package/skills/explainer-video-from-coursework/PIPELINE.md +343 -0
  108. package/skills/explainer-video-from-coursework/ROADMAP.md +725 -0
  109. package/skills/explainer-video-from-coursework/SERIES.md +232 -0
  110. package/skills/explainer-video-from-coursework/SETUP.md +105 -0
  111. package/skills/explainer-video-from-coursework/SKILL.md +858 -0
  112. package/skills/explainer-video-from-coursework/scenes/_pt-chart.js +12 -0
  113. package/skills/explainer-video-from-coursework/scenes/_pt-equation.js +10 -0
  114. package/skills/explainer-video-from-coursework/scenes/_pt-hubspoke.js +7 -0
  115. package/skills/explainer-video-from-coursework/scenes/_pt-loopflow.js +7 -0
  116. package/skills/explainer-video-from-coursework/scenes/_pt-sequence.js +12 -0
  117. package/skills/explainer-video-from-coursework/scenes/_pt-timeline.js +11 -0
  118. package/skills/explainer-video-from-coursework/src/scene-code.html +127 -0
  119. package/skills/explainer-video-from-coursework/src/scene-code.js +379 -0
  120. package/skills/explainer-video-from-coursework/src/scene-core.js +1250 -0
  121. package/skills/explainer-video-from-coursework/src/scene-diagram.html +50 -0
  122. package/skills/explainer-video-from-coursework/src/scene-svg.html +50 -0
  123. package/skills/explainer-video-from-coursework/src/timeline.js +297 -0
  124. package/skills/explainer-video-from-coursework/src/timing-chart.js +160 -0
  125. package/skills/explainer-video-from-coursework/tools/align.mjs +419 -0
  126. package/skills/explainer-video-from-coursework/tools/apply-enrichment.mjs +206 -0
  127. package/skills/explainer-video-from-coursework/tools/assemble.mjs +223 -0
  128. package/skills/explainer-video-from-coursework/tools/build-audio.mjs +234 -0
  129. package/skills/explainer-video-from-coursework/tools/chromium.mjs +73 -0
  130. package/skills/explainer-video-from-coursework/tools/debug-page.mjs +75 -0
  131. package/skills/explainer-video-from-coursework/tools/env.mjs +30 -0
  132. package/skills/explainer-video-from-coursework/tools/frame-dedup.mjs +131 -0
  133. package/skills/explainer-video-from-coursework/tools/key-source.mjs +151 -0
  134. package/skills/explainer-video-from-coursework/tools/model.mjs +21 -0
  135. package/skills/explainer-video-from-coursework/tools/naming.mjs +38 -0
  136. package/skills/explainer-video-from-coursework/tools/onset-audit.mjs +51 -0
  137. package/skills/explainer-video-from-coursework/tools/pacing-check.mjs +350 -0
  138. package/skills/explainer-video-from-coursework/tools/planner.mjs +616 -0
  139. package/skills/explainer-video-from-coursework/tools/publish.mjs +230 -0
  140. package/skills/explainer-video-from-coursework/tools/render.mjs +205 -0
  141. package/skills/explainer-video-from-coursework/tools/resolve-beats.mjs +239 -0
  142. package/skills/explainer-video-from-coursework/tools/script-check.mjs +174 -0
  143. package/skills/explainer-video-from-coursework/tools/stamp.mjs +71 -0
  144. package/skills/explainer-video-from-coursework/tools/timing-report.mjs +299 -0
  145. package/skills/explainer-video-from-coursework/tools/transcribe-gemini.mjs +96 -0
  146. package/skills/explainer-video-from-coursework/tools/tts-gemini.mjs +394 -0
  147. package/skills/explainer-video-from-coursework/tools/verify-audio.mjs +88 -0
  148. package/skills/explainer-video-from-coursework/tools/workdir.mjs +124 -0
  149. package/skills/explainer-video-from-coursework/vendor/plotly-basic.min.js +8 -0
  150. 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
+ }