circle-ir 3.8.4 → 3.9.7
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/README.md +82 -5
- package/dist/analysis/dfg-verifier.d.ts +3 -14
- package/dist/analysis/dfg-verifier.js +43 -74
- package/dist/analysis/dfg-verifier.js.map +1 -1
- package/dist/analysis/interprocedural.d.ts +5 -1
- package/dist/analysis/interprocedural.js +62 -60
- package/dist/analysis/interprocedural.js.map +1 -1
- package/dist/analysis/metrics/index.d.ts +2 -0
- package/dist/analysis/metrics/index.js +2 -0
- package/dist/analysis/metrics/index.js.map +1 -0
- package/dist/analysis/metrics/metric-pass.d.ts +27 -0
- package/dist/analysis/metrics/metric-pass.js +2 -0
- package/dist/analysis/metrics/metric-pass.js.map +1 -0
- package/dist/analysis/metrics/metric-runner.d.ts +21 -0
- package/dist/analysis/metrics/metric-runner.js +47 -0
- package/dist/analysis/metrics/metric-runner.js.map +1 -0
- package/dist/analysis/metrics/passes/cohesion-metrics-pass.d.ts +21 -0
- package/dist/analysis/metrics/passes/cohesion-metrics-pass.js +100 -0
- package/dist/analysis/metrics/passes/cohesion-metrics-pass.js.map +1 -0
- package/dist/analysis/metrics/passes/complexity-metrics-pass.d.ts +15 -0
- package/dist/analysis/metrics/passes/complexity-metrics-pass.js +76 -0
- package/dist/analysis/metrics/passes/complexity-metrics-pass.js.map +1 -0
- package/dist/analysis/metrics/passes/composite-metrics-pass.d.ts +17 -0
- package/dist/analysis/metrics/passes/composite-metrics-pass.js +77 -0
- package/dist/analysis/metrics/passes/composite-metrics-pass.js.map +1 -0
- package/dist/analysis/metrics/passes/coupling-metrics-pass.d.ts +19 -0
- package/dist/analysis/metrics/passes/coupling-metrics-pass.js +94 -0
- package/dist/analysis/metrics/passes/coupling-metrics-pass.js.map +1 -0
- package/dist/analysis/metrics/passes/data-flow-metrics-pass.d.ts +14 -0
- package/dist/analysis/metrics/passes/data-flow-metrics-pass.js +25 -0
- package/dist/analysis/metrics/passes/data-flow-metrics-pass.js.map +1 -0
- package/dist/analysis/metrics/passes/documentation-metrics-pass.d.ts +15 -0
- package/dist/analysis/metrics/passes/documentation-metrics-pass.js +64 -0
- package/dist/analysis/metrics/passes/documentation-metrics-pass.js.map +1 -0
- package/dist/analysis/metrics/passes/halstead-metrics-pass.d.ts +16 -0
- package/dist/analysis/metrics/passes/halstead-metrics-pass.js +95 -0
- package/dist/analysis/metrics/passes/halstead-metrics-pass.js.map +1 -0
- package/dist/analysis/metrics/passes/inheritance-metrics-pass.d.ts +18 -0
- package/dist/analysis/metrics/passes/inheritance-metrics-pass.js +73 -0
- package/dist/analysis/metrics/passes/inheritance-metrics-pass.js.map +1 -0
- package/dist/analysis/metrics/passes/size-metrics-pass.d.ts +11 -0
- package/dist/analysis/metrics/passes/size-metrics-pass.js +64 -0
- package/dist/analysis/metrics/passes/size-metrics-pass.js.map +1 -0
- package/dist/analysis/passes/circular-dependency-pass.d.ts +18 -0
- package/dist/analysis/passes/circular-dependency-pass.js +39 -0
- package/dist/analysis/passes/circular-dependency-pass.js.map +1 -0
- package/dist/analysis/passes/constant-propagation-pass.d.ts +22 -0
- package/dist/analysis/passes/constant-propagation-pass.js +44 -0
- package/dist/analysis/passes/constant-propagation-pass.js.map +1 -0
- package/dist/analysis/passes/cross-file-pass.d.ts +27 -0
- package/dist/analysis/passes/cross-file-pass.js +102 -0
- package/dist/analysis/passes/cross-file-pass.js.map +1 -0
- package/dist/analysis/passes/dead-code-pass.d.ts +25 -0
- package/dist/analysis/passes/dead-code-pass.js +117 -0
- package/dist/analysis/passes/dead-code-pass.js.map +1 -0
- package/dist/analysis/passes/dependency-fan-out-pass.d.ts +19 -0
- package/dist/analysis/passes/dependency-fan-out-pass.js +35 -0
- package/dist/analysis/passes/dependency-fan-out-pass.js.map +1 -0
- package/dist/analysis/passes/interprocedural-pass.d.ts +29 -0
- package/dist/analysis/passes/interprocedural-pass.js +169 -0
- package/dist/analysis/passes/interprocedural-pass.js.map +1 -0
- package/dist/analysis/passes/language-sources-pass.d.ts +76 -0
- package/dist/analysis/passes/language-sources-pass.js +491 -0
- package/dist/analysis/passes/language-sources-pass.js.map +1 -0
- package/dist/analysis/passes/leaked-global-pass.d.ts +34 -0
- package/dist/analysis/passes/leaked-global-pass.js +108 -0
- package/dist/analysis/passes/leaked-global-pass.js.map +1 -0
- package/dist/analysis/passes/missing-await-pass.d.ts +29 -0
- package/dist/analysis/passes/missing-await-pass.js +90 -0
- package/dist/analysis/passes/missing-await-pass.js.map +1 -0
- package/dist/analysis/passes/missing-public-doc-pass.d.ts +35 -0
- package/dist/analysis/passes/missing-public-doc-pass.js +148 -0
- package/dist/analysis/passes/missing-public-doc-pass.js.map +1 -0
- package/dist/analysis/passes/n-plus-one-pass.d.ts +29 -0
- package/dist/analysis/passes/n-plus-one-pass.js +100 -0
- package/dist/analysis/passes/n-plus-one-pass.js.map +1 -0
- package/dist/analysis/passes/null-deref-pass.d.ts +32 -0
- package/dist/analysis/passes/null-deref-pass.js +130 -0
- package/dist/analysis/passes/null-deref-pass.js.map +1 -0
- package/dist/analysis/passes/orphan-module-pass.d.ts +21 -0
- package/dist/analysis/passes/orphan-module-pass.js +38 -0
- package/dist/analysis/passes/orphan-module-pass.js.map +1 -0
- package/dist/analysis/passes/resource-leak-pass.d.ts +43 -0
- package/dist/analysis/passes/resource-leak-pass.js +156 -0
- package/dist/analysis/passes/resource-leak-pass.js.map +1 -0
- package/dist/analysis/passes/sink-filter-pass.d.ts +39 -0
- package/dist/analysis/passes/sink-filter-pass.js +231 -0
- package/dist/analysis/passes/sink-filter-pass.js.map +1 -0
- package/dist/analysis/passes/stale-doc-ref-pass.d.ts +21 -0
- package/dist/analysis/passes/stale-doc-ref-pass.js +96 -0
- package/dist/analysis/passes/stale-doc-ref-pass.js.map +1 -0
- package/dist/analysis/passes/string-concat-loop-pass.d.ts +26 -0
- package/dist/analysis/passes/string-concat-loop-pass.js +87 -0
- package/dist/analysis/passes/string-concat-loop-pass.js.map +1 -0
- package/dist/analysis/passes/sync-io-async-pass.d.ts +28 -0
- package/dist/analysis/passes/sync-io-async-pass.js +80 -0
- package/dist/analysis/passes/sync-io-async-pass.js.map +1 -0
- package/dist/analysis/passes/taint-matcher-pass.d.ts +24 -0
- package/dist/analysis/passes/taint-matcher-pass.js +71 -0
- package/dist/analysis/passes/taint-matcher-pass.js.map +1 -0
- package/dist/analysis/passes/taint-propagation-pass.d.ts +22 -0
- package/dist/analysis/passes/taint-propagation-pass.js +266 -0
- package/dist/analysis/passes/taint-propagation-pass.js.map +1 -0
- package/dist/analysis/passes/todo-in-prod-pass.d.ts +28 -0
- package/dist/analysis/passes/todo-in-prod-pass.js +71 -0
- package/dist/analysis/passes/todo-in-prod-pass.js.map +1 -0
- package/dist/analysis/passes/unchecked-return-pass.d.ts +34 -0
- package/dist/analysis/passes/unchecked-return-pass.js +106 -0
- package/dist/analysis/passes/unchecked-return-pass.js.map +1 -0
- package/dist/analysis/passes/unused-variable-pass.d.ts +36 -0
- package/dist/analysis/passes/unused-variable-pass.js +150 -0
- package/dist/analysis/passes/unused-variable-pass.js.map +1 -0
- package/dist/analysis/passes/variable-shadowing-pass.d.ts +41 -0
- package/dist/analysis/passes/variable-shadowing-pass.js +211 -0
- package/dist/analysis/passes/variable-shadowing-pass.js.map +1 -0
- package/dist/analysis/path-finder.d.ts +3 -13
- package/dist/analysis/path-finder.js +48 -63
- package/dist/analysis/path-finder.js.map +1 -1
- package/dist/analysis/taint-matcher.js +8 -1
- package/dist/analysis/taint-matcher.js.map +1 -1
- package/dist/analysis/taint-propagation.d.ts +5 -1
- package/dist/analysis/taint-propagation.js +44 -41
- package/dist/analysis/taint-propagation.js.map +1 -1
- package/dist/analyzer.d.ts +42 -1
- package/dist/analyzer.js +234 -1476
- package/dist/analyzer.js.map +1 -1
- package/dist/browser/circle-ir.js +3413 -1271
- package/dist/core/circle-ir-core.cjs +360 -106
- package/dist/core/circle-ir-core.js +360 -106
- package/dist/core/extractors/imports.js +18 -0
- package/dist/core/extractors/imports.js.map +1 -1
- package/dist/graph/analysis-pass.d.ts +68 -0
- package/dist/graph/analysis-pass.js +51 -0
- package/dist/graph/analysis-pass.js.map +1 -0
- package/dist/graph/code-graph.d.ts +92 -0
- package/dist/graph/code-graph.js +262 -0
- package/dist/graph/code-graph.js.map +1 -0
- package/dist/graph/import-graph.d.ts +33 -0
- package/dist/graph/import-graph.js +170 -0
- package/dist/graph/import-graph.js.map +1 -0
- package/dist/graph/index.d.ts +4 -0
- package/dist/graph/index.js +5 -0
- package/dist/graph/index.js.map +1 -0
- package/dist/graph/project-graph.d.ts +43 -0
- package/dist/graph/project-graph.js +80 -0
- package/dist/graph/project-graph.js.map +1 -0
- package/dist/graph/scope-graph.d.ts +63 -0
- package/dist/graph/scope-graph.js +89 -0
- package/dist/graph/scope-graph.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/resolution/cross-file.js +52 -19
- package/dist/resolution/cross-file.js.map +1 -1
- package/dist/types/index.d.ts +151 -0
- package/docs/SPEC.md +10 -6
- package/package.json +1 -1
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ImportGraph
|
|
3
|
+
*
|
|
4
|
+
* Builds a directed file→file import graph from a ProjectGraph.
|
|
5
|
+
* Only relative imports (from_package starting with '.') are resolved to edges —
|
|
6
|
+
* stdlib and npm package imports are ignored.
|
|
7
|
+
*
|
|
8
|
+
* Used by CircularDependencyPass and OrphanModulePass.
|
|
9
|
+
*/
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Path helpers (no Node.js 'path' module — must run in browser too)
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
function dirname(filePath) {
|
|
14
|
+
const idx = filePath.lastIndexOf('/');
|
|
15
|
+
return idx >= 0 ? filePath.slice(0, idx) : '';
|
|
16
|
+
}
|
|
17
|
+
function normalizePath(p) {
|
|
18
|
+
const isAbsolute = p.startsWith('/');
|
|
19
|
+
const parts = p.split('/');
|
|
20
|
+
const result = [];
|
|
21
|
+
for (const part of parts) {
|
|
22
|
+
if (part === '' || part === '.')
|
|
23
|
+
continue;
|
|
24
|
+
if (part === '..') {
|
|
25
|
+
result.pop();
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
result.push(part);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return (isAbsolute ? '/' : '') + result.join('/');
|
|
32
|
+
}
|
|
33
|
+
const EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.py', '.java', '.rs', ''];
|
|
34
|
+
// Entry-point filenames (base without extension) — not treated as orphans
|
|
35
|
+
const ENTRY_POINT_NAMES = /^(index|main|app|server|mod|cli|bin|start|run|entry|init)$/i;
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// ImportGraph
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
export class ImportGraph {
|
|
40
|
+
/** file → set of files it imports */
|
|
41
|
+
outEdges;
|
|
42
|
+
/** file → set of files that import it */
|
|
43
|
+
inEdges;
|
|
44
|
+
/** All known file paths */
|
|
45
|
+
allFiles;
|
|
46
|
+
constructor(projectGraph) {
|
|
47
|
+
this.outEdges = new Map();
|
|
48
|
+
this.inEdges = new Map();
|
|
49
|
+
this.allFiles = new Set(projectGraph.filePaths);
|
|
50
|
+
// Initialize edge sets for all files
|
|
51
|
+
for (const file of this.allFiles) {
|
|
52
|
+
this.outEdges.set(file, new Set());
|
|
53
|
+
this.inEdges.set(file, new Set());
|
|
54
|
+
}
|
|
55
|
+
// Build edges from imports
|
|
56
|
+
for (const filePath of projectGraph.filePaths) {
|
|
57
|
+
const ir = projectGraph.getIR(filePath);
|
|
58
|
+
if (!ir)
|
|
59
|
+
continue;
|
|
60
|
+
const dir = dirname(filePath);
|
|
61
|
+
for (const imp of ir.imports) {
|
|
62
|
+
const pkg = imp.from_package;
|
|
63
|
+
if (!pkg || !pkg.startsWith('.'))
|
|
64
|
+
continue; // skip stdlib/npm
|
|
65
|
+
// Resolve relative path
|
|
66
|
+
const rawCandidate = dir ? `${dir}/${pkg}` : pkg;
|
|
67
|
+
const base = normalizePath(rawCandidate);
|
|
68
|
+
// Try with each extension
|
|
69
|
+
let resolved = null;
|
|
70
|
+
for (const ext of EXTENSIONS) {
|
|
71
|
+
const candidate = base + ext;
|
|
72
|
+
if (this.allFiles.has(candidate)) {
|
|
73
|
+
resolved = candidate;
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// TypeScript ESM convention: `import './foo.js'` refers to `./foo.ts` on disk.
|
|
78
|
+
// Strip the .js suffix and retry with TypeScript extensions.
|
|
79
|
+
if (!resolved && base.endsWith('.js')) {
|
|
80
|
+
const stripped = base.slice(0, -3);
|
|
81
|
+
for (const ext of ['.ts', '.tsx', '.js']) {
|
|
82
|
+
const candidate = stripped + ext;
|
|
83
|
+
if (this.allFiles.has(candidate)) {
|
|
84
|
+
resolved = candidate;
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (resolved && resolved !== filePath) {
|
|
90
|
+
this.outEdges.get(filePath).add(resolved);
|
|
91
|
+
this.inEdges.get(resolved).add(filePath);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/** Files directly imported by `filePath`. */
|
|
97
|
+
edgesFrom(filePath) {
|
|
98
|
+
return [...(this.outEdges.get(filePath) ?? [])];
|
|
99
|
+
}
|
|
100
|
+
/** Files that directly import `filePath`. */
|
|
101
|
+
edgesTo(filePath) {
|
|
102
|
+
return [...(this.inEdges.get(filePath) ?? [])];
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Tarjan's SCC — returns groups of files that form import cycles.
|
|
106
|
+
* Each returned Set has size ≥ 2 (only actual cycles).
|
|
107
|
+
*/
|
|
108
|
+
findCycles() {
|
|
109
|
+
const index = new Map();
|
|
110
|
+
const lowlink = new Map();
|
|
111
|
+
const onStack = new Map();
|
|
112
|
+
const stack = [];
|
|
113
|
+
const cycles = [];
|
|
114
|
+
let counter = 0;
|
|
115
|
+
const strongConnect = (v) => {
|
|
116
|
+
index.set(v, counter);
|
|
117
|
+
lowlink.set(v, counter);
|
|
118
|
+
counter++;
|
|
119
|
+
stack.push(v);
|
|
120
|
+
onStack.set(v, true);
|
|
121
|
+
for (const w of (this.outEdges.get(v) ?? [])) {
|
|
122
|
+
if (!index.has(w)) {
|
|
123
|
+
strongConnect(w);
|
|
124
|
+
lowlink.set(v, Math.min(lowlink.get(v), lowlink.get(w)));
|
|
125
|
+
}
|
|
126
|
+
else if (onStack.get(w)) {
|
|
127
|
+
lowlink.set(v, Math.min(lowlink.get(v), index.get(w)));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (lowlink.get(v) === index.get(v)) {
|
|
131
|
+
// Root of SCC — pop the component
|
|
132
|
+
const scc = new Set();
|
|
133
|
+
let w;
|
|
134
|
+
do {
|
|
135
|
+
w = stack.pop();
|
|
136
|
+
onStack.set(w, false);
|
|
137
|
+
scc.add(w);
|
|
138
|
+
} while (w !== v);
|
|
139
|
+
if (scc.size > 1) {
|
|
140
|
+
cycles.push(scc);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
for (const v of this.allFiles) {
|
|
145
|
+
if (!index.has(v)) {
|
|
146
|
+
strongConnect(v);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return cycles;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Returns file paths with zero incoming import edges that are not entry points.
|
|
153
|
+
* Entry points: filename base (without extension) matches /^(index|main|app|server|mod)$/i
|
|
154
|
+
*/
|
|
155
|
+
findOrphans() {
|
|
156
|
+
const orphans = [];
|
|
157
|
+
for (const file of this.allFiles) {
|
|
158
|
+
if ((this.inEdges.get(file)?.size ?? 0) > 0)
|
|
159
|
+
continue;
|
|
160
|
+
// Check if entry point
|
|
161
|
+
const base = file.split('/').pop() ?? '';
|
|
162
|
+
const baseName = base.includes('.') ? base.slice(0, base.lastIndexOf('.')) : base;
|
|
163
|
+
if (ENTRY_POINT_NAMES.test(baseName))
|
|
164
|
+
continue;
|
|
165
|
+
orphans.push(file);
|
|
166
|
+
}
|
|
167
|
+
return orphans.sort();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
//# sourceMappingURL=import-graph.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"import-graph.js","sourceRoot":"","sources":["../../src/graph/import-graph.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,8EAA8E;AAC9E,oEAAoE;AACpE,8EAA8E;AAE9E,SAAS,OAAO,CAAC,QAAgB;IAC/B,MAAM,GAAG,GAAG,QAAQ,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IACtC,OAAO,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;AAChD,CAAC;AAED,SAAS,aAAa,CAAC,CAAS;IAC9B,MAAM,UAAU,GAAG,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;IACrC,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC3B,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,IAAI,KAAK,EAAE,IAAI,IAAI,KAAK,GAAG;YAAE,SAAS;QAC1C,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;YAClB,MAAM,CAAC,GAAG,EAAE,CAAC;QACf,CAAC;aAAM,CAAC;YACN,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACpB,CAAC;IACH,CAAC;IACD,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACpD,CAAC;AAED,MAAM,UAAU,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC;AAE7E,0EAA0E;AAC1E,MAAM,iBAAiB,GAAG,6DAA6D,CAAC;AAExF,8EAA8E;AAC9E,cAAc;AACd,8EAA8E;AAE9E,MAAM,OAAO,WAAW;IACtB,qCAAqC;IACpB,QAAQ,CAA2B;IACpD,yCAAyC;IACxB,OAAO,CAA2B;IACnD,2BAA2B;IACV,QAAQ,CAAc;IAEvC,YAAY,YAA0B;QACpC,IAAI,CAAC,QAAQ,GAAG,IAAI,GAAG,EAAE,CAAC;QAC1B,IAAI,CAAC,OAAO,GAAI,IAAI,GAAG,EAAE,CAAC;QAC1B,IAAI,CAAC,QAAQ,GAAG,IAAI,GAAG,CAAC,YAAY,CAAC,SAAS,CAAC,CAAC;QAEhD,qCAAqC;QACrC,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YACjC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,GAAG,EAAE,CAAC,CAAC;YACnC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,GAAG,EAAE,CAAC,CAAC;QACpC,CAAC;QAED,2BAA2B;QAC3B,KAAK,MAAM,QAAQ,IAAI,YAAY,CAAC,SAAS,EAAE,CAAC;YAC9C,MAAM,EAAE,GAAG,YAAY,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;YACxC,IAAI,CAAC,EAAE;gBAAE,SAAS;YAElB,MAAM,GAAG,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;YAE9B,KAAK,MAAM,GAAG,IAAI,EAAE,CAAC,OAAO,EAAE,CAAC;gBAC7B,MAAM,GAAG,GAAG,GAAG,CAAC,YAAY,CAAC;gBAC7B,IAAI,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC;oBAAE,SAAS,CAAC,kBAAkB;gBAE9D,wBAAwB;gBACxB,MAAM,YAAY,GAAG,GAAG,CAAC,CAAC,CAAC,GAAG,GAAG,IAAI,GAAG,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC;gBACjD,MAAM,IAAI,GAAG,aAAa,CAAC,YAAY,CAAC,CAAC;gBAEzC,0BAA0B;gBAC1B,IAAI,QAAQ,GAAkB,IAAI,CAAC;gBACnC,KAAK,MAAM,GAAG,IAAI,UAAU,EAAE,CAAC;oBAC7B,MAAM,SAAS,GAAG,IAAI,GAAG,GAAG,CAAC;oBAC7B,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC;wBACjC,QAAQ,GAAG,SAAS,CAAC;wBACrB,MAAM;oBACR,CAAC;gBACH,CAAC;gBAED,+EAA+E;gBAC/E,6DAA6D;gBAC7D,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;oBACtC,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;oBACnC,KAAK,MAAM,GAAG,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,CAAC;wBACzC,MAAM,SAAS,GAAG,QAAQ,GAAG,GAAG,CAAC;wBACjC,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC;4BACjC,QAAQ,GAAG,SAAS,CAAC;4BACrB,MAAM;wBACR,CAAC;oBACH,CAAC;gBACH,CAAC;gBAED,IAAI,QAAQ,IAAI,QAAQ,KAAK,QAAQ,EAAE,CAAC;oBACtC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAE,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;oBAC3C,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAE,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;gBAC5C,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC;IAED,6CAA6C;IAC7C,SAAS,CAAC,QAAgB;QACxB,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;IAClD,CAAC;IAED,6CAA6C;IAC7C,OAAO,CAAC,QAAgB;QACtB,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;IACjD,CAAC;IAED;;;OAGG;IACH,UAAU;QACR,MAAM,KAAK,GAA2B,IAAI,GAAG,EAAE,CAAC;QAChD,MAAM,OAAO,GAAyB,IAAI,GAAG,EAAE,CAAC;QAChD,MAAM,OAAO,GAAyB,IAAI,GAAG,EAAE,CAAC;QAChD,MAAM,KAAK,GAA2B,EAAE,CAAC;QACzC,MAAM,MAAM,GAA0B,EAAE,CAAC;QACzC,IAAM,OAAO,GAAI,CAAC,CAAC;QAEnB,MAAM,aAAa,GAAG,CAAC,CAAS,EAAQ,EAAE;YACxC,KAAK,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;YACtB,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;YACxB,OAAO,EAAE,CAAC;YACV,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YACd,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;YAErB,KAAK,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC;gBAC7C,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;oBAClB,aAAa,CAAC,CAAC,CAAC,CAAC;oBACjB,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAE,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC,CAAE,CAAC,CAAC,CAAC;gBAC7D,CAAC;qBAAM,IAAI,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;oBAC1B,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAE,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAE,CAAC,CAAC,CAAC;gBAC3D,CAAC;YACH,CAAC;YAED,IAAI,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;gBACpC,kCAAkC;gBAClC,MAAM,GAAG,GAAgB,IAAI,GAAG,EAAE,CAAC;gBACnC,IAAI,CAAS,CAAC;gBACd,GAAG,CAAC;oBACF,CAAC,GAAG,KAAK,CAAC,GAAG,EAAG,CAAC;oBACjB,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;oBACtB,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;gBACb,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE;gBAElB,IAAI,GAAG,CAAC,IAAI,GAAG,CAAC,EAAE,CAAC;oBACjB,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;gBACnB,CAAC;YACH,CAAC;QACH,CAAC,CAAC;QAEF,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAC9B,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;gBAClB,aAAa,CAAC,CAAC,CAAC,CAAC;YACnB,CAAC;QACH,CAAC;QAED,OAAO,MAAM,CAAC;IAChB,CAAC;IAED;;;OAGG;IACH,WAAW;QACT,MAAM,OAAO,GAAa,EAAE,CAAC;QAC7B,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YACjC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,IAAI,CAAC,CAAC,GAAG,CAAC;gBAAE,SAAS;YACtD,uBAAuB;YACvB,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC;YACzC,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;YAClF,IAAI,iBAAiB,CAAC,IAAI,CAAC,QAAQ,CAAC;gBAAE,SAAS;YAC/C,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACrB,CAAC;QACD,OAAO,OAAO,CAAC,IAAI,EAAE,CAAC;IACxB,CAAC;CACF"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/graph/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EACL,gBAAgB,GAIjB,MAAM,oBAAoB,CAAC"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProjectGraph
|
|
3
|
+
*
|
|
4
|
+
* Wraps multiple per-file CodeGraph instances with lazy cross-file resolution
|
|
5
|
+
* infrastructure (SymbolTable, TypeHierarchyResolver, CrossFileResolver).
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* const pg = new ProjectGraph();
|
|
9
|
+
* pg.addFile('/src/A.java', graphA);
|
|
10
|
+
* pg.addFile('/src/B.java', graphB);
|
|
11
|
+
* const flows = pg.resolver.findCrossFileTaintFlows();
|
|
12
|
+
*/
|
|
13
|
+
import type { CircleIR } from '../types/index.js';
|
|
14
|
+
import { CodeGraph } from './code-graph.js';
|
|
15
|
+
import { SymbolTable, TypeHierarchyResolver, CrossFileResolver } from '../resolution/index.js';
|
|
16
|
+
export declare class ProjectGraph {
|
|
17
|
+
private readonly files;
|
|
18
|
+
private _symbolTable;
|
|
19
|
+
private _typeHierarchy;
|
|
20
|
+
private _resolver;
|
|
21
|
+
/**
|
|
22
|
+
* Register a file's CodeGraph. Invalidates all lazy caches.
|
|
23
|
+
*/
|
|
24
|
+
addFile(filePath: string, graph: CodeGraph): void;
|
|
25
|
+
/** Registered file paths in insertion order. */
|
|
26
|
+
get filePaths(): string[];
|
|
27
|
+
/** Total number of registered files. */
|
|
28
|
+
get fileCount(): number;
|
|
29
|
+
/** Retrieve a file's CodeGraph, or undefined if not registered. */
|
|
30
|
+
getGraph(filePath: string): CodeGraph | undefined;
|
|
31
|
+
/** Retrieve a file's CircleIR, or undefined if not registered. */
|
|
32
|
+
getIR(filePath: string): CircleIR | undefined;
|
|
33
|
+
/** Lazily-built SymbolTable covering all registered files. */
|
|
34
|
+
get symbolTable(): SymbolTable;
|
|
35
|
+
/** Lazily-built TypeHierarchyResolver covering all registered files. */
|
|
36
|
+
get typeHierarchy(): TypeHierarchyResolver;
|
|
37
|
+
/**
|
|
38
|
+
* Lazily-built CrossFileResolver.
|
|
39
|
+
* Accesses `this.symbolTable` and `this.typeHierarchy` (also lazy) so all
|
|
40
|
+
* three are computed together on the first call after any `addFile()`.
|
|
41
|
+
*/
|
|
42
|
+
get resolver(): CrossFileResolver;
|
|
43
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProjectGraph
|
|
3
|
+
*
|
|
4
|
+
* Wraps multiple per-file CodeGraph instances with lazy cross-file resolution
|
|
5
|
+
* infrastructure (SymbolTable, TypeHierarchyResolver, CrossFileResolver).
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* const pg = new ProjectGraph();
|
|
9
|
+
* pg.addFile('/src/A.java', graphA);
|
|
10
|
+
* pg.addFile('/src/B.java', graphB);
|
|
11
|
+
* const flows = pg.resolver.findCrossFileTaintFlows();
|
|
12
|
+
*/
|
|
13
|
+
import { SymbolTable, TypeHierarchyResolver, CrossFileResolver, } from '../resolution/index.js';
|
|
14
|
+
export class ProjectGraph {
|
|
15
|
+
files = new Map();
|
|
16
|
+
// Lazy caches — nulled whenever a new file is added
|
|
17
|
+
_symbolTable = null;
|
|
18
|
+
_typeHierarchy = null;
|
|
19
|
+
_resolver = null;
|
|
20
|
+
/**
|
|
21
|
+
* Register a file's CodeGraph. Invalidates all lazy caches.
|
|
22
|
+
*/
|
|
23
|
+
addFile(filePath, graph) {
|
|
24
|
+
this.files.set(filePath, { graph, ir: graph.ir });
|
|
25
|
+
this._symbolTable = null;
|
|
26
|
+
this._typeHierarchy = null;
|
|
27
|
+
this._resolver = null;
|
|
28
|
+
}
|
|
29
|
+
/** Registered file paths in insertion order. */
|
|
30
|
+
get filePaths() {
|
|
31
|
+
return [...this.files.keys()];
|
|
32
|
+
}
|
|
33
|
+
/** Total number of registered files. */
|
|
34
|
+
get fileCount() {
|
|
35
|
+
return this.files.size;
|
|
36
|
+
}
|
|
37
|
+
/** Retrieve a file's CodeGraph, or undefined if not registered. */
|
|
38
|
+
getGraph(filePath) {
|
|
39
|
+
return this.files.get(filePath)?.graph;
|
|
40
|
+
}
|
|
41
|
+
/** Retrieve a file's CircleIR, or undefined if not registered. */
|
|
42
|
+
getIR(filePath) {
|
|
43
|
+
return this.files.get(filePath)?.ir;
|
|
44
|
+
}
|
|
45
|
+
/** Lazily-built SymbolTable covering all registered files. */
|
|
46
|
+
get symbolTable() {
|
|
47
|
+
if (!this._symbolTable) {
|
|
48
|
+
const st = new SymbolTable();
|
|
49
|
+
for (const [path, { ir }] of this.files)
|
|
50
|
+
st.addFromIR(ir, path);
|
|
51
|
+
this._symbolTable = st;
|
|
52
|
+
}
|
|
53
|
+
return this._symbolTable;
|
|
54
|
+
}
|
|
55
|
+
/** Lazily-built TypeHierarchyResolver covering all registered files. */
|
|
56
|
+
get typeHierarchy() {
|
|
57
|
+
if (!this._typeHierarchy) {
|
|
58
|
+
const th = new TypeHierarchyResolver();
|
|
59
|
+
for (const [path, { ir }] of this.files)
|
|
60
|
+
th.addFromIR(ir, path);
|
|
61
|
+
this._typeHierarchy = th;
|
|
62
|
+
}
|
|
63
|
+
return this._typeHierarchy;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Lazily-built CrossFileResolver.
|
|
67
|
+
* Accesses `this.symbolTable` and `this.typeHierarchy` (also lazy) so all
|
|
68
|
+
* three are computed together on the first call after any `addFile()`.
|
|
69
|
+
*/
|
|
70
|
+
get resolver() {
|
|
71
|
+
if (!this._resolver) {
|
|
72
|
+
const r = new CrossFileResolver(this.symbolTable, this.typeHierarchy);
|
|
73
|
+
for (const [path, { ir }] of this.files)
|
|
74
|
+
r.addFile(path, ir);
|
|
75
|
+
this._resolver = r;
|
|
76
|
+
}
|
|
77
|
+
return this._resolver;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=project-graph.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"project-graph.js","sourceRoot":"","sources":["../../src/graph/project-graph.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,OAAO,EACL,WAAW,EACX,qBAAqB,EACrB,iBAAiB,GAClB,MAAM,wBAAwB,CAAC;AAEhC,MAAM,OAAO,YAAY;IACN,KAAK,GAAG,IAAI,GAAG,EAA8C,CAAC;IAE/E,oDAAoD;IAC5C,YAAY,GAAyB,IAAI,CAAC;IAC1C,cAAc,GAAiC,IAAI,CAAC;IACpD,SAAS,GAAkC,IAAI,CAAC;IAExD;;OAEG;IACH,OAAO,CAAC,QAAgB,EAAE,KAAgB;QACxC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,QAAQ,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC;QAClD,IAAI,CAAC,YAAY,GAAK,IAAI,CAAC;QAC3B,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC;QAC3B,IAAI,CAAC,SAAS,GAAQ,IAAI,CAAC;IAC7B,CAAC;IAED,gDAAgD;IAChD,IAAI,SAAS;QACX,OAAO,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;IAChC,CAAC;IAED,wCAAwC;IACxC,IAAI,SAAS;QACX,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC;IACzB,CAAC;IAED,mEAAmE;IACnE,QAAQ,CAAC,QAAgB;QACvB,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,KAAK,CAAC;IACzC,CAAC;IAED,kEAAkE;IAClE,KAAK,CAAC,QAAgB;QACpB,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;IACtC,CAAC;IAED,8DAA8D;IAC9D,IAAI,WAAW;QACb,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC;YACvB,MAAM,EAAE,GAAG,IAAI,WAAW,EAAE,CAAC;YAC7B,KAAK,MAAM,CAAC,IAAI,EAAE,EAAE,EAAE,EAAE,CAAC,IAAI,IAAI,CAAC,KAAK;gBAAE,EAAE,CAAC,SAAS,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;YAChE,IAAI,CAAC,YAAY,GAAG,EAAE,CAAC;QACzB,CAAC;QACD,OAAO,IAAI,CAAC,YAAY,CAAC;IAC3B,CAAC;IAED,wEAAwE;IACxE,IAAI,aAAa;QACf,IAAI,CAAC,IAAI,CAAC,cAAc,EAAE,CAAC;YACzB,MAAM,EAAE,GAAG,IAAI,qBAAqB,EAAE,CAAC;YACvC,KAAK,MAAM,CAAC,IAAI,EAAE,EAAE,EAAE,EAAE,CAAC,IAAI,IAAI,CAAC,KAAK;gBAAE,EAAE,CAAC,SAAS,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;YAChE,IAAI,CAAC,cAAc,GAAG,EAAE,CAAC;QAC3B,CAAC;QACD,OAAO,IAAI,CAAC,cAAc,CAAC;IAC7B,CAAC;IAED;;;;OAIG;IACH,IAAI,QAAQ;QACV,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;YACpB,MAAM,CAAC,GAAG,IAAI,iBAAiB,CAAC,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC,aAAa,CAAC,CAAC;YACtE,KAAK,MAAM,CAAC,IAAI,EAAE,EAAE,EAAE,EAAE,CAAC,IAAI,IAAI,CAAC,KAAK;gBAAE,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;YAC7D,IAAI,CAAC,SAAS,GAAG,CAAC,CAAC;QACrB,CAAC;QACD,OAAO,IAAI,CAAC,SAAS,CAAC;IACxB,CAAC;CACF"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ScopeGraph
|
|
3
|
+
*
|
|
4
|
+
* Thin wrapper over CodeGraph that enriches each DFGDef with declaration-keyword
|
|
5
|
+
* awareness. Needed by the three Group-3 passes (variable-shadowing, leaked-global,
|
|
6
|
+
* unused-variable) which must distinguish between a "real" variable declaration
|
|
7
|
+
* (`let x = 5`) and a bare reassignment (`x = 5`).
|
|
8
|
+
*
|
|
9
|
+
* The DFG extractor treats both as `kind='local'` because reassignments also
|
|
10
|
+
* create new reaching definitions. ScopeGraph recovers the distinction by
|
|
11
|
+
* scanning the source line for language-specific declaration keywords.
|
|
12
|
+
*
|
|
13
|
+
* Design:
|
|
14
|
+
* - Built once per pass via `new ScopeGraph(graph, code, language)`
|
|
15
|
+
* - O(n) construction where n = number of DFG defs
|
|
16
|
+
* - No mutations — all state is computed in the constructor
|
|
17
|
+
* - Browser + Node.js safe (no platform APIs)
|
|
18
|
+
*/
|
|
19
|
+
import type { DFGDef } from '../types/index.js';
|
|
20
|
+
import type { CodeGraph } from './code-graph.js';
|
|
21
|
+
/** Enriched view of a single DFGDef with scope metadata. */
|
|
22
|
+
export interface ScopeEntry {
|
|
23
|
+
/** The underlying DFG definition. */
|
|
24
|
+
readonly def: DFGDef;
|
|
25
|
+
/**
|
|
26
|
+
* True when the source line that created this def contains a declaration
|
|
27
|
+
* keyword appropriate for the language (e.g. `let`/`const`/`var` for JS,
|
|
28
|
+
* type keyword for Java, `let` for Rust). False for bare reassignments.
|
|
29
|
+
*/
|
|
30
|
+
readonly hasDeclKeyword: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* `start_line` of the enclosing method, or -1 if the def is at the
|
|
33
|
+
* top/module level (no enclosing method found).
|
|
34
|
+
*/
|
|
35
|
+
readonly methodStart: number;
|
|
36
|
+
/**
|
|
37
|
+
* `end_line` of the enclosing method, or -1 if top-level.
|
|
38
|
+
*/
|
|
39
|
+
readonly methodEnd: number;
|
|
40
|
+
}
|
|
41
|
+
export declare class ScopeGraph {
|
|
42
|
+
/** One entry per DFGDef in the IR, in original order. */
|
|
43
|
+
readonly entries: ScopeEntry[];
|
|
44
|
+
constructor(graph: CodeGraph, code: string, language: string);
|
|
45
|
+
/**
|
|
46
|
+
* Returns all entries whose def falls within the inclusive range
|
|
47
|
+
* [start, end] (both are 1-based source line numbers).
|
|
48
|
+
*/
|
|
49
|
+
defsInMethod(start: number, end: number): ScopeEntry[];
|
|
50
|
+
/**
|
|
51
|
+
* Returns true if the given variable has at least one def with
|
|
52
|
+
* `hasDeclKeyword === true` inside the method whose start line is
|
|
53
|
+
* `methodStart` OR at module level (methodStart === -1).
|
|
54
|
+
*
|
|
55
|
+
* Module-level declarations are included because JavaScript/TypeScript
|
|
56
|
+
* module-level `let`/`const`/`var` variables are legitimately reassigned
|
|
57
|
+
* inside functions — that is not a global leak.
|
|
58
|
+
*
|
|
59
|
+
* Used by `leaked-global` to determine whether a bare assignment is truly
|
|
60
|
+
* undeclared within its enclosing function.
|
|
61
|
+
*/
|
|
62
|
+
hasDeclaredDef(variable: string, methodStart: number): boolean;
|
|
63
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ScopeGraph
|
|
3
|
+
*
|
|
4
|
+
* Thin wrapper over CodeGraph that enriches each DFGDef with declaration-keyword
|
|
5
|
+
* awareness. Needed by the three Group-3 passes (variable-shadowing, leaked-global,
|
|
6
|
+
* unused-variable) which must distinguish between a "real" variable declaration
|
|
7
|
+
* (`let x = 5`) and a bare reassignment (`x = 5`).
|
|
8
|
+
*
|
|
9
|
+
* The DFG extractor treats both as `kind='local'` because reassignments also
|
|
10
|
+
* create new reaching definitions. ScopeGraph recovers the distinction by
|
|
11
|
+
* scanning the source line for language-specific declaration keywords.
|
|
12
|
+
*
|
|
13
|
+
* Design:
|
|
14
|
+
* - Built once per pass via `new ScopeGraph(graph, code, language)`
|
|
15
|
+
* - O(n) construction where n = number of DFG defs
|
|
16
|
+
* - No mutations — all state is computed in the constructor
|
|
17
|
+
* - Browser + Node.js safe (no platform APIs)
|
|
18
|
+
*/
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Per-language declaration keyword patterns
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
/**
|
|
23
|
+
* Returns true when the source line text contains a variable-declaration keyword
|
|
24
|
+
* for the given language.
|
|
25
|
+
*
|
|
26
|
+
* Java/Rust/JS/TS: explicit keywords precede the variable name.
|
|
27
|
+
* Python: no declaration keyword exists → always returns false.
|
|
28
|
+
*/
|
|
29
|
+
function hasDeclKeyword(lineText, language) {
|
|
30
|
+
switch (language) {
|
|
31
|
+
case 'java':
|
|
32
|
+
// Primitive types, `var`, `final`, generic types (upper-case first letter)
|
|
33
|
+
return /\b(?:int|long|float|double|boolean|byte|char|short|var|final)\b/.test(lineText)
|
|
34
|
+
|| /\b[A-Z]\w*(?:<[^>]*>)?\s+\w/.test(lineText);
|
|
35
|
+
case 'javascript':
|
|
36
|
+
case 'typescript':
|
|
37
|
+
return /\b(?:let|const|var)\s+[\w{[]/.test(lineText);
|
|
38
|
+
case 'rust':
|
|
39
|
+
return /\blet\s+(?:mut\s+)?\w/.test(lineText);
|
|
40
|
+
default:
|
|
41
|
+
// Python and other languages: no reliable declaration keyword
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// ScopeGraph
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
export class ScopeGraph {
|
|
49
|
+
/** One entry per DFGDef in the IR, in original order. */
|
|
50
|
+
entries;
|
|
51
|
+
constructor(graph, code, language) {
|
|
52
|
+
const codeLines = code.split('\n');
|
|
53
|
+
this.entries = graph.ir.dfg.defs.map((def) => {
|
|
54
|
+
const m = graph.methodAtLine(def.line);
|
|
55
|
+
const lineText = codeLines[def.line - 1] ?? '';
|
|
56
|
+
return {
|
|
57
|
+
def,
|
|
58
|
+
hasDeclKeyword: hasDeclKeyword(lineText, language),
|
|
59
|
+
methodStart: m?.method.start_line ?? -1,
|
|
60
|
+
methodEnd: m?.method.end_line ?? -1,
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Returns all entries whose def falls within the inclusive range
|
|
66
|
+
* [start, end] (both are 1-based source line numbers).
|
|
67
|
+
*/
|
|
68
|
+
defsInMethod(start, end) {
|
|
69
|
+
return this.entries.filter(e => e.def.line >= start && e.def.line <= end);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Returns true if the given variable has at least one def with
|
|
73
|
+
* `hasDeclKeyword === true` inside the method whose start line is
|
|
74
|
+
* `methodStart` OR at module level (methodStart === -1).
|
|
75
|
+
*
|
|
76
|
+
* Module-level declarations are included because JavaScript/TypeScript
|
|
77
|
+
* module-level `let`/`const`/`var` variables are legitimately reassigned
|
|
78
|
+
* inside functions — that is not a global leak.
|
|
79
|
+
*
|
|
80
|
+
* Used by `leaked-global` to determine whether a bare assignment is truly
|
|
81
|
+
* undeclared within its enclosing function.
|
|
82
|
+
*/
|
|
83
|
+
hasDeclaredDef(variable, methodStart) {
|
|
84
|
+
return this.entries.some(e => e.def.variable === variable
|
|
85
|
+
&& e.hasDeclKeyword
|
|
86
|
+
&& (e.methodStart === methodStart || e.methodStart === -1));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=scope-graph.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scope-graph.js","sourceRoot":"","sources":["../../src/graph/scope-graph.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAKH,8EAA8E;AAC9E,4CAA4C;AAC5C,8EAA8E;AAE9E;;;;;;GAMG;AACH,SAAS,cAAc,CAAC,QAAgB,EAAE,QAAgB;IACxD,QAAQ,QAAQ,EAAE,CAAC;QACjB,KAAK,MAAM;YACT,2EAA2E;YAC3E,OAAO,iEAAiE,CAAC,IAAI,CAAC,QAAQ,CAAC;mBAChF,6BAA6B,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAEtD,KAAK,YAAY,CAAC;QAClB,KAAK,YAAY;YACf,OAAO,8BAA8B,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAEvD,KAAK,MAAM;YACT,OAAO,uBAAuB,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAEhD;YACE,8DAA8D;YAC9D,OAAO,KAAK,CAAC;IACjB,CAAC;AACH,CAAC;AA2BD,8EAA8E;AAC9E,aAAa;AACb,8EAA8E;AAE9E,MAAM,OAAO,UAAU;IACrB,yDAAyD;IAChD,OAAO,CAAe;IAE/B,YAAY,KAAgB,EAAE,IAAY,EAAE,QAAgB;QAC1D,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACnC,IAAI,CAAC,OAAO,GAAG,KAAK,CAAC,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAc,EAAE;YACvD,MAAM,CAAC,GAAG,KAAK,CAAC,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YACvC,MAAM,QAAQ,GAAG,SAAS,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;YAC/C,OAAO;gBACL,GAAG;gBACH,cAAc,EAAE,cAAc,CAAC,QAAQ,EAAE,QAAQ,CAAC;gBAClD,WAAW,EAAE,CAAC,EAAE,MAAM,CAAC,UAAU,IAAI,CAAC,CAAC;gBACvC,SAAS,EAAI,CAAC,EAAE,MAAM,CAAC,QAAQ,IAAM,CAAC,CAAC;aACxC,CAAC;QACJ,CAAC,CAAC,CAAC;IACL,CAAC;IAED;;;OAGG;IACH,YAAY,CAAC,KAAa,EAAE,GAAW;QACrC,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,IAAI,KAAK,IAAI,CAAC,CAAC,GAAG,CAAC,IAAI,IAAI,GAAG,CAAC,CAAC;IAC5E,CAAC;IAED;;;;;;;;;;;OAWG;IACH,cAAc,CAAC,QAAgB,EAAE,WAAmB;QAClD,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,CACtB,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,KAAK,QAAQ;eAC3B,CAAC,CAAC,cAAc;eAChB,CAAC,CAAC,CAAC,WAAW,KAAK,WAAW,IAAI,CAAC,CAAC,WAAW,KAAK,CAAC,CAAC,CAAC,CAC7D,CAAC;IACJ,CAAC;CACF"}
|
package/dist/index.d.ts
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* A universal library for static analysis and taint tracking.
|
|
5
5
|
*/
|
|
6
|
-
export { initAnalyzer, analyze, analyzeForAPI, isAnalyzerInitialized, resetAnalyzer, type AnalyzerOptions, } from './analyzer.js';
|
|
7
|
-
export type { CircleIR, Meta, TypeInfo, MethodInfo, ParameterInfo, FieldInfo, CallInfo, ArgumentInfo, CallResolution, CFG, CFGBlock, CFGEdge, DFG, DFGDef, DFGUse, DFGChain, Taint, TaintSource, TaintSink, TaintSanitizer, ImportInfo, ExportInfo, UnresolvedItem, Enriched, EnrichedFunction, ResolvedCall, Finding, TaintHop, Vulnerability, AnalysisResponse, SourceType, SinkType, Severity, ProjectAnalysis, ProjectMeta, FileAnalysis, TypeHierarchy, ClassHierarchyInfo, InterfaceHierarchyInfo, CrossFileCall, ArgMapping, TaintPath, } from './types/index.js';
|
|
6
|
+
export { initAnalyzer, analyze, analyzeForAPI, analyzeProject, isAnalyzerInitialized, resetAnalyzer, type AnalyzerOptions, } from './analyzer.js';
|
|
7
|
+
export type { CircleIR, Meta, TypeInfo, MethodInfo, ParameterInfo, FieldInfo, CallInfo, ArgumentInfo, CallResolution, CFG, CFGBlock, CFGEdge, DFG, DFGDef, DFGUse, DFGChain, Taint, TaintSource, TaintSink, TaintSanitizer, ImportInfo, ExportInfo, UnresolvedItem, Enriched, EnrichedFunction, ResolvedCall, Finding, TaintHop, Vulnerability, AnalysisResponse, SourceType, SinkType, Severity, PassCategory, SarifLevel, SastFinding, MetricCategory, MetricValue, FileMetrics, ProjectAnalysis, ProjectMeta, FileAnalysis, TypeHierarchy, ClassHierarchyInfo, InterfaceHierarchyInfo, CrossFileCall, ArgMapping, TaintPath, } from './types/index.js';
|
|
8
8
|
export type { SourceConfig, SinkConfig, TaintConfig, SourcePattern, SinkPattern, SanitizerPattern, } from './types/config.js';
|
|
9
9
|
export { initParser, parse, walkTree, findNodes, findAncestor, getNodeText, collectAllNodes, type SupportedLanguage, type SyntaxNode, type Node, type NodeCache, type Tree, } from './core/index.js';
|
|
10
10
|
export { extractMeta, extractTypes, extractCalls, extractImports, extractExports, buildCFG, buildDFG, } from './core/index.js';
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* A universal library for static analysis and taint tracking.
|
|
5
5
|
*/
|
|
6
6
|
// Main analyzer
|
|
7
|
-
export { initAnalyzer, analyze, analyzeForAPI, isAnalyzerInitialized, resetAnalyzer, } from './analyzer.js';
|
|
7
|
+
export { initAnalyzer, analyze, analyzeForAPI, analyzeProject, isAnalyzerInitialized, resetAnalyzer, } from './analyzer.js';
|
|
8
8
|
// Core utilities (for advanced usage)
|
|
9
9
|
export { initParser, parse, walkTree, findNodes, findAncestor, getNodeText, collectAllNodes, } from './core/index.js';
|
|
10
10
|
// Core extractors
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,gBAAgB;AAChB,OAAO,EACL,YAAY,EACZ,OAAO,EACP,aAAa,EACb,qBAAqB,EACrB,aAAa,GAEd,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,gBAAgB;AAChB,OAAO,EACL,YAAY,EACZ,OAAO,EACP,aAAa,EACb,cAAc,EACd,qBAAqB,EACrB,aAAa,GAEd,MAAM,eAAe,CAAC;AAuEvB,sCAAsC;AACtC,OAAO,EACL,UAAU,EACV,KAAK,EACL,QAAQ,EACR,SAAS,EACT,YAAY,EACZ,WAAW,EACX,eAAe,GAMhB,MAAM,iBAAiB,CAAC;AAEzB,kBAAkB;AAClB,OAAO,EACL,WAAW,EACX,YAAY,EACZ,YAAY,EACZ,cAAc,EACd,cAAc,EACd,QAAQ,EACR,QAAQ,GACT,MAAM,iBAAiB,CAAC;AAEzB,qBAAqB;AACrB,OAAO,EACL,gBAAgB,EAChB,iBAAiB,EACjB,YAAY,EACZ,gBAAgB,EAChB,cAAc,EACd,gBAAgB,EAChB,0BAA0B,EAC1B,kBAAkB,EAClB,OAAO,EACP,aAAa,EACb,WAAW,EACX,eAAe,EACf,aAAa,EACb,kBAAkB,GAMnB,MAAM,qBAAqB,CAAC;AAE7B,mBAAmB;AACnB,OAAO,EACL,WAAW,EACX,gBAAgB,GAEjB,MAAM,qBAAqB,CAAC;AAE7B,uBAAuB;AACvB,OAAO,EACL,qBAAqB,EACrB,kBAAkB,EAClB,WAAW,EACX,gBAAgB,EAChB,iBAAiB,EACjB,sBAAsB,GACvB,MAAM,uBAAuB,CAAC;AAE/B,mBAAmB;AACnB,OAAO,EACL,mBAAmB,EACnB,gBAAgB,EAChB,iBAAiB,EACjB,kBAAkB,EAClB,cAAc,EACd,mBAAmB,EACnB,sBAAsB,EACtB,UAAU,EACV,gBAAgB,EAChB,YAAY,EACZ,UAAU,EACV,kBAAkB,GACnB,MAAM,sBAAsB,CAAC;AAY9B,gCAAgC;AAChC,OAAO,EACL,MAAM,EACN,SAAS,EACT,eAAe,EACf,WAAW,EACX,WAAW,GAIZ,MAAM,mBAAmB,CAAC"}
|
|
@@ -336,35 +336,68 @@ export class CrossFileResolver {
|
|
|
336
336
|
*/
|
|
337
337
|
findCrossFileTaintFlows() {
|
|
338
338
|
const flows = [];
|
|
339
|
+
// Deduplicate: same source + target sink should only emit one flow
|
|
340
|
+
const seen = new Set();
|
|
339
341
|
for (const [filePath, ir] of this.fileIRs) {
|
|
340
342
|
// Check each source in the file
|
|
341
343
|
for (const source of ir.taint.sources) {
|
|
344
|
+
// `interprocedural_param` sources represent "this method's parameter MIGHT be
|
|
345
|
+
// tainted when called by tainted code" — they are a per-file meta-analysis result
|
|
346
|
+
// and are not confirmed external inputs suitable for cross-file taint flows.
|
|
347
|
+
// Using them here causes false positives on every internal library function that
|
|
348
|
+
// has typed parameters and happens to call another file that contains any sink.
|
|
349
|
+
if (source.type === 'interprocedural_param')
|
|
350
|
+
continue;
|
|
342
351
|
// Find calls at or after the source line
|
|
343
352
|
for (const call of ir.calls) {
|
|
344
353
|
if (call.location.line < source.line)
|
|
345
354
|
continue;
|
|
346
355
|
const resolved = this.resolveCall(call, filePath);
|
|
347
|
-
if (resolved
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
356
|
+
if (!resolved || resolved.targetFile === filePath)
|
|
357
|
+
continue;
|
|
358
|
+
// Only proceed if the target file has any YAML-matched sinks at all.
|
|
359
|
+
// Skipping sink-free files prevents the cartesian explosion of flows into
|
|
360
|
+
// utility modules (AST helpers, string utilities, etc.) that have no
|
|
361
|
+
// dangerous operations.
|
|
362
|
+
const targetIR = this.fileIRs.get(resolved.targetFile);
|
|
363
|
+
if (!targetIR || targetIR.taint.sinks.length === 0)
|
|
364
|
+
continue;
|
|
365
|
+
// Find the target method in the target file so we can locate sinks within it.
|
|
366
|
+
// `resolved.targetMethod` is a FQN like "ClassName.methodName"; we match on
|
|
367
|
+
// the suffix after the last dot.
|
|
368
|
+
const shortName = resolved.targetMethod.split('.').pop() ?? resolved.targetMethod;
|
|
369
|
+
let targetMethod;
|
|
370
|
+
for (const type of targetIR.types) {
|
|
371
|
+
const m = type.methods.find(m => m.name === shortName);
|
|
372
|
+
if (m) {
|
|
373
|
+
targetMethod = m;
|
|
374
|
+
break;
|
|
366
375
|
}
|
|
367
376
|
}
|
|
377
|
+
if (!targetMethod)
|
|
378
|
+
continue;
|
|
379
|
+
// Only emit a flow when at least one known sink falls inside the target method.
|
|
380
|
+
// This means `targetLine` now correctly points to an actual dangerous operation
|
|
381
|
+
// in the target file (not the caller's line in the source file).
|
|
382
|
+
const sinksInMethod = targetIR.taint.sinks.filter(s => s.line >= targetMethod.start_line && s.line <= targetMethod.end_line);
|
|
383
|
+
if (sinksInMethod.length === 0)
|
|
384
|
+
continue;
|
|
385
|
+
for (const sink of sinksInMethod) {
|
|
386
|
+
const key = `${filePath}:${source.line}→${resolved.targetFile}:${sink.line}`;
|
|
387
|
+
if (seen.has(key))
|
|
388
|
+
continue;
|
|
389
|
+
seen.add(key);
|
|
390
|
+
flows.push({
|
|
391
|
+
sourceFile: filePath,
|
|
392
|
+
sourceLine: source.line,
|
|
393
|
+
sourceType: source.type,
|
|
394
|
+
targetFile: resolved.targetFile,
|
|
395
|
+
targetLine: sink.line, // actual sink line in target file
|
|
396
|
+
targetMethod: resolved.targetMethod,
|
|
397
|
+
flowType: 'call_arg',
|
|
398
|
+
taintedArgPositions: call.arguments.map((_, i) => i),
|
|
399
|
+
});
|
|
400
|
+
}
|
|
368
401
|
}
|
|
369
402
|
}
|
|
370
403
|
}
|