explainer-video 0.0.0-stage → 0.4.1
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 +154 -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 +70 -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/craft-video-thumbnail/SKILL.md +168 -0
- package/skills/explainer-video-from-codebase/FLOWS.md +577 -0
- package/skills/explainer-video-from-codebase/SKILL.md +698 -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 +61 -0
- package/skills/explainer-video-from-coursework/PIPELINE.md +369 -0
- package/skills/explainer-video-from-coursework/ROADMAP.md +727 -0
- package/skills/explainer-video-from-coursework/SERIES.md +243 -0
- package/skills/explainer-video-from-coursework/SETUP.md +105 -0
- package/skills/explainer-video-from-coursework/SKILL.md +998 -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/thumbnail.html +119 -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/thumbnail.mjs +224 -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,963 @@
|
|
|
1
|
+
/* The TypeScript / JavaScript adapter: facts per file (.ts .tsx .mts .cts .js .jsx .mjs .cjs), then module, name,
|
|
2
|
+
member and constructor resolution into the same typed edges the Python adapter writes. Rule packs (../rules/)
|
|
3
|
+
read the same facts and add framework links on top.
|
|
4
|
+
|
|
5
|
+
Ids: a module is its repo-relative path with its extension (`web/src/api.ts`), so no TS id can meet a Python one;
|
|
6
|
+
a symbol is `<module>:<qualname>` with the Python adapter's scoping (`K.m`, `f.<locals>.g`).
|
|
7
|
+
|
|
8
|
+
Resolution rules (design rev3 §3.14, TS/JS row): ESM imports (default, named, namespace, re-exports, `export *`,
|
|
9
|
+
`export default`) and CommonJS (`require`, destructured `require`, `module.exports`, `exports.x`); module
|
|
10
|
+
specifiers the way graphify resolves them (relative paths with TypeScript's `.js` -> `.ts` convention, extension
|
|
11
|
+
probing, directory indexes, tsconfig/jsconfig `paths` and `baseUrl` through `extends`, workspace packages, and the
|
|
12
|
+
`@/` -> `src/` convention when no tsconfig exists); `this.m()` through the class chain plus a `virtual` edge per
|
|
13
|
+
subclass override; `super.m()`; receivers typed by `new K()`, an annotated parameter or variable, a class field or
|
|
14
|
+
constructor parameter property, a module-level singleton, or a factory's returns (with branch guards); object-literal
|
|
15
|
+
members (`api.list()`); a rendered JSX component; a function handed to a call or a JSX prop; a call inside `catch`
|
|
16
|
+
is labelled "on failure", and one under a config-reading `if` carries its guard. */
|
|
17
|
+
import { posix } from 'node:path';
|
|
18
|
+
|
|
19
|
+
export const TS_LANGS = new Set(['typescript', 'tsx', 'javascript']);
|
|
20
|
+
const EXTS = ['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs'];
|
|
21
|
+
const INDEX = ['index.ts', 'index.tsx', 'index.js', 'index.jsx', 'index.mjs', 'index.cjs'];
|
|
22
|
+
|
|
23
|
+
export function newTsModel() {
|
|
24
|
+
return {
|
|
25
|
+
symbols: new Map(), classes: new Map(), modules: new Map(),
|
|
26
|
+
rawCalls: [], handed: [], returns: [], mains: [], fnAssigns: new Map(),
|
|
27
|
+
routeDefs: [], testCalls: [], mockCalls: [], envReads: [],
|
|
28
|
+
edges: [], callStats: { resolved: 0, unresolvedName: 0, unresolvedAttr: 0 },
|
|
29
|
+
unresolved: { inRepo: 0, external: 0, builtin: 0, local: 0 }, externalImports: new Map(), unresolvedImports: new Map(),
|
|
30
|
+
defNodes: 0, defsVisited: 0, defScopes: 0,
|
|
31
|
+
groupOf: null, // id -> group across every language; set by mine.mjs before edges are added
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/* ---------------- expression IR: what const folding and the rule packs read, kept as plain data ---------------- */
|
|
36
|
+
|
|
37
|
+
const unquote = n => n.namedChildren.filter(c => c.type === 'string_fragment' || c.type === 'escape_sequence').map(c => c.text).join('');
|
|
38
|
+
export const strOf = n => !n ? null : n.type === 'string' ? unquote(n)
|
|
39
|
+
: n.type === 'template_string' && !n.namedChildren.some(c => c.type === 'template_substitution') ? unquote(n) : null;
|
|
40
|
+
const ENV = /^(?:process\.env|import\.meta\.env)\.([A-Za-z_]\w*)$/;
|
|
41
|
+
const ENV_SUB = /^(?:process\.env|import\.meta\.env)\[\s*["'`]([A-Za-z_]\w*)["'`]\s*\]$/;
|
|
42
|
+
export function ir(n, depth = 0) {
|
|
43
|
+
if (!n || depth > 8) return { k: 'unknown' };
|
|
44
|
+
switch (n.type) {
|
|
45
|
+
case 'string': return { k: 'str', v: unquote(n) };
|
|
46
|
+
case 'number': return { k: 'str', v: n.text };
|
|
47
|
+
case 'template_string': return { k: 'tpl', parts: n.namedChildren.map(c => c.type === 'template_substitution' ? ir(c.namedChildren[0], depth + 1)
|
|
48
|
+
: { k: 'str', v: c.text }) };
|
|
49
|
+
case 'identifier': return { k: 'id', name: n.text };
|
|
50
|
+
case 'member_expression': { const m = ENV.exec(n.text); if (m) return { k: 'env', key: m[1] };
|
|
51
|
+
return { k: 'member', obj: ir(n.childForFieldName('object'), depth + 1), prop: n.childForFieldName('property').text }; }
|
|
52
|
+
case 'subscript_expression': { const m = ENV_SUB.exec(n.text); return m ? { k: 'env', key: m[1] } : { k: 'unknown' }; }
|
|
53
|
+
case 'binary_expression': { const op = n.childForFieldName('operator')?.text, l = ir(n.childForFieldName('left'), depth + 1), r = ir(n.childForFieldName('right'), depth + 1);
|
|
54
|
+
return op === '+' ? { k: 'cat', l, r } : op === '??' || op === '||' ? { k: 'or', l, r } : { k: 'unknown' }; }
|
|
55
|
+
case 'parenthesized_expression': case 'as_expression': case 'satisfies_expression': case 'non_null_expression': case 'await_expression':
|
|
56
|
+
return ir(n.namedChildren[0], depth + 1);
|
|
57
|
+
case 'object': {
|
|
58
|
+
const pairs = {};
|
|
59
|
+
for (const p of n.namedChildren) {
|
|
60
|
+
if (p.type === 'pair') { const k = p.childForFieldName('key'); const key = k.type === 'string' ? unquote(k) : k.text; pairs[key] = ir(p.childForFieldName('value'), depth + 1); }
|
|
61
|
+
else if (p.type === 'shorthand_property_identifier') pairs[p.text] = { k: 'id', name: p.text };
|
|
62
|
+
else if (p.type === 'spread_element') pairs['...'] = { k: 'spread', text: p.text };
|
|
63
|
+
}
|
|
64
|
+
return { k: 'obj', pairs };
|
|
65
|
+
}
|
|
66
|
+
case 'call_expression': return { k: 'call', fn: n.childForFieldName('function')?.text };
|
|
67
|
+
default: return { k: 'unknown' };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/* ---------------- types ---------------- */
|
|
72
|
+
|
|
73
|
+
// `X`, `ns.X`, `X<T>`, `X | null`, `X | undefined` -> the class name; anything else (unions, literals, functions) -> null
|
|
74
|
+
function simpleType(t) {
|
|
75
|
+
if (!t) return null;
|
|
76
|
+
if (t.type === 'type_annotation') return simpleType(t.namedChildren[0]);
|
|
77
|
+
if (t.type === 'type_identifier' || t.type === 'nested_type_identifier' || t.type === 'identifier') return t.text;
|
|
78
|
+
if (t.type === 'generic_type') return simpleType(t.childForFieldName('name') || t.namedChildren[0]);
|
|
79
|
+
if (t.type === 'union_type') { const parts = t.namedChildren.filter(c => !/^(null|undefined)$/.test(c.text) && !(c.type === 'literal_type' && /^(null|undefined)$/.test(c.text)));
|
|
80
|
+
return parts.length === 1 ? simpleType(parts[0]) : null; }
|
|
81
|
+
if (t.type === 'parenthesized_type') return simpleType(t.namedChildren[0]);
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
// a variable's value as a receiver spec: `new K()`, `make()`, `x as K`, `await …`
|
|
85
|
+
function valueSpec(v) {
|
|
86
|
+
if (!v) return null;
|
|
87
|
+
if (/^(await_expression|parenthesized_expression|non_null_expression)$/.test(v.type)) return valueSpec(v.namedChildren[0]);
|
|
88
|
+
if (v.type === 'as_expression' || v.type === 'satisfies_expression') { const t = simpleType(v.namedChildren[1]); return t ? { t: 'annot', expr: t } : valueSpec(v.namedChildren[0]); }
|
|
89
|
+
if (v.type === 'new_expression') return { t: 'new', expr: v.childForFieldName('constructor').text };
|
|
90
|
+
if (v.type === 'call_expression') { const f = v.childForFieldName('function'); const a0 = v.childForFieldName('arguments')?.namedChildren?.[0];
|
|
91
|
+
return { t: 'call', expr: f.text, a0: a0 ? ir(a0) : null }; }
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const FUNC = new Set(['arrow_function', 'function_expression', 'function', 'generator_function']);
|
|
96
|
+
const CLASS_DECL = new Set(['class_declaration', 'abstract_class_declaration']);
|
|
97
|
+
// calls that hand back the function they are given: the function is named by the variable it is assigned to
|
|
98
|
+
const WRAPPERS = /^(React\.)?(memo|forwardRef|useCallback|useMemo|observer|debounce|throttle|_\.debounce|_\.throttle|lazyWithRetry)$/;
|
|
99
|
+
const LAZY = /^(React\.)?lazy$|^loadable$/;
|
|
100
|
+
const TEST_API = /^(it|test|describe|suite|context)$/;
|
|
101
|
+
const HOOK_API = /^(beforeEach|beforeAll|afterEach|afterAll|before|after)$/;
|
|
102
|
+
const ROUTE_REG = /^(get|post|put|patch|delete|del|all|options|head|use)$/;
|
|
103
|
+
const safeTitle = s => s.replace(/:/g, '-').replace(/\s+/g, ' ').trim();
|
|
104
|
+
|
|
105
|
+
export function extractTsFile(ts, rel, root, group, text, lang) {
|
|
106
|
+
const module = rel;
|
|
107
|
+
const M = { file: rel, group, lang, imports: new Map(), defs: new Map(), objects: new Map(), vars: new Map(), consts: new Map(),
|
|
108
|
+
exports: new Map(), starExports: [], defaultExport: null, cjsExports: false, importsRouter: false };
|
|
109
|
+
ts.modules.set(module, M);
|
|
110
|
+
const lines = text.split('\n');
|
|
111
|
+
const spanText = (a, b) => lines.slice(a - 1, b).join('\n');
|
|
112
|
+
const MOD = `${module}:<module>`;
|
|
113
|
+
const noteExternal = (spec) => { const top = spec.startsWith('@') ? spec.split('/').slice(0, 2).join('/') : spec.split('/')[0];
|
|
114
|
+
if (!spec.startsWith('.') && !ts.externalImports.has(top)) ts.externalImports.set(top, rel); };
|
|
115
|
+
|
|
116
|
+
// definitions the self-check expects: declarations, class members, and functions or classes a variable names
|
|
117
|
+
const counted = new Set();
|
|
118
|
+
for (const n of root.descendantsOfType(['function_declaration', 'generator_function_declaration', 'class_declaration', 'abstract_class_declaration', 'method_definition', 'variable_declarator'])) {
|
|
119
|
+
if (n.type === 'method_definition' && n.parent?.type !== 'class_body') continue;
|
|
120
|
+
if (n.type === 'variable_declarator') { const v = n.childForFieldName('value'); if (n.childForFieldName('name')?.type !== 'identifier' || !v || !(FUNC.has(v.type) || v.type === 'class')) continue; }
|
|
121
|
+
if (!n.childForFieldName('name') && n.type !== 'variable_declarator') continue;
|
|
122
|
+
counted.add(n.id);
|
|
123
|
+
}
|
|
124
|
+
ts.defNodes += counted.size;
|
|
125
|
+
const quals = new Set();
|
|
126
|
+
|
|
127
|
+
const qualIn = (ctx, name) => ctx.inClass ? `${ctx.clsQual}.${name}` : ctx.fn ? `${ctx.qual}.<locals>.${name}` : ctx.describe?.length ? `${ctx.describe.join(' > ')}.<locals>.${name}` : name;
|
|
128
|
+
const uniqueId = q => { let id = `${module}:${q}`, i = 2; while (ts.symbols.has(id)) id = `${module}:${q}#${i++}`; return id; };
|
|
129
|
+
const params = fnNode => {
|
|
130
|
+
const ps = fnNode.childForFieldName('parameters') || fnNode.childForFieldName('parameter');
|
|
131
|
+
if (!ps) return [];
|
|
132
|
+
const list = ps.type === 'formal_parameters' ? ps.namedChildren : [ps];
|
|
133
|
+
return list.map(p => {
|
|
134
|
+
const pat = p.type === 'required_parameter' || p.type === 'optional_parameter' ? p.childForFieldName('pattern') : p.type === 'assignment_pattern' ? p.childForFieldName('left') : p;
|
|
135
|
+
const dflt = p.childForFieldName('value') || (p.type === 'assignment_pattern' ? p.childForFieldName('right') : null);
|
|
136
|
+
return { name: pat?.type === 'identifier' ? pat.text : null, type: simpleType(p.childForFieldName('type')), dflt,
|
|
137
|
+
prop: p.namedChildren.some(c => c.type === 'accessibility_modifier' || c.text === 'readonly') };
|
|
138
|
+
});
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
// a function-like node becomes a symbol; its body is visited as that symbol
|
|
142
|
+
function defineFn(node, name, ctx, { kind, counts = false, extra = {} } = {}) {
|
|
143
|
+
if (counts) ts.defsVisited++;
|
|
144
|
+
const q = qualIn(ctx, name);
|
|
145
|
+
const id = uniqueId(q);
|
|
146
|
+
quals.add(q);
|
|
147
|
+
const start = node.startPosition.row + 1, end = node.endPosition.row + 1;
|
|
148
|
+
const ps = params(node);
|
|
149
|
+
const sym = { id, group, kind: kind || (ctx.inClass ? 'method' : 'function'), file: rel, start, end, name, module, lang,
|
|
150
|
+
cls: ctx.inClass ? ctx.cls : null, decos: extra.decos || [], params: ps.map(p => p.name).filter(Boolean), text: spanText(start, end),
|
|
151
|
+
...(node.children.some(c => c.type === 'async') ? { async: true } : {}), ...(extra.sym || {}) };
|
|
152
|
+
ts.symbols.set(id, sym);
|
|
153
|
+
if (ctx.inClass) ts.classes.get(ctx.cls).methods.set(name, id);
|
|
154
|
+
else if (!extra.noBind) (ctx.fn || ctx.describe?.length ? ctx.locals : M.defs).set(name, id);
|
|
155
|
+
const inner = { ...ctx, fn: id, qual: q, inClass: false, locals: new Map(ctx.locals), types: new Map(ctx.types), consts: new Map(ctx.consts),
|
|
156
|
+
guards: [], onFailure: null, decos: null, test: extra.test || ctx.test, declared: new Set(), outerTypes: extra.outerTypes || null };
|
|
157
|
+
// a parameter is typed by its annotation, else by its default (`log = new Log()`); defaults run in the function
|
|
158
|
+
for (const p of ps) { const spec = p.type ? { t: 'annot', expr: p.type } : valueSpec(p.dflt);
|
|
159
|
+
if (p.name && spec) inner.types.set(p.name, spec);
|
|
160
|
+
if (ctx.inClass && name === 'constructor' && p.prop && p.name && spec) addAttr(ctx.cls, p.name, spec, ctx);
|
|
161
|
+
if (p.dflt) visit(p.dflt, inner); }
|
|
162
|
+
const body = node.childForFieldName('body');
|
|
163
|
+
if (body) {
|
|
164
|
+
if (body.type === 'statement_block') visitBlock(body, inner);
|
|
165
|
+
else { visit(body, inner); noteReturn(body, inner); } // an arrow's expression body is its return value
|
|
166
|
+
}
|
|
167
|
+
return id;
|
|
168
|
+
}
|
|
169
|
+
function addAttr(clsId, name, spec, ctx) {
|
|
170
|
+
const k = ts.classes.get(clsId); if (!k || !spec) return;
|
|
171
|
+
(k.attrTypes.get(name) || k.attrTypes.set(name, []).get(name)).push({ ...spec, guards: ctx.guards || [], module, locals: ctx.locals, types: ctx.types });
|
|
172
|
+
}
|
|
173
|
+
function defineClass(node, name, ctx, counts) {
|
|
174
|
+
if (counts) ts.defsVisited++;
|
|
175
|
+
const q = qualIn(ctx, name);
|
|
176
|
+
const id = uniqueId(q);
|
|
177
|
+
quals.add(q);
|
|
178
|
+
const her = node.namedChildren.find(c => c.type === 'class_heritage');
|
|
179
|
+
let baseExprs = [], implementsExprs = [];
|
|
180
|
+
if (her) for (const c of her.namedChildren) {
|
|
181
|
+
if (c.type === 'extends_clause') { const v = c.childForFieldName('value') || c.namedChildren[0]; if (v) baseExprs.push(v.type === 'call_expression' ? v.childForFieldName('function').text : v.text); }
|
|
182
|
+
else if (c.type === 'implements_clause') implementsExprs = c.namedChildren.map(simpleType).filter(Boolean);
|
|
183
|
+
else if (c.type === 'identifier' || c.type === 'member_expression') baseExprs.push(c.text); // JS: `extends X`
|
|
184
|
+
}
|
|
185
|
+
const start = node.startPosition.row + 1, end = node.endPosition.row + 1;
|
|
186
|
+
const decos = node.namedChildren.filter(c => c.type === 'decorator').map(d => d.text.replace(/^@/, ''));
|
|
187
|
+
ts.classes.set(id, { id, module, name, baseExprs, implementsExprs, methods: new Map(), attrTypes: new Map(), decos, file: rel, line: start,
|
|
188
|
+
abstract: node.type === 'abstract_class_declaration', scope: { module, locals: ctx.locals } });
|
|
189
|
+
ts.symbols.set(id, { id, group, kind: 'class', file: rel, start, end, name, module, lang, decos, text: spanText(start, end) });
|
|
190
|
+
if (!ctx.inClass) (ctx.fn || ctx.describe?.length ? ctx.locals : M.defs).set(name, id);
|
|
191
|
+
const cctx = { ...ctx, cls: id, clsQual: q, inClass: true };
|
|
192
|
+
for (const m of node.childForFieldName('body').namedChildren) {
|
|
193
|
+
if (m.type === 'method_definition') {
|
|
194
|
+
const nm = m.childForFieldName('name').text;
|
|
195
|
+
const getset = m.children.find(c => c.type === 'get' || c.type === 'set')?.type;
|
|
196
|
+
const d = [...(m.namedChildren.filter(c => c.type === 'decorator').map(x => x.text.replace(/^@/, ''))),
|
|
197
|
+
...(m.children.some(c => c.type === 'static') ? ['static'] : []), ...(getset ? [getset === 'get' ? 'property' : 'setter'] : [])];
|
|
198
|
+
defineFn(m, getset === 'set' ? `${nm}.setter` : nm, cctx, { counts: true, extra: { decos: d } });
|
|
199
|
+
} else if (m.type === 'abstract_method_signature') {
|
|
200
|
+
const nm = m.childForFieldName('name').text;
|
|
201
|
+
defineFn(m, nm, cctx, { extra: { decos: ['abstract'] } });
|
|
202
|
+
} else if (m.type === 'public_field_definition' || m.type === 'field_definition') {
|
|
203
|
+
const nm = (m.childForFieldName('name') || m.childForFieldName('property'))?.text, v = m.childForFieldName('value');
|
|
204
|
+
if (!nm) continue;
|
|
205
|
+
if (v && FUNC.has(v.type)) defineFn(v, nm, cctx, {});
|
|
206
|
+
else { const spec = valueSpec(v) || (simpleType(m.childForFieldName('type')) ? { t: 'annot', expr: simpleType(m.childForFieldName('type')) } : null);
|
|
207
|
+
addAttr(id, nm, spec, { ...ctx, guards: [] }); if (v) visit(v, { ...ctx, fn: null, cls: id, inClass: false }); }
|
|
208
|
+
} else if (m.type === 'class_static_block') visit(m, { ...cctx, inClass: false });
|
|
209
|
+
}
|
|
210
|
+
return id;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/* imports and exports */
|
|
214
|
+
function addImport(node) {
|
|
215
|
+
const src = strOf(node.childForFieldName('source'));
|
|
216
|
+
if (src == null) return;
|
|
217
|
+
noteExternal(src);
|
|
218
|
+
if (/^react-router(-dom)?$|^@remix-run\/router$/.test(src)) M.importsRouter = true;
|
|
219
|
+
if (node.children.some(c => c.type === 'type')) return; // `import type`
|
|
220
|
+
const clause = node.namedChildren.find(c => c.type === 'import_clause');
|
|
221
|
+
if (!clause) return;
|
|
222
|
+
const line = node.startPosition.row + 1;
|
|
223
|
+
for (const c of clause.namedChildren) {
|
|
224
|
+
if (c.type === 'identifier') M.imports.set(c.text, { source: src, name: 'default', line });
|
|
225
|
+
else if (c.type === 'namespace_import') M.imports.set(c.namedChildren[0].text, { source: src, name: '*', line });
|
|
226
|
+
else if (c.type === 'named_imports') for (const s of c.namedChildren.filter(x => x.type === 'import_specifier')) {
|
|
227
|
+
if (s.children.some(x => x.type === 'type')) continue;
|
|
228
|
+
const name = s.childForFieldName('name'), alias = s.childForFieldName('alias');
|
|
229
|
+
M.imports.set((alias || name).text, { source: src, name: name.type === 'string' ? unquote(name) : name.text, line });
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
function addExport(node, ctx) {
|
|
234
|
+
const src = strOf(node.childForFieldName('source'));
|
|
235
|
+
const isDefault = node.children.some(c => c.type === 'default');
|
|
236
|
+
const decl = node.childForFieldName('declaration');
|
|
237
|
+
const val = node.childForFieldName('value');
|
|
238
|
+
if (src != null) noteExternal(src);
|
|
239
|
+
if (decl) {
|
|
240
|
+
const id = visit(decl, ctx);
|
|
241
|
+
const names = decl.type === 'lexical_declaration' || decl.type === 'variable_declaration'
|
|
242
|
+
? decl.namedChildren.filter(d => d.type === 'variable_declarator').map(d => d.childForFieldName('name')).filter(n => n?.type === 'identifier').map(n => n.text)
|
|
243
|
+
: [decl.childForFieldName('name')?.text].filter(Boolean);
|
|
244
|
+
if (isDefault) M.defaultExport = names[0] ? { local: names[0] } : id ? { id } : null;
|
|
245
|
+
else for (const n of names) M.exports.set(n, { local: n });
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
const clause = node.namedChildren.find(c => c.type === 'export_clause');
|
|
249
|
+
if (clause) {
|
|
250
|
+
for (const s of clause.namedChildren.filter(x => x.type === 'export_specifier')) {
|
|
251
|
+
const name = s.childForFieldName('name'), alias = s.childForFieldName('alias');
|
|
252
|
+
const nm = name.type === 'string' ? unquote(name) : name.text, as = alias ? (alias.type === 'string' ? unquote(alias) : alias.text) : nm;
|
|
253
|
+
const target = src != null ? { source: src, name: nm } : { local: nm };
|
|
254
|
+
if (as === 'default') M.defaultExport = target; else M.exports.set(as, target);
|
|
255
|
+
}
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
const ns = node.namedChildren.find(c => c.type === 'namespace_export');
|
|
259
|
+
if (src != null && ns) { M.exports.set(ns.namedChildren[0].text, { source: src, name: '*' }); return; }
|
|
260
|
+
if (src != null) { M.starExports.push(src); return; }
|
|
261
|
+
if (isDefault && val) {
|
|
262
|
+
if (val.type === 'identifier') M.defaultExport = { local: val.text };
|
|
263
|
+
else if (FUNC.has(val.type)) M.defaultExport = { id: defineFn(val, 'default', ctx, {}) };
|
|
264
|
+
else if (val.type === 'class') M.defaultExport = { id: defineClass(val, val.childForFieldName('name')?.text || 'default', ctx, false) };
|
|
265
|
+
else if (val.type === 'object') { defineObject(val, 'default', ctx); M.defaultExport = { local: 'default' }; }
|
|
266
|
+
else if (val.type === 'call_expression') { // `export default memo(Page)`, `connect(...)(Page)`, `withRouter(Page)`
|
|
267
|
+
visit(val, ctx);
|
|
268
|
+
let c = val, inner = null;
|
|
269
|
+
while (c?.type === 'call_expression') { const as = c.childForFieldName('arguments')?.namedChildren || [];
|
|
270
|
+
inner = as.find(a => a.type === 'identifier') || inner; const fnArg = as.find(a => FUNC.has(a.type));
|
|
271
|
+
if (fnArg && !inner) { M.defaultExport = { id: defineFn(fnArg, 'default', ctx, {}) }; return; }
|
|
272
|
+
c = c.childForFieldName('function'); }
|
|
273
|
+
M.defaultExport = inner ? { local: inner.text } : null;
|
|
274
|
+
} else visit(val, ctx);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
function defineObject(obj, name, ctx) {
|
|
278
|
+
const members = new Map();
|
|
279
|
+
for (const p of obj.namedChildren) {
|
|
280
|
+
if (p.type === 'method_definition') { const k = p.childForFieldName('name').text; members.set(k, defineFn(p, `${name}.${k}`, ctx, { extra: { noBind: true } })); }
|
|
281
|
+
else if (p.type === 'pair') {
|
|
282
|
+
const kn = p.childForFieldName('key'), v = p.childForFieldName('value'), k = kn.type === 'string' ? unquote(kn) : kn.text;
|
|
283
|
+
if (FUNC.has(v.type)) members.set(k, defineFn(v, `${name}.${k}`, ctx, { extra: { noBind: true } }));
|
|
284
|
+
else if (v.type === 'identifier') members.set(k, { local: v.text });
|
|
285
|
+
else visit(v, ctx);
|
|
286
|
+
} else if (p.type === 'shorthand_property_identifier') members.set(p.text, { local: p.text });
|
|
287
|
+
else visit(p, ctx);
|
|
288
|
+
}
|
|
289
|
+
const key = ctx.fn ? null : name;
|
|
290
|
+
if (key) M.objects.set(key, members);
|
|
291
|
+
return members;
|
|
292
|
+
}
|
|
293
|
+
function requireOf(v) { // `require('x')`, `require('x').y`
|
|
294
|
+
if (v?.type === 'call_expression' && v.childForFieldName('function').text === 'require') { const s = strOf(v.childForFieldName('arguments').namedChildren[0]); return s != null ? { source: s, name: '*' } : null; }
|
|
295
|
+
if (v?.type === 'member_expression') { const r = requireOf(v.childForFieldName('object')); return r && r.name === '*' ? { source: r.source, name: v.childForFieldName('property').text } : null; }
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
function lazyImport(v) { // `lazy(() => import('./Page'))`
|
|
299
|
+
if (v?.type !== 'call_expression' || !LAZY.test(v.childForFieldName('function').text)) return null;
|
|
300
|
+
const f = v.childForFieldName('arguments').namedChildren[0];
|
|
301
|
+
const call = f && FUNC.has(f.type) ? f.descendantsOfType('call_expression').find(c => c.childForFieldName('function').type === 'import') : null;
|
|
302
|
+
const s = call && strOf(call.childForFieldName('arguments').namedChildren[0]);
|
|
303
|
+
return s != null ? { source: s, name: 'default', lazy: true } : null;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function declarator(d, ctx) {
|
|
307
|
+
const nameN = d.childForFieldName('name'), v = d.childForFieldName('value');
|
|
308
|
+
const line = d.startPosition.row + 1;
|
|
309
|
+
const req = requireOf(v);
|
|
310
|
+
if (req) {
|
|
311
|
+
noteExternal(req.source);
|
|
312
|
+
if (nameN?.type === 'identifier') M.imports.set(nameN.text, { ...req, line, cjs: true });
|
|
313
|
+
else if (nameN?.type === 'object_pattern') for (const p of nameN.namedChildren) {
|
|
314
|
+
if (p.type === 'shorthand_property_identifier_pattern') M.imports.set(p.text, { source: req.source, name: p.text, line, cjs: true });
|
|
315
|
+
else if (p.type === 'pair_pattern' && p.childForFieldName('value')?.type === 'identifier') M.imports.set(p.childForFieldName('value').text, { source: req.source, name: p.childForFieldName('key').text, line, cjs: true });
|
|
316
|
+
}
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
if (nameN?.type !== 'identifier') { if (v) visit(v, ctx); return; }
|
|
320
|
+
const name = nameN.text;
|
|
321
|
+
const lz = lazyImport(v);
|
|
322
|
+
if (lz) { noteExternal(lz.source); M.imports.set(name, { ...lz, line }); return; }
|
|
323
|
+
if (v && FUNC.has(v.type)) return defineFn(v, name, ctx, { counts: true });
|
|
324
|
+
if (v?.type === 'class') return defineClass(v, name, ctx, true);
|
|
325
|
+
if (v?.type === 'object' && !ctx.fn) { M.consts.set(name, ir(v)); defineObject(v, name, ctx); return; }
|
|
326
|
+
if (v?.type === 'call_expression' && WRAPPERS.test(v.childForFieldName('function').text)) {
|
|
327
|
+
const fnArg = v.childForFieldName('arguments').namedChildren.find(a => FUNC.has(a.type));
|
|
328
|
+
if (fnArg) {
|
|
329
|
+
const id = defineFn(fnArg, name, ctx, {});
|
|
330
|
+
if (ctx.fn) ts.handed.push({ from: ctx.fn, to: id, line, file: rel, how: v.childForFieldName('function').text });
|
|
331
|
+
for (const a of v.childForFieldName('arguments').namedChildren) if (a.id !== fnArg.id) visit(a, ctx); // node objects are fresh per access: compare ids
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
const t = simpleType(d.childForFieldName('type'));
|
|
336
|
+
const spec = t ? { t: 'annot', expr: t } : valueSpec(v);
|
|
337
|
+
if (ctx.fn) {
|
|
338
|
+
ctx.declared?.add(name);
|
|
339
|
+
if (spec) ctx.types.set(name, spec);
|
|
340
|
+
if (v) ctx.consts.set(name, ir(v));
|
|
341
|
+
if (spec && spec.t !== 'annot') (ts.fnAssigns.get(ctx.fn) || ts.fnAssigns.set(ctx.fn, new Map()).get(ctx.fn)).set(name, { ...spec, guards: ctx.guards, module, locals: ctx.locals, types: ctx.types });
|
|
342
|
+
} else if (ctx.describe?.length) { if (spec) ctx.types.set(name, spec); if (v) ctx.consts.set(name, ir(v)); }
|
|
343
|
+
else { if (spec) M.vars.set(name, { ...spec, line, module, locals: ctx.locals }); if (v) M.consts.set(name, ir(v)); }
|
|
344
|
+
if (v) visit(v, ctx);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function noteReturn(v, ctx) {
|
|
348
|
+
if (!ctx.fn || !v) return;
|
|
349
|
+
const s = valueSpec(v), line = v.startPosition.row + 1;
|
|
350
|
+
if (s?.t === 'new') ts.returns.push({ fn: ctx.fn, kind: 'new', expr: s.expr, guards: ctx.guards, line, module, locals: ctx.locals, types: ctx.types });
|
|
351
|
+
else if (s?.t === 'call') ts.returns.push({ fn: ctx.fn, kind: 'call', expr: s.expr, guards: ctx.guards, line, module, locals: ctx.locals, types: ctx.types });
|
|
352
|
+
else if (v.type === 'identifier') ts.returns.push({ fn: ctx.fn, kind: 'name', name: v.text, guards: ctx.guards, line, module, locals: ctx.locals, types: ctx.types });
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/* react-router route tables: objects with `path`/`index` and an element in an array, and <Route> elements */
|
|
356
|
+
const elementName = n => {
|
|
357
|
+
if (!n) return null;
|
|
358
|
+
if (n.type === 'jsx_expression') return elementName(n.namedChildren[0]);
|
|
359
|
+
if (n.type === 'jsx_self_closing_element') return n.childForFieldName('name')?.text || null;
|
|
360
|
+
if (n.type === 'jsx_element') return n.childForFieldName('open_tag')?.childForFieldName('name')?.text || null;
|
|
361
|
+
if (n.type === 'identifier') return n.text;
|
|
362
|
+
return null;
|
|
363
|
+
};
|
|
364
|
+
const joinPath = (a, b) => b == null ? a : b.startsWith('/') ? b : `${a.replace(/\/$/, '')}/${b}`;
|
|
365
|
+
function routeObjects(arr, base, rule) {
|
|
366
|
+
for (const o of arr.namedChildren.filter(x => x.type === 'object')) {
|
|
367
|
+
const get = k => o.namedChildren.find(p => p.type === 'pair' && p.childForFieldName('key').text === k)?.childForFieldName('value');
|
|
368
|
+
const pathN = get('path'), path = pathN ? strOf(pathN) : null, isIndex = get('index')?.text === 'true';
|
|
369
|
+
const full = path != null ? joinPath(base, path) : base;
|
|
370
|
+
const el = elementName(get('element')) || elementName(get('Component')) || elementName(get('component'));
|
|
371
|
+
const lazyN = get('lazy'), lazyCall = lazyN?.descendantsOfType('call_expression').find(c => c.childForFieldName('function').type === 'import');
|
|
372
|
+
const lazySrc = lazyCall ? strOf(lazyCall.childForFieldName('arguments').namedChildren[0]) : null;
|
|
373
|
+
if ((path != null || isIndex) && (el || lazySrc)) ts.routeDefs.push({ file: rel, line: o.startPosition.row + 1, path: full || '/', comp: el, lazy: lazySrc, rule });
|
|
374
|
+
const kids = get('children');
|
|
375
|
+
if (kids?.type === 'array') routeObjects(kids, full, rule);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
const isRouteTable = arr => arr.namedChildren.some(o => o.type === 'object' && o.namedChildren.some(p => p.type === 'pair' && /^(path|index)$/.test(p.childForFieldName('key').text))
|
|
379
|
+
&& o.namedChildren.some(p => p.type === 'pair' && /^(element|Component|component|lazy|children)$/.test(p.childForFieldName('key').text)));
|
|
380
|
+
function jsxRoute(n) {
|
|
381
|
+
const open = n.type === 'jsx_element' ? n.childForFieldName('open_tag') : n;
|
|
382
|
+
if (open?.childForFieldName('name')?.text !== 'Route') return;
|
|
383
|
+
const attr = (o, k) => o.namedChildren.find(a => a.type === 'jsx_attribute' && a.namedChildren[0]?.text === k);
|
|
384
|
+
const attrVal = (o, k) => attr(o, k)?.namedChildren[1];
|
|
385
|
+
let base = '';
|
|
386
|
+
for (let p = n.parent; p; p = p.parent) if (p.type === 'jsx_element' && p.childForFieldName('open_tag')?.childForFieldName('name')?.text === 'Route') {
|
|
387
|
+
const pp = attrVal(p.childForFieldName('open_tag'), 'path'); const s = pp && (strOf(pp) ?? strOf(pp.namedChildren?.[0])); if (s != null) base = joinPath(s, base || null) || s; }
|
|
388
|
+
const pv = attrVal(open, 'path'), path = pv ? (strOf(pv) ?? strOf(pv.namedChildren?.[0])) : null;
|
|
389
|
+
const el = elementName(attrVal(open, 'element')) || elementName(attrVal(open, 'Component')) || elementName(attrVal(open, 'component'));
|
|
390
|
+
if ((path != null || attr(open, 'index')) && el) ts.routeDefs.push({ file: rel, line: n.startPosition.row + 1, path: (path != null ? joinPath(base, path) : base) || '/', comp: el, rule: 'react-router.jsx' });
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function visitBlock(node, ctx) { // an `if` whose body always exits guards what follows
|
|
394
|
+
let guards = ctx.guards;
|
|
395
|
+
for (const c of node.namedChildren) {
|
|
396
|
+
visit(c, { ...ctx, guards });
|
|
397
|
+
if (c.type === 'if_statement' && !c.childForFieldName('alternative')) {
|
|
398
|
+
const body = c.childForFieldName('consequence'), last = body?.type === 'statement_block' ? body.namedChildren.at(-1) : body;
|
|
399
|
+
if (last && /return_statement|throw_statement/.test(last.type)) guards = [...guards, { text: cond(c), neg: true }];
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
const cond = c => c.childForFieldName('condition').text.replace(/^\(|\)$/g, '');
|
|
404
|
+
const argInfo = (a, i) => ({ type: a.type, text: a.text.length > 200 ? a.text.slice(0, 200) : a.text, ...(i < 2 ? { ir: ir(a) } : {}),
|
|
405
|
+
...(a.type === 'identifier' ? { name: a.text } : {}), ...(a.type === 'member_expression' ? { member: a.text } : {}) });
|
|
406
|
+
|
|
407
|
+
function call(node, ctx) {
|
|
408
|
+
const fnNode = node.childForFieldName('function');
|
|
409
|
+
const argsN = node.childForFieldName('arguments');
|
|
410
|
+
const args = argsN?.type === 'arguments' ? argsN.namedChildren.filter(a => a.type !== 'comment') : [];
|
|
411
|
+
const from = ctx.fn || MOD;
|
|
412
|
+
const line = node.startPosition.row + 1;
|
|
413
|
+
const expr = fnNode.text;
|
|
414
|
+
// test API in a test-group file: `describe(title, fn)` scopes, `it/test(title, fn)` and hooks are symbols
|
|
415
|
+
const head = fnNode.type === 'call_expression' ? fnNode.childForFieldName('function') : fnNode; // `it.each([...])(title, fn)`
|
|
416
|
+
const base = head.type === 'member_expression' ? head.childForFieldName('object').text : head.text;
|
|
417
|
+
const fnArg = args.find(a => FUNC.has(a.type));
|
|
418
|
+
if (group === 'test' && fnArg && (TEST_API.test(base) || HOOK_API.test(base)) && !ctx.fn) {
|
|
419
|
+
const titleN = args[0].id !== fnArg.id ? args[0] : null;
|
|
420
|
+
const title = safeTitle(titleN ? (strOf(titleN) ?? titleN.text) : base);
|
|
421
|
+
const modifiers = head.type === 'member_expression' ? head.childForFieldName('property').text : null;
|
|
422
|
+
if (/^(describe|suite|context)$/.test(base)) {
|
|
423
|
+
ts.testCalls.push({ kind: 'describe', file: rel, line, title, describe: ctx.describe || [] });
|
|
424
|
+
visitFnBody(fnArg, { ...ctx, describe: [...(ctx.describe || []), title], locals: new Map(ctx.locals), types: new Map(ctx.types), consts: new Map(ctx.consts) });
|
|
425
|
+
} else if (HOOK_API.test(base)) {
|
|
426
|
+
// `let cart; beforeEach(() => { cart = new Cart(); })`: what a hook assigns types the variable for the tests after it
|
|
427
|
+
const id = defineFn(fnArg, [...(ctx.describe || []), base].join(' > '), { ...ctx, describe: [] }, { extra: { noBind: true, test: true, outerTypes: ctx.types, sym: { hook: base, scope: ctx.describe || [] } } });
|
|
428
|
+
ts.testCalls.push({ kind: 'hook', hook: base, id, file: rel, line, describe: ctx.describe || [] });
|
|
429
|
+
} else {
|
|
430
|
+
const id = defineFn(fnArg, safeTitle([...(ctx.describe || []), title].join(' > ')), { ...ctx, describe: [] },
|
|
431
|
+
{ extra: { noBind: true, test: true, sym: { test: true, title, describe: ctx.describe || [], ...(modifiers && modifiers !== 'each' ? { modifier: modifiers } : {}) } } });
|
|
432
|
+
ts.testCalls.push({ kind: 'test', id, file: rel, line, title, describe: ctx.describe || [] });
|
|
433
|
+
}
|
|
434
|
+
for (const a of args) if (a.id !== fnArg.id) visit(a, ctx);
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
if (/^(jest|vi)\.(mock|doMock|unmock|spyOn)$/.test(expr)) {
|
|
438
|
+
const how = expr;
|
|
439
|
+
const a1 = args[1];
|
|
440
|
+
const factory = a1 && FUNC.has(a1.type) ? a1 : null;
|
|
441
|
+
const objN = factory ? (factory.childForFieldName('body')?.type === 'object' ? factory.childForFieldName('body')
|
|
442
|
+
: factory.childForFieldName('body')?.type === 'parenthesized_expression' ? factory.childForFieldName('body').namedChildren[0]
|
|
443
|
+
: factory.childForFieldName('body')?.descendantsOfType('return_statement')[0]?.namedChildren[0]) : null;
|
|
444
|
+
const keys = objN?.type === 'object' ? objN.namedChildren.map(p => p.type === 'pair' ? p.childForFieldName('key').text : p.type === 'method_definition' ? p.childForFieldName('name').text : p.type === 'shorthand_property_identifier' ? p.text : null).filter(Boolean) : null;
|
|
445
|
+
const partial = objN?.type === 'object' && objN.namedChildren.some(p => p.type === 'spread_element');
|
|
446
|
+
const replaced = /spyOn$/.test(how) ? /^mock(Implementation|ReturnValue|ResolvedValue|RejectedValue)(Once)?$/.test(node.parent?.type === 'member_expression' ? node.parent.childForFieldName('property').text : '') : true;
|
|
447
|
+
ts.mockCalls.push({ fn: from, file: rel, line, how, spec: strOf(args[0]), obj: /spyOn$/.test(how) ? args[0]?.text : null, prop: /spyOn$/.test(how) ? strOf(args[1]) : null,
|
|
448
|
+
keys: partial ? keys : null, replaced, module, locals: ctx.locals, types: ctx.types });
|
|
449
|
+
}
|
|
450
|
+
// an inline route handler (`app.get('/x', (req, res) => …)`) is a symbol named for its route
|
|
451
|
+
const routeArg = fnNode.type === 'member_expression' && ROUTE_REG.test(fnNode.childForFieldName('property').text) ? args[0] : null;
|
|
452
|
+
const routeName = routeArg && (routeArg.type === 'string' || routeArg.type === 'template_string') && strOf(routeArg) != null
|
|
453
|
+
? `${fnNode.childForFieldName('property').text.toUpperCase()} ${strOf(routeArg).replace(/:(\w+)/g, '{$1}')}` : null;
|
|
454
|
+
// `r.route(path).get(h).post(h2)`: down the chain of method calls to the `.route(path)` they hang off
|
|
455
|
+
let routeChain = null;
|
|
456
|
+
if (fnNode.type === 'member_expression' && ROUTE_REG.test(fnNode.childForFieldName('property').text)) {
|
|
457
|
+
let o = fnNode.childForFieldName('object');
|
|
458
|
+
while (o?.type === 'call_expression' && o.childForFieldName('function').type === 'member_expression' && ROUTE_REG.test(o.childForFieldName('function').childForFieldName('property').text))
|
|
459
|
+
o = o.childForFieldName('function').childForFieldName('object');
|
|
460
|
+
const f = o?.type === 'call_expression' ? o.childForFieldName('function') : null;
|
|
461
|
+
if (f?.type === 'member_expression' && f.childForFieldName('property').text === 'route') {
|
|
462
|
+
const a0 = o.childForFieldName('arguments').namedChildren[0];
|
|
463
|
+
routeChain = { owner: f.childForFieldName('object').text, path: a0 ? ir(a0) : null, s: strOf(a0) };
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
const handlerName = routeName || (routeChain?.s != null ? `${fnNode.childForFieldName('property').text.toUpperCase()} ${routeChain.s.replace(/:(\w+)/g, '{$1}')}` : null);
|
|
467
|
+
const info = args.map(argInfo);
|
|
468
|
+
// a handler named for its route still runs from here when no rule pack claims the call as a route (the call's index)
|
|
469
|
+
if (handlerName) args.forEach((a, i) => { if (FUNC.has(a.type)) { info[i].fnId = defineFn(a, handlerName, ctx, { extra: { noBind: true } });
|
|
470
|
+
ts.handed.push({ from, to: info[i].fnId, line, file: rel, how: expr, call: ts.rawCalls.length }); } });
|
|
471
|
+
const obj = fnNode.type === 'member_expression' ? fnNode.childForFieldName('object') : null;
|
|
472
|
+
ts.rawCalls.push({ from, module, file: rel, expr, line, attr: obj ? fnNode.childForFieldName('property').text : null, obj: obj?.text ?? null,
|
|
473
|
+
objCall: obj?.type === 'call_expression' ? { expr: obj.childForFieldName('function').text, args: (obj.childForFieldName('arguments')?.namedChildren || []).map(argInfo) } : null,
|
|
474
|
+
objNew: obj?.type === 'new_expression' ? obj.childForFieldName('constructor').text : null, ...(routeChain ? { routeChain } : {}),
|
|
475
|
+
locals: ctx.locals, types: ctx.types, consts: ctx.consts, cls: ctx.cls, guards: ctx.guards, onFailure: ctx.onFailure, top: !ctx.fn, args: info });
|
|
476
|
+
for (const [i, a] of args.entries()) if ((a.type === 'identifier' || a.type === 'member_expression') && !/^(this|undefined|null)$/.test(a.text))
|
|
477
|
+
ts.handed.push({ from, name: a.text, line, file: rel, callee: expr, argIndex: i, call: ts.rawCalls.length - 1, module, locals: ctx.locals, types: ctx.types, cls: ctx.cls });
|
|
478
|
+
if (fnNode.type !== 'identifier' && fnNode.type !== 'member_expression') visit(fnNode, ctx);
|
|
479
|
+
else if (obj) visit(obj, ctx);
|
|
480
|
+
args.forEach((a, i) => { if (!info[i].fnId) visit(a, ctx); });
|
|
481
|
+
}
|
|
482
|
+
function visitFnBody(f, ctx) { const b = f.childForFieldName('body'); if (!b) return; if (b.type === 'statement_block') visitBlock(b, ctx); else visit(b, ctx); }
|
|
483
|
+
|
|
484
|
+
function jsx(node, ctx) {
|
|
485
|
+
const nameN = node.type === 'jsx_element' ? node.childForFieldName('open_tag')?.childForFieldName('name') : node.childForFieldName('name');
|
|
486
|
+
const open = node.type === 'jsx_element' ? node.childForFieldName('open_tag') : node;
|
|
487
|
+
if (M.importsRouter) jsxRoute(node);
|
|
488
|
+
if (nameN && /^[A-Z]/.test(nameN.text.split('.').pop()) && nameN.text !== 'Route')
|
|
489
|
+
ts.rawCalls.push({ from: ctx.fn || MOD, module, file: rel, expr: nameN.text, line: node.startPosition.row + 1, attr: null, obj: null, jsx: true,
|
|
490
|
+
locals: ctx.locals, types: ctx.types, consts: ctx.consts, cls: ctx.cls, guards: ctx.guards, onFailure: ctx.onFailure, top: !ctx.fn, args: [] });
|
|
491
|
+
for (const a of open?.namedChildren.filter(x => x.type === 'jsx_attribute') || []) {
|
|
492
|
+
const v = a.namedChildren[1]?.type === 'jsx_expression' ? a.namedChildren[1].namedChildren[0] : null;
|
|
493
|
+
if (v && (v.type === 'identifier' || v.type === 'member_expression') && /^on[A-Z]|^(action|loader|onSubmit|handler)$/.test(a.namedChildren[0].text))
|
|
494
|
+
ts.handed.push({ from: ctx.fn || MOD, name: v.text, line: a.startPosition.row + 1, file: rel, callee: `<${nameN?.text || '>'} ${a.namedChildren[0].text}>`, jsx: true, module, locals: ctx.locals, types: ctx.types, cls: ctx.cls });
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function visit(node, ctx) {
|
|
499
|
+
switch (node.type) {
|
|
500
|
+
case 'comment': return;
|
|
501
|
+
case 'statement_block': visitBlock(node, ctx); return;
|
|
502
|
+
case 'import_statement': addImport(node); return;
|
|
503
|
+
case 'export_statement': addExport(node, ctx); return;
|
|
504
|
+
case 'function_declaration': case 'generator_function_declaration': {
|
|
505
|
+
const n = node.childForFieldName('name');
|
|
506
|
+
return defineFn(node, n ? n.text : 'default', ctx, { counts: !!n });
|
|
507
|
+
}
|
|
508
|
+
case 'class_declaration': case 'abstract_class_declaration': {
|
|
509
|
+
const n = node.childForFieldName('name');
|
|
510
|
+
return defineClass(node, n ? n.text : 'default', ctx, !!n);
|
|
511
|
+
}
|
|
512
|
+
case 'class': // a class expression anywhere else (`register(class Foo {…})`): its methods are still symbols
|
|
513
|
+
return defineClass(node, node.childForFieldName('name')?.text || 'class', ctx, false);
|
|
514
|
+
case 'lexical_declaration': case 'variable_declaration': {
|
|
515
|
+
let id = null; for (const d of node.namedChildren.filter(c => c.type === 'variable_declarator')) id = declarator(d, ctx) || id;
|
|
516
|
+
return id;
|
|
517
|
+
}
|
|
518
|
+
case 'expression_statement': {
|
|
519
|
+
const e = node.namedChildren[0];
|
|
520
|
+
if (e?.type === 'assignment_expression') { assignment(e, ctx); return; }
|
|
521
|
+
break;
|
|
522
|
+
}
|
|
523
|
+
case 'if_statement': {
|
|
524
|
+
const c = cond(node);
|
|
525
|
+
if (!ctx.fn && /require\.main\s*===?\s*module|import\.meta\.main\b|import\.meta\.url\s*===?.*process\.argv\[1\]|process\.argv\[1\].*===?\s*import\.meta\.url|fileURLToPath\(\s*import\.meta\.url\s*\)\s*===?\s*process\.argv\[1\]/.test(c)) {
|
|
526
|
+
const id = `${module}:<main>`, start = node.startPosition.row + 1, end = node.endPosition.row + 1;
|
|
527
|
+
ts.symbols.set(id, { id, group, kind: 'main', file: rel, start, end, name: '<main>', module, lang, text: spanText(start, end) });
|
|
528
|
+
ts.mains.push(id);
|
|
529
|
+
visit(node.childForFieldName('consequence'), { ...ctx, fn: id, qual: '<main>', locals: new Map(ctx.locals), types: new Map(), consts: new Map(ctx.consts), guards: [] });
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
visit(node.childForFieldName('condition'), ctx);
|
|
533
|
+
const g0 = ctx.guards || [];
|
|
534
|
+
visit(node.childForFieldName('consequence'), { ...ctx, guards: [...g0, { text: c, neg: false }] });
|
|
535
|
+
const alt = node.childForFieldName('alternative');
|
|
536
|
+
if (alt) visit(alt, { ...ctx, guards: [...g0, { text: c, neg: true }] });
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
case 'catch_clause':
|
|
540
|
+
for (const c of node.namedChildren) visit(c, { ...ctx, onFailure: 'catch' + (node.childForFieldName('parameter') ? ` (${node.childForFieldName('parameter').text})` : '') });
|
|
541
|
+
return;
|
|
542
|
+
case 'return_statement': noteReturn(node.namedChildren[0], ctx); break;
|
|
543
|
+
case 'call_expression': call(node, ctx); return;
|
|
544
|
+
case 'new_expression': {
|
|
545
|
+
const ctor = node.childForFieldName('constructor');
|
|
546
|
+
const argsN = node.childForFieldName('arguments');
|
|
547
|
+
ts.rawCalls.push({ from: ctx.fn || MOD, module, file: rel, expr: ctor.text, line: node.startPosition.row + 1, attr: null, obj: null, isNew: true,
|
|
548
|
+
locals: ctx.locals, types: ctx.types, consts: ctx.consts, cls: ctx.cls, guards: ctx.guards, onFailure: ctx.onFailure, top: !ctx.fn,
|
|
549
|
+
args: (argsN?.namedChildren || []).filter(a => a.type !== 'comment').map(argInfo) });
|
|
550
|
+
if (ctor.type !== 'identifier' && ctor.type !== 'member_expression') visit(ctor, ctx); // `new (class extends X {…})()`
|
|
551
|
+
if (argsN) visit(argsN, ctx);
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
case 'jsx_element': case 'jsx_self_closing_element': jsx(node, ctx); break;
|
|
555
|
+
case 'array':
|
|
556
|
+
if ((M.importsRouter) && isRouteTable(node) && !node.parent?.type?.startsWith('pair')) { routeObjects(node, '', 'react-router.object'); }
|
|
557
|
+
break;
|
|
558
|
+
case 'member_expression': case 'subscript_expression': {
|
|
559
|
+
const m = ENV.exec(node.text) || ENV_SUB.exec(node.text);
|
|
560
|
+
if (m) {
|
|
561
|
+
const p = node.parent;
|
|
562
|
+
const dflt = p?.type === 'binary_expression' && /^(\?\?|\|\|)$/.test(p.childForFieldName('operator')?.text) && p.childForFieldName('left')?.id === node.id ? strOf(p.childForFieldName('right')) : null;
|
|
563
|
+
// a read that tests the env names it a guard key
|
|
564
|
+
ts.envReads.push({ key: m[1], file: rel, line: node.startPosition.row + 1, group, ...(dflt != null ? { default: dflt } : {}), via: node.text.startsWith('import.meta') ? 'import.meta.env' : 'process.env' });
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
break;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
for (const c of node.namedChildren) visit(c, ctx);
|
|
571
|
+
}
|
|
572
|
+
function assignment(e, ctx) {
|
|
573
|
+
const l = e.childForFieldName('left'), r = e.childForFieldName('right');
|
|
574
|
+
const lt = l.text;
|
|
575
|
+
if (/^module\.exports$/.test(lt)) {
|
|
576
|
+
M.cjsExports = true;
|
|
577
|
+
if (r.type === 'identifier') M.defaultExport = { local: r.text, cjs: true };
|
|
578
|
+
else if (r.type === 'object') { const mem = defineObject(r, 'module.exports', { ...ctx, fn: null }); for (const [k, v] of mem) M.exports.set(k, typeof v === 'string' ? { id: v } : v); M.defaultExport = { cjsObject: true }; }
|
|
579
|
+
else if (FUNC.has(r.type)) M.defaultExport = { id: defineFn(r, r.childForFieldName('name')?.text || 'module.exports', ctx, {}), cjs: true };
|
|
580
|
+
else if (r.type === 'class') M.defaultExport = { id: defineClass(r, r.childForFieldName('name')?.text || 'module.exports', ctx, false), cjs: true };
|
|
581
|
+
else { const req = requireOf(r); if (req) M.starExports.push(req.source); else visit(r, ctx); }
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
const ex = /^(?:module\.)?exports\.(\w+)$/.exec(lt);
|
|
585
|
+
if (ex) {
|
|
586
|
+
M.cjsExports = true;
|
|
587
|
+
if (r.type === 'identifier') M.exports.set(ex[1], { local: r.text });
|
|
588
|
+
else if (FUNC.has(r.type)) M.exports.set(ex[1], { id: defineFn(r, ex[1], ctx, { extra: { noBind: false } }) });
|
|
589
|
+
else if (r.type === 'class') M.exports.set(ex[1], { id: defineClass(r, r.childForFieldName('name')?.text || ex[1], ctx, false) });
|
|
590
|
+
else { const req = requireOf(r); if (req) M.exports.set(ex[1], req); else visit(r, ctx); }
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
if (l.type === 'member_expression' && l.childForFieldName('object').type === 'this' && ctx.cls) {
|
|
594
|
+
const nm = l.childForFieldName('property').text;
|
|
595
|
+
const spec = valueSpec(r) || (r.type === 'identifier' && ctx.types.get(r.text)) || null;
|
|
596
|
+
addAttr(ctx.cls, nm, spec, ctx);
|
|
597
|
+
}
|
|
598
|
+
if (l.type === 'identifier' && ctx.fn) { const s = valueSpec(r); if (s) { ctx.types.set(l.text, s); if (ctx.outerTypes && !ctx.declared?.has(l.text)) ctx.outerTypes.set(l.text, s); if (s.t !== 'annot') (ts.fnAssigns.get(ctx.fn) || ts.fnAssigns.set(ctx.fn, new Map()).get(ctx.fn)).set(l.text, { ...s, guards: ctx.guards, module, locals: ctx.locals, types: ctx.types }); } }
|
|
599
|
+
if (FUNC.has(r.type)) { defineFn(r, lt.replace(/^this\./, ''), ctx, { extra: { noBind: true } }); visit(l, ctx); return; }
|
|
600
|
+
visit(l, ctx); visit(r, ctx);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
visitBlock(root, { fn: null, qual: null, cls: null, inClass: false, locals: new Map(), types: new Map(), consts: new Map(), guards: [], onFailure: null, describe: null });
|
|
604
|
+
ts.defScopes += quals.size;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/* ---------------- module resolution (graphify's order, extractors/resolution.py) ---------------- */
|
|
608
|
+
|
|
609
|
+
const stripJsonc = t => t.replace(/"(?:\\.|[^"\\])*"|\/\*[\s\S]*?\*\/|\/\/[^\n]*/g, m => m.startsWith('"') ? m : '').replace(/,(\s*[}\]])/g, '$1');
|
|
610
|
+
const parseJsonc = t => { try { return JSON.parse(stripJsonc(t)); } catch { return null; } };
|
|
611
|
+
|
|
612
|
+
export function tsModuleResolver({ files, readText }) {
|
|
613
|
+
const fileSet = new Set(files);
|
|
614
|
+
const dirs = new Set(); for (const f of fileSet) { let d = posix.dirname(f); while (d !== '.' && !dirs.has(d)) { dirs.add(d); d = posix.dirname(d); } }
|
|
615
|
+
const norm = p => { const n = posix.normalize(p); return n === '.' ? '' : n.replace(/^\.\//, ''); };
|
|
616
|
+
const probe = cand => {
|
|
617
|
+
cand = norm(cand);
|
|
618
|
+
if (cand.startsWith('..')) return null;
|
|
619
|
+
if (fileSet.has(cand)) return cand;
|
|
620
|
+
const ext = posix.extname(cand);
|
|
621
|
+
if (/^\.(js|jsx|mjs|cjs)$/.test(ext)) for (const t of { '.js': ['.ts', '.tsx'], '.jsx': ['.tsx'], '.mjs': ['.mts'], '.cjs': ['.cts'] }[ext])
|
|
622
|
+
if (fileSet.has(cand.slice(0, -ext.length) + t)) return cand.slice(0, -ext.length) + t;
|
|
623
|
+
for (const e of EXTS) if (fileSet.has(cand + e)) return cand + e;
|
|
624
|
+
if (dirs.has(cand) || cand === '') for (const i of INDEX) { const p = cand ? `${cand}/${i}` : i; if (fileSet.has(p)) return p; }
|
|
625
|
+
return null;
|
|
626
|
+
};
|
|
627
|
+
const read = rel => { try { return readText(rel); } catch { return null; } };
|
|
628
|
+
// the nearest tsconfig.json / jsconfig.json up from a dir, with `extends` followed (relative ones only)
|
|
629
|
+
const configMemo = new Map();
|
|
630
|
+
function configFor(dir) {
|
|
631
|
+
if (configMemo.has(dir)) return configMemo.get(dir);
|
|
632
|
+
let found = null;
|
|
633
|
+
for (let d = dir; ; d = posix.dirname(d)) {
|
|
634
|
+
for (const name of ['tsconfig.json', 'jsconfig.json']) { const p = d === '.' || d === '' ? name : `${d}/${name}`; const t = read(p); if (t != null) { found = loadConfig(p, new Set()); break; } }
|
|
635
|
+
if (found || d === '.' || d === '') break;
|
|
636
|
+
}
|
|
637
|
+
configMemo.set(dir, found);
|
|
638
|
+
return found;
|
|
639
|
+
}
|
|
640
|
+
function loadConfig(p, seen) {
|
|
641
|
+
if (seen.has(p)) return null; seen.add(p);
|
|
642
|
+
const j = parseJsonc(read(p) || ''); if (!j) return null;
|
|
643
|
+
const dir = posix.dirname(p) === '.' ? '' : posix.dirname(p);
|
|
644
|
+
let out = { paths: {}, baseUrl: null };
|
|
645
|
+
const ext = typeof j.extends === 'string' && j.extends.startsWith('.') ? norm(posix.join(dir, j.extends.endsWith('.json') ? j.extends : `${j.extends}.json`)) : null;
|
|
646
|
+
if (ext) { const b = loadConfig(ext, seen); if (b) out = { paths: { ...b.paths }, baseUrl: b.baseUrl }; }
|
|
647
|
+
const co = j.compilerOptions || {};
|
|
648
|
+
if (typeof co.baseUrl === 'string') out.baseUrl = norm(posix.join(dir, co.baseUrl));
|
|
649
|
+
if (co.paths && typeof co.paths === 'object') { const base = out.baseUrl ?? dir; for (const [k, v] of Object.entries(co.paths)) if (Array.isArray(v)) out.paths[k] = v.map(t => norm(posix.join(base, t))); }
|
|
650
|
+
return out;
|
|
651
|
+
}
|
|
652
|
+
// workspace packages: a package.json with a name, mapped to its directory and entry fields
|
|
653
|
+
const packages = new Map();
|
|
654
|
+
for (const f of files.length ? [...new Set([...fileSet].map(f => posix.dirname(f)))].concat(['.']) : ['.']) {
|
|
655
|
+
let d = f; while (true) { const p = d === '.' ? 'package.json' : `${d}/package.json`; if (!packages.has(p)) { const j = parseJsonc(read(p) || ''); packages.set(p, j); } if (d === '.') break; d = posix.dirname(d); }
|
|
656
|
+
}
|
|
657
|
+
const byName = new Map(); for (const [p, j] of packages) if (j?.name) byName.set(j.name, { dir: posix.dirname(p) === '.' ? '' : posix.dirname(p), j });
|
|
658
|
+
const hasConfigAnywhere = dir => !!configFor(dir);
|
|
659
|
+
const memo = new Map();
|
|
660
|
+
function resolveModule(spec, fromRel) {
|
|
661
|
+
const key = spec + '\0' + fromRel;
|
|
662
|
+
if (memo.has(key)) return memo.get(key);
|
|
663
|
+
let r = null;
|
|
664
|
+
const dir = posix.dirname(fromRel) === '.' ? '' : posix.dirname(fromRel);
|
|
665
|
+
if (spec.startsWith('.')) r = probe(posix.join(dir, spec));
|
|
666
|
+
else if (spec.startsWith('/')) r = null;
|
|
667
|
+
else {
|
|
668
|
+
const cfg = configFor(dir);
|
|
669
|
+
if (cfg) {
|
|
670
|
+
let best = null;
|
|
671
|
+
for (const [pat, targets] of Object.entries(cfg.paths)) {
|
|
672
|
+
let spec2 = null, score = null;
|
|
673
|
+
if (pat.includes('*')) { const [pre, suf] = pat.split('*'); if (spec.startsWith(pre) && spec.endsWith(suf) && spec.length >= pre.length + suf.length) { spec2 = spec.slice(pre.length, spec.length - suf.length); score = [1, -pre.length]; } }
|
|
674
|
+
else if (spec === pat) { spec2 = ''; score = [0, -pat.length]; }
|
|
675
|
+
if (score && (!best || score[0] < best.score[0] || (score[0] === best.score[0] && score[1] < best.score[1]))) best = { score, spec2, targets };
|
|
676
|
+
}
|
|
677
|
+
if (best) for (const t of best.targets) { const hit = probe(t.replace('*', best.spec2)); if (hit) { r = hit; break; } }
|
|
678
|
+
if (!r && !best && cfg.baseUrl != null) r = probe(posix.join(cfg.baseUrl, spec));
|
|
679
|
+
}
|
|
680
|
+
if (!r) { // a workspace package
|
|
681
|
+
const parts = spec.split('/'), name = spec.startsWith('@') ? parts.slice(0, 2).join('/') : parts[0], sub = spec.slice(name.length + 1);
|
|
682
|
+
const pkg = byName.get(name);
|
|
683
|
+
if (pkg) {
|
|
684
|
+
const at = p => probe(pkg.dir ? `${pkg.dir}/${p}` : p);
|
|
685
|
+
if (sub) r = at(sub) || at(`src/${sub}`);
|
|
686
|
+
else for (const f of [pkg.j.source, pkg.j.module, pkg.j.main, pkg.j.types, 'src/index', 'index']) if (typeof f === 'string' && (r = at(f.replace(/^\.\//, '')))) break;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
if (!r && spec.startsWith('@/') && !hasConfigAnywhere(dir)) { // the unmapped `@/` -> src/ convention
|
|
690
|
+
let anchor = dir; while (anchor && !packages.get(`${anchor}/package.json`)) anchor = posix.dirname(anchor) === '.' ? '' : posix.dirname(anchor);
|
|
691
|
+
r = probe(posix.join(anchor, 'src', spec.slice(2))) || probe(posix.join(anchor, spec.slice(2)));
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
memo.set(key, r);
|
|
695
|
+
return r;
|
|
696
|
+
}
|
|
697
|
+
return { resolveModule, packages: byName };
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/* ---------------- name, member and receiver resolution ---------------- */
|
|
701
|
+
|
|
702
|
+
export function tsResolverFor(ts, mods) {
|
|
703
|
+
const { modules, classes, symbols } = ts;
|
|
704
|
+
const { resolveModule } = mods;
|
|
705
|
+
const isCls = id => typeof id === 'string' && classes.has(id);
|
|
706
|
+
function noteUnresolvedImport(spec, from) { if (!spec.startsWith('.')) return; const k = `${from}\0${spec}`; ts.unresolvedImports.set(k, { from, spec }); }
|
|
707
|
+
// what a module exports under a name: { id } | { module } | { obj } | { var } | { ext } | null
|
|
708
|
+
function exportOf(rel, name, depth = 0) {
|
|
709
|
+
const M = modules.get(rel);
|
|
710
|
+
if (!M || depth > 8) return null;
|
|
711
|
+
if (name === '*') return M.defaultExport?.cjs && !M.exports.size ? local(M, M.defaultExport, depth) : { module: rel };
|
|
712
|
+
if (name === 'default') {
|
|
713
|
+
if (M.defaultExport && !M.defaultExport.cjsObject) return local(M, M.defaultExport, depth);
|
|
714
|
+
return M.cjsExports ? { module: rel } : null;
|
|
715
|
+
}
|
|
716
|
+
const e = M.exports.get(name);
|
|
717
|
+
if (e) return local(M, e, depth);
|
|
718
|
+
for (const s of M.starExports) { const t = resolveModule(s, rel); const r = t && exportOf(t, name, depth + 1); if (r) return r; }
|
|
719
|
+
if (M.defaultExport?.cjs) { const d = local(M, M.defaultExport, depth); if (d?.id && isCls(d.id)) { const sm = lookup(d.id, name); if (sm) return { id: sm }; } }
|
|
720
|
+
return null;
|
|
721
|
+
}
|
|
722
|
+
function local(M, e, depth) {
|
|
723
|
+
if (e.id) return { id: e.id };
|
|
724
|
+
if (e.source) { const t = resolveModule(e.source, M.file); if (!t) { noteUnresolvedImport(e.source, M.file); return { ext: e.source + (e.name && e.name !== '*' && e.name !== 'default' ? '.' + e.name : '') }; } return exportOf(t, e.name, depth + 1); }
|
|
725
|
+
if (e.local) return resolveName(e.local, { module: M.file, locals: null }, depth + 1);
|
|
726
|
+
return null;
|
|
727
|
+
}
|
|
728
|
+
function resolveName(name, c, depth = 0) {
|
|
729
|
+
if (depth > 10) return null;
|
|
730
|
+
if (c.locals && c.locals.has(name)) return { id: c.locals.get(name) };
|
|
731
|
+
const M = modules.get(c.module);
|
|
732
|
+
if (!M) return null;
|
|
733
|
+
if (M.defs.has(name)) return { id: M.defs.get(name) };
|
|
734
|
+
if (M.objects.has(name)) return { obj: M.objects.get(name), in: c.module, name };
|
|
735
|
+
if (M.imports.has(name)) {
|
|
736
|
+
const t = M.imports.get(name), target = resolveModule(t.source, c.module);
|
|
737
|
+
if (!target) { noteUnresolvedImport(t.source, c.module); return { ext: t.source + (t.name !== '*' && t.name !== 'default' ? '.' + t.name : '') }; }
|
|
738
|
+
return exportOf(target, t.name, depth + 1) || { missing: `${target}:${t.name}` };
|
|
739
|
+
}
|
|
740
|
+
if (M.vars.has(name)) return { var: M.vars.get(name), in: c.module, name };
|
|
741
|
+
return null;
|
|
742
|
+
}
|
|
743
|
+
const resolveExpr = (expr, c) => { // `K`, `ns.K`, `obj.member`
|
|
744
|
+
if (!expr) return null;
|
|
745
|
+
const parts = expr.split('.');
|
|
746
|
+
let r = resolveName(parts[0], c);
|
|
747
|
+
for (const p of parts.slice(1)) {
|
|
748
|
+
if (!r) return null;
|
|
749
|
+
if (r.module) r = exportOf(r.module, p);
|
|
750
|
+
else if (r.obj) { const m = r.obj.get(p); r = m ? (typeof m === 'string' ? { id: m } : resolveName(m.local, { module: r.in, locals: null })) : null; }
|
|
751
|
+
else if (r.ext) r = { ext: `${r.ext}.${p}` };
|
|
752
|
+
else if (r.id && isCls(r.id)) { const m = lookup(r.id, p); r = m ? { id: m } : null; }
|
|
753
|
+
else return null;
|
|
754
|
+
}
|
|
755
|
+
return r;
|
|
756
|
+
};
|
|
757
|
+
for (const k of classes.values()) {
|
|
758
|
+
k.bases = k.baseExprs.map(b => resolveExpr(b, k.scope)?.id).filter(isCls);
|
|
759
|
+
k.implementsIds = k.implementsExprs.map(b => resolveExpr(b, k.scope)?.id).filter(isCls);
|
|
760
|
+
}
|
|
761
|
+
const subclasses = new Map();
|
|
762
|
+
for (const k of classes.values()) for (const b of [...k.bases, ...k.implementsIds]) (subclasses.get(b) || subclasses.set(b, []).get(b)).push(k.id);
|
|
763
|
+
const mro = (id, seen = new Set()) => seen.has(id) ? [] : (seen.add(id), [id, ...(classes.get(id)?.bases || []).flatMap(b => mro(b, seen))]);
|
|
764
|
+
function lookup(clsId, m) { for (const c of mro(clsId)) { const x = classes.get(c)?.methods.get(m); if (x) return x; } return null; }
|
|
765
|
+
const overrides = (clsId, m, out = [], seen = new Set()) => { for (const s of subclasses.get(clsId) || []) { if (seen.has(s)) continue; seen.add(s);
|
|
766
|
+
const x = classes.get(s).methods.get(m); if (x) out.push(x); overrides(s, m, out, seen); } return out; };
|
|
767
|
+
|
|
768
|
+
const retMemo = new Map(), returnsOf = new Map();
|
|
769
|
+
for (const r of ts.returns) (returnsOf.get(r.fn) || returnsOf.set(r.fn, []).get(r.fn)).push(r);
|
|
770
|
+
function factoryReturns(fnId, depth = 0) {
|
|
771
|
+
if (retMemo.has(fnId)) return retMemo.get(fnId);
|
|
772
|
+
if (depth > 3) return [];
|
|
773
|
+
retMemo.set(fnId, []);
|
|
774
|
+
const out = [];
|
|
775
|
+
const via = (spec, sc, guards, line) => {
|
|
776
|
+
const r = resolveExpr(spec.expr, sc);
|
|
777
|
+
if (spec.t === 'new') { if (r?.id && isCls(r.id)) out.push({ cls: r.id, guards, line }); else if (r?.ext) out.push({ ext: r.ext, guards, line }); return; }
|
|
778
|
+
if (r?.id && symbols.get(r.id)?.kind !== 'class') for (const x of factoryReturns(r.id, depth + 1)) out.push({ ...x, guards: [...guards, ...x.guards], via: r.id });
|
|
779
|
+
};
|
|
780
|
+
for (const ret of returnsOf.get(fnId) || []) {
|
|
781
|
+
if (ret.kind === 'new' || ret.kind === 'call') via({ t: ret.kind, expr: ret.expr }, ret, ret.guards, ret.line);
|
|
782
|
+
else { const a = ts.fnAssigns.get(fnId)?.get(ret.name); if (a) via(a, a, [...a.guards, ...ret.guards], ret.line); }
|
|
783
|
+
}
|
|
784
|
+
retMemo.set(fnId, out);
|
|
785
|
+
return out;
|
|
786
|
+
}
|
|
787
|
+
const byReturns = fnId => { const fr = factoryReturns(fnId).filter(x => x.cls); const cs = [...new Set(fr.map(x => x.cls))];
|
|
788
|
+
return cs.length ? { mode: cs.length === 1 ? 'ctor' : 'factory', factory: fnId, cls: cs, returns: fr } : null; };
|
|
789
|
+
function receiverOf(spec, sc) {
|
|
790
|
+
if (!spec) return null;
|
|
791
|
+
const r = resolveExpr(spec.expr, sc);
|
|
792
|
+
if (spec.t === 'new') return r?.id && isCls(r.id) ? { mode: 'ctor', cls: [r.id] } : null;
|
|
793
|
+
if (spec.t === 'annot') return r?.id && isCls(r.id) ? { mode: 'annot', cls: [r.id] } : null;
|
|
794
|
+
if (spec.t === 'call' && r?.id && symbols.get(r.id)?.kind !== 'class') return byReturns(r.id);
|
|
795
|
+
return null;
|
|
796
|
+
}
|
|
797
|
+
function attrReceiver(clsId, attr) {
|
|
798
|
+
for (const c of mro(clsId)) { const specs = classes.get(c)?.attrTypes.get(attr); if (!specs) continue;
|
|
799
|
+
const cls = [], facts = [];
|
|
800
|
+
for (const s of specs) { const rc = receiverOf(s, s); if (rc) { cls.push(...rc.cls); facts.push({ mode: rc.mode, cls: rc.cls, factory: rc.factory }); } }
|
|
801
|
+
if (cls.length) { const narrowed = facts.filter(f => f.mode !== 'annot');
|
|
802
|
+
return { mode: narrowed.length && narrowed.length === facts.length ? (new Set(cls).size === 1 ? 'ctor' : 'factory') : narrowed.length ? 'mixed' : 'annot', cls: [...new Set(cls)], facts }; }
|
|
803
|
+
}
|
|
804
|
+
return null;
|
|
805
|
+
}
|
|
806
|
+
return { resolveModule, exportOf, resolveName, resolveExpr, mro, lookup, overrides, subclasses, factoryReturns, receiverOf, attrReceiver };
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
export function addTsEdge(ts, from, to, kind, line, extra = {}) {
|
|
810
|
+
if (!from || !to || from === to) return;
|
|
811
|
+
const e = { from, to, kind, line, ...extra };
|
|
812
|
+
const gf = ts.groupOf(from), gt = ts.groupOf(to);
|
|
813
|
+
if (gf && gt && gf !== gt) e.groups = [gf, gt];
|
|
814
|
+
ts.edges.push(e);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
const BUILTINS = new Set(['console', 'Math', 'JSON', 'Object', 'Array', 'Promise', 'String', 'Number', 'Boolean', 'Date', 'RegExp', 'Error', 'Map', 'Set',
|
|
818
|
+
'WeakMap', 'WeakSet', 'Symbol', 'BigInt', 'Reflect', 'Proxy', 'Intl', 'parseInt', 'parseFloat', 'isNaN', 'isFinite', 'setTimeout', 'clearTimeout',
|
|
819
|
+
'setInterval', 'clearInterval', 'setImmediate', 'queueMicrotask', 'structuredClone', 'encodeURIComponent', 'decodeURIComponent', 'encodeURI', 'decodeURI',
|
|
820
|
+
'fetch', 'require', 'process', 'window', 'document', 'globalThis', 'Buffer', 'URL', 'URLSearchParams', 'TextEncoder', 'TextDecoder', 'AbortController',
|
|
821
|
+
'Response', 'Request', 'Headers', 'FormData', 'Blob', 'alert', 'confirm', 'localStorage', 'sessionStorage', 'navigator', 'location', 'history',
|
|
822
|
+
'super', 'import', 'Function', 'eval', 'atob', 'btoa', 'TypeError', 'RangeError', 'expect', 'describe', 'it', 'test', 'jest', 'vi', 'module', 'exports']);
|
|
823
|
+
// callees that look at a function rather than run it: assertions, mock helpers, logging
|
|
824
|
+
const NOT_RUN = /^(expect|assert(\.\w+)?|(jest|vi)\.\w+|console\.\w+|JSON\.stringify|Object\.\w+|typeof)$|^expect\(/;
|
|
825
|
+
const CFG_GUARD = /process\.env|import\.meta\.env|\bconfig\.|\bsettings\.|\benv\.|isProd|isDev|NODE_ENV/;
|
|
826
|
+
|
|
827
|
+
/* resolve every recorded call into edges. `skipHanded`: calls whose function arguments a rule pack has claimed
|
|
828
|
+
(an Express route registration hands its handlers to the router, not to the caller) */
|
|
829
|
+
export function resolveTsCalls(ts, R, { skipHanded = new Set() } = {}) {
|
|
830
|
+
const { classes, symbols } = ts;
|
|
831
|
+
const noteMiss = (c, head) => {
|
|
832
|
+
const r = head && !/[(<]/.test(head) ? R.resolveName(head, c) : null;
|
|
833
|
+
// a variable counts where its value comes from: an external library's object is an external call
|
|
834
|
+
const v = r?.var || (c.types?.get(head)) || null;
|
|
835
|
+
const vr = v && R.resolveExpr(v.expr, v.module ? v : c);
|
|
836
|
+
if (v && vr?.ext) ts.unresolved.external++;
|
|
837
|
+
else if (v && !vr) ts.unresolved.local++;
|
|
838
|
+
else if (r && (r.id || r.module || r.missing || r.obj || r.var)) ts.unresolved.inRepo++;
|
|
839
|
+
else if (r?.ext) ts.unresolved.external++;
|
|
840
|
+
else if (BUILTINS.has(head)) ts.unresolved.builtin++;
|
|
841
|
+
else ts.unresolved.local++;
|
|
842
|
+
};
|
|
843
|
+
const targetOf = r => r?.id ? r.id : null;
|
|
844
|
+
for (const c of ts.rawCalls) {
|
|
845
|
+
const cg = (c.guards || []).filter(g => CFG_GUARD.test(g.text));
|
|
846
|
+
const extra = { ...(cg.length ? { cfg: cg.map(g => (g.neg ? 'not ' : '') + g.text.replace(/\s+/g, ' ')) } : {}), ...(c.onFailure ? { onFailure: c.onFailure } : {}) };
|
|
847
|
+
const add = (to, kind, more = {}) => addTsEdge(ts, c.from, to, kind, c.line, { ...more, ...extra });
|
|
848
|
+
const ctorOf = (clsId, more = {}) => { add(clsId, 'ctor', more); const init = R.lookup(clsId, 'constructor'); if (init) add(init, 'ctor', more); };
|
|
849
|
+
if (c.jsx || c.isNew) {
|
|
850
|
+
const r = R.resolveExpr(c.expr, c);
|
|
851
|
+
const id = targetOf(r);
|
|
852
|
+
if (id && classes.has(id)) { ctorOf(id, c.jsx ? { jsx: true } : {}); if (c.jsx) { const render = R.lookup(id, 'render'); if (render) add(render, 'static', { jsx: true, recvClass: id }); } ts.callStats.resolved++; }
|
|
853
|
+
else if (id && symbols.get(id) && c.jsx) { add(id, 'static', { jsx: true }); ts.callStats.resolved++; }
|
|
854
|
+
else { ts.callStats.unresolvedName++; noteMiss(c, c.expr.split('.')[0]); }
|
|
855
|
+
continue;
|
|
856
|
+
}
|
|
857
|
+
if (!c.attr) {
|
|
858
|
+
const r = c.expr.includes('(') ? null : R.resolveExpr(c.expr, c);
|
|
859
|
+
const id = targetOf(r);
|
|
860
|
+
if (id && !classes.has(id)) { add(id, 'static'); ts.callStats.resolved++; }
|
|
861
|
+
else { ts.callStats.unresolvedName++; noteMiss(c, c.expr.split('.')[0]); }
|
|
862
|
+
continue;
|
|
863
|
+
}
|
|
864
|
+
let clsId = null, recv = null;
|
|
865
|
+
if (c.obj === 'super' && c.cls) {
|
|
866
|
+
let target = null; for (const k of R.mro(c.cls).slice(1)) { target = classes.get(k)?.methods.get(c.attr); if (target) break; }
|
|
867
|
+
if (target) { add(target, 'static', { passCtx: true, super: true }); ts.callStats.resolved++; } else ts.callStats.unresolvedAttr++;
|
|
868
|
+
continue;
|
|
869
|
+
}
|
|
870
|
+
if (c.obj === 'this' && c.cls) { clsId = c.cls; recv = { mode: 'self' }; }
|
|
871
|
+
else if (c.objNew) { const r = R.resolveExpr(c.objNew, c); if (r?.id && classes.has(r.id)) { clsId = r.id; recv = { mode: 'ctor', cls: [r.id] }; } }
|
|
872
|
+
else if (c.types && c.types.has(c.obj)) { const rc = R.receiverOf(c.types.get(c.obj), c); if (rc) { recv = rc; clsId = rc.cls[0]; } }
|
|
873
|
+
else if (c.objCall) { const rc = R.receiverOf({ t: 'call', expr: c.objCall.expr }, c); if (rc) { recv = rc; clsId = rc.cls[0]; } }
|
|
874
|
+
else if (/^this\.[\w$]+$/.test(c.obj) && c.cls) { const ar = R.attrReceiver(c.cls, c.obj.slice(5)); if (ar) { recv = ar; clsId = ar.cls[0]; } }
|
|
875
|
+
else if (/^[\w$]+$/.test(c.obj)) { const r = R.resolveName(c.obj, c); if (r?.var) { const rc = R.receiverOf(r.var, r.var); if (rc) { recv = rc; clsId = rc.cls[0]; } } }
|
|
876
|
+
if (clsId) {
|
|
877
|
+
const site = `${c.from}@${c.line}:${c.attr}`;
|
|
878
|
+
if (recv.mode === 'self') {
|
|
879
|
+
const target = R.lookup(clsId, c.attr);
|
|
880
|
+
if (target) add(target, 'static', { self: c.attr, site });
|
|
881
|
+
for (const o of R.overrides(clsId, c.attr)) add(o, 'virtual', { self: c.attr, site, evidence: `override of ${c.attr} in subclass` });
|
|
882
|
+
if (target || R.overrides(clsId, c.attr).length) { ts.callStats.resolved++; continue; }
|
|
883
|
+
} else if (recv.mode === 'ctor') {
|
|
884
|
+
const target = R.lookup(clsId, c.attr);
|
|
885
|
+
if (target) { add(target, 'static', { recvClass: clsId, site }); ts.callStats.resolved++; continue; }
|
|
886
|
+
} else if (recv.mode === 'annot') {
|
|
887
|
+
const target = R.lookup(clsId, c.attr), ov = R.overrides(clsId, c.attr);
|
|
888
|
+
if (target) add(target, 'static');
|
|
889
|
+
for (const o of ov) add(o, 'virtual', { evidence: `override of ${c.attr} in subclass`, site });
|
|
890
|
+
if (target || ov.length) { ts.callStats.resolved++; continue; }
|
|
891
|
+
} else {
|
|
892
|
+
const narrowed = [...new Set(recv.cls.map(k => R.lookup(k, c.attr)).filter(Boolean))];
|
|
893
|
+
if (narrowed.length) { for (const t of narrowed) add(t, narrowed.length === 1 ? 'static' : 'dispatch', { site, narrowedBy: recv.factory || 'class-attr', recvClass: narrowed.length === 1 ? recv.cls[0] : undefined });
|
|
894
|
+
ts.callStats.resolved++; continue; }
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
const r = /^[\w$.]+$/.test(c.obj) ? R.resolveExpr(`${c.obj}.${c.attr}`, c) : null;
|
|
898
|
+
if (r?.id && !classes.has(r.id)) { add(r.id, 'static'); ts.callStats.resolved++; continue; }
|
|
899
|
+
ts.callStats.unresolvedAttr++;
|
|
900
|
+
noteMiss(c, /^[\w$]+/.exec(c.obj || '')?.[0]);
|
|
901
|
+
}
|
|
902
|
+
/* a function handed to a call or a JSX event prop runs from there: the caller links to it */
|
|
903
|
+
for (const h of ts.handed) {
|
|
904
|
+
if (h.call != null && skipHanded.has(h.call)) continue;
|
|
905
|
+
if (h.to) { addTsEdge(ts, h.from, h.to, 'static', h.line, { rule: 'js.callback', evidence: `${h.file}:${h.line} hands ${symbols.get(h.to)?.name} to ${h.how}` }); continue; }
|
|
906
|
+
if (NOT_RUN.test(h.callee)) continue;
|
|
907
|
+
const r = h.name.startsWith('this.') && h.cls ? { id: R.lookup(h.cls, h.name.slice(5)) } : R.resolveExpr(h.name, h);
|
|
908
|
+
const s = r?.id && symbols.get(r.id);
|
|
909
|
+
if (!s || (s.kind !== 'function' && s.kind !== 'method')) continue;
|
|
910
|
+
addTsEdge(ts, h.from, r.id, 'static', h.line, { rule: 'js.callback', evidence: `${h.file}:${h.line} hands ${h.name} to ${h.callee}` });
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
/* language conventions that make an entry: `if (require.main === module)` and its ESM spellings */
|
|
915
|
+
export function tsEntries(ts) {
|
|
916
|
+
return ts.mains.map(m => ({ kind: 'main', id: m, file: ts.symbols.get(m).file, line: ts.symbols.get(m).start, rule: 'js.main' }));
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
/* const folding (B15): a URL-ish expression to a string, `{}` for what cannot be known. Identifiers are followed to a
|
|
920
|
+
local or module-level const, through imports; `process.env.X ?? '/api'` folds to its default, naming the key */
|
|
921
|
+
export function foldString(ts, R, node, scope, depth = 0, acc = { folded: [], env: [] }) {
|
|
922
|
+
const unknown = { s: '{}', acc };
|
|
923
|
+
if (!node || depth > 12) return unknown;
|
|
924
|
+
const go = (n, sc) => foldString(ts, R, n, sc, depth + 1, acc).s;
|
|
925
|
+
switch (node.k) {
|
|
926
|
+
case 'str': return { s: node.v, acc };
|
|
927
|
+
case 'tpl': return { s: node.parts.map(p => go(p, scope)).join(''), acc };
|
|
928
|
+
case 'cat': return { s: go(node.l, scope) + go(node.r, scope), acc };
|
|
929
|
+
case 'or': { const l = go(node.l, scope); return { s: l.includes('{}') ? go(node.r, scope) : l, acc }; }
|
|
930
|
+
case 'env': acc.env.push(node.key); return unknown;
|
|
931
|
+
case 'id': {
|
|
932
|
+
const local = scope.consts?.get(node.name);
|
|
933
|
+
if (local) { acc.folded.push(node.name); return { s: go(local, scope), acc }; }
|
|
934
|
+
const found = constOf(ts, R, node.name, scope.module);
|
|
935
|
+
if (found) { acc.folded.push(node.name); return { s: go(found.ir, { module: found.module, consts: null }), acc }; }
|
|
936
|
+
return unknown;
|
|
937
|
+
}
|
|
938
|
+
case 'member': {
|
|
939
|
+
if (node.obj.k !== 'id') return unknown;
|
|
940
|
+
const base = scope.consts?.get(node.obj.name) ? { ir: scope.consts.get(node.obj.name), module: scope.module } : constOf(ts, R, node.obj.name, scope.module);
|
|
941
|
+
const v = base?.ir?.k === 'obj' ? base.ir.pairs[node.prop] : null;
|
|
942
|
+
if (v) { acc.folded.push(`${node.obj.name}.${node.prop}`); return { s: go(v, { module: base.module, consts: null }), acc }; }
|
|
943
|
+
return unknown;
|
|
944
|
+
}
|
|
945
|
+
default: return unknown;
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
// a module-level const by name as seen from a module: its own, or an imported one followed to where it is defined
|
|
949
|
+
export function constOf(ts, R, name, module, depth = 0) {
|
|
950
|
+
const M = ts.modules.get(module);
|
|
951
|
+
if (!M || depth > 6) return null;
|
|
952
|
+
if (M.consts.has(name)) return { ir: M.consts.get(name), module };
|
|
953
|
+
const t = M.imports.get(name);
|
|
954
|
+
if (!t) return null;
|
|
955
|
+
const target = R.resolveModule(t.source, module);
|
|
956
|
+
if (!target) return null;
|
|
957
|
+
const T = ts.modules.get(target);
|
|
958
|
+
const e = t.name === 'default' ? T?.defaultExport : T?.exports.get(t.name);
|
|
959
|
+
if (e?.local) return constOf(ts, R, e.local, target, depth + 1);
|
|
960
|
+
if (e?.source) { const t2 = R.resolveModule(e.source, target); return t2 ? constOf(ts, R, e.name, t2, depth + 1) : null; }
|
|
961
|
+
if (!e) for (const s of T?.starExports || []) { const t2 = R.resolveModule(s, target); const r = t2 && constOf(ts, R, name, t2, depth + 1); if (r) return r; }
|
|
962
|
+
return null;
|
|
963
|
+
}
|