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,673 @@
|
|
|
1
|
+
/* The Go adapter: facts per .go file, then package, name, method and interface resolution into the same typed edges
|
|
2
|
+
the Python and TypeScript adapters write. Rule packs (../rules/go-http.mjs, grpc.mjs, go-test.mjs) read the same
|
|
3
|
+
facts and add framework links on top.
|
|
4
|
+
|
|
5
|
+
Ids: a module is the file's repo-relative path (`cmd/server/main.go`), so no Go id can meet a Python or TS one; a
|
|
6
|
+
symbol is `<file>:<Func>`, `<file>:<Type>` (where the type is declared) or `<file>:<Type>.<Method>` (where the
|
|
7
|
+
method is declared, which may be another file of the package); a function literal bound to a local variable is
|
|
8
|
+
`<file>:<Func>.<locals>.<name>`.
|
|
9
|
+
|
|
10
|
+
Resolution rules (design rev3 §3.14, Go row), the way graphify reads Go (extractors/go.py, extract.py):
|
|
11
|
+
- packages: every file in a directory with the same `package` clause shares one namespace; an import path names a
|
|
12
|
+
directory through the nearest go.mod's `module` line (`<module>/<dir>`), so an import that maps to no repo
|
|
13
|
+
directory is external (the standard library or a dependency), never matched by a bare name; the local name is the
|
|
14
|
+
alias, else the package's own `package` name, else the path's last element (a `/vN` major version dropped);
|
|
15
|
+
- a bare call to a predeclared function (`append`, `len`, `make`, `new`, …) is the builtin, never a same-named
|
|
16
|
+
method; a call of a type (`T(x)`) is a conversion, not a call;
|
|
17
|
+
- methods by receiver: `x.M()` where x is typed by a parameter or receiver, a `var x T`, `x := T{}` / `&T{}` /
|
|
18
|
+
`new(T)`, `x := f()` (f's declared result type), a type assertion `x.(T)`, a struct field (`s.store.Get()`), an
|
|
19
|
+
element of a typed slice or map (`range`, `xs[i]`); methods promoted from embedded fields are found through them;
|
|
20
|
+
- interfaces by method set: a type whose methods (its own and promoted) include every method of an interface, by
|
|
21
|
+
name with as many parameters and results and no clashing predeclared type (`int` for `string`), implements it; a
|
|
22
|
+
call through an interface-typed value is a `static` edge to the interface method (abstract) plus a `virtual` edge
|
|
23
|
+
to each implementation; when the value came from a function whose returns construct concrete types, it is
|
|
24
|
+
narrowed to those (`dispatch`, with each return's branch guards);
|
|
25
|
+
- a function or method value handed to a call (`sort.Slice(xs, less)`, `go worker()`) runs from there
|
|
26
|
+
(`go.callback`), unless a rule pack claims the call (a route registration hands its handler to the router);
|
|
27
|
+
- a call under an `if` that reads config (`os.Getenv`, `cfg.`, `config.`, flags) carries its guard; a call in a
|
|
28
|
+
`defer` or `go` statement says so. */
|
|
29
|
+
import { posix } from 'node:path';
|
|
30
|
+
|
|
31
|
+
export function newGoModel() {
|
|
32
|
+
return {
|
|
33
|
+
symbols: new Map(), classes: new Map(), modules: new Map(), packages: new Map(),
|
|
34
|
+
rawCalls: [], handed: [], returns: [], mains: [], fnAssigns: new Map(), literals: [], envReads: [], testCalls: [], serviceNames: [],
|
|
35
|
+
edges: [], callStats: { resolved: 0, unresolvedName: 0, unresolvedAttr: 0 },
|
|
36
|
+
unresolved: { inRepo: 0, external: 0, builtin: 0, local: 0 }, externalImports: new Map(), unresolvedImports: new Map(),
|
|
37
|
+
defNodes: 0, defsVisited: 0, defScopes: 0,
|
|
38
|
+
groupOf: null, // id -> group across every language; set by mine.mjs before edges are added
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/* ---------------- plain-data IR: types and expressions, kept after the tree is freed ---------------- */
|
|
43
|
+
|
|
44
|
+
const strText = n => n.type === 'raw_string_literal' ? n.text.slice(1, -1)
|
|
45
|
+
: n.namedChildren.filter(c => c.type === 'interpreted_string_literal_content' || c.type === 'escape_sequence').map(c => c.text).join('');
|
|
46
|
+
export const goStr = n => n && (n.type === 'interpreted_string_literal' || n.type === 'raw_string_literal') ? strText(n) : null;
|
|
47
|
+
|
|
48
|
+
export function typeIr(t) {
|
|
49
|
+
if (!t) return { k: 'other' };
|
|
50
|
+
switch (t.type) {
|
|
51
|
+
case 'type_identifier': return { k: 'name', name: t.text };
|
|
52
|
+
case 'qualified_type': return { k: 'qual', pkg: t.childForFieldName('package').text, name: t.childForFieldName('name').text };
|
|
53
|
+
case 'pointer_type': return { k: 'ptr', of: typeIr(t.namedChildren[0]) };
|
|
54
|
+
case 'slice_type': case 'array_type': return { k: 'slice', of: typeIr(t.childForFieldName('element')) };
|
|
55
|
+
case 'map_type': return { k: 'map', of: typeIr(t.childForFieldName('value')) };
|
|
56
|
+
case 'channel_type': return { k: 'chan', of: typeIr(t.childForFieldName('value')) };
|
|
57
|
+
case 'generic_type': return typeIr(t.childForFieldName('type'));
|
|
58
|
+
case 'parenthesized_type': return typeIr(t.namedChildren[0]);
|
|
59
|
+
case 'negated_type': return typeIr(t.namedChildren[0]);
|
|
60
|
+
default: return { k: 'other' };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export function goIr(n, depth = 0) {
|
|
64
|
+
if (!n || depth > 10) return { k: 'unknown' };
|
|
65
|
+
switch (n.type) {
|
|
66
|
+
case 'identifier': return { k: 'id', name: n.text };
|
|
67
|
+
case 'selector_expression': return { k: 'sel', obj: goIr(n.childForFieldName('operand'), depth + 1), field: n.childForFieldName('field').text };
|
|
68
|
+
case 'call_expression': {
|
|
69
|
+
const f = n.childForFieldName('function'), args = n.childForFieldName('arguments')?.namedChildren || [];
|
|
70
|
+
if (f.type === 'identifier' && f.text === 'new' && args[0]) return { k: 'lit', type: typeIr(args[0]) };
|
|
71
|
+
return { k: 'call', fn: goIr(f, depth + 1), args: args.slice(0, 3).map(a => goIr(a, depth + 1)) };
|
|
72
|
+
}
|
|
73
|
+
case 'composite_literal': return { k: 'lit', type: typeIr(n.childForFieldName('type')) };
|
|
74
|
+
case 'unary_expression': { const op = n.childForFieldName('operator')?.text; return op === '&' || op === '*' ? goIr(n.childForFieldName('operand'), depth + 1) : { k: 'unknown' }; }
|
|
75
|
+
case 'parenthesized_expression': return goIr(n.namedChildren[0], depth + 1);
|
|
76
|
+
case 'index_expression': return { k: 'idx', obj: goIr(n.childForFieldName('operand'), depth + 1) };
|
|
77
|
+
case 'type_assertion_expression': return { k: 'lit', type: typeIr(n.childForFieldName('type')) };
|
|
78
|
+
case 'type_conversion_expression': return { k: 'lit', type: typeIr(n.childForFieldName('type')) };
|
|
79
|
+
case 'interpreted_string_literal': case 'raw_string_literal': return { k: 'str', v: strText(n) };
|
|
80
|
+
case 'binary_expression': return n.childForFieldName('operator')?.text === '+' ? { k: 'cat', l: goIr(n.childForFieldName('left'), depth + 1), r: goIr(n.childForFieldName('right'), depth + 1) } : { k: 'unknown' };
|
|
81
|
+
case 'func_literal': return { k: 'fn' };
|
|
82
|
+
default: return { k: 'unknown' };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/* ---------------- extraction ---------------- */
|
|
87
|
+
|
|
88
|
+
const PREDECLARED = new Set(['append', 'cap', 'clear', 'close', 'complex', 'copy', 'delete', 'imag', 'len', 'make', 'max', 'min', 'new', 'panic',
|
|
89
|
+
'print', 'println', 'real', 'recover']);
|
|
90
|
+
const BASIC = new Set(['bool', 'byte', 'complex64', 'complex128', 'error', 'float32', 'float64', 'int', 'int8', 'int16', 'int32', 'int64', 'rune', 'string',
|
|
91
|
+
'uint', 'uint8', 'uint16', 'uint32', 'uint64', 'uintptr', 'any', 'comparable']);
|
|
92
|
+
// a call that registers a route handler, whose function-literal argument is named for its route (`GET /items/{id}`)
|
|
93
|
+
const ROUTE_REG = /^(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS|Any|Handle|HandleFunc|Get|Post|Put|Patch|Delete|Head|Options|Connect|Trace|Method|MethodFunc|Add|Match)$/;
|
|
94
|
+
const VERB = { GET: 'GET', POST: 'POST', PUT: 'PUT', PATCH: 'PATCH', DELETE: 'DELETE', HEAD: 'HEAD', OPTIONS: 'OPTIONS', Get: 'GET', Post: 'POST', Put: 'PUT',
|
|
95
|
+
Patch: 'PATCH', Delete: 'DELETE', Head: 'HEAD', Options: 'OPTIONS', Connect: 'CONNECT', Trace: 'TRACE' };
|
|
96
|
+
const routeName = (attr, args) => {
|
|
97
|
+
const strs = args.map(goStr);
|
|
98
|
+
if (/^(Method|MethodFunc|Add|Handle)$/.test(attr) && strs[0] && /^[A-Z]+$/.test(strs[0]) && strs[1]?.startsWith('/')) return `${strs[0]} ${strs[1]}`; // chi Method, echo Add, gin Handle
|
|
99
|
+
const p = strs[0];
|
|
100
|
+
if (p == null) return null;
|
|
101
|
+
const m = /^([A-Z]+)\s+(\S+)$/.exec(p); // Go 1.22 ServeMux patterns: "GET /items/{id}"
|
|
102
|
+
if (m) return `${m[1]} ${m[2]}`;
|
|
103
|
+
return p.startsWith('/') || /^[\w.-]+\//.test(p) ? `${VERB[attr] || '*'} ${p}` : null;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export function extractGoFile(go, rel, root, group, text) {
|
|
107
|
+
const module = rel;
|
|
108
|
+
const dir = posix.dirname(rel) === '.' ? '' : posix.dirname(rel);
|
|
109
|
+
const pkgName = root.namedChildren.find(c => c.type === 'package_clause')?.namedChildren[0]?.text || 'main';
|
|
110
|
+
const M = { file: rel, group, dir, pkg: pkgName, imports: [], dotImports: [], consts: new Map(), lang: 'go' };
|
|
111
|
+
go.modules.set(module, M);
|
|
112
|
+
const pkgKey = `${dir}\0${pkgName}`;
|
|
113
|
+
const P = go.packages.get(pkgKey) || go.packages.set(pkgKey, { key: pkgKey, dir, name: pkgName, files: [], funcs: new Map(), types: new Map(), vars: new Map(), consts: new Map(), aliases: new Map() }).get(pkgKey);
|
|
114
|
+
P.files.push(rel);
|
|
115
|
+
const lines = text.split('\n');
|
|
116
|
+
const spanText = (a, b) => lines.slice(a - 1, b).join('\n');
|
|
117
|
+
const MOD = `${module}:<module>`;
|
|
118
|
+
const quals = new Set();
|
|
119
|
+
const uniqueId = q => { let id = `${module}:${q}`, i = 2; while (go.symbols.has(id)) id = `${module}:${q}#${i++}`; return id; };
|
|
120
|
+
|
|
121
|
+
// definitions the self-check expects: functions, methods, named types, a named interface's methods (an anonymous
|
|
122
|
+
// one, `x.(interface{ M() })`, defines nothing)
|
|
123
|
+
const counted = root.descendantsOfType(['function_declaration', 'method_declaration', 'type_spec', 'type_alias', 'method_elem'])
|
|
124
|
+
.filter(n => n.type !== 'method_elem' || (n.parent?.type === 'interface_type' && n.parent.parent?.type === 'type_spec'));
|
|
125
|
+
go.defNodes += counted.length;
|
|
126
|
+
|
|
127
|
+
for (const spec of root.descendantsOfType('import_spec')) {
|
|
128
|
+
const path = goStr(spec.childForFieldName('path')); if (path == null) continue;
|
|
129
|
+
const nameN = spec.childForFieldName('name');
|
|
130
|
+
const imp = { path, alias: nameN?.type === 'package_identifier' ? nameN.text : null, dot: nameN?.type === 'dot', blank: nameN?.type === 'blank_identifier', line: spec.startPosition.row + 1 };
|
|
131
|
+
if (imp.dot) M.dotImports.push(imp); else if (!imp.blank) M.imports.push(imp);
|
|
132
|
+
if (!go.externalImports.has(path)) go.externalImports.set(path, rel);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const params = list => (list?.namedChildren || []).flatMap(p => {
|
|
136
|
+
if (p.type !== 'parameter_declaration' && p.type !== 'variadic_parameter_declaration') return [];
|
|
137
|
+
const t = typeIr(p.childForFieldName('type'));
|
|
138
|
+
const names = p.namedChildren.filter(c => c.type === 'identifier');
|
|
139
|
+
return names.length ? names.map(n => ({ name: n.text, type: p.type === 'variadic_parameter_declaration' ? { k: 'slice', of: t } : t })) : [{ name: null, type: t }];
|
|
140
|
+
});
|
|
141
|
+
const resultsOf = node => { const r = node.childForFieldName('result'); if (!r) return [];
|
|
142
|
+
return r.type === 'parameter_list' ? params(r).map(p => p.type) : [typeIr(r)]; };
|
|
143
|
+
|
|
144
|
+
function defineFn(node, name, ctx, { kind = 'function', cls = null, recvType = null, recv = null, counts = true, bind = true } = {}) {
|
|
145
|
+
if (counts) go.defsVisited++;
|
|
146
|
+
const q = ctx.fn ? `${ctx.qual}.<locals>.${name}` : recvType ? `${recvType}.${name}` : name;
|
|
147
|
+
const id = uniqueId(q);
|
|
148
|
+
quals.add(q);
|
|
149
|
+
const start = node.startPosition.row + 1, end = node.endPosition.row + 1;
|
|
150
|
+
const ps = params(node.childForFieldName('parameters'));
|
|
151
|
+
const sym = { id, group, kind, file: rel, start, end, name: recvType && !ctx.fn ? `${recvType}.${name}` : name, module, lang: 'go', cls, decos: [], params: ps.map(p => p.name).filter(Boolean),
|
|
152
|
+
paramTypes: ps.map(p => p.type), results: resultsOf(node), pkg: pkgKey, text: spanText(start, end), ...(recvType && !ctx.fn ? { recvType } : {}) };
|
|
153
|
+
go.symbols.set(id, sym);
|
|
154
|
+
if (!ctx.fn && !recvType && bind) { if (!P.funcs.has(name) || name !== 'init') P.funcs.set(name, id); }
|
|
155
|
+
else if (ctx.fn && bind) ctx.locals.set(name, id);
|
|
156
|
+
const inner = { fn: id, qual: q, locals: new Map(ctx.locals), types: new Map(ctx.types), consts: new Map(ctx.consts), guards: [], onFailure: null, range: null, closure: null, litCount: ctx.litCount };
|
|
157
|
+
if (recv?.name) inner.types.set(recv.name, { t: 'annot', type: recv.type });
|
|
158
|
+
ps.forEach((p, i) => { if (p.name) inner.types.set(p.name, { t: 'annot', type: p.type, ...(ctx.closureOf != null ? { closureOf: ctx.closureOf, argIndex: ctx.closureArg, param: i } : {}) }); });
|
|
159
|
+
const body = node.childForFieldName('body');
|
|
160
|
+
if (body) visit(body, inner);
|
|
161
|
+
return id;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function defineType(spec) {
|
|
165
|
+
go.defsVisited++;
|
|
166
|
+
const name = spec.childForFieldName('name').text, start = spec.startPosition.row + 1, end = spec.endPosition.row + 1;
|
|
167
|
+
if (spec.type === 'type_alias') { P.aliases.set(name, typeIr(spec.childForFieldName('type'))); return; }
|
|
168
|
+
const id = uniqueId(name); quals.add(name);
|
|
169
|
+
const tn = spec.childForFieldName('type');
|
|
170
|
+
const k = { id, module, name, pkg: pkgKey, file: rel, line: start, iface: tn?.type === 'interface_type', fields: new Map(), embeds: [], methods: new Map(), baseExprs: [], decos: [] };
|
|
171
|
+
go.classes.set(id, k);
|
|
172
|
+
go.symbols.set(id, { id, group, kind: 'class', file: rel, start, end, name, module, lang: 'go', pkg: pkgKey, decos: [], text: spanText(start, end) });
|
|
173
|
+
P.types.set(name, id);
|
|
174
|
+
if (tn?.type === 'struct_type') for (const f of tn.descendantsOfType('field_declaration')) {
|
|
175
|
+
if (f.parent?.parent?.id !== tn.id) continue; // a nested anonymous struct's fields are not this type's
|
|
176
|
+
const t = typeIr(f.childForFieldName('type')), names = f.namedChildren.filter(c => c.type === 'field_identifier');
|
|
177
|
+
if (names.length) for (const n of names) k.fields.set(n.text, t);
|
|
178
|
+
else { k.embeds.push(t); const base = t.k === 'ptr' ? t.of : t; if (base.name) k.fields.set(base.name, t); k.baseExprs.push(typeText(t)); }
|
|
179
|
+
}
|
|
180
|
+
if (tn?.type === 'interface_type') for (const m of tn.namedChildren) {
|
|
181
|
+
if (m.type === 'method_elem') {
|
|
182
|
+
go.defsVisited++;
|
|
183
|
+
const mn = m.childForFieldName('name').text, mid = uniqueId(`${name}.${mn}`); quals.add(`${name}.${mn}`);
|
|
184
|
+
const ms = m.startPosition.row + 1, me = m.endPosition.row + 1, ps = params(m.childForFieldName('parameters'));
|
|
185
|
+
go.symbols.set(mid, { id: mid, group, kind: 'method', file: rel, start: ms, end: me, name: `${name}.${mn}`, module, lang: 'go', cls: id, decos: ['abstractmethod'],
|
|
186
|
+
params: ps.map(p => p.name).filter(Boolean), paramTypes: ps.map(p => p.type), results: resultsOf(m), pkg: pkgKey, text: spanText(ms, me) });
|
|
187
|
+
k.methods.set(mn, mid);
|
|
188
|
+
} else if (m.type === 'type_elem' && !m.children.some(c => c.type === '|')) {
|
|
189
|
+
for (const c of m.namedChildren) { const t = typeIr(c); if (t.k === 'name' || t.k === 'qual') { k.embeds.push(t); k.baseExprs.push(typeText(t)); } }
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function valueSpecs(names, values, typeN, ctx, line) {
|
|
195
|
+
const declared = typeN ? typeIr(typeN) : null;
|
|
196
|
+
names.forEach((n, i) => {
|
|
197
|
+
const v = values[values.length === 1 && names.length > 1 ? 0 : i];
|
|
198
|
+
const spec = declared ? { t: 'annot', type: declared } : v ? { t: 'expr', ir: goIr(v), index: values.length === 1 && names.length > 1 ? i : 0 } : null;
|
|
199
|
+
if (!spec || n === '_') return;
|
|
200
|
+
if (ctx.fn) { ctx.types.set(n, spec); if (spec.t === 'expr') (go.fnAssigns.get(ctx.fn) || go.fnAssigns.set(ctx.fn, new Map()).get(ctx.fn)).set(n, { ...spec, guards: ctx.guards, module, types: ctx.types }); }
|
|
201
|
+
else P.vars.set(n, { ...spec, module, line });
|
|
202
|
+
const sv = v && goIr(v);
|
|
203
|
+
if (sv && (sv.k === 'str' || sv.k === 'cat' || sv.k === 'id')) (ctx.fn ? ctx.consts : P.consts).set(n, sv);
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function noteReturn(exprs, ctx) {
|
|
208
|
+
if (!ctx.fn || !exprs.length) return;
|
|
209
|
+
const ir = goIr(exprs[0]), line = exprs[0].startPosition.row + 1;
|
|
210
|
+
if (ir.k === 'lit' || ir.k === 'call' || ir.k === 'id') go.returns.push({ fn: ctx.fn, ir, guards: ctx.guards, line, module, types: ctx.types });
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const cond = n => n.childForFieldName('condition')?.text || '';
|
|
214
|
+
function visitBlock(node, ctx) { // `if err != nil { return }`: an `if` whose body always exits guards what follows
|
|
215
|
+
let guards = ctx.guards;
|
|
216
|
+
const stmts = node.type === 'block' ? (node.namedChildren.find(c => c.type === 'statement_list')?.namedChildren || node.namedChildren) : node.namedChildren;
|
|
217
|
+
for (const c of stmts) {
|
|
218
|
+
visit(c, { ...ctx, guards });
|
|
219
|
+
if (c.type === 'if_statement' && !c.childForFieldName('alternative')) {
|
|
220
|
+
const body = c.childForFieldName('consequence'), last = body?.namedChildren.find(x => x.type === 'statement_list')?.namedChildren.at(-1);
|
|
221
|
+
if (last && /^(return_statement|goto_statement)$/.test(last.type) || (last?.type === 'expression_statement' && /^(panic|log\.Fatal\w*|os\.Exit)\(/.test(last.text)))
|
|
222
|
+
guards = [...guards, { text: cond(c), neg: true }];
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const argInfo = a => ({ type: a.type, ir: goIr(a), text: a.text.length > 200 ? a.text.slice(0, 200) : a.text });
|
|
228
|
+
function call(node, ctx, how) {
|
|
229
|
+
const fnNode = node.childForFieldName('function');
|
|
230
|
+
const args = (node.childForFieldName('arguments')?.namedChildren || []).filter(a => a.type !== 'comment');
|
|
231
|
+
const from = ctx.fn || MOD, line = node.startPosition.row + 1;
|
|
232
|
+
if (fnNode.type === 'func_literal') { // `func() { … }()`: its body runs here
|
|
233
|
+
visitFnLit(fnNode, ctx); args.forEach(a => visit(a, ctx)); return;
|
|
234
|
+
}
|
|
235
|
+
if (fnNode.type === 'parenthesized_expression') { args.forEach(a => visit(a, ctx)); return; } // `(*T)(nil)`: a conversion
|
|
236
|
+
if (fnNode.type === 'identifier' && fnNode.text === 'new' && args[0]) { const t = typeIr(args[0]); // new(T) constructs a T
|
|
237
|
+
if (t.k === 'name' || t.k === 'qual') go.literals.push({ from: ctx.fn || MOD, module, file: rel, line: node.startPosition.row + 1, type: t, guards: ctx.guards }); }
|
|
238
|
+
const attr = fnNode.type === 'selector_expression' ? fnNode.childForFieldName('field').text : null;
|
|
239
|
+
const objN = attr ? fnNode.childForFieldName('operand') : null;
|
|
240
|
+
const expr = fnNode.text;
|
|
241
|
+
// os.Getenv("X") / os.LookupEnv("X"), and flag.String("name", "default", …)
|
|
242
|
+
if (/^(os|syscall)\.(Getenv|LookupEnv)$/.test(expr) && goStr(args[0]) != null)
|
|
243
|
+
go.envReads.push({ key: goStr(args[0]), file: rel, line, group, via: expr });
|
|
244
|
+
const fl = /^flag\.(String|Int|Int64|Uint|Uint64|Bool|Duration|Float64)(Var)?$/.exec(expr);
|
|
245
|
+
if (fl) { const k = goStr(args[fl[2] ? 1 : 0]), d = args[fl[2] ? 2 : 1]; if (k != null) go.envReads.push({ key: `-${k}`, file: rel, line, group, via: 'flag', ...(d && d.type !== 'identifier' ? { default: goStr(d) ?? d.text } : {}) }); }
|
|
246
|
+
// the call's record goes in first, so the calls inside its function-literal arguments come after it
|
|
247
|
+
const index = go.rawCalls.length;
|
|
248
|
+
const name = attr && ROUTE_REG.test(attr) ? routeName(attr, args) : null;
|
|
249
|
+
const info = args.map(argInfo);
|
|
250
|
+
go.rawCalls.push({ from, module, file: rel, expr, line, attr, obj: objN ? goIr(objN) : null, fn: attr ? null : goIr(fnNode), how,
|
|
251
|
+
types: ctx.types, locals: ctx.locals, consts: ctx.consts, guards: ctx.guards, top: !ctx.fn, args: info, range: ctx.range, closureOf: ctx.closure,
|
|
252
|
+
...(ctx.argOf != null ? { argOf: ctx.argOf } : {}) });
|
|
253
|
+
args.forEach((a, i) => {
|
|
254
|
+
if (a.type !== 'func_literal' || !name) return;
|
|
255
|
+
info[i].fnId = defineFn(a, name, { ...ctx, closureOf: index, closureArg: i }, { counts: false, bind: false });
|
|
256
|
+
go.handed.push({ from, to: info[i].fnId, line, file: rel, how: expr, call: index });
|
|
257
|
+
});
|
|
258
|
+
for (const [i, a] of args.entries()) if (a.type === 'identifier' || a.type === 'selector_expression')
|
|
259
|
+
go.handed.push({ from, ir: goIr(a), name: a.text, line, file: rel, callee: expr, argIndex: i, call: index, module, types: ctx.types, locals: ctx.locals });
|
|
260
|
+
if (objN && objN.type !== 'identifier') visit(objN, ctx);
|
|
261
|
+
args.forEach((a, i) => { if (a.type === 'func_literal' && !info[i].fnId) visitFnLit(a, { ...ctx, closure: index, closureArg: i, argOf: null });
|
|
262
|
+
else if (a.type !== 'func_literal') visit(a, { ...ctx, argOf: index }); });
|
|
263
|
+
}
|
|
264
|
+
// a function literal's body runs as part of the enclosing function; its parameters are typed in its own scope
|
|
265
|
+
function visitFnLit(fl, ctx) {
|
|
266
|
+
const inner = { ...ctx, types: new Map(ctx.types), locals: new Map(ctx.locals), consts: new Map(ctx.consts), guards: ctx.guards };
|
|
267
|
+
params(fl.childForFieldName('parameters')).forEach((p, i) => { if (p.name) inner.types.set(p.name, { t: 'annot', type: p.type, ...(ctx.closure != null ? { closureOf: ctx.closure, argIndex: ctx.closureArg, param: i } : {}) }); });
|
|
268
|
+
const body = fl.childForFieldName('body'); if (body) visit(body, inner);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function visit(node, ctx) {
|
|
272
|
+
switch (node.type) {
|
|
273
|
+
case 'comment': return;
|
|
274
|
+
case 'block': visitBlock(node, ctx); return;
|
|
275
|
+
case 'function_declaration': {
|
|
276
|
+
const name = node.childForFieldName('name').text;
|
|
277
|
+
const id = defineFn(node, name, ctx);
|
|
278
|
+
if (name === 'main' && pkgName === 'main') go.mains.push(id);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
case 'method_declaration': {
|
|
282
|
+
const rp = params(node.childForFieldName('receiver'))[0];
|
|
283
|
+
const base = rp?.type?.k === 'ptr' ? rp.type.of : rp?.type;
|
|
284
|
+
defineFn(node, node.childForFieldName('name').text, ctx, { kind: 'method', recvType: base?.name || '?', recv: rp });
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
case 'type_declaration': for (const s of node.namedChildren) if (s.type === 'type_spec' || s.type === 'type_alias') defineType(s); return;
|
|
288
|
+
case 'var_declaration': case 'const_declaration':
|
|
289
|
+
for (const s of node.descendantsOfType(['var_spec', 'const_spec'])) {
|
|
290
|
+
const names = s.childrenForFieldName('name').map(c => c.text);
|
|
291
|
+
const vals = s.childForFieldName('value')?.namedChildren || [];
|
|
292
|
+
valueSpecs(names, vals, s.childForFieldName('type'), ctx, s.startPosition.row + 1);
|
|
293
|
+
for (const v of vals) visit(v, ctx);
|
|
294
|
+
}
|
|
295
|
+
return;
|
|
296
|
+
case 'short_var_declaration': {
|
|
297
|
+
const names = node.childForFieldName('left').namedChildren.map(c => c.text), vals = node.childForFieldName('right').namedChildren;
|
|
298
|
+
if (ctx.fn && names.length === 1 && vals[0]?.type === 'func_literal') { defineFn(vals[0], names[0], ctx, { counts: false }); return; }
|
|
299
|
+
valueSpecs(names, vals, null, ctx, node.startPosition.row + 1);
|
|
300
|
+
for (const v of vals) visit(v, ctx);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
case 'assignment_statement': {
|
|
304
|
+
const l = node.childForFieldName('left').namedChildren, r = node.childForFieldName('right').namedChildren;
|
|
305
|
+
if (ctx.fn) l.forEach((x, i) => { if (x.type === 'identifier' && r[i]) { const spec = { t: 'expr', ir: goIr(r[i]), index: 0 };
|
|
306
|
+
if (!ctx.types.has(x.text) || ctx.types.get(x.text).t === 'expr') ctx.types.set(x.text, spec); } });
|
|
307
|
+
for (const x of [...l, ...r]) visit(x, ctx);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
case 'for_statement': {
|
|
311
|
+
const rc = node.namedChildren.find(c => c.type === 'range_clause');
|
|
312
|
+
let inner = ctx;
|
|
313
|
+
if (rc) {
|
|
314
|
+
const right = rc.childForFieldName('right'), left = rc.childForFieldName('left')?.namedChildren || [];
|
|
315
|
+
inner = { ...ctx, types: new Map(ctx.types) };
|
|
316
|
+
if (left[1]?.type === 'identifier') inner.types.set(left[1].text, { t: 'range', ir: goIr(right) });
|
|
317
|
+
inner.range = { over: right.text, line: rc.startPosition.row + 1, cases: rangeCases(right, ctx) };
|
|
318
|
+
visit(right, ctx);
|
|
319
|
+
}
|
|
320
|
+
for (const c of node.namedChildren) if (c.type !== 'range_clause') visit(c, inner);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
case 'if_statement': {
|
|
324
|
+
const init = node.childForFieldName('initializer');
|
|
325
|
+
const sub = { ...ctx, types: new Map(ctx.types) };
|
|
326
|
+
if (init) visit(init, sub);
|
|
327
|
+
const c = cond(node);
|
|
328
|
+
visit(node.childForFieldName('condition'), sub);
|
|
329
|
+
const g0 = ctx.guards || [];
|
|
330
|
+
visit(node.childForFieldName('consequence'), { ...sub, guards: [...g0, { text: c, neg: false }] });
|
|
331
|
+
const alt = node.childForFieldName('alternative');
|
|
332
|
+
if (alt) visit(alt, { ...sub, guards: [...g0, { text: c, neg: true }] });
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
case 'go_statement': case 'defer_statement': {
|
|
336
|
+
const ce = node.namedChildren.find(c => c.type === 'call_expression');
|
|
337
|
+
if (ce) { call(ce, ctx, node.type === 'go_statement' ? 'go' : 'defer'); return; }
|
|
338
|
+
break;
|
|
339
|
+
}
|
|
340
|
+
case 'return_statement': noteReturn(node.namedChildren.find(c => c.type === 'expression_list')?.namedChildren || node.namedChildren, ctx); break;
|
|
341
|
+
case 'call_expression': call(node, ctx, null); return;
|
|
342
|
+
case 'func_literal': visitFnLit(node, ctx); return;
|
|
343
|
+
case 'composite_literal': {
|
|
344
|
+
const t = typeIr(node.childForFieldName('type'));
|
|
345
|
+
if (t.k === 'name' || t.k === 'qual') go.literals.push({ from: ctx.fn || MOD, module, file: rel, line: node.startPosition.row + 1, type: t, guards: ctx.guards });
|
|
346
|
+
// a gRPC service descriptor names the service it registers: grpc.ServiceDesc{ServiceName: "pkg.Svc", HandlerType: (*SvcServer)(nil)}
|
|
347
|
+
if (t.k === 'qual' && t.name === 'ServiceDesc') {
|
|
348
|
+
const kv = node.childForFieldName('body')?.namedChildren.filter(e => e.type === 'keyed_element') || [];
|
|
349
|
+
const get = k => kv.find(e => e.namedChildren[0]?.text === k)?.namedChildren[1];
|
|
350
|
+
const sn = get('ServiceName'), ht = get('HandlerType');
|
|
351
|
+
const s = sn && goStr(sn.namedChildren[0] || sn);
|
|
352
|
+
const iface = ht?.text.match(/\(\s*\*\s*([\w.]+)\s*\)\s*\(\s*nil\s*\)/)?.[1];
|
|
353
|
+
if (s) go.serviceNames.push({ name: s, iface: iface?.split('.').pop() || null, file: rel, line: node.startPosition.row + 1 });
|
|
354
|
+
}
|
|
355
|
+
break;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
for (const c of node.namedChildren) visit(c, ctx);
|
|
359
|
+
}
|
|
360
|
+
// `for _, tc := range cases` where `cases` (or the range expression itself) is a composite literal: its element count
|
|
361
|
+
function rangeCases(right, ctx) {
|
|
362
|
+
const lit = right.type === 'composite_literal' ? right : null;
|
|
363
|
+
const count = l => l.childForFieldName('body')?.namedChildren.filter(e => e.type === 'literal_element' || e.type === 'keyed_element').length ?? null;
|
|
364
|
+
if (lit) return count(lit);
|
|
365
|
+
if (right.type === 'identifier') return ctx.litCount?.get(right.text) ?? null;
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// the composite literals a function binds to names, for table tests: `cases := []struct{…}{{…}, {…}}`
|
|
370
|
+
const litCounts = fn => { const m = new Map(); for (const d of fn.descendantsOfType('short_var_declaration')) {
|
|
371
|
+
const l = d.childForFieldName('left').namedChildren, r = d.childForFieldName('right').namedChildren;
|
|
372
|
+
if (l.length === 1 && r[0]?.type === 'composite_literal') m.set(l[0].text, r[0].childForFieldName('body')?.namedChildren.filter(e => e.type === 'literal_element' || e.type === 'keyed_element').length ?? 0); }
|
|
373
|
+
return m; };
|
|
374
|
+
const top = { fn: null, qual: null, locals: new Map(), types: new Map(), consts: P.consts, guards: [], onFailure: null, range: null, closure: null };
|
|
375
|
+
for (const c of root.namedChildren) {
|
|
376
|
+
if (c.type === 'function_declaration' || c.type === 'method_declaration') visit(c, { ...top, litCount: litCounts(c) });
|
|
377
|
+
else visit(c, top);
|
|
378
|
+
}
|
|
379
|
+
go.defScopes += quals.size;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/* ---------------- packages, imports and types ---------------- */
|
|
383
|
+
|
|
384
|
+
const typeText = t => t.k === 'name' ? t.name : t.k === 'qual' ? `${t.pkg}.${t.name}` : t.k === 'ptr' ? `*${typeText(t.of)}` : t.k === 'slice' ? `[]${typeText(t.of)}` : t.k === 'map' ? `map[…]${typeText(t.of)}` : '?';
|
|
385
|
+
|
|
386
|
+
// go.mod files -> { dir, module }; an import path -> the repo directory it names (the longest module prefix wins)
|
|
387
|
+
export function goModules(goMods) {
|
|
388
|
+
return goMods.map(({ rel, text }) => ({ dir: posix.dirname(rel) === '.' ? '' : posix.dirname(rel), module: /^\s*module\s+(\S+)/m.exec(text)?.[1] }))
|
|
389
|
+
.filter(m => m.module).sort((a, b) => b.module.length - a.module.length);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
export function goResolverFor(go, mods) {
|
|
393
|
+
const { classes, symbols, packages } = go;
|
|
394
|
+
const pkgsByDir = new Map(); for (const P of packages.values()) (pkgsByDir.get(P.dir) || pkgsByDir.set(P.dir, []).get(P.dir)).push(P);
|
|
395
|
+
const dirOfImport = path => { for (const m of mods) if (path === m.module || path.startsWith(m.module + '/')) {
|
|
396
|
+
const sub = path.slice(m.module.length + 1), d = [m.dir, sub].filter(Boolean).join('/'); return pkgsByDir.has(d) ? d : null; } return null; };
|
|
397
|
+
// the package an import path names: the directory's non-test package
|
|
398
|
+
const pkgOfImport = path => { const d = dirOfImport(path); if (d == null) return null; const ps = pkgsByDir.get(d);
|
|
399
|
+
return ps.find(p => !p.name.endsWith('_test')) || ps[0]; };
|
|
400
|
+
const lastElem = path => { const parts = path.split('/'); let l = parts.at(-1); if (/^v\d+$/.test(l) && parts.length > 1) l = parts.at(-2); return l.replace(/^go-/, '').replace(/\.v\d+$/, '').replace(/[^\w]/g, '_'); };
|
|
401
|
+
// a module's imports by local name: { pkg } in the repo or { ext: path }
|
|
402
|
+
const importsMemo = new Map();
|
|
403
|
+
function importsOf(module) {
|
|
404
|
+
if (importsMemo.has(module)) return importsMemo.get(module);
|
|
405
|
+
const M = go.modules.get(module), out = new Map();
|
|
406
|
+
for (const imp of M?.imports || []) { const P = pkgOfImport(imp.path); out.set(imp.alias || P?.name || lastElem(imp.path), P ? { pkg: P, path: imp.path } : { ext: imp.path }); }
|
|
407
|
+
importsMemo.set(module, out);
|
|
408
|
+
return out;
|
|
409
|
+
}
|
|
410
|
+
const pkgOf = module => packages.get(go.modules.get(module) && `${go.modules.get(module).dir}\0${go.modules.get(module).pkg}`);
|
|
411
|
+
// the same package's namespace, plus a dot import's
|
|
412
|
+
const scopes = module => { const M = go.modules.get(module), own = pkgOf(module); return [own, ...(M?.dotImports || []).map(i => pkgOfImport(i.path)).filter(Boolean)].filter(Boolean); };
|
|
413
|
+
|
|
414
|
+
// a type IR, seen from a module, to a class id ({ cls }) or an external type ({ ext }) or null
|
|
415
|
+
function classOf(t, module, depth = 0) {
|
|
416
|
+
if (!t || depth > 6) return null;
|
|
417
|
+
if (t.k === 'ptr') return classOf(t.of, module, depth + 1);
|
|
418
|
+
if (t.k === 'name') {
|
|
419
|
+
if (BASIC.has(t.name)) return null;
|
|
420
|
+
for (const P of scopes(module)) { if (P.types.has(t.name)) return { cls: P.types.get(t.name) }; if (P.aliases.has(t.name)) return classOf(P.aliases.get(t.name), P.files[0], depth + 1); }
|
|
421
|
+
return null;
|
|
422
|
+
}
|
|
423
|
+
if (t.k === 'qual') {
|
|
424
|
+
const imp = importsOf(module).get(t.pkg);
|
|
425
|
+
if (!imp) return null;
|
|
426
|
+
if (imp.ext) return { ext: `${imp.ext}.${t.name}` };
|
|
427
|
+
if (imp.pkg.types.has(t.name)) return { cls: imp.pkg.types.get(t.name) };
|
|
428
|
+
if (imp.pkg.aliases.has(t.name)) return classOf(imp.pkg.aliases.get(t.name), imp.pkg.files[0], depth + 1);
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
return null;
|
|
432
|
+
}
|
|
433
|
+
for (const k of classes.values()) {
|
|
434
|
+
k.embedIds = k.embeds.map(t => classOf(t, k.module)?.cls).filter(Boolean);
|
|
435
|
+
}
|
|
436
|
+
// methods declared on a type in any file of its package
|
|
437
|
+
for (const s of symbols.values()) if (s.recvType) {
|
|
438
|
+
const P = packages.get(s.pkg), cls = P?.types.get(s.recvType);
|
|
439
|
+
if (cls) { s.cls = cls; classes.get(cls).methods.set(s.name.split('.').pop(), s.id); }
|
|
440
|
+
}
|
|
441
|
+
const embedChain = (id, seen = new Set()) => seen.has(id) ? [] : (seen.add(id), [id, ...(classes.get(id)?.embedIds || []).flatMap(b => embedChain(b, seen))]);
|
|
442
|
+
function lookup(clsId, m) { for (const c of embedChain(clsId)) { const x = classes.get(c)?.methods.get(m); if (x) return x; } return null; }
|
|
443
|
+
function fieldType(clsId, f) { for (const c of embedChain(clsId)) { const k = classes.get(c); if (k?.fields.has(f)) return { t: k.fields.get(f), module: k.module }; } return null; }
|
|
444
|
+
const methodSet = id => { const out = new Map(); for (const c of embedChain(id)) for (const [n, mid] of classes.get(c)?.methods || []) if (!out.has(n)) out.set(n, mid); return out; };
|
|
445
|
+
// two parameter or result types that cannot be one type: different predeclared types, or differently built ones
|
|
446
|
+
const comp = t => t?.k === 'ptr' || t?.k === 'slice' || t?.k === 'map' || t?.k === 'chan', basic = t => t?.k === 'name' && BASIC.has(t.name);
|
|
447
|
+
const canon = n => n === 'byte' ? 'uint8' : n === 'rune' ? 'int32' : n;
|
|
448
|
+
const clash = (a, b) => basic(a) && basic(b) ? canon(a.name) !== canon(b.name) : comp(a) && comp(b) ? a.k !== b.k || clash(a.of, b.of) : (basic(a) && comp(b)) || (comp(a) && basic(b));
|
|
449
|
+
const fits = (impl, want) => { const a = symbols.get(impl), b = symbols.get(want), ap = a?.paramTypes || [], bp = b?.paramTypes || [], ar = a?.results || [], br = b?.results || [];
|
|
450
|
+
return ap.length === bp.length && ar.length === br.length && ap.every((t, i) => !clash(t, bp[i])) && ar.every((t, i) => !clash(t, br[i])); };
|
|
451
|
+
// implicit interface satisfaction, by method set: every concrete type that has each of the interface's methods, by
|
|
452
|
+
// name with as many parameters and results, none of them a clashing type
|
|
453
|
+
const ifaces = [...classes.values()].filter(k => k.iface).map(k => ({ id: k.id, methods: methodSet(k.id) })).filter(i => i.methods.size);
|
|
454
|
+
for (const k of classes.values()) if (!k.iface) {
|
|
455
|
+
const have = methodSet(k.id);
|
|
456
|
+
k.implementsIds = ifaces.filter(i => [...i.methods].every(([n, mid]) => have.has(n) && fits(have.get(n), mid))).map(i => i.id);
|
|
457
|
+
k.bases = [...k.embedIds, ...k.implementsIds];
|
|
458
|
+
} else { k.implementsIds = []; k.bases = [...k.embedIds]; }
|
|
459
|
+
const implementers = new Map(); for (const k of classes.values()) for (const i of k.implementsIds) (implementers.get(i) || implementers.set(i, []).get(i)).push(k.id);
|
|
460
|
+
const isIface = id => !!classes.get(id)?.iface;
|
|
461
|
+
|
|
462
|
+
// what a name means in a scope ({ module, types, locals }): { local: spec } | { id } | { var } | { pkg } | { ext } | { type } | null
|
|
463
|
+
function resolveName(name, sc) {
|
|
464
|
+
if (sc.types?.has(name)) return { local: sc.types.get(name) };
|
|
465
|
+
if (sc.locals?.has(name)) return { id: sc.locals.get(name) };
|
|
466
|
+
for (const P of scopes(sc.module)) {
|
|
467
|
+
if (P.funcs.has(name)) return { id: P.funcs.get(name) };
|
|
468
|
+
if (P.types.has(name)) return { type: P.types.get(name) };
|
|
469
|
+
if (P.vars.has(name)) return { var: P.vars.get(name) };
|
|
470
|
+
if (P.aliases.has(name)) return { type: classOf({ k: 'name', name }, sc.module)?.cls || null };
|
|
471
|
+
}
|
|
472
|
+
const imp = importsOf(sc.module).get(name);
|
|
473
|
+
if (imp) return imp.pkg ? { pkg: imp.pkg, path: imp.path } : { ext: imp.ext };
|
|
474
|
+
return null;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// the type IR of an expression IR, with the module that IR is read in; `narrow` when it came from a factory
|
|
478
|
+
function typeOf(ir, sc, depth = 0) {
|
|
479
|
+
if (!ir || depth > 8) return null;
|
|
480
|
+
switch (ir.k) {
|
|
481
|
+
case 'lit': return { t: ir.type, module: sc.module };
|
|
482
|
+
case 'id': {
|
|
483
|
+
const r = resolveName(ir.name, sc);
|
|
484
|
+
if (r?.local) return typeOfSpec(r.local, sc, depth + 1);
|
|
485
|
+
if (r?.var) return typeOfSpec(r.var, { module: r.var.module }, depth + 1);
|
|
486
|
+
return null;
|
|
487
|
+
}
|
|
488
|
+
case 'sel': {
|
|
489
|
+
if (ir.obj.k === 'id') { // pkg.Var
|
|
490
|
+
const r = resolveName(ir.obj.name, sc);
|
|
491
|
+
if (r?.pkg) { const v = r.pkg.vars.get(ir.field); return v ? typeOfSpec(v, { module: v.module }, depth + 1) : null; }
|
|
492
|
+
if (r?.ext) return null;
|
|
493
|
+
}
|
|
494
|
+
const o = typeOf(ir.obj, sc, depth + 1), k = o && classOf(o.t, o.module)?.cls;
|
|
495
|
+
const f = k && fieldType(k, ir.field);
|
|
496
|
+
return f ? { t: f.t, module: f.module } : null;
|
|
497
|
+
}
|
|
498
|
+
case 'idx': { const o = typeOf(ir.obj, sc, depth + 1); return o && (o.t.k === 'slice' || o.t.k === 'map') ? { t: o.t.of, module: o.module } : null; }
|
|
499
|
+
case 'call': return callResult(ir, sc, 0, depth + 1);
|
|
500
|
+
default: return null;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
function typeOfSpec(spec, sc, depth) {
|
|
504
|
+
if (spec.t === 'annot') return { t: spec.type, module: spec.module || sc.module };
|
|
505
|
+
if (spec.t === 'range') { const o = typeOf(spec.ir, { ...sc, ...(spec.types ? { types: spec.types } : {}) }, depth + 1); return o && (o.t.k === 'slice' || o.t.k === 'map' || o.t.k === 'chan') ? { t: o.t.of, module: o.module } : null; }
|
|
506
|
+
if (spec.t === 'expr') return spec.ir.k === 'call' ? callResult(spec.ir, { ...sc, ...(spec.types ? { types: spec.types } : {}) }, spec.index || 0, depth + 1) : typeOf(spec.ir, sc, depth + 1);
|
|
507
|
+
return null;
|
|
508
|
+
}
|
|
509
|
+
// the declared result type of the function a call names; a factory with an interface result narrows to what it returns
|
|
510
|
+
function callResult(ir, sc, index, depth) {
|
|
511
|
+
const target = calleeOf(ir.fn, sc, depth);
|
|
512
|
+
if (target?.type) return { t: { k: 'name', name: symbols.get(target.type)?.name }, module: symbols.get(target.type)?.file };
|
|
513
|
+
const s = target?.id && symbols.get(target.id);
|
|
514
|
+
if (!s?.results?.[index]) return null;
|
|
515
|
+
const out = { t: s.results[index], module: s.file };
|
|
516
|
+
const rc = classOf(out.t, out.module)?.cls;
|
|
517
|
+
if (rc && isIface(rc)) { const fr = factoryReturns(s.id).filter(x => x.cls); if (fr.length) out.narrow = { factory: s.id, cls: [...new Set(fr.map(x => x.cls))], returns: fr }; }
|
|
518
|
+
return out;
|
|
519
|
+
}
|
|
520
|
+
// what a call's function IR names: { id } (a function or method), { type } (a conversion), { ext }, or null
|
|
521
|
+
function calleeOf(fn, sc, depth = 0) {
|
|
522
|
+
if (!fn) return null;
|
|
523
|
+
if (fn.k === 'id') { const r = resolveName(fn.name, sc); return r?.id ? { id: r.id } : r?.type ? { type: r.type } : null; }
|
|
524
|
+
if (fn.k === 'sel') {
|
|
525
|
+
if (fn.obj.k === 'id') {
|
|
526
|
+
const r = resolveName(fn.obj.name, sc);
|
|
527
|
+
if (r?.pkg) return r.pkg.funcs.has(fn.field) ? { id: r.pkg.funcs.get(fn.field) } : r.pkg.types.has(fn.field) ? { type: r.pkg.types.get(fn.field) } : null;
|
|
528
|
+
if (r?.ext) return { ext: `${r.ext}.${fn.field}` };
|
|
529
|
+
if (r?.type) { const m = lookup(r.type, fn.field); return m ? { id: m } : null; } // a method expression T.M
|
|
530
|
+
}
|
|
531
|
+
const o = typeOf(fn.obj, sc, depth + 1), k = o && classOf(o.t, o.module)?.cls;
|
|
532
|
+
const m = k && lookup(k, fn.field);
|
|
533
|
+
return m ? { id: m } : null;
|
|
534
|
+
}
|
|
535
|
+
return null;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const retMemo = new Map(), returnsOf = new Map();
|
|
539
|
+
for (const r of go.returns) (returnsOf.get(r.fn) || returnsOf.set(r.fn, []).get(r.fn)).push(r);
|
|
540
|
+
function factoryReturns(fnId, depth = 0) {
|
|
541
|
+
if (retMemo.has(fnId)) return retMemo.get(fnId);
|
|
542
|
+
if (depth > 3) return [];
|
|
543
|
+
retMemo.set(fnId, []);
|
|
544
|
+
const out = [];
|
|
545
|
+
for (const ret of returnsOf.get(fnId) || []) {
|
|
546
|
+
let ir = ret.ir, guards = ret.guards;
|
|
547
|
+
if (ir.k === 'id') { const a = go.fnAssigns.get(fnId)?.get(ir.name); if (!a) continue; ir = a.ir; guards = [...a.guards, ...ret.guards]; }
|
|
548
|
+
if (ir.k === 'lit') { const c = classOf(ir.type, ret.module)?.cls; if (c && !isIface(c)) out.push({ cls: c, guards, line: ret.line }); }
|
|
549
|
+
else if (ir.k === 'call') { const t = calleeOf(ir.fn, ret, 0); const s = t?.id && symbols.get(t.id);
|
|
550
|
+
if (s) { const rc = s.results?.[0] && classOf(s.results[0], s.file)?.cls;
|
|
551
|
+
if (rc && !isIface(rc)) out.push({ cls: rc, guards, line: ret.line, via: s.id });
|
|
552
|
+
else for (const x of factoryReturns(s.id, depth + 1)) out.push({ ...x, guards: [...guards, ...x.guards], via: s.id }); } }
|
|
553
|
+
}
|
|
554
|
+
retMemo.set(fnId, out);
|
|
555
|
+
return out;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
return { pkgOfImport, importsOf, packageOf: pkgOf, classOf, resolveName, typeOf, calleeOf, lookup, fieldType, implementers, isIface, factoryReturns, embedChain, typeText };
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
export function addGoEdge(go, from, to, kind, line, extra = {}) {
|
|
562
|
+
if (!from || !to || from === to) return;
|
|
563
|
+
const e = { from, to, kind, line, ...extra };
|
|
564
|
+
const gf = go.groupOf(from), gt = go.groupOf(to);
|
|
565
|
+
if (gf && gt && gf !== gt) e.groups = [gf, gt];
|
|
566
|
+
go.edges.push(e);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
const CFG_GUARD = /\bos\.(Getenv|LookupEnv)\b|\b(cfg|config|conf|settings|opts|options|env)\.|\bflag\.|\b(isProd|IsProd|isDev|IsDev|debug|Debug)\b/;
|
|
570
|
+
// callees that look at a function rather than run it
|
|
571
|
+
const NOT_RUN = /^(fmt\.\w+|log\.\w+|errors\.\w+|reflect\.\w+|t\.(Log|Logf|Error|Errorf|Fatal|Fatalf|Helper|Run)|assert\.\w+|require\.\w+)$/;
|
|
572
|
+
|
|
573
|
+
/* resolve every recorded call into edges; each resolved call records its `target` for the rule packs */
|
|
574
|
+
export function resolveGoCalls(go, R) {
|
|
575
|
+
const { symbols } = go;
|
|
576
|
+
// a variable whose type, or the call that made it, is a dependency's
|
|
577
|
+
const external = (spec, c) => spec.t === 'annot' ? !!R.classOf(spec.type, c.module)?.ext : spec.t === 'expr' && spec.ir.k === 'call' && !!R.calleeOf(spec.ir.fn, c)?.ext;
|
|
578
|
+
const noteMiss = (c, head) => {
|
|
579
|
+
const r = head ? R.resolveName(head, c) : null;
|
|
580
|
+
if (r?.ext) go.unresolved.external++;
|
|
581
|
+
else if (r && (r.id || r.pkg || r.type || r.var)) go.unresolved.inRepo++;
|
|
582
|
+
else if (r?.local) external(r.local, c) ? go.unresolved.external++ : go.unresolved.local++;
|
|
583
|
+
else if (PREDECLARED.has(head) || BASIC.has(head)) go.unresolved.builtin++;
|
|
584
|
+
else go.unresolved.local++;
|
|
585
|
+
};
|
|
586
|
+
const cfgOf = guards => { const cg = (guards || []).filter(g => CFG_GUARD.test(g.text)); return cg.length ? { cfg: cg.map(g => (g.neg ? 'not ' : '') + g.text.replace(/\s+/g, ' ')) } : {}; };
|
|
587
|
+
go.rawCalls.forEach((c, i) => {
|
|
588
|
+
const extra = { ...cfgOf(c.guards), ...(c.how ? { [c.how === 'go' ? 'goroutine' : 'deferred']: true } : {}) };
|
|
589
|
+
const add = (to, kind, more = {}) => addGoEdge(go, c.from, to, kind, c.line, { ...more, ...extra });
|
|
590
|
+
if (!c.attr) {
|
|
591
|
+
const f = c.fn;
|
|
592
|
+
if (f.k === 'id' && PREDECLARED.has(f.name) && !R.resolveName(f.name, c)) { go.unresolved.builtin++; go.callStats.unresolvedName++; return; }
|
|
593
|
+
const t = R.calleeOf(f, c);
|
|
594
|
+
if (t?.id) { add(t.id, 'static'); c.target = t.id; go.callStats.resolved++; }
|
|
595
|
+
else if (t?.type || (f.k === 'id' && BASIC.has(f.name))) { /* a conversion, not a call */ }
|
|
596
|
+
else { go.callStats.unresolvedName++; noteMiss(c, f.k === 'id' ? f.name : null); }
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
const site = `${c.from}@${c.line}:${c.attr}`;
|
|
600
|
+
// pkg.F(), T.M (a method expression)
|
|
601
|
+
if (c.obj.k === 'id') {
|
|
602
|
+
const r = R.resolveName(c.obj.name, c);
|
|
603
|
+
if (r?.pkg || r?.ext || r?.type) {
|
|
604
|
+
const t = R.calleeOf({ k: 'sel', obj: c.obj, field: c.attr }, c);
|
|
605
|
+
if (t?.id) { add(t.id, 'static'); c.target = t.id; go.callStats.resolved++; }
|
|
606
|
+
else if (t?.type) { /* conversion */ }
|
|
607
|
+
else { go.callStats.unresolvedAttr++; if (r.ext) go.unresolved.external++; else go.unresolved.inRepo++; }
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
const o = R.typeOf(c.obj, c), k = o && R.classOf(o.t, o.module);
|
|
612
|
+
if (k?.cls) {
|
|
613
|
+
if (o.narrow) {
|
|
614
|
+
const targets = [...new Set(o.narrow.cls.map(x => R.lookup(x, c.attr)).filter(Boolean))];
|
|
615
|
+
if (targets.length) { for (const t of targets) add(t, targets.length === 1 ? 'static' : 'dispatch', { site, narrowedBy: o.narrow.factory, ...(targets.length === 1 ? { recvClass: o.narrow.cls[0] } : {}) });
|
|
616
|
+
c.target = targets[0]; go.callStats.resolved++; return; }
|
|
617
|
+
}
|
|
618
|
+
const target = R.lookup(k.cls, c.attr);
|
|
619
|
+
if (R.isIface(k.cls)) {
|
|
620
|
+
const impls = (R.implementers.get(k.cls) || []).map(x => R.lookup(x, c.attr)).filter(Boolean);
|
|
621
|
+
if (target) add(target, 'static', { site });
|
|
622
|
+
for (const t of [...new Set(impls)]) add(t, 'virtual', { site, evidence: `${symbols.get(t)?.name} implements ${symbols.get(k.cls)?.name}` });
|
|
623
|
+
if (target || impls.length) { c.target = target; go.callStats.resolved++; return; }
|
|
624
|
+
} else if (target) { add(target, 'static', { recvClass: k.cls, site }); c.target = target; go.callStats.resolved++; return; }
|
|
625
|
+
}
|
|
626
|
+
go.callStats.unresolvedAttr++;
|
|
627
|
+
if (k?.ext) go.unresolved.external++;
|
|
628
|
+
else { let h = c.obj; while (h.k === 'sel' || h.k === 'idx') h = h.obj; noteMiss(c, h.k === 'id' ? h.name : null); }
|
|
629
|
+
});
|
|
630
|
+
// composite literals of in-repo types: what a function constructs
|
|
631
|
+
for (const l of go.literals) { const k = R.classOf(l.type, l.module)?.cls; if (k) { l.cls = k; addGoEdge(go, l.from, k, 'ctor', l.line, cfgOf(l.guards)); } }
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/* a function or method value handed to a call runs from there: the caller links to it. `skipHanded`: calls whose
|
|
635
|
+
function arguments a rule pack has claimed (a route registration hands its handler to the router) */
|
|
636
|
+
export function linkGoCallbacks(go, R, { skipHanded = new Set() } = {}) {
|
|
637
|
+
const { symbols } = go;
|
|
638
|
+
const claimed = i => { for (let j = i; j != null; j = go.rawCalls[j]?.argOf) if (skipHanded.has(j)) return true; return false; };
|
|
639
|
+
for (const h of go.handed) {
|
|
640
|
+
if (h.call != null && claimed(h.call)) continue;
|
|
641
|
+
if (h.to) { addGoEdge(go, h.from, h.to, 'static', h.line, { rule: 'go.callback', evidence: `${h.file}:${h.line} hands ${symbols.get(h.to)?.name} to ${h.how}` }); continue; }
|
|
642
|
+
if (NOT_RUN.test(h.callee)) continue;
|
|
643
|
+
const t = R.calleeOf(h.ir, h);
|
|
644
|
+
const s = t?.id && symbols.get(t.id);
|
|
645
|
+
if (!s || (s.kind !== 'function' && s.kind !== 'method') || (s.decos || []).includes('abstractmethod')) continue;
|
|
646
|
+
addGoEdge(go, h.from, t.id, 'static', h.line, { rule: 'go.callback', evidence: `${h.file}:${h.line} hands ${h.name} to ${h.callee}` });
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/* `func main()` in a `package main` */
|
|
651
|
+
export function goEntries(go) {
|
|
652
|
+
return go.mains.map(m => ({ kind: 'main', id: m, file: go.symbols.get(m).file, line: go.symbols.get(m).start, rule: 'go.main' }));
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/* const folding for route paths: string literals, `+`, and identifiers naming a const of the function, its package
|
|
656
|
+
or an imported package (`api.Prefix`); what cannot be known is `{}` */
|
|
657
|
+
export function foldGo(R, ir, sc, depth = 0) {
|
|
658
|
+
if (!ir || depth > 10) return '{}';
|
|
659
|
+
if (ir.k === 'str') return ir.v;
|
|
660
|
+
if (ir.k === 'cat') return foldGo(R, ir.l, sc, depth + 1) + foldGo(R, ir.r, sc, depth + 1);
|
|
661
|
+
if (ir.k === 'id') {
|
|
662
|
+
const local = sc.consts?.get(ir.name);
|
|
663
|
+
if (local && local !== ir) return foldGo(R, local, sc, depth + 1);
|
|
664
|
+
const v = R.packageOf(sc.module)?.consts.get(ir.name);
|
|
665
|
+
return v ? foldGo(R, v, { module: sc.module }, depth + 1) : '{}';
|
|
666
|
+
}
|
|
667
|
+
if (ir.k === 'sel' && ir.obj.k === 'id') {
|
|
668
|
+
const r = R.resolveName(ir.obj.name, { module: sc.module });
|
|
669
|
+
const v = r?.pkg?.consts.get(ir.field);
|
|
670
|
+
if (v) return foldGo(R, v, { module: r.pkg.files[0] }, depth + 1);
|
|
671
|
+
}
|
|
672
|
+
return '{}';
|
|
673
|
+
}
|