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,609 @@
|
|
|
1
|
+
/* The Python adapter: facts per file, then name / member / constructor resolution into typed
|
|
2
|
+
edges. Rule packs (../rules/) read the same facts and add framework links on top.
|
|
3
|
+
|
|
4
|
+
Resolution rules (design rev3 §3.4): local defs, function-local imports, module imports with
|
|
5
|
+
aliases and relative imports, re-exports through __init__; `self.m()` through the MRO plus a
|
|
6
|
+
`virtual` edge per subclass override; receivers typed by a local constructor, an annotated
|
|
7
|
+
parameter, a class attribute or a factory's returns (with branch guards); `super().m()`; a
|
|
8
|
+
generic base `Base[T]` unwrapped. Build fixes over the spike: `self.x = self._make()` types the
|
|
9
|
+
attribute by the method's returns (B19), a callable or coroutine passed as an argument is linked
|
|
10
|
+
from the callee that runs it (B17), and a call inside an `except` is labelled "on failure". */
|
|
11
|
+
import { dirname } from 'node:path';
|
|
12
|
+
import { STDLIB } from './python-stdlib.mjs';
|
|
13
|
+
import { normaliseDep } from './python-deps.mjs';
|
|
14
|
+
|
|
15
|
+
export function newPythonModel() {
|
|
16
|
+
return {
|
|
17
|
+
symbols: new Map(), classes: new Map(), modules: new Map(),
|
|
18
|
+
rawCalls: [], decorated: [], dictPairs: [], returns: [], mains: [], annotParams: [], paramDefaults: [], moduleAssigns: [],
|
|
19
|
+
fnAssigns: new Map(), fnAlias: new Map(), fnLists: new Map(), settingsFields: [], settingsProps: [], externalImports: new Map(),
|
|
20
|
+
uses: new Map(), // fn -> { called, awaited, passed }: name -> first line (how a callee runs what it was handed)
|
|
21
|
+
typeAliases: new Map(), // alias symbol -> { type, provider, module, imports }: filled by rule packs before resolveCalls
|
|
22
|
+
edges: [], dispatchSites: [], callStats: { resolved: 0, unresolvedName: 0, unresolvedAttr: 0 },
|
|
23
|
+
unresolved: { inRepo: 0, external: 0, builtin: 0, local: 0 }, externalCalls: new Map(),
|
|
24
|
+
importNotes: { cross: new Map(), ambiguous: new Map() }, // calls whose head import crossed package roots / lost to two claims
|
|
25
|
+
defNodes: 0, defsVisited: 0, defScopes: 0,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const str = n => n && n.type === 'string'
|
|
30
|
+
? n.namedChildren.filter(c => c.type === 'string_content').map(c => c.text).join('') : null;
|
|
31
|
+
export const modOf = rel => rel.replace(/\.py$/, '').replace(/\/__init__$/, '').split('/').join('.');
|
|
32
|
+
const simpleType = t => { // `X | None`, `Optional[X]`, `"X"` -> X
|
|
33
|
+
const s = t.replace(/^["']|["']$/g, '').replace(/\s*\|\s*None\b|\bNone\s*\|\s*/g, '').replace(/^Optional\[(.*)\]$/, '$1').trim();
|
|
34
|
+
return /^[A-Za-z_][\w.]*$/.test(s) ? s : null;
|
|
35
|
+
};
|
|
36
|
+
const exits = blk => { const last = blk && blk.namedChildren[blk.namedChildren.length - 1]; return !!last && /return_statement|raise_statement/.test(last.type); };
|
|
37
|
+
// an argument, kept as plain data so rule packs never hold tree nodes
|
|
38
|
+
const argOf = a => {
|
|
39
|
+
const v = a.type === 'keyword_argument' ? a.childForFieldName('value') : a;
|
|
40
|
+
return { type: a.type, kw: a.type === 'keyword_argument' ? a.childForFieldName('name').text : null, text: a.text,
|
|
41
|
+
vtype: v?.type, vtext: v?.text, s: str(v), fn: v?.type === 'call' ? v.childForFieldName('function').text : null,
|
|
42
|
+
items: v?.type === 'list' || v?.type === 'tuple' ? v.namedChildren.map(x => ({ type: x.type, text: x.text, s: str(x) })) : null };
|
|
43
|
+
};
|
|
44
|
+
const exceptLabel = n => { const v = n.childForFieldName('value'); const t = v?.type === 'as_pattern' ? v.namedChildren[0] : v;
|
|
45
|
+
return 'except' + (t ? ' ' + t.text.replace(/\s+/g, ' ') : ''); };
|
|
46
|
+
|
|
47
|
+
export function extractPythonFile(py, rel, root, group, text) {
|
|
48
|
+
const module = modOf(rel);
|
|
49
|
+
const isPkg = rel.endsWith('__init__.py');
|
|
50
|
+
const pkg = isPkg ? module : module.split('.').slice(0, -1).join('.');
|
|
51
|
+
const M = { file: rel, group, imports: new Map(), defs: new Map() };
|
|
52
|
+
py.modules.set(module, M);
|
|
53
|
+
const lines = text.split('\n');
|
|
54
|
+
const spanText = (a, b) => lines.slice(a - 1, b).join('\n');
|
|
55
|
+
const defNodes = root.descendantsOfType(['function_definition', 'class_definition']);
|
|
56
|
+
py.defNodes += defNodes.length;
|
|
57
|
+
const qualname = n => { let q = n.childForFieldName('name').text;
|
|
58
|
+
for (let p = n.parent; p; p = p.parent) if (p.type === 'class_definition') q = `${p.childForFieldName('name').text}.${q}`;
|
|
59
|
+
else if (p.type === 'function_definition') q = `${p.childForFieldName('name').text}.<locals>.${q}`;
|
|
60
|
+
return q; };
|
|
61
|
+
py.defScopes += new Set(defNodes.map(qualname)).size;
|
|
62
|
+
const absImport = (relNode) => {
|
|
63
|
+
const dots = (relNode.text.match(/^\.+/) || [''])[0].length;
|
|
64
|
+
const rest = relNode.text.slice(dots);
|
|
65
|
+
const base = pkg.split('.').slice(0, Math.max(0, pkg.split('.').length - (dots - 1)));
|
|
66
|
+
return [...base, ...(rest ? [rest] : [])].filter(Boolean).join('.');
|
|
67
|
+
};
|
|
68
|
+
const noteExternal = (name, rel_) => { const top = name.split('.')[0]; if (!py.externalImports.has(top)) py.externalImports.set(top, rel_); };
|
|
69
|
+
function addImports(node, table) {
|
|
70
|
+
if (node.type === 'import_statement') {
|
|
71
|
+
for (const c of node.namedChildren) {
|
|
72
|
+
if (c.type === 'dotted_name') { table.set(c.text.split('.')[0], { module: c.text.split('.')[0], in: module }); noteExternal(c.text, rel); }
|
|
73
|
+
if (c.type === 'aliased_import') { table.set(c.childForFieldName('alias').text, { module: c.childForFieldName('name').text, in: module }); noteExternal(c.childForFieldName('name').text, rel); }
|
|
74
|
+
}
|
|
75
|
+
} else if (node.type === 'import_from_statement') {
|
|
76
|
+
const mn = node.childForFieldName('module_name');
|
|
77
|
+
const from = mn.type === 'relative_import' ? absImport(mn) : mn.text;
|
|
78
|
+
if (mn.type !== 'relative_import') noteExternal(from, rel);
|
|
79
|
+
for (const c of node.childrenForFieldName('name')) {
|
|
80
|
+
if (c.type === 'dotted_name') table.set(c.text, { from, name: c.text, in: module });
|
|
81
|
+
if (c.type === 'aliased_import') table.set(c.childForFieldName('alias').text, { from, name: c.childForFieldName('name').text, in: module });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const scope = ctx => ({ module, imports: ctx.imports, locals: ctx.locals, cls: ctx.cls });
|
|
86
|
+
// a definition's id names its scope: `m:f`, `m:Class.f`, `m:Outer.Inner`, `m:f.<locals>.g`
|
|
87
|
+
const scopedId = (ctx, name) => ctx.inClass ? `${ctx.cls}.${name}` : ctx.fn ? `${ctx.fn}.<locals>.${name}` : `${module}:${name}`;
|
|
88
|
+
const use = (fn, how, name, node) => { const u = py.uses.get(fn) || py.uses.set(fn, { called: new Map(), awaited: new Map(), passed: new Map() }).get(fn);
|
|
89
|
+
if (!u[how].has(name)) u[how].set(name, node.startPosition.row + 1); };
|
|
90
|
+
// `name = <call>` (also inside conditionals and `x or Default()`) for factory-return and receiver analysis
|
|
91
|
+
function recordAssign(ctx, name, right, line) {
|
|
92
|
+
const calls = [];
|
|
93
|
+
const grab = n => { if (!n) return; if (n.type === 'call') calls.push(n.childForFieldName('function').text);
|
|
94
|
+
else if (n.type === 'parenthesized_expression' || n.type === 'conditional_expression' || n.type === 'boolean_operator') n.namedChildren.forEach(grab);
|
|
95
|
+
else if (n.type === 'identifier' && ctx.ptypes && ctx.ptypes.has(n.text)) calls.push({ annot: ctx.ptypes.get(n.text) }); };
|
|
96
|
+
grab(right);
|
|
97
|
+
if (!calls.length) return null;
|
|
98
|
+
const list = py.fnAssigns.get(ctx.fn) || py.fnAssigns.set(ctx.fn, new Map()).get(ctx.fn);
|
|
99
|
+
for (const c of calls) (list.get(name) || list.set(name, []).get(name)).push({ expr: c, guards: ctx.guards, line, ...scope(ctx) });
|
|
100
|
+
return calls;
|
|
101
|
+
}
|
|
102
|
+
function visitBlock(node, ctx) { // sequential statements: an `if` whose body always exits guards what follows
|
|
103
|
+
let guards = ctx.guards;
|
|
104
|
+
for (const c of node.namedChildren) {
|
|
105
|
+
visit(c, { ...ctx, guards });
|
|
106
|
+
if (c.type === 'if_statement' && exits(c.childForFieldName('consequence')) && !c.childrenForFieldName('alternative').length)
|
|
107
|
+
guards = [...guards, { text: c.childForFieldName('condition').text, neg: true }];
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function visit(node, ctx) {
|
|
111
|
+
switch (node.type) {
|
|
112
|
+
case 'block': visitBlock(node, ctx); return;
|
|
113
|
+
case 'import_statement': case 'import_from_statement':
|
|
114
|
+
addImports(node, ctx.imports); return;
|
|
115
|
+
case 'except_clause': case 'except_group_clause':
|
|
116
|
+
for (const c of node.namedChildren) visit(c, { ...ctx, onFailure: exceptLabel(node) });
|
|
117
|
+
return;
|
|
118
|
+
case 'decorated_definition': {
|
|
119
|
+
const def = node.childForFieldName('definition');
|
|
120
|
+
const decos = node.namedChildren.filter(c => c.type === 'decorator');
|
|
121
|
+
const id = visit(def, { ...ctx, decos: decos.map(d => d.namedChildren[0].text) });
|
|
122
|
+
for (const d of decos) {
|
|
123
|
+
const e = d.namedChildren[0];
|
|
124
|
+
const call = e.type === 'call' ? e : null;
|
|
125
|
+
const expr = (call ? call.childForFieldName('function') : e).text;
|
|
126
|
+
const args = call ? call.childForFieldName('arguments').namedChildren : [];
|
|
127
|
+
py.decorated.push({ id, expr, line: d.startPosition.row + 1, text: e.text, module, fn: ctx.fn, imports: ctx.imports, locals: ctx.locals, types: ctx.types,
|
|
128
|
+
strArgs: args.map(str).filter(s => s != null),
|
|
129
|
+
kwNames: args.filter(a => a.type === 'keyword_argument').map(a => a.childForFieldName('name').text),
|
|
130
|
+
kwArgs: Object.fromEntries(args.filter(a => a.type === 'keyword_argument')
|
|
131
|
+
.map(a => [a.childForFieldName('name').text, str(a.childForFieldName('value'))])) });
|
|
132
|
+
}
|
|
133
|
+
return id;
|
|
134
|
+
}
|
|
135
|
+
case 'class_definition': {
|
|
136
|
+
py.defsVisited++;
|
|
137
|
+
const name = node.childForFieldName('name').text;
|
|
138
|
+
const id = scopedId(ctx, name);
|
|
139
|
+
const sup = node.childForFieldName('superclasses');
|
|
140
|
+
const baseOf = c => c.type === 'subscript' ? c.childForFieldName('value') : c; // Base[T]
|
|
141
|
+
const baseExprs = sup ? sup.namedChildren.map(baseOf).filter(c => c.type === 'identifier' || c.type === 'attribute').map(c => c.text) : [];
|
|
142
|
+
const start = node.startPosition.row + 1, end = node.endPosition.row + 1;
|
|
143
|
+
py.classes.set(id, { id, module, name, baseExprs, methods: new Map(), imports: ctx.imports, attrTypes: new Map(),
|
|
144
|
+
decos: ctx.decos || [], file: rel, line: start });
|
|
145
|
+
py.symbols.set(id, { id, group, kind: 'class', file: rel, start, end, name, module, text: spanText(start, end) });
|
|
146
|
+
if (!ctx.inClass) (ctx.fn ? ctx.locals : M.defs).set(name, id);
|
|
147
|
+
const isSettings = baseExprs.some(b => /BaseSettings$/.test(b));
|
|
148
|
+
for (const c of node.childForFieldName('body').namedChildren) {
|
|
149
|
+
if (isSettings && c.type === 'expression_statement' && c.namedChildren[0]?.type === 'assignment') {
|
|
150
|
+
const a = c.namedChildren[0], l = a.childForFieldName('left'), r = a.childForFieldName('right');
|
|
151
|
+
if (l?.type === 'identifier' && a.childForFieldName('type')) {
|
|
152
|
+
let def = r, alias = null;
|
|
153
|
+
if (r?.type === 'call' && /(^|\.)Field$/.test(r.childForFieldName('function').text)) {
|
|
154
|
+
const as = r.childForFieldName('arguments').namedChildren;
|
|
155
|
+
const kw = n => as.find(x => x.type === 'keyword_argument' && x.childForFieldName('name').text === n)?.childForFieldName('value');
|
|
156
|
+
def = kw('default') || as.find(x => x.type !== 'keyword_argument') || null;
|
|
157
|
+
alias = str(kw('alias')) || str(kw('validation_alias'));
|
|
158
|
+
}
|
|
159
|
+
py.settingsFields.push({ cls: id, name: l.text, alias, file: rel, line: a.startPosition.row + 1, group,
|
|
160
|
+
default: def ? (def.type === 'string' ? str(def) : def.text) : undefined, defaultIsString: def?.type === 'string' });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (isSettings && c.type === 'decorated_definition' && c.namedChildren.some(d => d.type === 'decorator' && d.text === '@property')) {
|
|
164
|
+
const fn = c.childForFieldName('definition');
|
|
165
|
+
const ret = fn.childForFieldName('body').namedChildren.find(s => s.type === 'return_statement');
|
|
166
|
+
if (ret) py.settingsProps.push({ cls: id, name: fn.childForFieldName('name').text, expr: ret.namedChildren[0]?.text, file: rel, line: fn.startPosition.row + 1 });
|
|
167
|
+
}
|
|
168
|
+
visit(c, { ...ctx, cls: id, inClass: true, decos: null });
|
|
169
|
+
}
|
|
170
|
+
return id;
|
|
171
|
+
}
|
|
172
|
+
case 'function_definition': {
|
|
173
|
+
py.defsVisited++;
|
|
174
|
+
const name = node.childForFieldName('name').text;
|
|
175
|
+
const id = scopedId(ctx, name);
|
|
176
|
+
const start = node.startPosition.row + 1, end = node.endPosition.row + 1;
|
|
177
|
+
const params = node.childForFieldName('parameters').namedChildren.map(p => (p.type === 'identifier' ? p : p.type === 'typed_parameter' ? p.namedChildren[0] : p.childForFieldName('name'))?.text).filter(Boolean);
|
|
178
|
+
const sym = { id, group, kind: ctx.inClass ? 'method' : 'function', file: rel, start, end, name, module,
|
|
179
|
+
cls: ctx.inClass ? ctx.cls : null, decos: ctx.decos || [], params, text: spanText(start, end),
|
|
180
|
+
...(node.children.some(c => c.type === 'async') ? { async: true } : {}) };
|
|
181
|
+
py.symbols.set(id, sym);
|
|
182
|
+
if (ctx.inClass) py.classes.get(ctx.cls).methods.set(name, id);
|
|
183
|
+
else if (ctx.fn) ctx.locals.set(name, id);
|
|
184
|
+
else M.defs.set(name, id);
|
|
185
|
+
if (group === 'test') { // assertion lines: what a test promises (the pytest pack decides which functions are tests)
|
|
186
|
+
const body = node.childForFieldName('body');
|
|
187
|
+
const asserts = [...body.descendantsOfType('assert_statement'), ...body.descendantsOfType('call').filter(c => /(^|\.)(assert\w*|raises)$/.test(c.childForFieldName('function').text))];
|
|
188
|
+
sym.asserts = asserts.length;
|
|
189
|
+
sym.assertLines = [...new Set(asserts.map(a => a.startPosition.row + 1))].sort((a, b) => a - b);
|
|
190
|
+
}
|
|
191
|
+
const isFixture = (ctx.decos || []).some(d => /(^|\.)fixture(\(|$)/.test(d));
|
|
192
|
+
const inner = { ...ctx, fn: id, imports: new Map(ctx.imports), locals: new Map(ctx.locals || []), types: new Map(), ptypes: new Map(),
|
|
193
|
+
cls: ctx.cls, inClass: false, guards: [], decos: null, fixture: isFixture, onFailure: null };
|
|
194
|
+
for (const p of node.childForFieldName('parameters').namedChildren) {
|
|
195
|
+
const pname = p.type === 'typed_parameter' ? p.namedChildren[0] : p.childForFieldName('name');
|
|
196
|
+
const ptype = p.childForFieldName('type');
|
|
197
|
+
const pval = p.childForFieldName('value');
|
|
198
|
+
if (pname && ptype) { const t = simpleType(ptype.text); if (t) { inner.types.set(pname.text, { t: 'annot', expr: t }); inner.ptypes.set(pname.text, t);
|
|
199
|
+
py.annotParams.push({ fn: id, param: pname.text, annot: t, line: p.startPosition.row + 1, ...scope(inner) }); } }
|
|
200
|
+
if (pname && pval && pval.type === 'call') {
|
|
201
|
+
const a0 = pval.childForFieldName('arguments').namedChildren[0];
|
|
202
|
+
py.paramDefaults.push({ fn: id, param: pname.text, call: pval.childForFieldName('function').text, a0: a0?.text ?? null,
|
|
203
|
+
type: ptype ? simpleType(ptype.text) : null, line: p.startPosition.row + 1, ...scope(inner) });
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
visit(node.childForFieldName('body'), inner);
|
|
207
|
+
return id;
|
|
208
|
+
}
|
|
209
|
+
case 'assignment': {
|
|
210
|
+
const l = node.childForFieldName('left'), r = node.childForFieldName('right');
|
|
211
|
+
if (ctx.fn && l && r && l.type === 'identifier') {
|
|
212
|
+
(py.fnAlias.get(ctx.fn) || py.fnAlias.set(ctx.fn, {}).get(ctx.fn))[l.text] = r.text;
|
|
213
|
+
if (r.type === 'call') ctx.types.set(l.text, { t: 'call', expr: r.childForFieldName('function').text });
|
|
214
|
+
if (r.type === 'list' || r.type === 'tuple') (py.fnLists.get(ctx.fn) || py.fnLists.set(ctx.fn, {}).get(ctx.fn))[l.text] = r.namedChildren.map(x => ({ type: x.type, text: x.text, s: str(x) }));
|
|
215
|
+
recordAssign(ctx, l.text, r, node.startPosition.row + 1);
|
|
216
|
+
}
|
|
217
|
+
// class attributes: self.x = Klass(...) / factory() / self._make() / annotated param / x or Default()
|
|
218
|
+
if (ctx.fn && ctx.cls && l && r && l.type === 'attribute' && l.childForFieldName('object').text === 'self') {
|
|
219
|
+
const calls = recordAssign(ctx, 'self.' + l.childForFieldName('attribute').text, r, node.startPosition.row + 1);
|
|
220
|
+
if (calls) { const k = py.classes.get(ctx.cls); const nm = l.childForFieldName('attribute').text;
|
|
221
|
+
(k.attrTypes.get(nm) || k.attrTypes.set(nm, []).get(nm)).push(...calls.map(c => ({ expr: c, guards: ctx.guards, ...scope(ctx) }))); }
|
|
222
|
+
}
|
|
223
|
+
if (!ctx.fn && !ctx.cls && l?.type === 'identifier' && r)
|
|
224
|
+
py.moduleAssigns.push({ module, file: rel, name: l.text, rtype: r.type, rtext: r.text, line: node.startPosition.row + 1,
|
|
225
|
+
fn: r.type === 'call' ? r.childForFieldName('function').text : null,
|
|
226
|
+
sub: r.type === 'subscript' ? { value: r.childForFieldName('value').text, items: r.namedChildren.slice(1).map(argOf) } : null,
|
|
227
|
+
args: r.type === 'call' ? r.childForFieldName('arguments').namedChildren.map(argOf) : null, imports: ctx.imports });
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
case 'return_statement': {
|
|
231
|
+
const v = node.namedChildren[0];
|
|
232
|
+
if (ctx.fn && v) {
|
|
233
|
+
if (v.type === 'call') py.returns.push({ fn: ctx.fn, kind: 'call', expr: v.childForFieldName('function').text, guards: ctx.guards, line: node.startPosition.row + 1, ...scope(ctx) });
|
|
234
|
+
else if (v.type === 'identifier') py.returns.push({ fn: ctx.fn, kind: 'name', name: v.text, guards: ctx.guards, line: node.startPosition.row + 1, ...scope(ctx) });
|
|
235
|
+
else if (v.type === 'attribute' && v.childForFieldName('object').text === 'self') py.returns.push({ fn: ctx.fn, kind: 'name', name: v.text, guards: ctx.guards, line: node.startPosition.row + 1, ...scope(ctx) });
|
|
236
|
+
}
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
case 'yield': { // a fixture's `yield X(...)` is its return value
|
|
240
|
+
const v = node.namedChildren[0];
|
|
241
|
+
if (ctx.fn && ctx.fixture && v) {
|
|
242
|
+
if (v.type === 'call') py.returns.push({ fn: ctx.fn, kind: 'call', expr: v.childForFieldName('function').text, guards: ctx.guards, line: node.startPosition.row + 1, ...scope(ctx) });
|
|
243
|
+
else if (v.type === 'identifier') py.returns.push({ fn: ctx.fn, kind: 'name', name: v.text, guards: ctx.guards, line: node.startPosition.row + 1, ...scope(ctx) });
|
|
244
|
+
}
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
case 'dictionary':
|
|
248
|
+
for (const p of node.namedChildren.filter(c => c.type === 'pair')) {
|
|
249
|
+
const k = str(p.childForFieldName('key')), v = str(p.childForFieldName('value'));
|
|
250
|
+
if (k != null && v != null) py.dictPairs.push({ key: k, value: v, file: rel, line: p.startPosition.row + 1 });
|
|
251
|
+
}
|
|
252
|
+
break;
|
|
253
|
+
case 'if_statement': {
|
|
254
|
+
const cond = node.childForFieldName('condition');
|
|
255
|
+
if (!ctx.fn && /__name__\s*==\s*['"]__main__['"]/.test(cond.text)) {
|
|
256
|
+
const id = `${module}:<main>`;
|
|
257
|
+
const start = node.startPosition.row + 1, end = node.endPosition.row + 1;
|
|
258
|
+
py.symbols.set(id, { id, group, kind: 'main', file: rel, start, end, name: '<main>', module, text: spanText(start, end) });
|
|
259
|
+
py.mains.push(id);
|
|
260
|
+
visit(node.childForFieldName('consequence'), { ...ctx, fn: id, inClass: false, imports: new Map(ctx.imports), locals: new Map(), types: new Map(), ptypes: new Map(), guards: [] });
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
visit(cond, ctx);
|
|
264
|
+
const g0 = ctx.guards || [];
|
|
265
|
+
visit(node.childForFieldName('consequence'), { ...ctx, guards: [...g0, { text: cond.text, neg: false }] });
|
|
266
|
+
let prior = [{ text: cond.text, neg: true }];
|
|
267
|
+
for (const alt of node.childrenForFieldName('alternative')) {
|
|
268
|
+
if (alt.type === 'elif_clause') {
|
|
269
|
+
const c2 = alt.childForFieldName('condition');
|
|
270
|
+
visit(c2, ctx);
|
|
271
|
+
visit(alt.childForFieldName('consequence'), { ...ctx, guards: [...g0, ...prior, { text: c2.text, neg: false }] });
|
|
272
|
+
prior = [...prior, { text: c2.text, neg: true }];
|
|
273
|
+
} else visit(alt.childForFieldName('body'), { ...ctx, guards: [...g0, ...prior] });
|
|
274
|
+
}
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
case 'await': {
|
|
278
|
+
const v = node.namedChildren[0];
|
|
279
|
+
if (ctx.fn && v?.type === 'identifier') use(ctx.fn, 'awaited', v.text, node);
|
|
280
|
+
break;
|
|
281
|
+
}
|
|
282
|
+
case 'call': {
|
|
283
|
+
const fnNode = node.childForFieldName('function');
|
|
284
|
+
const args = node.childForFieldName('arguments');
|
|
285
|
+
if (ctx.fn && fnNode.type === 'identifier') use(ctx.fn, 'called', fnNode.text, node);
|
|
286
|
+
if (ctx.fn && args) for (const a of args.namedChildren) { const v = a.type === 'keyword_argument' ? a.childForFieldName('value') : a; if (v?.type === 'identifier') use(ctx.fn, 'passed', v.text, node); }
|
|
287
|
+
py.rawCalls.push({ from: ctx.fn || `${module}:<module>`, module, file: rel, expr: fnNode.text, line: node.startPosition.row + 1,
|
|
288
|
+
obj: fnNode.type === 'attribute' ? fnNode.childForFieldName('object').text : null,
|
|
289
|
+
// `A().m()` / `make().m()`: the receiver is the value of the call it is attached to
|
|
290
|
+
objCall: fnNode.type === 'attribute' && fnNode.childForFieldName('object').type === 'call' ? fnNode.childForFieldName('object').childForFieldName('function').text : null,
|
|
291
|
+
attr: fnNode.type === 'attribute' ? fnNode.childForFieldName('attribute').text : null,
|
|
292
|
+
imports: ctx.imports, locals: ctx.locals, types: ctx.types, cls: ctx.cls, guards: ctx.guards, onFailure: ctx.onFailure || null,
|
|
293
|
+
top: !ctx.fn, args: args ? args.namedChildren.map(argOf) : [] });
|
|
294
|
+
break;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
for (const c of node.namedChildren) visit(c, ctx);
|
|
298
|
+
}
|
|
299
|
+
visit(root, { imports: M.imports, fn: null, cls: null, inClass: false, locals: new Map(), types: new Map(), guards: [], onFailure: null });
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/* ---------------- package roots ---------------- */
|
|
303
|
+
|
|
304
|
+
const packagesOf = py => { // every in-repo package: a directory holding modules, or an __init__.py
|
|
305
|
+
const out = new Set();
|
|
306
|
+
for (const [k, M] of py.modules) { const parts = k.split('.'); for (let i = 1; i < parts.length; i++) out.add(parts.slice(0, i).join('.'));
|
|
307
|
+
if (M.file.endsWith('__init__.py')) out.add(k); }
|
|
308
|
+
return out;
|
|
309
|
+
};
|
|
310
|
+
const initDirsOf = py => new Set([...py.modules.values()].filter(M => M.file.endsWith('__init__.py')).map(M => dirname(M.file)));
|
|
311
|
+
|
|
312
|
+
/* Modules are named from the repo root; an absolute import is also resolved the way Python finds it from a package
|
|
313
|
+
root below it. A file's package root is the top of the contiguous run of ancestor directories holding an
|
|
314
|
+
__init__.py, and its dotted name there is that run plus the file: `backend/app/x.py`, with `backend/app/__init__.py`
|
|
315
|
+
and no `backend/__init__.py`, is also `app.x` (claimed under the root `backend`). A name whose top-level part is a
|
|
316
|
+
standard-library module is never claimed (an in-repo `logging/` never captures `import logging`). Returns every
|
|
317
|
+
claim, `alias -> [{ key, root }]`, and the package roots. */
|
|
318
|
+
export function packageAliases(py) {
|
|
319
|
+
const initDirs = initDirsOf(py), claims = new Map(), roots = new Set();
|
|
320
|
+
for (const [key, M] of py.modules) {
|
|
321
|
+
const dirs = M.file.split('/').slice(0, -1);
|
|
322
|
+
let k = 0; while (k < dirs.length && initDirs.has(dirs.slice(0, dirs.length - k).join('/'))) k++;
|
|
323
|
+
const root = dirs.slice(0, dirs.length - k);
|
|
324
|
+
if (!k || !root.length) continue;
|
|
325
|
+
const alias = key.slice(root.join('.').length + 1);
|
|
326
|
+
if (STDLIB.has(alias.split('.')[0])) continue;
|
|
327
|
+
(claims.get(alias) || claims.set(alias, []).get(alias)).push({ key, root: root.join('/') });
|
|
328
|
+
roots.add(root.join('/'));
|
|
329
|
+
}
|
|
330
|
+
return { claims, roots: [...roots].sort() };
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/* ---------------- resolution ---------------- */
|
|
334
|
+
|
|
335
|
+
export function resolverFor(py, deps = new Set()) {
|
|
336
|
+
const { modules, classes, symbols } = py;
|
|
337
|
+
const pkgs = packagesOf(py), initDirs = initDirsOf(py);
|
|
338
|
+
const { claims, roots: packageRoots } = packageAliases(py);
|
|
339
|
+
const inRepo = m => modules.has(m) || pkgs.has(m);
|
|
340
|
+
// a standard-library name resolves at the repo root only to a package (`logging/__init__.py`), never a loose module
|
|
341
|
+
const atRoot = name => { const top = name.split('.')[0]; return inRepo(name) && (!STDLIB.has(top) || !!modules.get(top)?.file.endsWith('__init__.py')); };
|
|
342
|
+
// beside the importing file when its directory is no package (a script dir on sys.path), then the package roots
|
|
343
|
+
const sibling = (name, from) => { const f = modules.get(from)?.file, dir = f && dirname(f);
|
|
344
|
+
if (!dir || dir === '.' || initDirs.has(dir) || f.endsWith('__init__.py') || STDLIB.has(name.split('.')[0])) return null;
|
|
345
|
+
const m = `${dir.split('/').join('.')}.${name}`; return inRepo(m) ? m : null; };
|
|
346
|
+
/* through the package roots: the longest claimed dotted prefix, the rest appended (`app.api.routes` via `app` when
|
|
347
|
+
api/ has no __init__.py). Several claims: the one whose root holds the importing file wins, else the name stays
|
|
348
|
+
external (`ambiguous`). A claim under another root resolves (`cross`) unless the repo declares the top-level name
|
|
349
|
+
as a dependency (`integrations/stripe/` never captures a declared `stripe`). */
|
|
350
|
+
const claimed = (name, from) => {
|
|
351
|
+
const parts = name.split('.'), dir = dirname(modules.get(from)?.file || '.');
|
|
352
|
+
for (let i = parts.length; i >= 1; i--) {
|
|
353
|
+
const alias = parts.slice(0, i).join('.'), cs = claims.get(alias);
|
|
354
|
+
if (!cs) continue;
|
|
355
|
+
const at = c => [c.key, ...parts.slice(i)].join('.');
|
|
356
|
+
const live = cs.filter(c => inRepo(at(c)));
|
|
357
|
+
const own = live.filter(c => dir === c.root || dir.startsWith(c.root + '/')).sort((a, b) => b.root.length - a.root.length)[0];
|
|
358
|
+
if (own) return { key: at(own) };
|
|
359
|
+
if (!live.length || deps.has(normaliseDep(parts[0]))) return {}; // a declared library, however many roots claim it
|
|
360
|
+
if (live.length > 1) return { ambiguous: { name: alias, roots: live.map(c => c.root).sort() } };
|
|
361
|
+
return { key: at(live[0]), cross: { name: alias, root: live[0].root } };
|
|
362
|
+
}
|
|
363
|
+
return {};
|
|
364
|
+
};
|
|
365
|
+
const locate = (name, from) => atRoot(name) ? { key: name } : (n => n ? { key: n } : claimed(name, from))(sibling(name, from));
|
|
366
|
+
// an absolute module name as the importing module sees it, or null when no in-repo module answers to it
|
|
367
|
+
const findModule = (name, from) => locate(name, from).key || null;
|
|
368
|
+
const via = l => ({ ...(l.cross ? { cross: l.cross } : {}), ...(l.ambiguous ? { ambiguous: l.ambiguous } : {}) });
|
|
369
|
+
function resolveImport(t, depth = 0) {
|
|
370
|
+
if (!t || depth > 4) return null;
|
|
371
|
+
if (t.module) { const l = locate(t.module, t.in); return l.key ? { module: l.key, ...via(l) } : { ext: t.module, ...via(l) }; }
|
|
372
|
+
const l = locate(t.from, t.in), from = l.key, m = from && modules.get(from);
|
|
373
|
+
if (m?.defs.has(t.name)) return { id: m.defs.get(t.name), ...via(l) };
|
|
374
|
+
const followed = m?.imports.has(t.name) ? resolveImport(m.imports.get(t.name), depth + 1) : null;
|
|
375
|
+
// a re-export reached through another root still counts as cross-root; the inner lookup's own note wins
|
|
376
|
+
if (followed && !followed.missing) return { ...via(l), ...followed };
|
|
377
|
+
if (!from) { const s = locate(`${t.from}.${t.name}`, t.in); return s.key ? { module: s.key, ...via(s) } : { ext: `${t.from}.${t.name}`, ...via(l), ...via(s) }; }
|
|
378
|
+
const sub = `${from}.${t.name}`;
|
|
379
|
+
// `missing`: an in-repo module that does not define the name; counted as an in-repo miss
|
|
380
|
+
return inRepo(sub) ? { module: sub, ...via(l) } : { missing: sub };
|
|
381
|
+
}
|
|
382
|
+
function resolveName(name, c) {
|
|
383
|
+
if (c.locals && c.locals.has(name)) return { id: c.locals.get(name) };
|
|
384
|
+
if (c.imports.has(name)) return resolveImport(c.imports.get(name));
|
|
385
|
+
const M = modules.get(c.module);
|
|
386
|
+
return M && M.defs.has(name) ? { id: M.defs.get(name) } : null;
|
|
387
|
+
}
|
|
388
|
+
const resolveExpr = (expr, c) => { // `Klass` / `mod.Klass` / `fn`
|
|
389
|
+
if (!expr.includes('.')) return resolveName(expr, c);
|
|
390
|
+
const [head, ...rest] = expr.split('.');
|
|
391
|
+
const r = resolveName(head, c);
|
|
392
|
+
if (r && r.module) { const mod = rest.length > 1 ? `${r.module}.${rest.slice(0, -1).join('.')}` : r.module; const id = modules.get(mod)?.defs.get(rest[rest.length - 1]); return id ? { id } : null; }
|
|
393
|
+
if (r && r.ext) return { ext: `${r.ext}.${rest.join('.')}` };
|
|
394
|
+
return null;
|
|
395
|
+
};
|
|
396
|
+
for (const k of classes.values()) k.bases = k.baseExprs.map(b => {
|
|
397
|
+
const r = b.includes('.') ? null : resolveName(b, { imports: k.imports, module: k.module });
|
|
398
|
+
return r && r.id && classes.has(r.id) ? r.id : null; }).filter(Boolean);
|
|
399
|
+
const subclasses = new Map();
|
|
400
|
+
for (const k of classes.values()) for (const b of k.bases) (subclasses.get(b) || subclasses.set(b, []).get(b)).push(k.id);
|
|
401
|
+
const mro = (id, seen = new Set()) => seen.has(id) ? [] : (seen.add(id), [id, ...(classes.get(id)?.bases || []).flatMap(b => mro(b, seen))]);
|
|
402
|
+
const lookup = (clsId, m) => { for (const c of mro(clsId)) { const x = classes.get(c)?.methods.get(m); if (x) return x; } return null; };
|
|
403
|
+
const overrides = (clsId, m, out = []) => { for (const s of subclasses.get(clsId) || []) { const x = classes.get(s).methods.get(m); if (x) out.push(x); overrides(s, m, out); } return out; };
|
|
404
|
+
// `self._make` inside a class -> the method it names (B19: a receiver set by a self-method factory)
|
|
405
|
+
const selfMethod = (expr, sc) => { const m = /^(self|cls)\.(\w+)$/.exec(expr); return m && sc.cls ? lookup(sc.cls, m[2]) : null; };
|
|
406
|
+
|
|
407
|
+
/* factory return analysis: which classes can a function return, under which guards */
|
|
408
|
+
const retMemo = new Map();
|
|
409
|
+
const returnsOf = new Map(); for (const r of py.returns) (returnsOf.get(r.fn) || returnsOf.set(r.fn, []).get(r.fn)).push(r);
|
|
410
|
+
function factoryReturns(fnId, depth = 0) {
|
|
411
|
+
if (retMemo.has(fnId)) return retMemo.get(fnId);
|
|
412
|
+
if (depth > 3) return [];
|
|
413
|
+
retMemo.set(fnId, []);
|
|
414
|
+
const out = [];
|
|
415
|
+
const viaExpr = (expr, sc, guards, line) => {
|
|
416
|
+
if (typeof expr === 'string' && expr.includes('(')) expr = expr.slice(0, expr.indexOf('(')); // Builder(...).with_x(...)
|
|
417
|
+
if (typeof expr === 'object' && expr.annot) { const r = resolveExpr(expr.annot, sc); if (r?.id && classes.has(r.id)) out.push({ cls: r.id, guards, line, annot: true }); return; }
|
|
418
|
+
const sm = selfMethod(expr, sc);
|
|
419
|
+
if (sm) { for (const x of factoryReturns(sm, depth + 1)) out.push({ ...x, guards: [...guards, ...x.guards], via: sm }); return; }
|
|
420
|
+
const r = resolveExpr(expr, sc);
|
|
421
|
+
if (!r) return;
|
|
422
|
+
if (r.ext) { if (/^[A-Z]/.test(expr.split('.').pop())) out.push({ ext: r.ext, guards, line }); return; }
|
|
423
|
+
if (r.id && classes.has(r.id)) out.push({ cls: r.id, guards, line });
|
|
424
|
+
else if (r.id && symbols.get(r.id)?.kind === 'function') for (const x of factoryReturns(r.id, depth + 1)) out.push({ ...x, guards: [...guards, ...x.guards], via: r.id });
|
|
425
|
+
};
|
|
426
|
+
for (const ret of returnsOf.get(fnId) || []) {
|
|
427
|
+
if (ret.kind === 'call') viaExpr(ret.expr, ret, ret.guards, ret.line);
|
|
428
|
+
else for (const a of py.fnAssigns.get(fnId)?.get(ret.name) || []) viaExpr(a.expr, a, [...a.guards, ...ret.guards], a.line);
|
|
429
|
+
}
|
|
430
|
+
retMemo.set(fnId, out);
|
|
431
|
+
return out;
|
|
432
|
+
}
|
|
433
|
+
const byReturns = fnId => { const fr = factoryReturns(fnId).filter(x => x.cls); const cs = [...new Set(fr.map(x => x.cls))];
|
|
434
|
+
return cs.length ? { mode: 'factory', factory: fnId, cls: cs, returns: fr } : null; };
|
|
435
|
+
// receiver type from a type spec: concrete classes (ctor) or factory returns (narrowed) or annotation (CHA)
|
|
436
|
+
function receiverOf(spec, sc) {
|
|
437
|
+
if (!spec) return null;
|
|
438
|
+
if (spec.t === 'returns') { const r = byReturns(spec.fn); return r && r.cls.length === 1 ? { mode: 'ctor', cls: r.cls } : r; } // a param bound to a provider (pytest fixture)
|
|
439
|
+
if (spec.t === 'annot') { const r = resolveExpr(spec.expr, sc);
|
|
440
|
+
if (r?.id && py.typeAliases.has(r.id)) { // Annotated[T, Depends(provider)]: the provider's returns narrow T
|
|
441
|
+
const a = py.typeAliases.get(r.id); const pr = a.provider ? resolveExpr(a.provider, a) : null;
|
|
442
|
+
const fr = pr?.id ? byReturns(pr.id) : null;
|
|
443
|
+
if (fr) return fr;
|
|
444
|
+
const t = a.type ? resolveExpr(a.type, a) : null; return t?.id && classes.has(t.id) ? { mode: 'annot', cls: [t.id] } : null; }
|
|
445
|
+
return r?.id && classes.has(r.id) ? { mode: 'annot', cls: [r.id] } : null; }
|
|
446
|
+
const sm = selfMethod(spec.expr, sc);
|
|
447
|
+
if (sm) return byReturns(sm);
|
|
448
|
+
const r = resolveExpr(spec.expr, sc);
|
|
449
|
+
if (!r || !r.id) return null;
|
|
450
|
+
if (classes.has(r.id)) return { mode: 'ctor', cls: [r.id] };
|
|
451
|
+
if (symbols.get(r.id)?.kind === 'function') return byReturns(r.id);
|
|
452
|
+
return null;
|
|
453
|
+
}
|
|
454
|
+
function attrReceiver(clsId, attr) { // self.<attr> typed by assignments in the class (or bases)
|
|
455
|
+
for (const c of mro(clsId)) { const specs = classes.get(c)?.attrTypes.get(attr); if (!specs) continue;
|
|
456
|
+
const cls = [], facts = [];
|
|
457
|
+
for (const s of specs) {
|
|
458
|
+
if (typeof s.expr === 'object' && s.expr.annot) { const r = resolveExpr(s.expr.annot, s); if (r?.id && classes.has(r.id)) { cls.push(r.id); facts.push({ mode: 'annot', cls: r.id }); } continue; }
|
|
459
|
+
const rc = receiverOf({ t: 'call', expr: s.expr }, s); if (rc) { cls.push(...rc.cls); facts.push({ mode: rc.mode, cls: rc.cls, factory: rc.factory }); }
|
|
460
|
+
}
|
|
461
|
+
if (cls.length) { const narrowed = facts.filter(f => f.mode !== 'annot'); return { mode: narrowed.length && narrowed.length === facts.length ? 'factory' : (narrowed.length ? 'mixed' : 'annot'), cls: [...new Set(cls)], facts }; }
|
|
462
|
+
}
|
|
463
|
+
return null;
|
|
464
|
+
}
|
|
465
|
+
return { packageRoots, findModule, resolveImport, resolveName, resolveExpr, mro, lookup, overrides, subclasses, factoryReturns, receiverOf, attrReceiver };
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const CFG_GUARD = /settings\.|getattr\(\s*settings\b|is_prod|is_dev|env_mode/;
|
|
469
|
+
// a guard reads config when its text does, or when a local it names was assigned from config (`x = getattr(settings, …)`)
|
|
470
|
+
const readsConfig = (g, alias) => CFG_GUARD.test(g.text) || (g.text.match(/[A-Za-z_]\w*/g) || []).some(id => alias?.[id] && CFG_GUARD.test(alias[id]));
|
|
471
|
+
|
|
472
|
+
export function addEdge(py, from, to, kind, line, extra = {}) {
|
|
473
|
+
if (!from || !to || from === to) return;
|
|
474
|
+
const e = { from, to, kind, line, ...extra };
|
|
475
|
+
const gf = py.symbols.get(from)?.group || py.modules.get(from.split(':')[0])?.group, gt = py.symbols.get(to)?.group ?? py.groupOf?.(to); // a route in another language
|
|
476
|
+
if (gf && gt && gf !== gt) e.groups = [gf, gt];
|
|
477
|
+
py.edges.push(e);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const BUILTINS = new Set(['print', 'len', 'str', 'int', 'float', 'bool', 'dict', 'list', 'set', 'tuple', 'range', 'enumerate', 'zip', 'map',
|
|
481
|
+
'filter', 'sorted', 'reversed', 'min', 'max', 'sum', 'any', 'all', 'isinstance', 'issubclass', 'getattr', 'setattr', 'hasattr', 'delattr',
|
|
482
|
+
'open', 'iter', 'next', 'repr', 'type', 'super', 'id', 'hash', 'abs', 'round', 'format', 'vars', 'dir', 'callable', 'object', 'frozenset',
|
|
483
|
+
'bytes', 'bytearray', 'memoryview', 'divmod', 'pow', 'chr', 'ord', 'hex', 'oct', 'bin', 'globals', 'locals', 'input', 'exec', 'eval',
|
|
484
|
+
'compile', 'property', 'staticmethod', 'classmethod', 'slice', 'complex', 'ascii', 'breakpoint', 'aiter', 'anext']);
|
|
485
|
+
|
|
486
|
+
export function resolveCalls(py, R) {
|
|
487
|
+
const { classes, symbols, modules } = py;
|
|
488
|
+
const noteMiss = (c, head) => { // classify an unresolved call by where its head name comes from
|
|
489
|
+
const r = R.resolveName(head, c);
|
|
490
|
+
if (r && (r.id || r.module || r.missing)) py.unresolved.inRepo++;
|
|
491
|
+
else if (r && r.ext) { py.unresolved.external++; const top = r.ext.split('.')[0]; py.externalCalls.set(top, (py.externalCalls.get(top) || 0) + 1); }
|
|
492
|
+
else if (BUILTINS.has(head)) py.unresolved.builtin++;
|
|
493
|
+
else py.unresolved.local++;
|
|
494
|
+
};
|
|
495
|
+
const tally = (map, note) => { const k = JSON.stringify(note); (map.get(k) || map.set(k, { ...note, calls: 0 }).get(k)).calls++; };
|
|
496
|
+
for (const c of py.rawCalls) {
|
|
497
|
+
const head = (c.attr ? c.obj : c.expr).split('.')[0], r0 = head.includes('(') ? null : R.resolveName(head, c);
|
|
498
|
+
if (r0?.cross) tally(py.importNotes.cross, r0.cross);
|
|
499
|
+
if (r0?.ambiguous) tally(py.importNotes.ambiguous, r0.ambiguous);
|
|
500
|
+
const alias = py.fnAlias.get(c.from);
|
|
501
|
+
const cg = (c.guards || []).filter(x => readsConfig(x, alias));
|
|
502
|
+
const extra = { ...(cg.length ? { cfg: cg.map(x => (x.neg ? 'not ' : '') + x.text.replace(/\s+/g, ' ')) } : {}), ...(c.onFailure ? { onFailure: c.onFailure } : {}) };
|
|
503
|
+
const add = (to, kind, more = {}) => addEdge(py, c.from, to, kind, c.line, { ...more, ...extra });
|
|
504
|
+
if (!c.attr) {
|
|
505
|
+
const r = R.resolveName(c.expr, c);
|
|
506
|
+
if (r && r.id) {
|
|
507
|
+
if (classes.has(r.id)) { add(r.id, 'ctor'); const init = R.lookup(r.id, '__init__'); if (init) add(init, 'ctor'); }
|
|
508
|
+
else { add(r.id, 'static'); linkCallbacks(c, r.id); }
|
|
509
|
+
py.callStats.resolved++;
|
|
510
|
+
} else { py.callStats.unresolvedName++; noteMiss(c, c.expr.split('.')[0]); }
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
let clsId = null, recv = null;
|
|
514
|
+
if (c.obj === 'super()' && c.cls) { // super().m(): next class in the MRO, same receiver object
|
|
515
|
+
let target = null; for (const k of R.mro(c.cls).slice(1)) { target = classes.get(k)?.methods.get(c.attr); if (target) break; }
|
|
516
|
+
if (target) { add(target, 'static', { passCtx: true, super: true }); py.callStats.resolved++; } else py.callStats.unresolvedAttr++;
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
if ((c.obj === 'self' || c.obj === 'cls') && c.cls) { clsId = c.cls; recv = { mode: 'self' }; }
|
|
520
|
+
else if (c.types && c.types.has(c.obj)) {
|
|
521
|
+
const rc = R.receiverOf(c.types.get(c.obj), c);
|
|
522
|
+
if (rc) { recv = rc; clsId = rc.cls[0]; }
|
|
523
|
+
} else if (c.objCall) {
|
|
524
|
+
const rc = R.receiverOf({ t: 'call', expr: c.objCall }, c);
|
|
525
|
+
if (rc) { recv = rc; clsId = rc.cls[0]; }
|
|
526
|
+
} else if (/^self\.\w+$/.test(c.obj) && c.cls) {
|
|
527
|
+
const ar = R.attrReceiver(c.cls, c.obj.slice(5));
|
|
528
|
+
if (ar) { recv = ar; clsId = ar.cls[0]; }
|
|
529
|
+
}
|
|
530
|
+
if (clsId) {
|
|
531
|
+
const site = `${c.from}@${c.line}:${c.attr}`;
|
|
532
|
+
if (recv.mode === 'self') {
|
|
533
|
+
const target = R.lookup(clsId, c.attr);
|
|
534
|
+
if (target) { add(target, 'static', { self: c.attr, site }); linkCallbacks(c, target); }
|
|
535
|
+
for (const o of R.overrides(clsId, c.attr)) add(o, 'virtual', { self: c.attr, site, evidence: `override of ${c.attr} in subclass` });
|
|
536
|
+
if (target || R.overrides(clsId, c.attr).length) { py.callStats.resolved++; continue; }
|
|
537
|
+
} else if (recv.mode === 'ctor') { // concrete type: no subclass can be the receiver
|
|
538
|
+
const target = R.lookup(clsId, c.attr);
|
|
539
|
+
if (target) { add(target, 'static', { recvClass: clsId, site }); linkCallbacks(c, target); py.callStats.resolved++; continue; }
|
|
540
|
+
} else {
|
|
541
|
+
// annotation (CHA) or factory / class-attribute narrowing
|
|
542
|
+
const annotCls = recv.mode === 'annot' ? recv.cls[0] : null;
|
|
543
|
+
const chaRoot = annotCls || (recv.facts?.find(f => f.mode === 'annot')?.cls) || null;
|
|
544
|
+
const cha = chaRoot ? [R.lookup(chaRoot, c.attr), ...R.overrides(chaRoot, c.attr)].filter(Boolean) : [];
|
|
545
|
+
const narrowed = recv.mode === 'annot' ? null : [...new Set(recv.cls.map(k => R.lookup(k, c.attr)).filter(Boolean))];
|
|
546
|
+
if (recv.mode === 'annot') {
|
|
547
|
+
const target = R.lookup(clsId, c.attr);
|
|
548
|
+
if (target) { add(target, 'static'); linkCallbacks(c, target); }
|
|
549
|
+
for (const o of R.overrides(clsId, c.attr)) add(o, 'virtual', { evidence: `override of ${c.attr} in subclass`, site });
|
|
550
|
+
if (target || R.overrides(clsId, c.attr).length) { py.callStats.resolved++; if (R.overrides(clsId, c.attr).length) py.dispatchSites.push({ site, attr: c.attr, from: c.from, mode: 'annot', cha: cha.length, narrowed: null }); continue; }
|
|
551
|
+
} else if (narrowed.length) {
|
|
552
|
+
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 });
|
|
553
|
+
if (narrowed.length === 1) linkCallbacks(c, narrowed[0]);
|
|
554
|
+
py.dispatchSites.push({ site, attr: c.attr, from: c.from, mode: recv.mode, cha: cha.length, narrowed: narrowed.length, factory: recv.factory, recvCls: recv.cls });
|
|
555
|
+
py.callStats.resolved++; continue;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
const r = !c.obj.includes('(') ? R.resolveName(c.obj.split('.')[0], c) : null;
|
|
560
|
+
if (r && r.module) {
|
|
561
|
+
const mod = c.obj.includes('.') ? `${r.module}.${c.obj.split('.').slice(1).join('.')}` : r.module;
|
|
562
|
+
const id = modules.get(mod)?.defs.get(c.attr);
|
|
563
|
+
if (id) { add(id, 'static'); linkCallbacks(c, id); py.callStats.resolved++; continue; }
|
|
564
|
+
} else if (r && r.id && classes.has(r.id)) {
|
|
565
|
+
const id = R.lookup(r.id, c.attr);
|
|
566
|
+
if (id) { add(id, 'static'); linkCallbacks(c, id); py.callStats.resolved++; continue; }
|
|
567
|
+
}
|
|
568
|
+
py.callStats.unresolvedAttr++;
|
|
569
|
+
if (!c.obj.includes('(')) noteMiss(c, c.obj.split('.')[0]);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/* B17: a callable or a coroutine handed to an in-repo callee runs where the callee calls, awaits or forwards that
|
|
573
|
+
parameter, so the callee links to it. The link is only true for this caller (`passedBy`): a walk that reached the
|
|
574
|
+
callee some other way must not follow it. */
|
|
575
|
+
function linkCallbacks(c, calleeId) {
|
|
576
|
+
const callee = symbols.get(calleeId), u = py.uses.get(calleeId);
|
|
577
|
+
if (!callee?.params || !u) return;
|
|
578
|
+
const params = callee.kind === 'method' && /^(self|cls)$/.test(callee.params[0]) && c.attr ? callee.params.slice(1) : callee.params;
|
|
579
|
+
const positional = c.args.filter(a => a.type !== 'keyword_argument' && a.type !== 'comment');
|
|
580
|
+
const pass = (a, param) => {
|
|
581
|
+
if (!param) return;
|
|
582
|
+
// `f` handed over is run by `param(...)`; `f()` handed over is a coroutine, run by `await param` or `wait_for(param)`
|
|
583
|
+
const name = a.vtype === 'identifier' ? a.vtext : a.vtype === 'call' && /^[A-Za-z_]\w*$/.test(a.fn) ? a.fn : null;
|
|
584
|
+
const line = a.vtype === 'identifier' ? u.called.get(param) : (u.awaited.get(param) ?? u.passed.get(param));
|
|
585
|
+
if (!name || line == null) return;
|
|
586
|
+
const r = R.resolveName(name, c);
|
|
587
|
+
const fn = r?.id && symbols.get(r.id);
|
|
588
|
+
if (!fn || (fn.kind !== 'function' && fn.kind !== 'method') || (a.vtype === 'call' && !fn.async)) return;
|
|
589
|
+
addEdge(py, calleeId, r.id, 'static', line, { rule: 'py.callback', passedBy: c.from, passedAt: `${c.file}:${c.line}`,
|
|
590
|
+
evidence: `${c.file}:${c.line} hands ${a.vtext} to ${callee.name}(${param}); ${callee.file}:${line} runs it` });
|
|
591
|
+
};
|
|
592
|
+
positional.forEach((a, i) => pass(a, params[i]));
|
|
593
|
+
for (const a of c.args) if (a.type === 'keyword_argument') pass(a, a.kw);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/* language conventions that make an entry: `if __name__ == "__main__":`, and a module-level `Ctor(kw=fn)` */
|
|
598
|
+
export function pythonEntries(py, R) {
|
|
599
|
+
const entries = [];
|
|
600
|
+
for (const m of py.mains) entries.push({ kind: 'main', id: m, file: py.symbols.get(m).file });
|
|
601
|
+
for (const c of py.rawCalls) if (c.top) for (const a of c.args) {
|
|
602
|
+
if (a.type !== 'keyword_argument' || a.vtype !== 'identifier') continue;
|
|
603
|
+
const r = R.resolveName(a.vtext, { imports: py.modules.get(c.module).imports, module: c.module });
|
|
604
|
+
if (r && r.id && py.symbols.get(r.id)?.kind === 'function') entries.push({ kind: 'registered-callback', id: r.id, via: `${c.expr}(${a.kw}=…)`, line: c.line, file: py.symbols.get(r.id).file });
|
|
605
|
+
}
|
|
606
|
+
return entries;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
export const conftestScope = file => { const d = dirname(file); return d === '.' ? '' : d + '/'; };
|