circle-ir 4.17.0 → 4.18.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/dist/analysis/advisories.d.ts +186 -0
- package/dist/analysis/advisories.d.ts.map +1 -0
- package/dist/analysis/advisories.js +391 -0
- package/dist/analysis/advisories.js.map +1 -0
- package/dist/analysis/comment-lines.d.ts +14 -0
- package/dist/analysis/comment-lines.d.ts.map +1 -0
- package/dist/analysis/comment-lines.js +34 -0
- package/dist/analysis/comment-lines.js.map +1 -0
- package/dist/analysis/config-loader.d.ts.map +1 -1
- package/dist/analysis/config-loader.js +59 -5
- package/dist/analysis/config-loader.js.map +1 -1
- package/dist/analysis/constant-caller-gate.d.ts +2 -2
- package/dist/analysis/constant-caller-gate.d.ts.map +1 -1
- package/dist/analysis/constant-caller-gate.js +205 -29
- package/dist/analysis/constant-caller-gate.js.map +1 -1
- package/dist/analysis/constant-propagation/propagator.d.ts +4 -0
- package/dist/analysis/constant-propagation/propagator.d.ts.map +1 -1
- package/dist/analysis/constant-propagation/propagator.js +99 -0
- package/dist/analysis/constant-propagation/propagator.js.map +1 -1
- package/dist/analysis/entry-point-detection.d.ts.map +1 -1
- package/dist/analysis/entry-point-detection.js +72 -0
- package/dist/analysis/entry-point-detection.js.map +1 -1
- package/dist/analysis/interprocedural.d.ts.map +1 -1
- package/dist/analysis/interprocedural.js +75 -8
- package/dist/analysis/interprocedural.js.map +1 -1
- package/dist/analysis/maven-declared.d.ts +72 -0
- package/dist/analysis/maven-declared.d.ts.map +1 -0
- package/dist/analysis/maven-declared.js +216 -0
- package/dist/analysis/maven-declared.js.map +1 -0
- package/dist/analysis/maven-version.d.ts +28 -0
- package/dist/analysis/maven-version.d.ts.map +1 -0
- package/dist/analysis/maven-version.js +172 -0
- package/dist/analysis/maven-version.js.map +1 -0
- package/dist/analysis/passes/constant-propagation-pass.d.ts.map +1 -1
- package/dist/analysis/passes/constant-propagation-pass.js +29 -1
- package/dist/analysis/passes/constant-propagation-pass.js.map +1 -1
- package/dist/analysis/passes/cross-file-pass.d.ts.map +1 -1
- package/dist/analysis/passes/cross-file-pass.js +81 -1
- package/dist/analysis/passes/cross-file-pass.js.map +1 -1
- package/dist/analysis/passes/csharp-sanitizer-credit.d.ts +7 -0
- package/dist/analysis/passes/csharp-sanitizer-credit.d.ts.map +1 -1
- package/dist/analysis/passes/csharp-sanitizer-credit.js +44 -1
- package/dist/analysis/passes/csharp-sanitizer-credit.js.map +1 -1
- package/dist/analysis/passes/insecure-cookie-pass.d.ts +2 -0
- package/dist/analysis/passes/insecure-cookie-pass.d.ts.map +1 -1
- package/dist/analysis/passes/insecure-cookie-pass.js +25 -3
- package/dist/analysis/passes/insecure-cookie-pass.js.map +1 -1
- package/dist/analysis/passes/interprocedural-pass.d.ts +11 -14
- package/dist/analysis/passes/interprocedural-pass.d.ts.map +1 -1
- package/dist/analysis/passes/interprocedural-pass.js +562 -0
- package/dist/analysis/passes/interprocedural-pass.js.map +1 -1
- package/dist/analysis/passes/language-sources-pass.d.ts +18 -1
- package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
- package/dist/analysis/passes/language-sources-pass.js +1057 -14
- package/dist/analysis/passes/language-sources-pass.js.map +1 -1
- package/dist/analysis/passes/param-source-flow-gate-pass.d.ts.map +1 -1
- package/dist/analysis/passes/param-source-flow-gate-pass.js +66 -3
- package/dist/analysis/passes/param-source-flow-gate-pass.js.map +1 -1
- package/dist/analysis/passes/sink-filter-pass.d.ts +44 -2
- package/dist/analysis/passes/sink-filter-pass.d.ts.map +1 -1
- package/dist/analysis/passes/sink-filter-pass.js +690 -12
- package/dist/analysis/passes/sink-filter-pass.js.map +1 -1
- package/dist/analysis/passes/taint-propagation-pass.d.ts.map +1 -1
- package/dist/analysis/passes/taint-propagation-pass.js +133 -10
- package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
- package/dist/analysis/passes/tls-verify-disabled-pass.d.ts +18 -0
- package/dist/analysis/passes/tls-verify-disabled-pass.d.ts.map +1 -1
- package/dist/analysis/passes/tls-verify-disabled-pass.js +161 -0
- package/dist/analysis/passes/tls-verify-disabled-pass.js.map +1 -1
- package/dist/analysis/passes/weak-random-pass.d.ts +6 -0
- package/dist/analysis/passes/weak-random-pass.d.ts.map +1 -1
- package/dist/analysis/passes/weak-random-pass.js +29 -0
- package/dist/analysis/passes/weak-random-pass.js.map +1 -1
- package/dist/analysis/promote-cross-file-flows.d.ts +17 -0
- package/dist/analysis/promote-cross-file-flows.d.ts.map +1 -0
- package/dist/analysis/promote-cross-file-flows.js +32 -0
- package/dist/analysis/promote-cross-file-flows.js.map +1 -0
- package/dist/analysis/sbom.d.ts +100 -0
- package/dist/analysis/sbom.d.ts.map +1 -1
- package/dist/analysis/sbom.js +514 -3
- package/dist/analysis/sbom.js.map +1 -1
- package/dist/analysis/semver-version.d.ts +30 -0
- package/dist/analysis/semver-version.d.ts.map +1 -0
- package/dist/analysis/semver-version.js +76 -0
- package/dist/analysis/semver-version.js.map +1 -0
- package/dist/analysis/taint-matcher.d.ts +5 -0
- package/dist/analysis/taint-matcher.d.ts.map +1 -1
- package/dist/analysis/taint-matcher.js +225 -4
- package/dist/analysis/taint-matcher.js.map +1 -1
- package/dist/analyzer.d.ts +57 -0
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +69 -6
- package/dist/analyzer.js.map +1 -1
- package/dist/browser/circle-ir.js +2936 -291
- package/dist/core/circle-ir-core.cjs +634 -203
- package/dist/core/circle-ir-core.js +634 -203
- package/dist/core/extractors/dfg.js +205 -2
- package/dist/core/extractors/dfg.js.map +1 -1
- package/dist/core/extractors/ts-declaration-detail.d.ts +39 -0
- package/dist/core/extractors/ts-declaration-detail.d.ts.map +1 -0
- package/dist/core/extractors/ts-declaration-detail.js +266 -0
- package/dist/core/extractors/ts-declaration-detail.js.map +1 -0
- package/dist/graph/analysis-pass.d.ts +4 -1
- package/dist/graph/analysis-pass.d.ts.map +1 -1
- package/dist/graph/analysis-pass.js +4 -1
- package/dist/graph/analysis-pass.js.map +1 -1
- package/dist/graph-facts/dead-code.d.ts +62 -0
- package/dist/graph-facts/dead-code.d.ts.map +1 -0
- package/dist/graph-facts/dead-code.js +90 -0
- package/dist/graph-facts/dead-code.js.map +1 -0
- package/dist/graph-facts/dependency-calls.d.ts +86 -0
- package/dist/graph-facts/dependency-calls.d.ts.map +1 -0
- package/dist/graph-facts/dependency-calls.js +93 -0
- package/dist/graph-facts/dependency-calls.js.map +1 -0
- package/dist/graph-facts/index.d.ts +10 -0
- package/dist/graph-facts/index.d.ts.map +1 -0
- package/dist/graph-facts/index.js +10 -0
- package/dist/graph-facts/index.js.map +1 -0
- package/dist/graph-facts/maven-packages.d.ts +42 -0
- package/dist/graph-facts/maven-packages.d.ts.map +1 -0
- package/dist/graph-facts/maven-packages.js +133 -0
- package/dist/graph-facts/maven-packages.js.map +1 -0
- package/dist/graph-facts/reach.d.ts +203 -0
- package/dist/graph-facts/reach.d.ts.map +1 -0
- package/dist/graph-facts/reach.js +219 -0
- package/dist/graph-facts/reach.js.map +1 -0
- package/dist/graph-facts/removal-reach.d.ts +73 -0
- package/dist/graph-facts/removal-reach.d.ts.map +1 -0
- package/dist/graph-facts/removal-reach.js +64 -0
- package/dist/graph-facts/removal-reach.js.map +1 -0
- package/dist/index.d.ts +21 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -3
- package/dist/index.js.map +1 -1
- package/dist/navigation/go-graph.d.ts +68 -0
- package/dist/navigation/go-graph.d.ts.map +1 -0
- package/dist/navigation/go-graph.js +753 -0
- package/dist/navigation/go-graph.js.map +1 -0
- package/dist/navigation/index.d.ts +7 -1
- package/dist/navigation/index.d.ts.map +1 -1
- package/dist/navigation/index.js +6 -0
- package/dist/navigation/index.js.map +1 -1
- package/dist/navigation/java-entry-tables.d.ts +43 -0
- package/dist/navigation/java-entry-tables.d.ts.map +1 -0
- package/dist/navigation/java-entry-tables.js +56 -0
- package/dist/navigation/java-entry-tables.js.map +1 -0
- package/dist/navigation/java-graph.d.ts +64 -0
- package/dist/navigation/java-graph.d.ts.map +1 -0
- package/dist/navigation/java-graph.js +466 -0
- package/dist/navigation/java-graph.js.map +1 -0
- package/dist/navigation/navigation-index.d.ts +51 -1
- package/dist/navigation/navigation-index.d.ts.map +1 -1
- package/dist/navigation/navigation-index.js +72 -0
- package/dist/navigation/navigation-index.js.map +1 -1
- package/dist/navigation/python-graph.d.ts +73 -0
- package/dist/navigation/python-graph.d.ts.map +1 -0
- package/dist/navigation/python-graph.js +867 -0
- package/dist/navigation/python-graph.js.map +1 -0
- package/dist/navigation/ts-call-edges.d.ts +193 -0
- package/dist/navigation/ts-call-edges.d.ts.map +1 -0
- package/dist/navigation/ts-call-edges.js +2089 -0
- package/dist/navigation/ts-call-edges.js.map +1 -0
- package/dist/navigation/ts-entry-tables.d.ts +88 -0
- package/dist/navigation/ts-entry-tables.d.ts.map +1 -0
- package/dist/navigation/ts-entry-tables.js +50 -0
- package/dist/navigation/ts-entry-tables.js.map +1 -0
- package/dist/path-set/branches.d.ts +52 -0
- package/dist/path-set/branches.d.ts.map +1 -0
- package/dist/path-set/branches.js +124 -0
- package/dist/path-set/branches.js.map +1 -0
- package/dist/path-set/finding-diff.d.ts +63 -0
- package/dist/path-set/finding-diff.d.ts.map +1 -0
- package/dist/path-set/finding-diff.js +150 -0
- package/dist/path-set/finding-diff.js.map +1 -0
- package/dist/path-set/index.d.ts +18 -0
- package/dist/path-set/index.d.ts.map +1 -0
- package/dist/path-set/index.js +12 -0
- package/dist/path-set/index.js.map +1 -0
- package/dist/path-set/line-diff.d.ts +19 -0
- package/dist/path-set/line-diff.d.ts.map +1 -0
- package/dist/path-set/line-diff.js +98 -0
- package/dist/path-set/line-diff.js.map +1 -0
- package/dist/path-set/path-set.d.ts +88 -0
- package/dist/path-set/path-set.d.ts.map +1 -0
- package/dist/path-set/path-set.js +716 -0
- package/dist/path-set/path-set.js.map +1 -0
- package/dist/path-set/templates.d.ts +42 -0
- package/dist/path-set/templates.d.ts.map +1 -0
- package/dist/path-set/templates.js +119 -0
- package/dist/path-set/templates.js.map +1 -0
- package/dist/path-set/types.d.ts +195 -0
- package/dist/path-set/types.d.ts.map +1 -0
- package/dist/path-set/types.js +8 -0
- package/dist/path-set/types.js.map +1 -0
- package/dist/remedy-class/index.d.ts +12 -0
- package/dist/remedy-class/index.d.ts.map +1 -0
- package/dist/remedy-class/index.js +9 -0
- package/dist/remedy-class/index.js.map +1 -0
- package/dist/remedy-class/match.d.ts +165 -0
- package/dist/remedy-class/match.d.ts.map +1 -0
- package/dist/remedy-class/match.js +705 -0
- package/dist/remedy-class/match.js.map +1 -0
- package/dist/remedy-class/output-context.d.ts +25 -0
- package/dist/remedy-class/output-context.d.ts.map +1 -0
- package/dist/remedy-class/output-context.js +150 -0
- package/dist/remedy-class/output-context.js.map +1 -0
- package/dist/remedy-class/sha256.d.ts +18 -0
- package/dist/remedy-class/sha256.d.ts.map +1 -0
- package/dist/remedy-class/sha256.js +89 -0
- package/dist/remedy-class/sha256.js.map +1 -0
- package/dist/remedy-class/table.d.ts +130 -0
- package/dist/remedy-class/table.d.ts.map +1 -0
- package/dist/remedy-class/table.js +457 -0
- package/dist/remedy-class/table.js.map +1 -0
- package/dist/resolution/cross-file.d.ts +9 -0
- package/dist/resolution/cross-file.d.ts.map +1 -1
- package/dist/resolution/cross-file.js +60 -5
- package/dist/resolution/cross-file.js.map +1 -1
- package/dist/stable-ids/builder.d.ts +20 -0
- package/dist/stable-ids/builder.d.ts.map +1 -0
- package/dist/stable-ids/builder.js +65 -0
- package/dist/stable-ids/builder.js.map +1 -0
- package/dist/stable-ids/diff.d.ts +18 -0
- package/dist/stable-ids/diff.d.ts.map +1 -0
- package/dist/stable-ids/diff.js +37 -0
- package/dist/stable-ids/diff.js.map +1 -0
- package/dist/stable-ids/go.d.ts +28 -0
- package/dist/stable-ids/go.d.ts.map +1 -0
- package/dist/stable-ids/go.js +160 -0
- package/dist/stable-ids/go.js.map +1 -0
- package/dist/stable-ids/hash.d.ts +22 -0
- package/dist/stable-ids/hash.d.ts.map +1 -0
- package/dist/stable-ids/hash.js +167 -0
- package/dist/stable-ids/hash.js.map +1 -0
- package/dist/stable-ids/index.d.ts +30 -0
- package/dist/stable-ids/index.d.ts.map +1 -0
- package/dist/stable-ids/index.js +59 -0
- package/dist/stable-ids/index.js.map +1 -0
- package/dist/stable-ids/java.d.ts +28 -0
- package/dist/stable-ids/java.d.ts.map +1 -0
- package/dist/stable-ids/java.js +224 -0
- package/dist/stable-ids/java.js.map +1 -0
- package/dist/stable-ids/moves.d.ts +87 -0
- package/dist/stable-ids/moves.d.ts.map +1 -0
- package/dist/stable-ids/moves.js +222 -0
- package/dist/stable-ids/moves.js.map +1 -0
- package/dist/stable-ids/nodes.d.ts +32 -0
- package/dist/stable-ids/nodes.d.ts.map +1 -0
- package/dist/stable-ids/nodes.js +92 -0
- package/dist/stable-ids/nodes.js.map +1 -0
- package/dist/stable-ids/normalize.d.ts +35 -0
- package/dist/stable-ids/normalize.d.ts.map +1 -0
- package/dist/stable-ids/normalize.js +174 -0
- package/dist/stable-ids/normalize.js.map +1 -0
- package/dist/stable-ids/parse-errors.d.ts +24 -0
- package/dist/stable-ids/parse-errors.d.ts.map +1 -0
- package/dist/stable-ids/parse-errors.js +46 -0
- package/dist/stable-ids/parse-errors.js.map +1 -0
- package/dist/stable-ids/python.d.ts +27 -0
- package/dist/stable-ids/python.d.ts.map +1 -0
- package/dist/stable-ids/python.js +212 -0
- package/dist/stable-ids/python.js.map +1 -0
- package/dist/stable-ids/ts-parse-errors.d.ts +27 -0
- package/dist/stable-ids/ts-parse-errors.d.ts.map +1 -0
- package/dist/stable-ids/ts-parse-errors.js +75 -0
- package/dist/stable-ids/ts-parse-errors.js.map +1 -0
- package/dist/stable-ids/types.d.ts +106 -0
- package/dist/stable-ids/types.d.ts.map +1 -0
- package/dist/stable-ids/types.js +16 -0
- package/dist/stable-ids/types.js.map +1 -0
- package/dist/stable-ids/typescript.d.ts +30 -0
- package/dist/stable-ids/typescript.d.ts.map +1 -0
- package/dist/stable-ids/typescript.js +362 -0
- package/dist/stable-ids/typescript.js.map +1 -0
- package/dist/types/config.d.ts +9 -0
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/index.d.ts +35 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,867 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Python graph facts keyed by stable ids (`python-graph@0.1`): the reach
|
|
3
|
+
* core's input for Python.
|
|
4
|
+
*
|
|
5
|
+
* - **Declarations** from `derivePythonIds` (`ids@0.1`).
|
|
6
|
+
* - **Call edges**, resolved from the syntax tree:
|
|
7
|
+
* - `exact`: a name bound in the module or imported from a project module,
|
|
8
|
+
* `module.f` through an import alias, `Class.m`, `self.m` / `cls.m` in a
|
|
9
|
+
* method (the class's own or an inherited member), `super().m`, and
|
|
10
|
+
* `Class(...)` (the class and its `__init__`).
|
|
11
|
+
* - `polymorphic`: `self.m` also reaches the overrides in project subclasses.
|
|
12
|
+
* - `inferred`: `obj.m` on a receiver of unknown type reaches every project
|
|
13
|
+
* method named `m`, up to `maxInferred`; beyond it the site is unresolved.
|
|
14
|
+
* A target outside the project is an unresolved site (`external`), never
|
|
15
|
+
* dropped; `getattr` and friends are `dynamic`.
|
|
16
|
+
* - **References**: a project function or class used as a value.
|
|
17
|
+
* - **Imports**: a module loads the project modules it imports.
|
|
18
|
+
* - **Loads**: a class reaches its dunder methods (`__str__`, `__enter__`, …),
|
|
19
|
+
* which the runtime calls without a call site.
|
|
20
|
+
* - **Entries** (`entry-index@0.1`): Flask and FastAPI routes (blueprint and
|
|
21
|
+
* router prefixes composed), Django `urls.py` routes, click commands,
|
|
22
|
+
* `if __name__ == "__main__"` blocks and `main` files, console scripts,
|
|
23
|
+
* Celery tasks, Django management commands, pytest and unittest tests.
|
|
24
|
+
* - **typeOnly**: `Protocol` classes, `@overload` stubs and `.pyi` files.
|
|
25
|
+
*
|
|
26
|
+
* Library code: no file access; the caller passes the sources.
|
|
27
|
+
*/
|
|
28
|
+
import { disposeTree, parse } from '../core/parser.js';
|
|
29
|
+
import { derivePythonIds, pythonModuleName } from '../stable-ids/python.js';
|
|
30
|
+
import { declLocator } from '../stable-ids/nodes.js';
|
|
31
|
+
import { namedChildren } from '../stable-ids/normalize.js';
|
|
32
|
+
import { joinRoutePath } from './ts-call-edges.js';
|
|
33
|
+
export const PYTHON_GRAPH_SCHEME = 'python-graph@0.1';
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Tables
|
|
36
|
+
const HTTP_VERBS = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options'];
|
|
37
|
+
const DYNAMIC_CALLS = new Set(['getattr', 'setattr', 'eval', 'exec', '__import__', 'import_module']);
|
|
38
|
+
/** Called by the runtime, never through a call site. */
|
|
39
|
+
const DUNDER = /^__[a-z0-9_]+__$/;
|
|
40
|
+
const TEST_FILE = /(^|\/)(test_[^/]*|[^/]*_test)\.py$|(^|\/)tests?\/.*\.py$|(^|\/)conftest\.py$/;
|
|
41
|
+
const TEST_CASE_BASES = new Set(['TestCase', 'unittest.TestCase', 'IsolatedAsyncioTestCase', 'TransactionTestCase', 'SimpleTestCase', 'APITestCase', 'LiveServerTestCase']);
|
|
42
|
+
const TEST_LIFECYCLE = new Set(['setUp', 'tearDown', 'setUpClass', 'tearDownClass', 'asyncSetUp', 'asyncTearDown', 'setup_method', 'teardown_method', 'setup_class', 'teardown_class']);
|
|
43
|
+
const DJANGO_CBV_METHODS = new Set(['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'dispatch', 'get_queryset', 'get_context_data', 'get_object', 'form_valid', 'form_invalid', 'perform_create', 'perform_update', 'perform_destroy', 'list', 'create', 'retrieve', 'update', 'partial_update', 'destroy']);
|
|
44
|
+
const posKey = (line, col) => `${line}:${col}`;
|
|
45
|
+
function text(n) {
|
|
46
|
+
return n?.text ?? '';
|
|
47
|
+
}
|
|
48
|
+
function stringValue(n) {
|
|
49
|
+
if (!n || n.type !== 'string')
|
|
50
|
+
return null;
|
|
51
|
+
const parts = namedChildren(n).filter(c => c.type === 'string_content');
|
|
52
|
+
if (namedChildren(n).some(c => c.type === 'interpolation'))
|
|
53
|
+
return null;
|
|
54
|
+
return parts.map(c => c.text).join('');
|
|
55
|
+
}
|
|
56
|
+
function walk(node, visit) {
|
|
57
|
+
if (visit(node) === false)
|
|
58
|
+
return;
|
|
59
|
+
for (const c of namedChildren(node))
|
|
60
|
+
walk(c, visit);
|
|
61
|
+
}
|
|
62
|
+
/** Resolve a relative module (`.`, `..pkg`) against a module's package. */
|
|
63
|
+
function resolveRelative(current, isPackage, written) {
|
|
64
|
+
const dots = written.length - written.replace(/^\.+/, '').length;
|
|
65
|
+
const rest = written.slice(dots);
|
|
66
|
+
const parts = current.split('.');
|
|
67
|
+
// A module's package is its name minus the last part; a package (`__init__`) is its own package.
|
|
68
|
+
let base = isPackage ? parts : parts.slice(0, -1);
|
|
69
|
+
base = base.slice(0, Math.max(0, base.length - (dots - 1)));
|
|
70
|
+
return [...base, ...(rest ? rest.split('.') : [])].filter(x => x !== '').join('.');
|
|
71
|
+
}
|
|
72
|
+
function importsOf(root, module, isPackage) {
|
|
73
|
+
const bindings = new Map();
|
|
74
|
+
const modules = [];
|
|
75
|
+
walk(root, n => {
|
|
76
|
+
if (n.type === 'function_definition' || n.type === 'class_definition')
|
|
77
|
+
return; // module-level imports only bind names here; nested ones handled loosely below
|
|
78
|
+
if (n.type === 'import_statement') {
|
|
79
|
+
for (const c of namedChildren(n)) {
|
|
80
|
+
if (c.type === 'dotted_name') {
|
|
81
|
+
const full = c.text;
|
|
82
|
+
modules.push(full);
|
|
83
|
+
bindings.set(full.split('.')[0], { kind: 'module', module: full.split('.')[0] });
|
|
84
|
+
// `import a.b` also makes `a.b` reachable through attribute access.
|
|
85
|
+
bindings.set(full, { kind: 'module', module: full });
|
|
86
|
+
}
|
|
87
|
+
else if (c.type === 'aliased_import') {
|
|
88
|
+
const name = text(c.childForFieldName('name'));
|
|
89
|
+
const alias = text(c.childForFieldName('alias'));
|
|
90
|
+
modules.push(name);
|
|
91
|
+
bindings.set(alias, { kind: 'module', module: name });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
if (n.type === 'import_from_statement') {
|
|
97
|
+
const modNode = n.childForFieldName('module_name');
|
|
98
|
+
const written = text(modNode);
|
|
99
|
+
const from = written.startsWith('.') ? resolveRelative(module, isPackage, written) : written;
|
|
100
|
+
modules.push(from);
|
|
101
|
+
for (const c of namedChildren(n)) {
|
|
102
|
+
if (c === modNode)
|
|
103
|
+
continue;
|
|
104
|
+
if (c.type === 'dotted_name') {
|
|
105
|
+
bindings.set(c.text, { kind: 'member', module: from, name: c.text });
|
|
106
|
+
}
|
|
107
|
+
else if (c.type === 'aliased_import') {
|
|
108
|
+
const name = text(c.childForFieldName('name'));
|
|
109
|
+
bindings.set(text(c.childForFieldName('alias')), { kind: 'member', module: from, name });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
return undefined;
|
|
115
|
+
});
|
|
116
|
+
return { bindings, modules };
|
|
117
|
+
}
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
// Builder
|
|
120
|
+
class PyGraphBuilder {
|
|
121
|
+
maxInferred;
|
|
122
|
+
ctxs = [];
|
|
123
|
+
byModule = new Map();
|
|
124
|
+
classes = new Map(); // by decl id
|
|
125
|
+
methodsByName = new Map();
|
|
126
|
+
declFile = new Map();
|
|
127
|
+
edges = [];
|
|
128
|
+
unresolved = [];
|
|
129
|
+
references = [];
|
|
130
|
+
imports = [];
|
|
131
|
+
loads = [];
|
|
132
|
+
entries = [];
|
|
133
|
+
unresolvedRegistrations = [];
|
|
134
|
+
typeOnly = new Set();
|
|
135
|
+
siteKeys = new Set();
|
|
136
|
+
edgeKeys = new Set();
|
|
137
|
+
constructor(maxInferred) {
|
|
138
|
+
this.maxInferred = maxInferred;
|
|
139
|
+
}
|
|
140
|
+
add(ctx) {
|
|
141
|
+
this.ctxs.push(ctx);
|
|
142
|
+
this.byModule.set(ctx.module, ctx);
|
|
143
|
+
for (const d of ctx.ids.decls)
|
|
144
|
+
this.declFile.set(d.id, ctx);
|
|
145
|
+
}
|
|
146
|
+
// -- indexes -------------------------------------------------------------
|
|
147
|
+
index() {
|
|
148
|
+
for (const ctx of this.ctxs) {
|
|
149
|
+
for (const d of ctx.ids.decls) {
|
|
150
|
+
if (d.kind === 'type' && !d.flags.includes('unstable:anonymous')) {
|
|
151
|
+
this.classes.set(d.id, { decl: d, ctx, members: new Map(), bases: [], parents: [], children: [] });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
for (const d of ctx.ids.decls) {
|
|
155
|
+
const parentClass = d.parent ? this.classes.get(d.parent) : undefined;
|
|
156
|
+
if (parentClass && !d.flags.includes('unstable:anonymous'))
|
|
157
|
+
parentClass.members.set(d.name, d);
|
|
158
|
+
if (d.kind === 'method' && !d.flags.includes('unstable:anonymous')) {
|
|
159
|
+
const list = this.methodsByName.get(d.name) ?? [];
|
|
160
|
+
list.push(d);
|
|
161
|
+
this.methodsByName.set(d.name, list);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
// Bases from the class definitions.
|
|
166
|
+
for (const ctx of this.ctxs) {
|
|
167
|
+
walk(ctx.root, n => {
|
|
168
|
+
if (n.type !== 'class_definition')
|
|
169
|
+
return;
|
|
170
|
+
const decl = this.declAt(ctx, n);
|
|
171
|
+
const info = decl ? this.classes.get(decl.id) : undefined;
|
|
172
|
+
const supers = n.childForFieldName('superclasses');
|
|
173
|
+
if (info && supers) {
|
|
174
|
+
info.bases = namedChildren(supers).filter(c => c.type === 'identifier' || c.type === 'attribute').map(c => c.text);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
for (const ctx of this.ctxs) {
|
|
179
|
+
for (const st of namedChildren(ctx.root)) {
|
|
180
|
+
if (st.type !== 'expression_statement')
|
|
181
|
+
continue;
|
|
182
|
+
const a = namedChildren(st)[0];
|
|
183
|
+
const left = a?.type === 'assignment' ? a.childForFieldName('left') : null;
|
|
184
|
+
const right = a?.childForFieldName('right');
|
|
185
|
+
if (left?.type !== 'identifier' || !right)
|
|
186
|
+
continue;
|
|
187
|
+
const callee = (right.type === 'call' ? text(right.childForFieldName('function')) : right.text).replace(/\s+/g, '');
|
|
188
|
+
if (/^[A-Za-z_][\w.]*$/.test(callee) && !this.resolveName(ctx, callee) && this.isExternal(ctx, callee))
|
|
189
|
+
ctx.external.add(left.text);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
for (const info of this.classes.values()) {
|
|
193
|
+
for (const b of info.bases) {
|
|
194
|
+
const target = this.resolveName(info.ctx, b);
|
|
195
|
+
const parent = target ? this.classes.get(target.id) : undefined;
|
|
196
|
+
if (parent) {
|
|
197
|
+
info.parents.push(parent);
|
|
198
|
+
parent.children.push(info);
|
|
199
|
+
// Defining a class evaluates its bases.
|
|
200
|
+
this.loads.push({ from: info.decl.id, to: parent.decl.id });
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
/** The declaration a definition node (or its decorated wrapper) declares. */
|
|
206
|
+
declAt(ctx, def) {
|
|
207
|
+
const holder = def.parent?.type === 'decorated_definition' ? def.parent : def;
|
|
208
|
+
return ctx.byLine.get(posKey(holder.startPosition.row + 1, holder.startPosition.column));
|
|
209
|
+
}
|
|
210
|
+
/** Resolve a dotted name as written in a file to a project declaration. */
|
|
211
|
+
resolveName(ctx, written) {
|
|
212
|
+
const parts = written.split('.');
|
|
213
|
+
// Local top-level name.
|
|
214
|
+
const local = ctx.top.get(parts[0]);
|
|
215
|
+
if (local && parts.length === 1)
|
|
216
|
+
return local;
|
|
217
|
+
if (local && local.kind === 'type')
|
|
218
|
+
return this.member(this.classes.get(local.id), parts.slice(1));
|
|
219
|
+
// Imported.
|
|
220
|
+
for (let i = parts.length; i >= 1; i--) {
|
|
221
|
+
const head = parts.slice(0, i).join('.');
|
|
222
|
+
const b = ctx.imports.get(head);
|
|
223
|
+
if (!b)
|
|
224
|
+
continue;
|
|
225
|
+
const rest = parts.slice(i);
|
|
226
|
+
if (b.kind === 'module')
|
|
227
|
+
return this.inModule(b.module, rest);
|
|
228
|
+
const target = this.byModule.get(b.module);
|
|
229
|
+
if (target) {
|
|
230
|
+
const d = target.top.get(b.name);
|
|
231
|
+
if (d)
|
|
232
|
+
return rest.length === 0 ? d : d.kind === 'type' ? this.member(this.classes.get(d.id), rest) : null;
|
|
233
|
+
}
|
|
234
|
+
// `from pkg import submodule`
|
|
235
|
+
return this.inModule(`${b.module}.${b.name}`, rest);
|
|
236
|
+
}
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
inModule(module, rest) {
|
|
240
|
+
// Longest module prefix.
|
|
241
|
+
for (let i = rest.length; i >= 0; i--) {
|
|
242
|
+
const m = [module, ...rest.slice(0, i)].join('.');
|
|
243
|
+
const ctx = this.byModule.get(m);
|
|
244
|
+
if (!ctx)
|
|
245
|
+
continue;
|
|
246
|
+
const tail = rest.slice(i);
|
|
247
|
+
if (tail.length === 0)
|
|
248
|
+
return ctx.ids.decls[0] ?? null;
|
|
249
|
+
const d = ctx.top.get(tail[0]);
|
|
250
|
+
if (!d)
|
|
251
|
+
return null;
|
|
252
|
+
if (tail.length === 1)
|
|
253
|
+
return d;
|
|
254
|
+
return d.kind === 'type' ? this.member(this.classes.get(d.id), tail.slice(1)) : null;
|
|
255
|
+
}
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
/** A member of a class, looking through project bases. */
|
|
259
|
+
member(info, path) {
|
|
260
|
+
if (!info || path.length !== 1)
|
|
261
|
+
return null;
|
|
262
|
+
const seen = new Set();
|
|
263
|
+
const queue = [info];
|
|
264
|
+
while (queue.length > 0) {
|
|
265
|
+
const c = queue.shift();
|
|
266
|
+
if (seen.has(c.decl.id))
|
|
267
|
+
continue;
|
|
268
|
+
seen.add(c.decl.id);
|
|
269
|
+
const m = c.members.get(path[0]);
|
|
270
|
+
if (m)
|
|
271
|
+
return m;
|
|
272
|
+
queue.push(...c.parents);
|
|
273
|
+
}
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
isExternal(ctx, written) {
|
|
277
|
+
const head = written.split('.')[0];
|
|
278
|
+
const b = ctx.imports.get(head);
|
|
279
|
+
if (!b)
|
|
280
|
+
return false;
|
|
281
|
+
return !this.byModule.has(b.module) && ![...this.byModule.keys()].some(m => m.startsWith(b.module + '.') || b.module.startsWith(m + '.'));
|
|
282
|
+
}
|
|
283
|
+
// -- recording -----------------------------------------------------------
|
|
284
|
+
site(ctx, n) {
|
|
285
|
+
return { file: ctx.path, line: n.startPosition.row + 1, col: n.startPosition.column };
|
|
286
|
+
}
|
|
287
|
+
edge(from, to, tier, methodName, site, evidence, via) {
|
|
288
|
+
if (from.id === to.id && tier !== 'exact')
|
|
289
|
+
return;
|
|
290
|
+
const key = `${from.id}>${to.id}@${site.file}:${site.line}:${site.col}`;
|
|
291
|
+
if (this.edgeKeys.has(key))
|
|
292
|
+
return;
|
|
293
|
+
this.edgeKeys.add(key);
|
|
294
|
+
this.siteKeys.add(`${site.file}:${site.line}:${site.col}:${methodName}`);
|
|
295
|
+
const e = { from: from.id, to: to.id, tier, methodName, site, evidence };
|
|
296
|
+
if (via)
|
|
297
|
+
e.via = via;
|
|
298
|
+
this.edges.push(e);
|
|
299
|
+
}
|
|
300
|
+
unresolvedSite(from, methodName, site, reason, evidence) {
|
|
301
|
+
this.siteKeys.add(`${site.file}:${site.line}:${site.col}:${methodName}`);
|
|
302
|
+
this.unresolved.push({ from: from.id, methodName, site, reason, evidence });
|
|
303
|
+
}
|
|
304
|
+
get sites() {
|
|
305
|
+
return this.siteKeys.size;
|
|
306
|
+
}
|
|
307
|
+
entry(e) {
|
|
308
|
+
if (this.entries.some(x => x.id === e.id && x.kind === e.kind && x.method === e.method && x.path === e.path))
|
|
309
|
+
return;
|
|
310
|
+
this.entries.push(e);
|
|
311
|
+
}
|
|
312
|
+
// -- calls ---------------------------------------------------------------
|
|
313
|
+
/** The enclosing declaration of a node: the innermost decl whose range holds it. */
|
|
314
|
+
enclosing(ctx, n) {
|
|
315
|
+
return ctx.locate(n.startPosition.row + 1, n.startPosition.column) ?? ctx.ids.decls[0];
|
|
316
|
+
}
|
|
317
|
+
/** The class whose method encloses `from`. */
|
|
318
|
+
classOf(from) {
|
|
319
|
+
let d = from;
|
|
320
|
+
const ctx = this.declFile.get(from.id);
|
|
321
|
+
while (d) {
|
|
322
|
+
const c = this.classes.get(d.id);
|
|
323
|
+
if (c)
|
|
324
|
+
return c;
|
|
325
|
+
d = d.parent ? ctx?.ids.decls.find(x => x.id === d.parent) : undefined;
|
|
326
|
+
}
|
|
327
|
+
return undefined;
|
|
328
|
+
}
|
|
329
|
+
overrides(info, name) {
|
|
330
|
+
const out = [];
|
|
331
|
+
const queue = [...info.children];
|
|
332
|
+
const seen = new Set();
|
|
333
|
+
while (queue.length > 0) {
|
|
334
|
+
const c = queue.shift();
|
|
335
|
+
if (seen.has(c.decl.id))
|
|
336
|
+
continue;
|
|
337
|
+
seen.add(c.decl.id);
|
|
338
|
+
const m = c.members.get(name);
|
|
339
|
+
if (m)
|
|
340
|
+
out.push(m);
|
|
341
|
+
queue.push(...c.children);
|
|
342
|
+
}
|
|
343
|
+
return out;
|
|
344
|
+
}
|
|
345
|
+
callTarget(ctx, from, call, isDecorator) {
|
|
346
|
+
const fn = call.type === 'call' ? call.childForFieldName('function') : call;
|
|
347
|
+
if (!fn)
|
|
348
|
+
return;
|
|
349
|
+
const site = this.site(ctx, fn);
|
|
350
|
+
if (fn.type === 'identifier') {
|
|
351
|
+
const name = fn.text;
|
|
352
|
+
if (DYNAMIC_CALLS.has(name)) {
|
|
353
|
+
this.unresolvedSite(from, `<${name}>`, site, 'dynamic', `${name}(): runtime-determined target`);
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
const d = this.resolveName(ctx, name);
|
|
357
|
+
if (d) {
|
|
358
|
+
this.callDecl(from, d, name, site, isDecorator ? 'decorator' : 'name bound in the module or imported');
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
if (this.isExternal(ctx, name))
|
|
362
|
+
this.unresolvedSite(from, name, site, 'external', `${name}: imported from outside the project`);
|
|
363
|
+
// Builtins and locals (parameters, nested scopes) are not project calls.
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
if (fn.type === 'attribute') {
|
|
367
|
+
const obj = fn.childForFieldName('object');
|
|
368
|
+
const attr = text(fn.childForFieldName('attribute'));
|
|
369
|
+
if (!obj || !attr)
|
|
370
|
+
return;
|
|
371
|
+
// super().m
|
|
372
|
+
if (obj.type === 'call' && text(obj.childForFieldName('function')) === 'super') {
|
|
373
|
+
const cls = this.classOf(from);
|
|
374
|
+
const target = cls ? cls.parents.map(p => this.member(p, [attr])).find(Boolean) : null;
|
|
375
|
+
if (target)
|
|
376
|
+
this.edge(from, target, 'exact', attr, site, 'super() member');
|
|
377
|
+
else
|
|
378
|
+
this.unresolvedSite(from, attr, site, cls && cls.bases.length > cls.parents.length ? 'external' : 'unknown', 'super() member not in the project');
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
// self.m / cls.m inside a class
|
|
382
|
+
if (obj.type === 'identifier' && (obj.text === 'self' || obj.text === 'cls')) {
|
|
383
|
+
const cls = this.classOf(from);
|
|
384
|
+
if (cls) {
|
|
385
|
+
const own = this.member(cls, [attr]);
|
|
386
|
+
if (own && own.kind !== 'field') {
|
|
387
|
+
this.edge(from, own, 'exact', attr, site, `${obj.text}.${attr} on ${cls.decl.name}`);
|
|
388
|
+
for (const o of this.overrides(cls, attr)) {
|
|
389
|
+
if (o.id !== own.id)
|
|
390
|
+
this.edge(from, o, 'polymorphic', attr, site, `override in a subclass of ${cls.decl.name}`, own.id);
|
|
391
|
+
}
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
if (own)
|
|
395
|
+
return; // a field read through a call: a stored callable
|
|
396
|
+
const subs = this.overrides(cls, attr);
|
|
397
|
+
for (const o of subs)
|
|
398
|
+
this.edge(from, o, 'polymorphic', attr, site, `defined only in subclasses of ${cls.decl.name}`);
|
|
399
|
+
if (subs.length > 0)
|
|
400
|
+
return;
|
|
401
|
+
if (cls.bases.length > cls.parents.length) {
|
|
402
|
+
this.unresolvedSite(from, attr, site, 'external', `${obj.text}.${attr}: inherited from a base outside the project`);
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
// A receiver bound to an object from outside the project.
|
|
408
|
+
const root = fn.text.split('.')[0];
|
|
409
|
+
if (ctx.external.has(root)) {
|
|
410
|
+
this.unresolvedSite(from, attr, site, 'external', `${root}.${attr}: ${root} comes from outside the project`);
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
// Dotted name: module.f, Class.m, alias.sub.f
|
|
414
|
+
const dotted = fn.text.replace(/\s+/g, '');
|
|
415
|
+
if (/^[A-Za-z_][\w.]*$/.test(dotted)) {
|
|
416
|
+
const d = this.resolveName(ctx, dotted);
|
|
417
|
+
if (d) {
|
|
418
|
+
this.callDecl(from, d, attr, site, isDecorator ? 'decorator' : 'dotted name through a module or class');
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
if (this.isExternal(ctx, dotted)) {
|
|
422
|
+
this.unresolvedSite(from, attr, site, 'external', `${dotted}: imported from outside the project`);
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
// Receiver of unknown type: every project method of that name.
|
|
427
|
+
const candidates = (this.methodsByName.get(attr) ?? []).filter(m => !DUNDER.test(m.name) || attr === m.name);
|
|
428
|
+
if (candidates.length === 0) {
|
|
429
|
+
this.unresolvedSite(from, attr, site, 'external', `.${attr}(): no project method of that name`);
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
if (candidates.length > this.maxInferred) {
|
|
433
|
+
this.unresolvedSite(from, attr, site, 'unknown', `.${attr}(): ${candidates.length} project methods of that name`);
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
for (const m of candidates)
|
|
437
|
+
this.edge(from, m, 'inferred', attr, site, `.${attr}() on a receiver of unknown type`);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
/** A call that resolved to `d`: a class runs its `__init__`. */
|
|
441
|
+
callDecl(from, d, name, site, evidence) {
|
|
442
|
+
if (d.kind === 'module')
|
|
443
|
+
return;
|
|
444
|
+
this.edge(from, d, 'exact', name, site, evidence);
|
|
445
|
+
if (d.kind === 'type') {
|
|
446
|
+
const init = this.member(this.classes.get(d.id), ['__init__']);
|
|
447
|
+
if (init)
|
|
448
|
+
this.edge(from, init, 'exact', '__init__', site, `${d.name}() runs __init__`);
|
|
449
|
+
const nw = this.member(this.classes.get(d.id), ['__new__']);
|
|
450
|
+
if (nw)
|
|
451
|
+
this.edge(from, nw, 'exact', '__new__', site, `${d.name}() runs __new__`);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
/** Walk calls and value references in a file. */
|
|
455
|
+
calls(ctx) {
|
|
456
|
+
walk(ctx.root, n => {
|
|
457
|
+
if (n.type === 'decorator') {
|
|
458
|
+
// A decorator runs where the definition is evaluated: in the enclosing scope.
|
|
459
|
+
const holder = n.parent;
|
|
460
|
+
const def = holder?.childForFieldName('definition');
|
|
461
|
+
const decl = def ? this.declAt(ctx, def) : undefined;
|
|
462
|
+
const scope = decl?.parent ? ctx.ids.decls.find(d => d.id === decl.parent) ?? ctx.ids.decls[0] : ctx.ids.decls[0];
|
|
463
|
+
const expr = namedChildren(n)[0];
|
|
464
|
+
if (expr) {
|
|
465
|
+
const target = expr.type === 'call' ? expr : expr;
|
|
466
|
+
this.callTarget(ctx, scope, target, true);
|
|
467
|
+
if (expr.type === 'call') {
|
|
468
|
+
const args = expr.childForFieldName('arguments');
|
|
469
|
+
if (args)
|
|
470
|
+
walk(args, m => this.visitExpr(ctx, scope, m));
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
return false;
|
|
474
|
+
}
|
|
475
|
+
return this.visitExpr(ctx, this.enclosing(ctx, n), n);
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
visitExpr(ctx, from, n) {
|
|
479
|
+
if (n.type === 'call') {
|
|
480
|
+
this.callTarget(ctx, this.enclosing(ctx, n), n, false);
|
|
481
|
+
// Walk the receiver and arguments, not the function name itself.
|
|
482
|
+
const fn = n.childForFieldName('function');
|
|
483
|
+
if (fn?.type === 'attribute') {
|
|
484
|
+
const obj = fn.childForFieldName('object');
|
|
485
|
+
if (obj)
|
|
486
|
+
walk(obj, m => this.visitExpr(ctx, from, m));
|
|
487
|
+
}
|
|
488
|
+
else if (fn && fn.type !== 'identifier') {
|
|
489
|
+
// `getattr(o, n)()`, `handlers[k]()`: the callee is itself computed.
|
|
490
|
+
walk(fn, m => this.visitExpr(ctx, from, m));
|
|
491
|
+
}
|
|
492
|
+
const args = n.childForFieldName('arguments');
|
|
493
|
+
if (args)
|
|
494
|
+
walk(args, m => this.visitExpr(ctx, this.enclosing(ctx, m), m));
|
|
495
|
+
return false;
|
|
496
|
+
}
|
|
497
|
+
if (n.type === 'identifier' || n.type === 'attribute') {
|
|
498
|
+
const parent = n.parent;
|
|
499
|
+
// Skip definitions' own names, parameters, import lists and assignment targets.
|
|
500
|
+
if (!parent || ['function_definition', 'class_definition', 'parameters', 'default_parameter', 'typed_parameter', 'keyword_argument_name', 'dotted_name', 'aliased_import', 'import_statement', 'import_from_statement'].includes(parent.type)) {
|
|
501
|
+
return n.type === 'attribute' ? false : undefined;
|
|
502
|
+
}
|
|
503
|
+
if (parent.type === 'keyword_argument' && parent.childForFieldName('name') === n)
|
|
504
|
+
return undefined;
|
|
505
|
+
if (parent.type === 'argument_list' && parent.parent?.type === 'class_definition')
|
|
506
|
+
return n.type === 'attribute' ? false : undefined;
|
|
507
|
+
if (parent.type === 'assignment' && parent.childForFieldName('left') === n)
|
|
508
|
+
return n.type === 'attribute' ? false : undefined;
|
|
509
|
+
if (parent.type === 'attribute' && parent.childForFieldName('attribute') === n)
|
|
510
|
+
return undefined;
|
|
511
|
+
const written = n.text.replace(/\s+/g, '');
|
|
512
|
+
if (!/^[A-Za-z_][\w.]*$/.test(written))
|
|
513
|
+
return undefined;
|
|
514
|
+
let d = this.resolveName(ctx, written);
|
|
515
|
+
const selfRef = /^(self|cls)\.([A-Za-z_]\w*)$/.exec(written);
|
|
516
|
+
if (!d && selfRef)
|
|
517
|
+
d = this.member(this.classOf(this.enclosing(ctx, n)), [selfRef[2]]);
|
|
518
|
+
if (d && (d.kind === 'function' || d.kind === 'method' || d.kind === 'type')) {
|
|
519
|
+
const enc = this.enclosing(ctx, n);
|
|
520
|
+
if (enc.id !== d.id)
|
|
521
|
+
this.references.push({ from: enc.id, to: d.id, site: this.site(ctx, n) });
|
|
522
|
+
}
|
|
523
|
+
return n.type === 'attribute' ? false : undefined;
|
|
524
|
+
}
|
|
525
|
+
return undefined;
|
|
526
|
+
}
|
|
527
|
+
// -- imports and loads ---------------------------------------------------
|
|
528
|
+
importsAndLoads(ctx, modules) {
|
|
529
|
+
const self = ctx.ids.decls[0];
|
|
530
|
+
const seen = new Set();
|
|
531
|
+
for (const m of modules) {
|
|
532
|
+
// `import a.b.c` loads a, a.b and a.b.c.
|
|
533
|
+
const parts = m.split('.');
|
|
534
|
+
for (let i = 1; i <= parts.length; i++) {
|
|
535
|
+
const target = this.byModule.get(parts.slice(0, i).join('.'));
|
|
536
|
+
if (!target || target === ctx || seen.has(target.module))
|
|
537
|
+
continue;
|
|
538
|
+
seen.add(target.module);
|
|
539
|
+
this.imports.push({ from: self.id, to: target.ids.decls[0].id });
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
for (const info of this.classes.values()) {
|
|
543
|
+
if (info.ctx !== ctx)
|
|
544
|
+
continue;
|
|
545
|
+
for (const m of info.members.values()) {
|
|
546
|
+
if (m.kind === 'method' && DUNDER.test(m.name) && m.name !== '__init__' && m.name !== '__new__') {
|
|
547
|
+
this.loads.push({ from: info.decl.id, to: m.id });
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
// -- entries -------------------------------------------------------------
|
|
553
|
+
/** Module-level names assigned from a call to one of `factories` (e.g. `app = Flask(__name__)`). */
|
|
554
|
+
factoryVars(ctx, factories) {
|
|
555
|
+
const out = new Map();
|
|
556
|
+
for (const s of namedChildren(ctx.root)) {
|
|
557
|
+
if (s.type !== 'expression_statement')
|
|
558
|
+
continue;
|
|
559
|
+
const a = namedChildren(s)[0];
|
|
560
|
+
if (a?.type !== 'assignment')
|
|
561
|
+
continue;
|
|
562
|
+
const left = a.childForFieldName('left');
|
|
563
|
+
const right = a.childForFieldName('right');
|
|
564
|
+
if (left?.type !== 'identifier' || right?.type !== 'call')
|
|
565
|
+
continue;
|
|
566
|
+
const callee = text(right.childForFieldName('function'));
|
|
567
|
+
const short = callee.split('.').pop() ?? callee;
|
|
568
|
+
const framework = factories[short];
|
|
569
|
+
if (!framework)
|
|
570
|
+
continue;
|
|
571
|
+
const b = ctx.imports.get(callee.split('.')[0]);
|
|
572
|
+
const fromPkg = b ? (b.kind === 'module' ? b.module : b.module).split('.')[0] : '';
|
|
573
|
+
const certainty = fromPkg === framework ? 'declared' : 'inferred';
|
|
574
|
+
let prefix = '';
|
|
575
|
+
for (const arg of namedChildren(right.childForFieldName('arguments') ?? right)) {
|
|
576
|
+
if (arg.type === 'keyword_argument' && ['url_prefix', 'prefix'].includes(text(arg.childForFieldName('name')))) {
|
|
577
|
+
prefix = stringValue(arg.childForFieldName('value')) ?? '';
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
out.set(left.text, { framework, prefix, certainty });
|
|
581
|
+
}
|
|
582
|
+
return out;
|
|
583
|
+
}
|
|
584
|
+
entriesOf(ctx, options) {
|
|
585
|
+
const module = ctx.ids.decls[0];
|
|
586
|
+
const isTestFile = TEST_FILE.test(ctx.path);
|
|
587
|
+
const apps = this.factoryVars(ctx, { Flask: 'flask', Blueprint: 'flask', FastAPI: 'fastapi', APIRouter: 'fastapi', Celery: 'celery' });
|
|
588
|
+
// Decorated definitions.
|
|
589
|
+
walk(ctx.root, n => {
|
|
590
|
+
if (n.type !== 'decorated_definition')
|
|
591
|
+
return;
|
|
592
|
+
const def = n.childForFieldName('definition');
|
|
593
|
+
const decl = def ? this.declAt(ctx, def) : undefined;
|
|
594
|
+
if (!def || !decl)
|
|
595
|
+
return;
|
|
596
|
+
const line = n.startPosition.row + 1;
|
|
597
|
+
for (const dec of namedChildren(n).filter(c => c.type === 'decorator')) {
|
|
598
|
+
const expr = namedChildren(dec)[0];
|
|
599
|
+
if (!expr)
|
|
600
|
+
continue;
|
|
601
|
+
const call = expr.type === 'call' ? expr : null;
|
|
602
|
+
const callee = text(call ? call.childForFieldName('function') : expr).replace(/\s+/g, '');
|
|
603
|
+
const args = call ? namedChildren(call.childForFieldName('arguments') ?? call) : [];
|
|
604
|
+
const recv = callee.includes('.') ? callee.slice(0, callee.lastIndexOf('.')) : '';
|
|
605
|
+
const verb = callee.split('.').pop() ?? '';
|
|
606
|
+
const app = apps.get(recv);
|
|
607
|
+
// Flask / FastAPI routes
|
|
608
|
+
if ((verb === 'route' || HTTP_VERBS.includes(verb) || verb === 'api_route' || verb === 'websocket') && recv) {
|
|
609
|
+
const path = stringValue(args.find(a => a.type === 'string')) ?? stringValue(args.find(a => a.type === 'keyword_argument' && text(a.childForFieldName('name')) === 'path')?.childForFieldName('value'));
|
|
610
|
+
if (path === null || path === undefined)
|
|
611
|
+
continue;
|
|
612
|
+
const certainty = app?.certainty ?? 'inferred';
|
|
613
|
+
const framework = app?.framework ?? (verb === 'route' ? 'flask' : 'fastapi');
|
|
614
|
+
let methods;
|
|
615
|
+
if (verb === 'route' || verb === 'api_route') {
|
|
616
|
+
const m = args.find(a => a.type === 'keyword_argument' && text(a.childForFieldName('name')) === 'methods');
|
|
617
|
+
const list = m?.childForFieldName('value');
|
|
618
|
+
methods = list ? namedChildren(list).map(x => (stringValue(x) ?? '').toUpperCase()).filter(Boolean) : [];
|
|
619
|
+
if (methods.length === 0)
|
|
620
|
+
methods = [verb === 'route' ? 'GET' : 'ANY'];
|
|
621
|
+
}
|
|
622
|
+
else
|
|
623
|
+
methods = [verb === 'websocket' ? 'WS' : verb.toUpperCase()];
|
|
624
|
+
const prefix = app?.prefix ?? '';
|
|
625
|
+
for (const method of methods) {
|
|
626
|
+
const e = { id: decl.id, kind: 'route', certainty, framework, method, path: joinRoutePath([prefix, path]), surface: 'api', file: ctx.path, line, evidence: `@${callee}(${JSON.stringify(path)})` };
|
|
627
|
+
if (prefix) {
|
|
628
|
+
e.registeredPath = path;
|
|
629
|
+
e.mounts = [prefix];
|
|
630
|
+
}
|
|
631
|
+
this.entry(e);
|
|
632
|
+
}
|
|
633
|
+
continue;
|
|
634
|
+
}
|
|
635
|
+
// FastAPI / Flask lifecycle hooks
|
|
636
|
+
if (recv && ['on_event', 'middleware', 'exception_handler', 'before_request', 'after_request', 'errorhandler', 'teardown_request', 'teardown_appcontext', 'context_processor', 'before_app_request'].includes(verb)) {
|
|
637
|
+
this.entry({ id: decl.id, kind: 'lifecycle', certainty: app?.certainty ?? 'inferred', framework: app?.framework, file: ctx.path, line, evidence: `@${callee}` });
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
// click
|
|
641
|
+
if (['command', 'group'].includes(verb) && (recv === 'click' || recv !== '')) {
|
|
642
|
+
const b = ctx.imports.get('click');
|
|
643
|
+
const declared = recv === 'click' ? (b ? 'declared' : 'inferred') : 'inferred';
|
|
644
|
+
this.entry({ id: decl.id, kind: 'main', certainty: declared, framework: 'click', file: ctx.path, line, evidence: `@${callee}` });
|
|
645
|
+
continue;
|
|
646
|
+
}
|
|
647
|
+
// Celery tasks
|
|
648
|
+
if (verb === 'task' || callee === 'shared_task' || callee === 'celery.shared_task') {
|
|
649
|
+
this.entry({ id: decl.id, kind: 'listener', certainty: app?.framework === 'celery' || callee.endsWith('shared_task') ? 'declared' : 'inferred', framework: 'celery', file: ctx.path, line, evidence: `@${callee}` });
|
|
650
|
+
continue;
|
|
651
|
+
}
|
|
652
|
+
// pytest fixtures
|
|
653
|
+
if (callee === 'pytest.fixture' || callee === 'fixture') {
|
|
654
|
+
this.entry({ id: decl.id, kind: 'test', certainty: 'declared', framework: 'pytest', file: ctx.path, line, evidence: `@${callee}` });
|
|
655
|
+
continue;
|
|
656
|
+
}
|
|
657
|
+
// typing.overload stubs
|
|
658
|
+
if (callee === 'overload' || callee === 'typing.overload')
|
|
659
|
+
this.typeOnly.add(decl.id);
|
|
660
|
+
}
|
|
661
|
+
return undefined;
|
|
662
|
+
});
|
|
663
|
+
// `if __name__ == "__main__":` — the module is a program.
|
|
664
|
+
for (const s of namedChildren(ctx.root)) {
|
|
665
|
+
if (s.type !== 'if_statement')
|
|
666
|
+
continue;
|
|
667
|
+
const cond = s.childForFieldName('condition')?.text.replace(/\s+/g, '').replace(/'/g, '"') ?? '';
|
|
668
|
+
if (cond === '__name__=="__main__"' || cond === '"__main__"==__name__') {
|
|
669
|
+
this.entry({ id: module.id, kind: 'main', certainty: 'conventional', file: ctx.path, line: s.startPosition.row + 1, evidence: 'if __name__ == "__main__"' });
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
if ((options.mainFiles ?? []).includes(ctx.path) || /(^|\/)__main__\.py$/.test(ctx.path)) {
|
|
673
|
+
this.entry({ id: module.id, kind: 'main', certainty: 'conventional', file: ctx.path, line: 1, evidence: 'program file' });
|
|
674
|
+
}
|
|
675
|
+
// Classes: tests, Django management commands and class-based views, Protocols.
|
|
676
|
+
for (const info of this.classes.values()) {
|
|
677
|
+
if (info.ctx !== ctx)
|
|
678
|
+
continue;
|
|
679
|
+
const bases = new Set(info.bases.map(b => b.split('.').pop() ?? b));
|
|
680
|
+
const allBases = this.allBaseNames(info);
|
|
681
|
+
if (bases.has('Protocol')) {
|
|
682
|
+
this.typeOnly.add(info.decl.id);
|
|
683
|
+
for (const m of info.members.values())
|
|
684
|
+
this.typeOnly.add(m.id);
|
|
685
|
+
}
|
|
686
|
+
const isTestCase = [...allBases].some(b => TEST_CASE_BASES.has(b));
|
|
687
|
+
const isPytestClass = isTestFile && /^Test/.test(info.decl.name);
|
|
688
|
+
if (isTestCase || isPytestClass) {
|
|
689
|
+
for (const m of info.members.values()) {
|
|
690
|
+
if (m.kind === 'method' && (/^test/.test(m.name) || TEST_LIFECYCLE.has(m.name))) {
|
|
691
|
+
this.entry({ id: m.id, kind: 'test', certainty: isTestCase ? 'declared' : 'conventional', framework: isTestCase ? 'unittest' : 'pytest', file: ctx.path, line: m.start_line, evidence: `${info.decl.name}.${m.name}` });
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
if (allBases.has('BaseCommand') && /(^|\/)management\/commands\/[^/]+\.py$/.test(ctx.path)) {
|
|
696
|
+
const handle = info.members.get('handle');
|
|
697
|
+
this.entry({ id: info.decl.id, kind: 'reflection', certainty: 'declared', framework: 'django', file: ctx.path, line: info.decl.start_line, evidence: 'management command class' });
|
|
698
|
+
this.entry({ id: (handle ?? info.decl).id, kind: 'main', certainty: 'declared', framework: 'django', file: ctx.path, line: (handle ?? info.decl).start_line, evidence: 'management command' });
|
|
699
|
+
const args = info.members.get('add_arguments');
|
|
700
|
+
if (args)
|
|
701
|
+
this.entry({ id: args.id, kind: 'lifecycle', certainty: 'declared', framework: 'django', file: ctx.path, line: args.start_line, evidence: 'management command add_arguments' });
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
// pytest functions in test files.
|
|
705
|
+
if (isTestFile) {
|
|
706
|
+
this.entry({ id: module.id, kind: 'test', certainty: 'conventional', framework: 'pytest', file: ctx.path, line: 1, evidence: 'test file' });
|
|
707
|
+
for (const d of ctx.top.values()) {
|
|
708
|
+
if (d.kind === 'function' && /^test/.test(d.name)) {
|
|
709
|
+
this.entry({ id: d.id, kind: 'test', certainty: 'conventional', framework: 'pytest', file: ctx.path, line: d.start_line, evidence: `test function ${d.name}` });
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
// Django urls.py, and the modules Django loads by convention.
|
|
714
|
+
if (/(^|\/)urls\.py$/.test(ctx.path))
|
|
715
|
+
this.djangoUrls(ctx);
|
|
716
|
+
const usesDjango = [...ctx.imports.values()].some(b => b.module === 'django' || b.module.startsWith('django.'));
|
|
717
|
+
const conventional = /(^|\/)(urls|admin|apps|models|signals|settings|wsgi|asgi|tasks)\.py$/.exec(ctx.path);
|
|
718
|
+
if (usesDjango && conventional) {
|
|
719
|
+
this.entry({ id: module.id, kind: 'reflection', certainty: 'conventional', framework: 'django', file: ctx.path, line: 1, evidence: `${conventional[2]}.py loaded by Django` });
|
|
720
|
+
}
|
|
721
|
+
// .pyi stubs are type-only.
|
|
722
|
+
if (ctx.path.endsWith('.pyi'))
|
|
723
|
+
for (const d of ctx.ids.decls.slice(1))
|
|
724
|
+
this.typeOnly.add(d.id);
|
|
725
|
+
}
|
|
726
|
+
allBaseNames(info) {
|
|
727
|
+
const out = new Set();
|
|
728
|
+
const queue = [info];
|
|
729
|
+
const seen = new Set();
|
|
730
|
+
while (queue.length > 0) {
|
|
731
|
+
const c = queue.shift();
|
|
732
|
+
if (seen.has(c.decl.id))
|
|
733
|
+
continue;
|
|
734
|
+
seen.add(c.decl.id);
|
|
735
|
+
for (const b of c.bases) {
|
|
736
|
+
out.add(b);
|
|
737
|
+
out.add(b.split('.').pop() ?? b);
|
|
738
|
+
}
|
|
739
|
+
queue.push(...c.parents);
|
|
740
|
+
}
|
|
741
|
+
return out;
|
|
742
|
+
}
|
|
743
|
+
djangoUrls(ctx) {
|
|
744
|
+
walk(ctx.root, n => {
|
|
745
|
+
if (n.type !== 'call')
|
|
746
|
+
return;
|
|
747
|
+
const callee = text(n.childForFieldName('function'));
|
|
748
|
+
if (!['path', 're_path', 'url', 'django.urls.path', 'django.urls.re_path'].includes(callee))
|
|
749
|
+
return;
|
|
750
|
+
const args = namedChildren(n.childForFieldName('arguments') ?? n).filter(a => a.type !== 'comment');
|
|
751
|
+
const route = stringValue(args[0]);
|
|
752
|
+
const view = args[1];
|
|
753
|
+
if (route === null || route === undefined || !view)
|
|
754
|
+
return false;
|
|
755
|
+
const site = this.site(ctx, n);
|
|
756
|
+
const path = '/' + route.replace(/^\^/, '').replace(/\$$/, '');
|
|
757
|
+
if (view.type === 'call' && text(view.childForFieldName('function')) === 'include')
|
|
758
|
+
return false;
|
|
759
|
+
let target = view;
|
|
760
|
+
let cbv = false;
|
|
761
|
+
if (view.type === 'call') {
|
|
762
|
+
const fn = view.childForFieldName('function');
|
|
763
|
+
if (fn?.type === 'attribute' && text(fn.childForFieldName('attribute')) === 'as_view') {
|
|
764
|
+
target = fn.childForFieldName('object') ?? view;
|
|
765
|
+
cbv = true;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
const written = target.text.replace(/\s+/g, '');
|
|
769
|
+
const d = /^[A-Za-z_][\w.]*$/.test(written) ? this.resolveName(ctx, written) : null;
|
|
770
|
+
if (!d) {
|
|
771
|
+
this.unresolvedRegistrations.push({ site, reason: 'handler-unresolved', evidence: `${callee}(${JSON.stringify(route)}, ${written})` });
|
|
772
|
+
return false;
|
|
773
|
+
}
|
|
774
|
+
this.entry({ id: d.id, kind: 'route', certainty: 'declared', framework: 'django', method: 'ANY', path, surface: 'api', file: ctx.path, line: site.line, evidence: `${callee}(${JSON.stringify(route)}, ${written})` });
|
|
775
|
+
if (cbv || d.kind === 'type') {
|
|
776
|
+
const info = this.classes.get(d.id);
|
|
777
|
+
const seen = new Set();
|
|
778
|
+
const queue = info ? [info] : [];
|
|
779
|
+
while (queue.length > 0) {
|
|
780
|
+
const c = queue.shift();
|
|
781
|
+
if (seen.has(c.decl.id))
|
|
782
|
+
continue;
|
|
783
|
+
seen.add(c.decl.id);
|
|
784
|
+
for (const m of c.members.values()) {
|
|
785
|
+
if (m.kind === 'method' && DJANGO_CBV_METHODS.has(m.name)) {
|
|
786
|
+
this.entry({ id: m.id, kind: 'route', certainty: 'declared', framework: 'django', method: HTTP_VERBS.includes(m.name) ? m.name.toUpperCase() : 'ANY', path, surface: 'api', file: ctx.path, line: site.line, evidence: `${written}.as_view() → ${m.name}` });
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
queue.push(...c.parents);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
return false;
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
consoleScripts(scripts) {
|
|
796
|
+
for (const s of scripts) {
|
|
797
|
+
const [mod, fn] = s.split(':');
|
|
798
|
+
const ctx = mod ? this.byModule.get(mod.trim()) : undefined;
|
|
799
|
+
const d = ctx && fn ? ctx.top.get(fn.trim()) : ctx?.ids.decls[0];
|
|
800
|
+
if (!ctx || !d) {
|
|
801
|
+
this.unresolvedRegistrations.push({ site: { file: '', line: 0, col: 0 }, reason: 'manifest-unresolved', evidence: `console script ${s}` });
|
|
802
|
+
continue;
|
|
803
|
+
}
|
|
804
|
+
this.entry({ id: d.id, kind: 'main', certainty: 'declared', framework: 'console_scripts', file: ctx.path, line: d.start_line, evidence: `console script ${s}` });
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
/**
|
|
809
|
+
* Build the Python graph facts for a set of files. Each file is parsed once;
|
|
810
|
+
* nothing is read from disk.
|
|
811
|
+
*/
|
|
812
|
+
export async function buildPythonGraph(files, options = {}) {
|
|
813
|
+
const b = new PyGraphBuilder(options.maxInferred ?? 16);
|
|
814
|
+
const trees = [];
|
|
815
|
+
const partial = [];
|
|
816
|
+
const sorted = [...files].sort((x, y) => (x.path < y.path ? -1 : x.path > y.path ? 1 : 0));
|
|
817
|
+
try {
|
|
818
|
+
for (const f of sorted) {
|
|
819
|
+
const tree = await parse(f.code, 'python');
|
|
820
|
+
const module = f.module ?? pythonModuleName(f.path);
|
|
821
|
+
const ids = derivePythonIds(tree, f.path, { module });
|
|
822
|
+
if (ids.partial)
|
|
823
|
+
partial.push(f.path);
|
|
824
|
+
const isPackage = /(^|\/)__init__\.pyi?$/.test(f.path);
|
|
825
|
+
const { bindings, modules } = importsOf(tree.rootNode, module, isPackage);
|
|
826
|
+
const top = new Map();
|
|
827
|
+
const moduleDecl = ids.decls[0];
|
|
828
|
+
for (const d of ids.decls)
|
|
829
|
+
if (d.parent === moduleDecl?.id && !d.flags.includes('unstable:anonymous'))
|
|
830
|
+
top.set(d.name, d);
|
|
831
|
+
const byLine = new Map();
|
|
832
|
+
for (const d of ids.decls)
|
|
833
|
+
byLine.set(posKey(d.start_line, d.start_column), d);
|
|
834
|
+
const ctx = { path: f.path, module, ids, root: tree.rootNode, top, imports: bindings, external: new Set(), locate: declLocator(ids), byLine };
|
|
835
|
+
b.add(ctx);
|
|
836
|
+
trees.push({ tree, modules, ctx });
|
|
837
|
+
}
|
|
838
|
+
b.index();
|
|
839
|
+
for (const t of trees) {
|
|
840
|
+
b.calls(t.ctx);
|
|
841
|
+
b.importsAndLoads(t.ctx, t.modules);
|
|
842
|
+
b.entriesOf(t.ctx, options);
|
|
843
|
+
}
|
|
844
|
+
b.consoleScripts(options.consoleScripts ?? []);
|
|
845
|
+
}
|
|
846
|
+
finally {
|
|
847
|
+
for (const t of trees)
|
|
848
|
+
disposeTree(t.tree);
|
|
849
|
+
}
|
|
850
|
+
return {
|
|
851
|
+
scheme: PYTHON_GRAPH_SCHEME,
|
|
852
|
+
ids: 'ids@0.1',
|
|
853
|
+
files: trees.map(t => t.ctx.ids),
|
|
854
|
+
edges: b.edges,
|
|
855
|
+
unresolved: b.unresolved,
|
|
856
|
+
references: b.references,
|
|
857
|
+
imports: b.imports,
|
|
858
|
+
loads: b.loads,
|
|
859
|
+
entryScheme: 'entry-index@0.1',
|
|
860
|
+
entries: b.entries,
|
|
861
|
+
unresolvedRegistrations: b.unresolvedRegistrations,
|
|
862
|
+
partial,
|
|
863
|
+
typeOnly: [...b.typeOnly],
|
|
864
|
+
sites: b.sites,
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
//# sourceMappingURL=python-graph.js.map
|