dsh-plugin-compat-check 0.1.0 → 0.1.2
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/LICENSE +21 -0
- package/README.md +21 -57
- package/index.js +93 -155
- package/kernel/lib/baseline.mjs +110 -34
- package/kernel/lib/contracts.mjs +493 -0
- package/kernel/lib/declarations.mjs +50 -0
- package/kernel/lib/extract-worker.mjs +10 -0
- package/kernel/lib/extract.mjs +235 -1515
- package/kernel/lib/flow-values.mjs +53 -0
- package/kernel/lib/observation.mjs +182 -0
- package/kernel/lib/report-service.mjs +55 -0
- package/kernel/lib/report.mjs +237 -0
- package/kernel/lib/source-analysis.mjs +1913 -0
- package/kernel/lib/source-graph.mjs +297 -0
- package/kernel/lib/state-store.mjs +126 -0
- package/kernel/lib/syntax.mjs +73 -0
- package/kernel/lib/verdict.mjs +492 -587
- package/kernel/lib/version.mjs +2 -2
- package/kernel/lib/worker-analysis.mjs +82 -0
- package/kernel/rules/README.md +46 -58
- package/kernel/rules/severity.json +161 -35
- package/kernel/rules/usage-inventory.json +369 -61
- package/lib/client.js +523 -10
- package/lib/host-resources.js +92 -0
- package/lib/report-contract.js +494 -0
- package/package.json +14 -5
- package/scripts/prepublish-check.mjs +14 -9
- package/scripts/stage-contract.mjs +43 -0
- package/scripts/stage-kernel.mjs +9 -21
- package/skill/SKILL.md +40 -39
|
@@ -0,0 +1,1913 @@
|
|
|
1
|
+
import { parseSource, childNodes } from "./syntax.mjs";
|
|
2
|
+
import { hasEvidence, hasProof, mergedValue } from "./flow-values.mjs";
|
|
3
|
+
|
|
4
|
+
const members = new Set(["MemberExpression", "OptionalMemberExpression"]);
|
|
5
|
+
const calls = new Set(["CallExpression", "OptionalCallExpression"]);
|
|
6
|
+
const functions = new Set([
|
|
7
|
+
"FunctionDeclaration",
|
|
8
|
+
"FunctionExpression",
|
|
9
|
+
"ArrowFunctionExpression",
|
|
10
|
+
"ObjectMethod",
|
|
11
|
+
"ClassMethod",
|
|
12
|
+
"ClassPrivateMethod",
|
|
13
|
+
]);
|
|
14
|
+
const propertyName = (n) =>
|
|
15
|
+
n?.computed
|
|
16
|
+
? n.property?.type === "StringLiteral"
|
|
17
|
+
? n.property.value
|
|
18
|
+
: null
|
|
19
|
+
: n?.property?.name;
|
|
20
|
+
const keyName = (n) => n?.key?.name ?? n?.key?.value;
|
|
21
|
+
const unwrap = (n) =>
|
|
22
|
+
[
|
|
23
|
+
"TSAsExpression",
|
|
24
|
+
"TSTypeAssertion",
|
|
25
|
+
"TSNonNullExpression",
|
|
26
|
+
"ParenthesizedExpression",
|
|
27
|
+
].includes(n?.type)
|
|
28
|
+
? unwrap(n.expression)
|
|
29
|
+
: n;
|
|
30
|
+
const namesIn = (n, out = []) => {
|
|
31
|
+
if (!n) return out;
|
|
32
|
+
if (n.type === "Identifier") out.push(n.name);
|
|
33
|
+
else if (n.type === "AssignmentPattern") namesIn(n.left, out);
|
|
34
|
+
else if (n.type === "RestElement") namesIn(n.argument, out);
|
|
35
|
+
else if (n.type === "ObjectPattern")
|
|
36
|
+
for (const p of n.properties) namesIn(p.value ?? p.argument, out);
|
|
37
|
+
else if (n.type === "ArrayPattern")
|
|
38
|
+
for (const p of n.elements) namesIn(p, out);
|
|
39
|
+
return out;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
class Scope {
|
|
43
|
+
constructor(parent, id, isFunction = !parent) {
|
|
44
|
+
this.parent = parent;
|
|
45
|
+
this.isFunction = isFunction;
|
|
46
|
+
this.id = id;
|
|
47
|
+
this.values = new Map();
|
|
48
|
+
}
|
|
49
|
+
functionScope() {
|
|
50
|
+
return this.isFunction ? this : this.parent.functionScope();
|
|
51
|
+
}
|
|
52
|
+
find(name) {
|
|
53
|
+
return this.values.has(name) ? this : this.parent?.find(name);
|
|
54
|
+
}
|
|
55
|
+
get(name) {
|
|
56
|
+
return this.find(name)?.values.get(name) ?? null;
|
|
57
|
+
}
|
|
58
|
+
set(name, value) {
|
|
59
|
+
(this.find(name) || this).values.set(name, value);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Bounded static value flow over canonical package-local source. No evaluation. */
|
|
64
|
+
export function analyzeSources(
|
|
65
|
+
blob,
|
|
66
|
+
catalogNames,
|
|
67
|
+
pkgName,
|
|
68
|
+
fileSides = {},
|
|
69
|
+
entryFiles = null,
|
|
70
|
+
moduleLinks = new Map(),
|
|
71
|
+
) {
|
|
72
|
+
const services = new Set(),
|
|
73
|
+
declared = new Set(),
|
|
74
|
+
softDeclared = new Set(),
|
|
75
|
+
addressable = new Set();
|
|
76
|
+
const callMap = new Map(),
|
|
77
|
+
serviceFiles = new Map(),
|
|
78
|
+
provided = new Set();
|
|
79
|
+
const sites = [],
|
|
80
|
+
bindings = [],
|
|
81
|
+
dependencies = [],
|
|
82
|
+
diagnostics = [],
|
|
83
|
+
subContexts = [];
|
|
84
|
+
let scopeId = 0,
|
|
85
|
+
invocationCount = 0,
|
|
86
|
+
constantCalls = 0;
|
|
87
|
+
const identifierCache = new WeakMap();
|
|
88
|
+
const guardSets = new Set();
|
|
89
|
+
const guardSet = (values = []) => {
|
|
90
|
+
const set = new Set(values);
|
|
91
|
+
if (set.size) guardSets.add(set);
|
|
92
|
+
return set;
|
|
93
|
+
};
|
|
94
|
+
const serviceCache = new WeakMap();
|
|
95
|
+
const stateFor = (value) => {
|
|
96
|
+
if (value?.kind === "object") return value;
|
|
97
|
+
if (value?.kind === "context" || value?.kind === "service")
|
|
98
|
+
return (value.state ||= { fields: new Map(), opaque: false });
|
|
99
|
+
return null;
|
|
100
|
+
};
|
|
101
|
+
const serviceFor = (context, key, form = "ctx-member") => {
|
|
102
|
+
let cache = serviceCache.get(context);
|
|
103
|
+
if (!cache) serviceCache.set(context, (cache = new Map()));
|
|
104
|
+
if (!cache.has(key))
|
|
105
|
+
cache.set(key, {
|
|
106
|
+
kind: "service",
|
|
107
|
+
service: key,
|
|
108
|
+
inferred: context.inferred === true,
|
|
109
|
+
state: { fields: new Map(), opaque: false },
|
|
110
|
+
});
|
|
111
|
+
return { ...cache.get(key), form };
|
|
112
|
+
};
|
|
113
|
+
let escapeBudgetExceeded = false;
|
|
114
|
+
const capturesEvidence = (fn, seen = new Set()) => {
|
|
115
|
+
if (!fn?.node || seen.has(fn)) return false;
|
|
116
|
+
if (seen.size >= 64) {
|
|
117
|
+
diagnose(
|
|
118
|
+
"flow-capture-budget",
|
|
119
|
+
fn.file || "",
|
|
120
|
+
fn.node,
|
|
121
|
+
"Captured-function graph exceeds 64 nodes",
|
|
122
|
+
);
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
seen.add(fn);
|
|
126
|
+
let names = identifierCache.get(fn.node);
|
|
127
|
+
if (!names) {
|
|
128
|
+
names = new Set();
|
|
129
|
+
const queue = [fn.node.body];
|
|
130
|
+
while (queue.length) {
|
|
131
|
+
const n = queue.pop();
|
|
132
|
+
if (!n) continue;
|
|
133
|
+
if (n.type === "Identifier") names.add(n.name);
|
|
134
|
+
queue.push(...childNodes(n));
|
|
135
|
+
}
|
|
136
|
+
identifierCache.set(fn.node, names);
|
|
137
|
+
}
|
|
138
|
+
for (const name of names) {
|
|
139
|
+
const value = fn.scope.get(name);
|
|
140
|
+
if (
|
|
141
|
+
hasProof(value) ||
|
|
142
|
+
(value?.kind === "function" && capturesEvidence(value, seen))
|
|
143
|
+
)
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
return false;
|
|
147
|
+
};
|
|
148
|
+
const constantOnly = (node) => {
|
|
149
|
+
if (!node) return false;
|
|
150
|
+
if (
|
|
151
|
+
[
|
|
152
|
+
"StringLiteral",
|
|
153
|
+
"NumericLiteral",
|
|
154
|
+
"BooleanLiteral",
|
|
155
|
+
"Identifier",
|
|
156
|
+
].includes(node.type)
|
|
157
|
+
)
|
|
158
|
+
return true;
|
|
159
|
+
if (node.type === "BinaryExpression" && node.operator === "+")
|
|
160
|
+
return constantOnly(node.left) && constantOnly(node.right);
|
|
161
|
+
if (node.type === "TemplateLiteral")
|
|
162
|
+
return node.expressions.every(constantOnly);
|
|
163
|
+
return false;
|
|
164
|
+
};
|
|
165
|
+
const knownParameters = new Map();
|
|
166
|
+
const concreteFunctions = new Set(),
|
|
167
|
+
exportReads = new Set();
|
|
168
|
+
const journals = [];
|
|
169
|
+
const newJournal = () => ({
|
|
170
|
+
objects: new Map(),
|
|
171
|
+
bindings: new Map(),
|
|
172
|
+
scopeCeiling: scopeId,
|
|
173
|
+
});
|
|
174
|
+
const put = (owner, key, value) => {
|
|
175
|
+
for (const journal of journals)
|
|
176
|
+
if (owner.id <= journal.scopeCeiling) {
|
|
177
|
+
if (!journal.bindings.has(owner))
|
|
178
|
+
journal.bindings.set(owner, new Map());
|
|
179
|
+
const before = journal.bindings.get(owner);
|
|
180
|
+
if (!before.has(key))
|
|
181
|
+
before.set(key, {
|
|
182
|
+
present: owner.values.has(key),
|
|
183
|
+
value: owner.values.get(key),
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
owner.values.set(key, value);
|
|
187
|
+
};
|
|
188
|
+
const noteMutation = (object) => {
|
|
189
|
+
for (const journal of journals)
|
|
190
|
+
if (!journal.objects.has(object))
|
|
191
|
+
journal.objects.set(object, {
|
|
192
|
+
fields: object.fields,
|
|
193
|
+
opaque: object.opaque,
|
|
194
|
+
});
|
|
195
|
+
};
|
|
196
|
+
const undo = (journal) => {
|
|
197
|
+
for (const [object, state] of journal.objects) {
|
|
198
|
+
object.fields = state.fields;
|
|
199
|
+
object.opaque = state.opaque;
|
|
200
|
+
}
|
|
201
|
+
for (const [owner, keys] of journal.bindings)
|
|
202
|
+
for (const [key, before] of keys) {
|
|
203
|
+
if (before.present) owner.values.set(key, before.value);
|
|
204
|
+
else owner.values.delete(key);
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
const writeField = (object, key, value) => {
|
|
208
|
+
noteMutation(object);
|
|
209
|
+
if (journals.length) object.fields = new Map(object.fields);
|
|
210
|
+
object.fields.set(key, value);
|
|
211
|
+
};
|
|
212
|
+
const markOpaque = (object) => {
|
|
213
|
+
noteMutation(object);
|
|
214
|
+
object.opaque = true;
|
|
215
|
+
};
|
|
216
|
+
const taint = (value, seen = new Set()) => {
|
|
217
|
+
if (!value || seen.has(value)) return;
|
|
218
|
+
if (seen.size >= 128) {
|
|
219
|
+
escapeBudgetExceeded = true;
|
|
220
|
+
diagnose(
|
|
221
|
+
"flow-escape-budget",
|
|
222
|
+
"",
|
|
223
|
+
null,
|
|
224
|
+
"Reachable escape graph exceeds 128 nodes; remaining ownership proof is withheld",
|
|
225
|
+
);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
seen.add(value);
|
|
229
|
+
if (value.kind === "proxy") {
|
|
230
|
+
taint(value.target, seen);
|
|
231
|
+
taint(value.handler, seen);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
const state = stateFor(value);
|
|
235
|
+
if (!state) return;
|
|
236
|
+
const children = [...state.fields.values()];
|
|
237
|
+
markOpaque(state);
|
|
238
|
+
for (const child of children) taint(child, seen);
|
|
239
|
+
if (value.kind === "context")
|
|
240
|
+
for (const service of serviceCache.get(value)?.values() || [])
|
|
241
|
+
taint(service, seen);
|
|
242
|
+
};
|
|
243
|
+
const modules = new Map(),
|
|
244
|
+
frames = [],
|
|
245
|
+
invoking = new Set();
|
|
246
|
+
const diagnose = (kind, file, node, message) => {
|
|
247
|
+
if (
|
|
248
|
+
diagnostics.length < 100 &&
|
|
249
|
+
!diagnostics.some(
|
|
250
|
+
(d) => d.kind === kind && d.file === file && d.start === node?.start,
|
|
251
|
+
)
|
|
252
|
+
)
|
|
253
|
+
diagnostics.push({
|
|
254
|
+
kind,
|
|
255
|
+
file,
|
|
256
|
+
line: node?.loc?.start.line ?? 1,
|
|
257
|
+
start: node?.start ?? null,
|
|
258
|
+
message,
|
|
259
|
+
});
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
for (const [file, text] of blob) {
|
|
263
|
+
const parsed = parseSource(text, file);
|
|
264
|
+
if (!parsed.ast) {
|
|
265
|
+
diagnostics.push(parsed.error);
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
const program = parsed.ast.program;
|
|
269
|
+
const exported = new Set(),
|
|
270
|
+
applyNames = new Set(),
|
|
271
|
+
injectNames = new Set(),
|
|
272
|
+
provideNames = new Set();
|
|
273
|
+
const isEntry = entryFiles === null || entryFiles.includes(file);
|
|
274
|
+
for (const n of program.body) {
|
|
275
|
+
if (n.type !== "ExportNamedDeclaration") continue;
|
|
276
|
+
for (const spec of n.specifiers || []) {
|
|
277
|
+
exported.add(spec.local?.name);
|
|
278
|
+
if (spec.exported?.name === "inject") injectNames.add(spec.local?.name);
|
|
279
|
+
if (spec.exported?.name === "provide")
|
|
280
|
+
provideNames.add(spec.local?.name);
|
|
281
|
+
if (spec.exported?.name === "apply") applyNames.add(spec.local?.name);
|
|
282
|
+
}
|
|
283
|
+
if (n.declaration?.id) {
|
|
284
|
+
exported.add(n.declaration.id.name);
|
|
285
|
+
if (n.declaration.id.name === "apply") applyNames.add("apply");
|
|
286
|
+
}
|
|
287
|
+
if (n.declaration?.type === "VariableDeclaration")
|
|
288
|
+
for (const d of n.declaration.declarations)
|
|
289
|
+
for (const name of namesIn(d.id)) {
|
|
290
|
+
exported.add(name);
|
|
291
|
+
if (name === "apply") applyNames.add(name);
|
|
292
|
+
if (name === "inject") injectNames.add(name);
|
|
293
|
+
if (name === "provide") provideNames.add(name);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
const root = new Scope(null, ++scopeId);
|
|
297
|
+
const module = {
|
|
298
|
+
file,
|
|
299
|
+
root,
|
|
300
|
+
initializing: false,
|
|
301
|
+
initialized: false,
|
|
302
|
+
deferred: [],
|
|
303
|
+
exports: new Map(),
|
|
304
|
+
stars: [],
|
|
305
|
+
};
|
|
306
|
+
modules.set(file, module);
|
|
307
|
+
const callResults = new WeakMap();
|
|
308
|
+
const effects = new WeakMap();
|
|
309
|
+
const hasEffect = (node, scope) => effects.get(node)?.has(scope.id);
|
|
310
|
+
const effectValue = (node, scope) =>
|
|
311
|
+
effects.get(node)?.get(scope.id) ?? null;
|
|
312
|
+
const remember = (node, scope, value) => {
|
|
313
|
+
let map = effects.get(node);
|
|
314
|
+
if (!map) effects.set(node, (map = new Map()));
|
|
315
|
+
map.set(scope.id, value);
|
|
316
|
+
return value;
|
|
317
|
+
};
|
|
318
|
+
const transparent = (value, node) => {
|
|
319
|
+
if (value?.kind !== "proxy") return value;
|
|
320
|
+
if (
|
|
321
|
+
value.handler?.kind === "object" &&
|
|
322
|
+
!value.handler.opaque &&
|
|
323
|
+
value.handler.fields.size === 0
|
|
324
|
+
)
|
|
325
|
+
return transparent(value.target, node);
|
|
326
|
+
diagnose(
|
|
327
|
+
"flow-opaque-proxy",
|
|
328
|
+
file,
|
|
329
|
+
node,
|
|
330
|
+
"Proxy handler was mutated or escaped",
|
|
331
|
+
);
|
|
332
|
+
return null;
|
|
333
|
+
};
|
|
334
|
+
const join = (values, node) => {
|
|
335
|
+
const value = mergedValue(values);
|
|
336
|
+
if (!value && values.some((v) => hasEvidence(v))) {
|
|
337
|
+
for (const v of values)
|
|
338
|
+
if (stateFor(v) || v?.kind === "proxy") taint(v);
|
|
339
|
+
diagnose(
|
|
340
|
+
"flow-ambiguous-value",
|
|
341
|
+
file,
|
|
342
|
+
node,
|
|
343
|
+
"Alternative values do not establish one identity",
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
return value;
|
|
347
|
+
};
|
|
348
|
+
let activeSides = null;
|
|
349
|
+
const cjsExports = { kind: "object", fields: new Map() };
|
|
350
|
+
const cjsModule = {
|
|
351
|
+
kind: "object",
|
|
352
|
+
fields: new Map([["exports", cjsExports]]),
|
|
353
|
+
};
|
|
354
|
+
let currentDeferred = null;
|
|
355
|
+
const imported = (spec, name) => ({
|
|
356
|
+
kind: "import",
|
|
357
|
+
target: moduleLinks.get(file)?.get(spec),
|
|
358
|
+
name,
|
|
359
|
+
});
|
|
360
|
+
const lookupExport = (target, name, seen = new Set()) => {
|
|
361
|
+
const other = modules.get(target),
|
|
362
|
+
key = target + ":" + name;
|
|
363
|
+
if (seen.size >= 32 || exportReads.size >= 32) {
|
|
364
|
+
diagnose(
|
|
365
|
+
"flow-module-budget",
|
|
366
|
+
file,
|
|
367
|
+
null,
|
|
368
|
+
"Export chain exceeds 32 modules",
|
|
369
|
+
);
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
if (!other || seen.has(key) || exportReads.has(key)) {
|
|
373
|
+
if (seen.has(key) || exportReads.has(key))
|
|
374
|
+
diagnose(
|
|
375
|
+
"flow-import-cycle",
|
|
376
|
+
file,
|
|
377
|
+
null,
|
|
378
|
+
"Cyclic export cannot be proven",
|
|
379
|
+
);
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
seen.add(key);
|
|
383
|
+
other.initialize();
|
|
384
|
+
const entry = other.exports.get(name);
|
|
385
|
+
if (entry) {
|
|
386
|
+
exportReads.add(key);
|
|
387
|
+
try {
|
|
388
|
+
return entry.source
|
|
389
|
+
? lookupExport(
|
|
390
|
+
moduleLinks.get(target)?.get(entry.source),
|
|
391
|
+
entry.name,
|
|
392
|
+
seen,
|
|
393
|
+
)
|
|
394
|
+
: other.resolveExport(entry, seen);
|
|
395
|
+
} finally {
|
|
396
|
+
exportReads.delete(key);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
if (other.stars.length) {
|
|
400
|
+
const values = other.stars
|
|
401
|
+
.map((spec) =>
|
|
402
|
+
lookupExport(
|
|
403
|
+
moduleLinks.get(target)?.get(spec),
|
|
404
|
+
name,
|
|
405
|
+
new Set(seen),
|
|
406
|
+
),
|
|
407
|
+
)
|
|
408
|
+
.filter(Boolean);
|
|
409
|
+
if (values.length === 1) return values[0];
|
|
410
|
+
if (values.length > 1)
|
|
411
|
+
diagnose(
|
|
412
|
+
"flow-ambiguous-export",
|
|
413
|
+
file,
|
|
414
|
+
null,
|
|
415
|
+
"Multiple star exports cannot prove ownership",
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
const value = other.cjs();
|
|
419
|
+
return name === "default" ? value : value?.fields?.get(name) || null;
|
|
420
|
+
};
|
|
421
|
+
const namespace = (target) => ({ kind: "namespace", target });
|
|
422
|
+
const fnValue = (node, scope) => ({
|
|
423
|
+
kind: "function",
|
|
424
|
+
node,
|
|
425
|
+
scope,
|
|
426
|
+
file,
|
|
427
|
+
guardFor: (args) => predicateFor(node, scope, args),
|
|
428
|
+
invoke: (args, guards, invokeSides, receiver) => {
|
|
429
|
+
const key = file + ":" + node.start;
|
|
430
|
+
const concreteCall =
|
|
431
|
+
frames.at(-1)?.executing || (!frames.length && module.initializing);
|
|
432
|
+
if (concreteCall) {
|
|
433
|
+
const old = knownParameters.get(key);
|
|
434
|
+
knownParameters.set(
|
|
435
|
+
key,
|
|
436
|
+
old
|
|
437
|
+
? Array.from(
|
|
438
|
+
{ length: Math.max(old.length, args.length) },
|
|
439
|
+
(_, i) => mergedValue([old[i] || null, args[i] || null]),
|
|
440
|
+
)
|
|
441
|
+
: args,
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
if (node.generator) {
|
|
445
|
+
if (concreteCall) concreteFunctions.add(key);
|
|
446
|
+
diagnose(
|
|
447
|
+
"flow-generator",
|
|
448
|
+
file,
|
|
449
|
+
node,
|
|
450
|
+
"Creating an iterator does not execute its body",
|
|
451
|
+
);
|
|
452
|
+
return null;
|
|
453
|
+
}
|
|
454
|
+
const relevant =
|
|
455
|
+
hasProof(receiver) ||
|
|
456
|
+
args.some(
|
|
457
|
+
(v) =>
|
|
458
|
+
hasProof(v) || (v?.kind === "function" && capturesEvidence(v)),
|
|
459
|
+
) ||
|
|
460
|
+
capturesEvidence({ node, scope, file });
|
|
461
|
+
if (!relevant) {
|
|
462
|
+
if (hasEvidence(receiver) || args.some((v) => hasEvidence(v)))
|
|
463
|
+
diagnose(
|
|
464
|
+
"flow-inferred-forwarding",
|
|
465
|
+
file,
|
|
466
|
+
node,
|
|
467
|
+
"Only proven Context/service bindings are expanded across calls",
|
|
468
|
+
);
|
|
469
|
+
const body =
|
|
470
|
+
node.body.type === "BlockStatement" &&
|
|
471
|
+
node.body.body.length === 1 &&
|
|
472
|
+
node.body.body[0].type === "ReturnStatement"
|
|
473
|
+
? node.body.body[0].argument
|
|
474
|
+
: node.body;
|
|
475
|
+
if (
|
|
476
|
+
!args.every((v) => v?.kind === "constant") ||
|
|
477
|
+
!constantOnly(body) ||
|
|
478
|
+
++constantCalls > 512
|
|
479
|
+
)
|
|
480
|
+
return null;
|
|
481
|
+
}
|
|
482
|
+
if (invoking.has(key)) {
|
|
483
|
+
diagnose(
|
|
484
|
+
"flow-recursion",
|
|
485
|
+
file,
|
|
486
|
+
node,
|
|
487
|
+
"Recursive call is not executed",
|
|
488
|
+
);
|
|
489
|
+
return null;
|
|
490
|
+
}
|
|
491
|
+
if (invoking.size >= 16 || (relevant && ++invocationCount > 256)) {
|
|
492
|
+
if (concreteCall) concreteFunctions.add(key);
|
|
493
|
+
diagnose(
|
|
494
|
+
"flow-budget-exceeded",
|
|
495
|
+
file,
|
|
496
|
+
node,
|
|
497
|
+
"Bounded call analysis limit reached",
|
|
498
|
+
);
|
|
499
|
+
return null;
|
|
500
|
+
}
|
|
501
|
+
if (concreteCall) concreteFunctions.add(key);
|
|
502
|
+
invoking.add(key);
|
|
503
|
+
const priorSides = activeSides;
|
|
504
|
+
activeSides = invokeSides;
|
|
505
|
+
const asyncJournal = node.async ? newJournal() : null;
|
|
506
|
+
if (asyncJournal) journals.push(asyncJournal);
|
|
507
|
+
try {
|
|
508
|
+
const value = visitFunction(
|
|
509
|
+
node,
|
|
510
|
+
scope,
|
|
511
|
+
guards,
|
|
512
|
+
0,
|
|
513
|
+
null,
|
|
514
|
+
args,
|
|
515
|
+
receiver,
|
|
516
|
+
);
|
|
517
|
+
return node.async
|
|
518
|
+
? { kind: "promise", value }
|
|
519
|
+
: node.generator
|
|
520
|
+
? null
|
|
521
|
+
: value;
|
|
522
|
+
} finally {
|
|
523
|
+
if (asyncJournal) {
|
|
524
|
+
journals.pop();
|
|
525
|
+
const objects = [...asyncJournal.objects].map(
|
|
526
|
+
([object, before]) => [
|
|
527
|
+
object,
|
|
528
|
+
before,
|
|
529
|
+
{ fields: object.fields, opaque: object.opaque },
|
|
530
|
+
],
|
|
531
|
+
);
|
|
532
|
+
const bindings = [...asyncJournal.bindings].flatMap(
|
|
533
|
+
([owner, keys]) =>
|
|
534
|
+
[...keys].map(([name, before]) => [
|
|
535
|
+
owner,
|
|
536
|
+
name,
|
|
537
|
+
before,
|
|
538
|
+
owner.values.get(name),
|
|
539
|
+
]),
|
|
540
|
+
);
|
|
541
|
+
undo(asyncJournal);
|
|
542
|
+
if (objects.length || bindings.length) {
|
|
543
|
+
diagnose(
|
|
544
|
+
"flow-async-effects",
|
|
545
|
+
file,
|
|
546
|
+
node,
|
|
547
|
+
"Async caller effects may occur after suspension",
|
|
548
|
+
);
|
|
549
|
+
for (const [object] of objects) markOpaque(object);
|
|
550
|
+
for (const [owner, name, before, after] of bindings)
|
|
551
|
+
put(owner, name, join([before.value, after], node));
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
activeSides = priorSides;
|
|
555
|
+
invoking.delete(key);
|
|
556
|
+
}
|
|
557
|
+
},
|
|
558
|
+
});
|
|
559
|
+
const heads = new Set();
|
|
560
|
+
const declarationOwners = new Map();
|
|
561
|
+
const sides = fileSides[file] || ["host"];
|
|
562
|
+
const mention = (svc) => {
|
|
563
|
+
if (!svc || typeof svc !== "string") return;
|
|
564
|
+
services.add(svc);
|
|
565
|
+
if (!serviceFiles.has(svc)) serviceFiles.set(svc, new Set());
|
|
566
|
+
serviceFiles.get(svc).add(file);
|
|
567
|
+
};
|
|
568
|
+
const dependency = (
|
|
569
|
+
service,
|
|
570
|
+
kind,
|
|
571
|
+
node,
|
|
572
|
+
inferred = kind === "descriptor-inject",
|
|
573
|
+
) => {
|
|
574
|
+
mention(service);
|
|
575
|
+
declared.add(service);
|
|
576
|
+
if (kind === "optional-read" || kind === "read")
|
|
577
|
+
softDeclared.add(service);
|
|
578
|
+
dependencies.push({
|
|
579
|
+
service,
|
|
580
|
+
kind,
|
|
581
|
+
inferred,
|
|
582
|
+
file,
|
|
583
|
+
line: node.loc?.start.line ?? 1,
|
|
584
|
+
start: node.start,
|
|
585
|
+
end: node.end,
|
|
586
|
+
sides: [...(activeSides || sides)],
|
|
587
|
+
});
|
|
588
|
+
};
|
|
589
|
+
const literalNames = (node, scope) => {
|
|
590
|
+
node = unwrap(node);
|
|
591
|
+
if (node?.type === "Identifier")
|
|
592
|
+
return scope.get(node.name)?.strings || [];
|
|
593
|
+
if (node?.type !== "ArrayExpression") return [];
|
|
594
|
+
return node.elements.flatMap((n) => {
|
|
595
|
+
if (n?.type === "SpreadElement") return literalNames(n.argument, scope);
|
|
596
|
+
const value = constant(n, scope);
|
|
597
|
+
return value === null ? [] : [value];
|
|
598
|
+
});
|
|
599
|
+
};
|
|
600
|
+
const keyFor = (node, scope, guards) =>
|
|
601
|
+
node?.computed
|
|
602
|
+
? constant(node.property, scope, guards)
|
|
603
|
+
: propertyName(node);
|
|
604
|
+
const constant = (node, scope, guards) => {
|
|
605
|
+
const value = resolve(node, scope, guards);
|
|
606
|
+
return value?.kind === "constant" && typeof value.value === "string"
|
|
607
|
+
? value.value
|
|
608
|
+
: null;
|
|
609
|
+
};
|
|
610
|
+
const readField = (value, key, node) => {
|
|
611
|
+
value = transparent(value, node);
|
|
612
|
+
const state = stateFor(value);
|
|
613
|
+
if (
|
|
614
|
+
(escapeBudgetExceeded &&
|
|
615
|
+
["context", "service"].includes(value?.kind)) ||
|
|
616
|
+
state?.opaque
|
|
617
|
+
) {
|
|
618
|
+
if (hasEvidence(value))
|
|
619
|
+
diagnose(
|
|
620
|
+
"flow-opaque-object",
|
|
621
|
+
file,
|
|
622
|
+
node,
|
|
623
|
+
"Unknown mutation/spread/accessor prevents field proof",
|
|
624
|
+
);
|
|
625
|
+
return null;
|
|
626
|
+
}
|
|
627
|
+
if (state?.fields.has(key)) return state.fields.get(key);
|
|
628
|
+
if (value?.kind === "context") return serviceFor(value, key);
|
|
629
|
+
if (value?.kind === "service")
|
|
630
|
+
return {
|
|
631
|
+
kind: "method",
|
|
632
|
+
service: value.service,
|
|
633
|
+
method: key,
|
|
634
|
+
receiver: value,
|
|
635
|
+
};
|
|
636
|
+
return null;
|
|
637
|
+
};
|
|
638
|
+
const resolve = (node, scope, guards = guardSet()) => {
|
|
639
|
+
node = unwrap(node);
|
|
640
|
+
if (!node) return null;
|
|
641
|
+
if (
|
|
642
|
+
[
|
|
643
|
+
"AssignmentExpression",
|
|
644
|
+
"UpdateExpression",
|
|
645
|
+
"SequenceExpression",
|
|
646
|
+
"AwaitExpression",
|
|
647
|
+
].includes(node.type) ||
|
|
648
|
+
(node.type === "UnaryExpression" && node.operator === "delete")
|
|
649
|
+
) {
|
|
650
|
+
visit(node, scope, guards);
|
|
651
|
+
return effectValue(node, scope);
|
|
652
|
+
}
|
|
653
|
+
if (node.type === "Identifier") {
|
|
654
|
+
if (scope.find(node.name)) {
|
|
655
|
+
const value = scope.get(node.name);
|
|
656
|
+
return value?.kind === "import"
|
|
657
|
+
? value.name === "*"
|
|
658
|
+
? namespace(value.target)
|
|
659
|
+
: lookupExport(value.target, value.name)
|
|
660
|
+
: value;
|
|
661
|
+
}
|
|
662
|
+
if (node.name === "exports") return cjsExports;
|
|
663
|
+
if (node.name === "module") return cjsModule;
|
|
664
|
+
return node.name === "ctx"
|
|
665
|
+
? { kind: "context", head: "ctx", inferred: true }
|
|
666
|
+
: null;
|
|
667
|
+
}
|
|
668
|
+
if (
|
|
669
|
+
["StringLiteral", "NumericLiteral", "BooleanLiteral"].includes(
|
|
670
|
+
node.type,
|
|
671
|
+
)
|
|
672
|
+
)
|
|
673
|
+
return { kind: "constant", value: node.value };
|
|
674
|
+
if (node.type === "TemplateLiteral") {
|
|
675
|
+
const values = node.expressions.map((n) => resolve(n, scope, guards));
|
|
676
|
+
if (values.every((v) => v?.kind === "constant")) {
|
|
677
|
+
const chunks = node.quasis.flatMap((q, i) => [
|
|
678
|
+
q.value.cooked ?? q.value.raw,
|
|
679
|
+
i < values.length ? String(values[i].value) : "",
|
|
680
|
+
]);
|
|
681
|
+
if (chunks.reduce((n, v) => n + v.length, 0) > 8192) {
|
|
682
|
+
diagnose(
|
|
683
|
+
"flow-string-budget",
|
|
684
|
+
file,
|
|
685
|
+
node,
|
|
686
|
+
"Constant template expansion exceeds budget",
|
|
687
|
+
);
|
|
688
|
+
return null;
|
|
689
|
+
}
|
|
690
|
+
return { kind: "constant", value: chunks.join("") };
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
if (node.type === "BinaryExpression" && node.operator === "+") {
|
|
694
|
+
const left = resolve(node.left, scope, guards),
|
|
695
|
+
right = resolve(node.right, scope, guards);
|
|
696
|
+
if (left?.kind === "constant" && right?.kind === "constant") {
|
|
697
|
+
if (
|
|
698
|
+
(typeof left.value === "string" ||
|
|
699
|
+
typeof right.value === "string") &&
|
|
700
|
+
String(left.value).length + String(right.value).length > 8192
|
|
701
|
+
) {
|
|
702
|
+
diagnose(
|
|
703
|
+
"flow-string-budget",
|
|
704
|
+
file,
|
|
705
|
+
node,
|
|
706
|
+
"Constant concatenation exceeds budget",
|
|
707
|
+
);
|
|
708
|
+
return null;
|
|
709
|
+
}
|
|
710
|
+
return { kind: "constant", value: left.value + right.value };
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
if (node.type === "ConditionalExpression") {
|
|
714
|
+
const values = [
|
|
715
|
+
resolve(node.consequent, scope, guards),
|
|
716
|
+
resolve(node.alternate, scope, guards),
|
|
717
|
+
],
|
|
718
|
+
result = join(values, node);
|
|
719
|
+
if (!result && values.some((v) => hasEvidence(v)))
|
|
720
|
+
diagnose(
|
|
721
|
+
"flow-ambiguous-value",
|
|
722
|
+
file,
|
|
723
|
+
node,
|
|
724
|
+
"Conditional ownership is unknown",
|
|
725
|
+
);
|
|
726
|
+
return result;
|
|
727
|
+
}
|
|
728
|
+
if (functions.has(node.type)) return fnValue(node, scope);
|
|
729
|
+
if (
|
|
730
|
+
node.type === "NewExpression" &&
|
|
731
|
+
node.callee.type === "Identifier" &&
|
|
732
|
+
node.callee.name === "Proxy" &&
|
|
733
|
+
!scope.find("Proxy")
|
|
734
|
+
) {
|
|
735
|
+
const target = resolve(node.arguments[0], scope, guards),
|
|
736
|
+
handler = resolve(node.arguments[1], scope, guards);
|
|
737
|
+
if (hasEvidence(target)) {
|
|
738
|
+
if (
|
|
739
|
+
handler?.kind === "object" &&
|
|
740
|
+
!handler.opaque &&
|
|
741
|
+
handler.fields.size === 0
|
|
742
|
+
)
|
|
743
|
+
return { kind: "proxy", target, handler };
|
|
744
|
+
diagnose(
|
|
745
|
+
"flow-opaque-proxy",
|
|
746
|
+
file,
|
|
747
|
+
node,
|
|
748
|
+
"Proxy traps can replace Context/service access",
|
|
749
|
+
);
|
|
750
|
+
}
|
|
751
|
+
return null;
|
|
752
|
+
}
|
|
753
|
+
if (node.type === "ThisExpression")
|
|
754
|
+
return scope.functionScope().receiver || { kind: "instance" };
|
|
755
|
+
if (node.type === "ArrayExpression")
|
|
756
|
+
return { kind: "literal", strings: literalNames(node, scope) };
|
|
757
|
+
if (node.type === "ObjectExpression") {
|
|
758
|
+
const fields = new Map();
|
|
759
|
+
for (const p of node.properties)
|
|
760
|
+
if (p.type === "ObjectProperty")
|
|
761
|
+
fields.set(
|
|
762
|
+
p.computed ? constant(p.key, scope, guards) : keyName(p),
|
|
763
|
+
resolve(p.value, scope, guards),
|
|
764
|
+
);
|
|
765
|
+
else if (p.type === "ObjectMethod")
|
|
766
|
+
fields.set(
|
|
767
|
+
p.computed ? constant(p.key, scope, guards) : keyName(p),
|
|
768
|
+
fnValue(p, scope),
|
|
769
|
+
);
|
|
770
|
+
const opaque = node.properties.some(
|
|
771
|
+
(p) =>
|
|
772
|
+
p.type === "SpreadElement" ||
|
|
773
|
+
(p.type === "ObjectMethod" && p.kind !== "method") ||
|
|
774
|
+
(p.computed && constant(p.key, scope, guards) === null),
|
|
775
|
+
);
|
|
776
|
+
return { kind: "object", fields, opaque };
|
|
777
|
+
}
|
|
778
|
+
if (members.has(node.type)) {
|
|
779
|
+
const object = transparent(resolve(node.object, scope, guards), node),
|
|
780
|
+
key = keyFor(node, scope, guards);
|
|
781
|
+
if (!key) {
|
|
782
|
+
if (hasEvidence(object))
|
|
783
|
+
diagnose(
|
|
784
|
+
"flow-dynamic-access",
|
|
785
|
+
file,
|
|
786
|
+
node,
|
|
787
|
+
"Computed property cannot be resolved to one string",
|
|
788
|
+
);
|
|
789
|
+
return null;
|
|
790
|
+
}
|
|
791
|
+
if (object?.kind === "promise") {
|
|
792
|
+
diagnose(
|
|
793
|
+
"flow-async-return",
|
|
794
|
+
file,
|
|
795
|
+
node,
|
|
796
|
+
"Async factory does not return a synchronous service",
|
|
797
|
+
);
|
|
798
|
+
return null;
|
|
799
|
+
}
|
|
800
|
+
if (object?.kind === "namespace")
|
|
801
|
+
return lookupExport(object.target, key);
|
|
802
|
+
if (object?.kind === "instance" && key === "ctx")
|
|
803
|
+
return { kind: "context", head: "this.ctx", inferred: true };
|
|
804
|
+
const value = readField(object, key, node);
|
|
805
|
+
if (value?.kind === "context")
|
|
806
|
+
heads.add(text.slice(node.start, node.end));
|
|
807
|
+
return value;
|
|
808
|
+
return null;
|
|
809
|
+
}
|
|
810
|
+
if (calls.has(node.type)) {
|
|
811
|
+
if (
|
|
812
|
+
node.callee.type === "Identifier" &&
|
|
813
|
+
node.callee.name === "require" &&
|
|
814
|
+
!scope.find("require") &&
|
|
815
|
+
node.arguments[0]?.type === "StringLiteral"
|
|
816
|
+
)
|
|
817
|
+
return namespace(moduleLinks.get(file)?.get(node.arguments[0].value));
|
|
818
|
+
let memo = callResults.get(node);
|
|
819
|
+
if (!memo) {
|
|
820
|
+
memo = new Map();
|
|
821
|
+
callResults.set(node, memo);
|
|
822
|
+
}
|
|
823
|
+
if (memo.has(scope.id)) return memo.get(scope.id);
|
|
824
|
+
memo.set(scope.id, null);
|
|
825
|
+
visit(node.callee, scope, guards);
|
|
826
|
+
let callee = resolve(node.callee, scope, guards);
|
|
827
|
+
if (callee?.kind === "namespace")
|
|
828
|
+
callee = lookupExport(callee.target, "default");
|
|
829
|
+
const receiver = members.has(node.callee.type)
|
|
830
|
+
? resolve(node.callee.object, scope, guards)
|
|
831
|
+
: null;
|
|
832
|
+
const values = node.arguments.map((arg) => {
|
|
833
|
+
visit(arg, scope, guards);
|
|
834
|
+
return resolve(arg, scope, guards);
|
|
835
|
+
});
|
|
836
|
+
if (callee?.kind === "function") {
|
|
837
|
+
const result = callee.invoke(
|
|
838
|
+
values,
|
|
839
|
+
guards,
|
|
840
|
+
activeSides || sides,
|
|
841
|
+
receiver,
|
|
842
|
+
);
|
|
843
|
+
memo.set(scope.id, result);
|
|
844
|
+
return result;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
if (
|
|
848
|
+
calls.has(node.type) &&
|
|
849
|
+
members.has(node.callee?.type) &&
|
|
850
|
+
keyFor(node.callee, scope, guards) === "get" &&
|
|
851
|
+
resolve(node.callee.object, scope, guards)?.kind === "context" &&
|
|
852
|
+
constant(node.arguments[0], scope, guards) !== null
|
|
853
|
+
) {
|
|
854
|
+
const context = resolve(node.callee.object, scope, guards);
|
|
855
|
+
const value = {
|
|
856
|
+
...serviceFor(
|
|
857
|
+
context,
|
|
858
|
+
constant(node.arguments[0], scope, guards),
|
|
859
|
+
"ctx-get",
|
|
860
|
+
),
|
|
861
|
+
optional:
|
|
862
|
+
node.arguments[1]?.value === false ||
|
|
863
|
+
node.arguments[1]?.value === 0,
|
|
864
|
+
};
|
|
865
|
+
callResults.get(node)?.set(scope.id, value);
|
|
866
|
+
return value;
|
|
867
|
+
}
|
|
868
|
+
return null;
|
|
869
|
+
};
|
|
870
|
+
const bind = (pattern, value, scope, from) => {
|
|
871
|
+
if (pattern?.type === "AssignmentPattern")
|
|
872
|
+
return bind(pattern.left, value, scope, from);
|
|
873
|
+
if (pattern?.type === "Identifier") {
|
|
874
|
+
put(scope, pattern.name, value);
|
|
875
|
+
if (value?.kind === "context") heads.add(pattern.name);
|
|
876
|
+
if (value?.kind === "service" || value?.kind === "method") {
|
|
877
|
+
mention(value.service);
|
|
878
|
+
bindings.push({
|
|
879
|
+
variable: pattern.name,
|
|
880
|
+
file,
|
|
881
|
+
scopeId: scope.id,
|
|
882
|
+
start: pattern.start,
|
|
883
|
+
service: value.service,
|
|
884
|
+
form:
|
|
885
|
+
value.form ||
|
|
886
|
+
(value.kind === "method" ? "ctx-get-destructure" : "alias"),
|
|
887
|
+
...(from ? { from } : {}),
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
if (pattern?.type === "ObjectPattern") {
|
|
893
|
+
for (const p of pattern.properties) {
|
|
894
|
+
if (p.type !== "ObjectProperty") continue;
|
|
895
|
+
const key = p.computed ? constant(p.key, scope) : keyName(p);
|
|
896
|
+
if (key === null || key === undefined) {
|
|
897
|
+
if (hasEvidence(value))
|
|
898
|
+
diagnose(
|
|
899
|
+
"flow-dynamic-pattern",
|
|
900
|
+
file,
|
|
901
|
+
p,
|
|
902
|
+
"Destructured key cannot establish one service",
|
|
903
|
+
);
|
|
904
|
+
bind(p.value, null, scope);
|
|
905
|
+
continue;
|
|
906
|
+
}
|
|
907
|
+
const child = readField(value, key, p);
|
|
908
|
+
if (child?.kind === "method") child.form = "ctx-get-destructure";
|
|
909
|
+
bind(p.value, child, scope);
|
|
910
|
+
}
|
|
911
|
+
} else for (const name of namesIn(pattern)) put(scope, name, null);
|
|
912
|
+
};
|
|
913
|
+
const predeclare = (body, scope) => {
|
|
914
|
+
for (const entry of body) {
|
|
915
|
+
const n = entry.declaration || entry;
|
|
916
|
+
if (n.type === "VariableDeclaration")
|
|
917
|
+
for (const d of n.declarations)
|
|
918
|
+
for (const name of namesIn(d.id)) {
|
|
919
|
+
const owner = n.kind === "var" ? scope.functionScope() : scope;
|
|
920
|
+
if (n.kind !== "var" || !owner.values.has(name))
|
|
921
|
+
put(owner, name, null);
|
|
922
|
+
}
|
|
923
|
+
if (n.type === "FunctionDeclaration" && n.id)
|
|
924
|
+
put(scope, n.id.name, fnValue(n, scope));
|
|
925
|
+
if (n.type === "ClassDeclaration" && n.id) put(scope, n.id.name, null);
|
|
926
|
+
if (n.type === "ImportDeclaration")
|
|
927
|
+
for (const s of n.specifiers)
|
|
928
|
+
put(
|
|
929
|
+
scope,
|
|
930
|
+
s.local.name,
|
|
931
|
+
imported(
|
|
932
|
+
n.source.value,
|
|
933
|
+
s.type === "ImportDefaultSpecifier"
|
|
934
|
+
? "default"
|
|
935
|
+
: s.type === "ImportNamespaceSpecifier"
|
|
936
|
+
? "*"
|
|
937
|
+
: (s.imported.name ?? s.imported.value),
|
|
938
|
+
),
|
|
939
|
+
);
|
|
940
|
+
}
|
|
941
|
+
};
|
|
942
|
+
const methodKey = (n, scope) => {
|
|
943
|
+
const r = resolve(n, scope);
|
|
944
|
+
return r?.kind === "method" ? `${r.service}.${r.method}` : null;
|
|
945
|
+
};
|
|
946
|
+
// Only a helper whose boolean result exactly follows the capability test is accepted.
|
|
947
|
+
const predicateFor = (fn, definitionScope, values) => {
|
|
948
|
+
if (!fn || fn.async || fn.generator || fn.body?.type !== "BlockStatement")
|
|
949
|
+
return null;
|
|
950
|
+
const body = fn.body.body;
|
|
951
|
+
let condition = null;
|
|
952
|
+
if (body.length === 1 && body[0].type === "ReturnStatement")
|
|
953
|
+
condition = body[0].argument;
|
|
954
|
+
else if (
|
|
955
|
+
body[0]?.type === "IfStatement" &&
|
|
956
|
+
body[0].consequent.type === "ReturnStatement" &&
|
|
957
|
+
body[0].consequent.argument?.value === true &&
|
|
958
|
+
body.at(-1)?.type === "ReturnStatement" &&
|
|
959
|
+
body.at(-1).argument?.value === false &&
|
|
960
|
+
body.slice(1, -1).every((s) => s.type === "ExpressionStatement")
|
|
961
|
+
)
|
|
962
|
+
condition = body[0].test;
|
|
963
|
+
if (
|
|
964
|
+
!condition ||
|
|
965
|
+
condition.type !== "BinaryExpression" ||
|
|
966
|
+
!["===", "=="].includes(condition.operator)
|
|
967
|
+
)
|
|
968
|
+
return null;
|
|
969
|
+
const target =
|
|
970
|
+
condition.left.type === "UnaryExpression" &&
|
|
971
|
+
condition.left.operator === "typeof" &&
|
|
972
|
+
condition.right.value === "function"
|
|
973
|
+
? condition.left.argument
|
|
974
|
+
: condition.right.type === "UnaryExpression" &&
|
|
975
|
+
condition.right.operator === "typeof" &&
|
|
976
|
+
condition.left.value === "function"
|
|
977
|
+
? condition.right.argument
|
|
978
|
+
: null;
|
|
979
|
+
if (!members.has(target?.type) || target.object.type !== "Identifier")
|
|
980
|
+
return null;
|
|
981
|
+
const parameters = new Scope(definitionScope, ++scopeId, true);
|
|
982
|
+
for (const [i, p] of fn.params.entries())
|
|
983
|
+
bind(p, values[i] || null, parameters);
|
|
984
|
+
const resolved = resolve(target, parameters);
|
|
985
|
+
return resolved?.kind === "method"
|
|
986
|
+
? `${resolved.service}.${resolved.method}`
|
|
987
|
+
: null;
|
|
988
|
+
};
|
|
989
|
+
const helperCondition = (call, scope) => {
|
|
990
|
+
if (!calls.has(call?.type)) return null;
|
|
991
|
+
let fn = resolve(call.callee, scope);
|
|
992
|
+
if (fn?.kind === "namespace") fn = lookupExport(fn.target, "default");
|
|
993
|
+
return (
|
|
994
|
+
fn?.guardFor?.(call.arguments.map((n) => resolve(n, scope))) || null
|
|
995
|
+
);
|
|
996
|
+
};
|
|
997
|
+
const conditionGuards = (node, scope, truth = true) => {
|
|
998
|
+
node = unwrap(node);
|
|
999
|
+
const out = new Set();
|
|
1000
|
+
if (!node) return out;
|
|
1001
|
+
if (node.type === "UnaryExpression" && node.operator === "!")
|
|
1002
|
+
return conditionGuards(node.argument, scope, !truth);
|
|
1003
|
+
if (
|
|
1004
|
+
node.type === "LogicalExpression" &&
|
|
1005
|
+
((truth && node.operator === "&&") ||
|
|
1006
|
+
(!truth && node.operator === "||"))
|
|
1007
|
+
) {
|
|
1008
|
+
for (const k of conditionGuards(node.left, scope, truth)) out.add(k);
|
|
1009
|
+
for (const k of conditionGuards(node.right, scope, truth)) out.add(k);
|
|
1010
|
+
}
|
|
1011
|
+
if (node.type === "BinaryExpression") {
|
|
1012
|
+
const equals = ["===", "=="].includes(node.operator),
|
|
1013
|
+
differs = ["!==", "!="].includes(node.operator);
|
|
1014
|
+
if ((truth && equals) || (!truth && differs)) {
|
|
1015
|
+
const target =
|
|
1016
|
+
node.left.type === "UnaryExpression" &&
|
|
1017
|
+
node.left.operator === "typeof" &&
|
|
1018
|
+
node.right.value === "function"
|
|
1019
|
+
? node.left.argument
|
|
1020
|
+
: node.right.type === "UnaryExpression" &&
|
|
1021
|
+
node.right.operator === "typeof" &&
|
|
1022
|
+
node.left.value === "function"
|
|
1023
|
+
? node.right.argument
|
|
1024
|
+
: null;
|
|
1025
|
+
const key = methodKey(target, scope);
|
|
1026
|
+
if (key) out.add(key);
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
if (truth) {
|
|
1030
|
+
const key = helperCondition(node, scope);
|
|
1031
|
+
if (key) out.add(key);
|
|
1032
|
+
}
|
|
1033
|
+
return out;
|
|
1034
|
+
};
|
|
1035
|
+
const merged = (a, b) => guardSet([...a, ...b]);
|
|
1036
|
+
const exits = (n) =>
|
|
1037
|
+
n?.type === "ReturnStatement" ||
|
|
1038
|
+
n?.type === "ThrowStatement" ||
|
|
1039
|
+
(n?.type === "IfStatement" &&
|
|
1040
|
+
exits(n.consequent) &&
|
|
1041
|
+
exits(n.alternate)) ||
|
|
1042
|
+
(n?.type === "BlockStatement" &&
|
|
1043
|
+
n.body.length > 0 &&
|
|
1044
|
+
exits(n.body.at(-1)));
|
|
1045
|
+
const statementList = (body, scope, guards, depth) => {
|
|
1046
|
+
predeclare(body, scope);
|
|
1047
|
+
let active = guardSet(guards);
|
|
1048
|
+
for (const n of body) {
|
|
1049
|
+
visit(n, scope, active, depth + 1);
|
|
1050
|
+
if (
|
|
1051
|
+
exits(n) ||
|
|
1052
|
+
["BreakStatement", "ContinueStatement"].includes(n.type)
|
|
1053
|
+
)
|
|
1054
|
+
break;
|
|
1055
|
+
if (n.type === "IfStatement" && !n.alternate && exits(n.consequent))
|
|
1056
|
+
active = merged(active, conditionGuards(n.test, scope, false));
|
|
1057
|
+
}
|
|
1058
|
+
};
|
|
1059
|
+
const recordCall = (node, scope, guards) => {
|
|
1060
|
+
const value = resolve(node.callee, scope, guards);
|
|
1061
|
+
if (value?.kind !== "method") return;
|
|
1062
|
+
mention(value.service);
|
|
1063
|
+
addressable.add(value.service);
|
|
1064
|
+
declared.add(value.service);
|
|
1065
|
+
const receiver = members.has(node.callee.type)
|
|
1066
|
+
? node.callee.object
|
|
1067
|
+
: node.callee;
|
|
1068
|
+
const isAlias =
|
|
1069
|
+
receiver.type === "Identifier" && value.receiver?.kind !== "context";
|
|
1070
|
+
const via = isAlias ? "alias" : "ctx-member";
|
|
1071
|
+
const matched = isAlias
|
|
1072
|
+
? receiver.name
|
|
1073
|
+
: text.slice(receiver.start, receiver.end);
|
|
1074
|
+
const guarded =
|
|
1075
|
+
node.optional === true ||
|
|
1076
|
+
guards.has(`${value.service}.${value.method}`);
|
|
1077
|
+
if (!callMap.has(value.service)) callMap.set(value.service, new Set());
|
|
1078
|
+
callMap.get(value.service).add(value.method);
|
|
1079
|
+
sites.push({
|
|
1080
|
+
service: value.service,
|
|
1081
|
+
method: value.method,
|
|
1082
|
+
plugin: pkgName,
|
|
1083
|
+
file,
|
|
1084
|
+
line: node.loc.start.line,
|
|
1085
|
+
column: node.loc.start.column,
|
|
1086
|
+
start: node.start,
|
|
1087
|
+
end: node.end,
|
|
1088
|
+
via,
|
|
1089
|
+
matched,
|
|
1090
|
+
guarded,
|
|
1091
|
+
capabilityGuarded: guards.has(`${value.service}.${value.method}`),
|
|
1092
|
+
guardKind: node.optional
|
|
1093
|
+
? "optional-method"
|
|
1094
|
+
: guarded
|
|
1095
|
+
? "controlling-capability-check"
|
|
1096
|
+
: "none",
|
|
1097
|
+
evidenceSource: "ast",
|
|
1098
|
+
inferredReceiver: value.receiver?.inferred === true,
|
|
1099
|
+
sides: [...(activeSides || sides)],
|
|
1100
|
+
});
|
|
1101
|
+
};
|
|
1102
|
+
const visitFunction = (
|
|
1103
|
+
node,
|
|
1104
|
+
scope,
|
|
1105
|
+
guards,
|
|
1106
|
+
depth,
|
|
1107
|
+
contextParams,
|
|
1108
|
+
argumentsForCall = null,
|
|
1109
|
+
receiverForCall = null,
|
|
1110
|
+
) => {
|
|
1111
|
+
const entryBody =
|
|
1112
|
+
(scope === root &&
|
|
1113
|
+
node.id &&
|
|
1114
|
+
isEntry &&
|
|
1115
|
+
applyNames.has(node.id.name)) ||
|
|
1116
|
+
contextParams?.size > 0;
|
|
1117
|
+
if (
|
|
1118
|
+
argumentsForCall === null &&
|
|
1119
|
+
concreteFunctions.has(file + ":" + node.start)
|
|
1120
|
+
)
|
|
1121
|
+
return null;
|
|
1122
|
+
if (
|
|
1123
|
+
argumentsForCall === null &&
|
|
1124
|
+
(module.initializing || (!entryBody && currentDeferred !== node))
|
|
1125
|
+
) {
|
|
1126
|
+
const queuedSides = activeSides;
|
|
1127
|
+
module.deferred.push({
|
|
1128
|
+
node,
|
|
1129
|
+
entry: !!entryBody,
|
|
1130
|
+
run: () => {
|
|
1131
|
+
const before = activeSides;
|
|
1132
|
+
activeSides = queuedSides;
|
|
1133
|
+
try {
|
|
1134
|
+
return visitFunction(node, scope, guards, depth, contextParams);
|
|
1135
|
+
} finally {
|
|
1136
|
+
activeSides = before;
|
|
1137
|
+
}
|
|
1138
|
+
},
|
|
1139
|
+
});
|
|
1140
|
+
return null;
|
|
1141
|
+
}
|
|
1142
|
+
// Only synchronous, directly resolved calls can inherit controlling guards.
|
|
1143
|
+
if (argumentsForCall === null || node.async || node.generator)
|
|
1144
|
+
guards = guardSet();
|
|
1145
|
+
// Inspect a deferred function without applying its assignments to callers.
|
|
1146
|
+
const journal = argumentsForCall === null ? newJournal() : null;
|
|
1147
|
+
if (journal) journals.push(journal);
|
|
1148
|
+
const child = new Scope(scope, ++scopeId, true);
|
|
1149
|
+
child.receiver =
|
|
1150
|
+
node.type === "ArrowFunctionExpression"
|
|
1151
|
+
? scope.functionScope().receiver
|
|
1152
|
+
: receiverForCall;
|
|
1153
|
+
if (node.id) put(child, node.id.name, fnValue(node, scope));
|
|
1154
|
+
const provenApply =
|
|
1155
|
+
scope === root && node.id && isEntry && applyNames.has(node.id.name);
|
|
1156
|
+
const parameterFacts =
|
|
1157
|
+
argumentsForCall ??
|
|
1158
|
+
knownParameters.get(file + ":" + node.start) ??
|
|
1159
|
+
null;
|
|
1160
|
+
if (
|
|
1161
|
+
parameterFacts !== null &&
|
|
1162
|
+
node.params.some(
|
|
1163
|
+
(p) => p.type === "AssignmentPattern" || p.type === "RestElement",
|
|
1164
|
+
)
|
|
1165
|
+
)
|
|
1166
|
+
diagnose(
|
|
1167
|
+
"flow-parameter-pattern",
|
|
1168
|
+
file,
|
|
1169
|
+
node,
|
|
1170
|
+
"Default/rest parameter flow is not expanded",
|
|
1171
|
+
);
|
|
1172
|
+
for (const [index, p] of (node.params || []).entries()) {
|
|
1173
|
+
const known =
|
|
1174
|
+
contextParams?.has(p.start) || (provenApply && index === 0);
|
|
1175
|
+
const inferred = known ? contextParams?.get?.(p.start) === true : true;
|
|
1176
|
+
bind(
|
|
1177
|
+
p,
|
|
1178
|
+
parameterFacts !== null
|
|
1179
|
+
? parameterFacts[index] || null
|
|
1180
|
+
: known || (p.type === "Identifier" && p.name === "ctx")
|
|
1181
|
+
? { kind: "context", head: namesIn(p)[0], inferred }
|
|
1182
|
+
: null,
|
|
1183
|
+
child,
|
|
1184
|
+
);
|
|
1185
|
+
}
|
|
1186
|
+
const frame = {
|
|
1187
|
+
returns: [],
|
|
1188
|
+
guards,
|
|
1189
|
+
executing:
|
|
1190
|
+
argumentsForCall !== null
|
|
1191
|
+
? (frames.at(-1)?.executing ?? module.initializing)
|
|
1192
|
+
: !!entryBody,
|
|
1193
|
+
};
|
|
1194
|
+
frames.push(frame);
|
|
1195
|
+
try {
|
|
1196
|
+
if (node.body.type === "BlockStatement")
|
|
1197
|
+
statementList(node.body.body, child, guards, depth + 1);
|
|
1198
|
+
else {
|
|
1199
|
+
visit(node.body, child, guards, depth + 1);
|
|
1200
|
+
frame.returns.push(resolve(node.body, child, guards));
|
|
1201
|
+
}
|
|
1202
|
+
if (node.body.type === "BlockStatement" && !exits(node.body))
|
|
1203
|
+
frame.returns.push(null);
|
|
1204
|
+
const result = join(frame.returns, node);
|
|
1205
|
+
if (
|
|
1206
|
+
argumentsForCall !== null &&
|
|
1207
|
+
!result &&
|
|
1208
|
+
frame.returns.some((v) => hasEvidence(v))
|
|
1209
|
+
)
|
|
1210
|
+
diagnose(
|
|
1211
|
+
"flow-ambiguous-return",
|
|
1212
|
+
file,
|
|
1213
|
+
node,
|
|
1214
|
+
"Function returns do not establish one receiver",
|
|
1215
|
+
);
|
|
1216
|
+
return result;
|
|
1217
|
+
} finally {
|
|
1218
|
+
frames.pop();
|
|
1219
|
+
if (journal) {
|
|
1220
|
+
journals.pop();
|
|
1221
|
+
undo(journal);
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
};
|
|
1225
|
+
const branch = (scope, branches, depth) => {
|
|
1226
|
+
const mutations = [],
|
|
1227
|
+
bindings = [],
|
|
1228
|
+
originals = new Map(),
|
|
1229
|
+
originalBindings = new Map();
|
|
1230
|
+
for (const [node, guards, after] of branches) {
|
|
1231
|
+
const journal = newJournal();
|
|
1232
|
+
journals.push(journal);
|
|
1233
|
+
try {
|
|
1234
|
+
visit(node, scope, guards, depth + 1);
|
|
1235
|
+
visit(after, scope, guards, depth + 1);
|
|
1236
|
+
mutations.push(
|
|
1237
|
+
new Map(
|
|
1238
|
+
[...journal.objects].map(([object]) => [
|
|
1239
|
+
object,
|
|
1240
|
+
{ fields: object.fields, opaque: object.opaque },
|
|
1241
|
+
]),
|
|
1242
|
+
),
|
|
1243
|
+
);
|
|
1244
|
+
bindings.push(
|
|
1245
|
+
new Map(
|
|
1246
|
+
[...journal.bindings].map(([owner, keys]) => [
|
|
1247
|
+
owner,
|
|
1248
|
+
new Map(
|
|
1249
|
+
[...keys].map(([key]) => [
|
|
1250
|
+
key,
|
|
1251
|
+
{
|
|
1252
|
+
present: owner.values.has(key),
|
|
1253
|
+
value: owner.values.get(key),
|
|
1254
|
+
},
|
|
1255
|
+
]),
|
|
1256
|
+
),
|
|
1257
|
+
]),
|
|
1258
|
+
),
|
|
1259
|
+
);
|
|
1260
|
+
} finally {
|
|
1261
|
+
journals.pop();
|
|
1262
|
+
for (const [object, state] of journal.objects)
|
|
1263
|
+
if (!originals.has(object)) originals.set(object, state);
|
|
1264
|
+
for (const [owner, keys] of journal.bindings) {
|
|
1265
|
+
if (!originalBindings.has(owner))
|
|
1266
|
+
originalBindings.set(owner, new Map());
|
|
1267
|
+
for (const [key, value] of keys)
|
|
1268
|
+
if (!originalBindings.get(owner).has(key))
|
|
1269
|
+
originalBindings.get(owner).set(key, value);
|
|
1270
|
+
}
|
|
1271
|
+
undo(journal);
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
for (const [object, original] of originals) {
|
|
1275
|
+
const variants = mutations.map((m) => m.get(object) || original),
|
|
1276
|
+
keys = new Set(variants.flatMap((v) => [...v.fields.keys()])),
|
|
1277
|
+
fields = new Map();
|
|
1278
|
+
for (const key of keys) {
|
|
1279
|
+
const values = variants.map((v) => v.fields.get(key) || null),
|
|
1280
|
+
value = join(values, branches[0][0]);
|
|
1281
|
+
fields.set(key, value);
|
|
1282
|
+
if (!value && values.some((v) => hasEvidence(v)))
|
|
1283
|
+
diagnose(
|
|
1284
|
+
"flow-ambiguous-binding",
|
|
1285
|
+
file,
|
|
1286
|
+
branches[0][0],
|
|
1287
|
+
"Object fields differ between branches",
|
|
1288
|
+
);
|
|
1289
|
+
}
|
|
1290
|
+
noteMutation(object);
|
|
1291
|
+
object.fields = fields;
|
|
1292
|
+
object.opaque = variants.some((v) => v.opaque);
|
|
1293
|
+
}
|
|
1294
|
+
for (const [owner, keys] of originalBindings)
|
|
1295
|
+
for (const [key, before] of keys) {
|
|
1296
|
+
const variants = bindings.map(
|
|
1297
|
+
(m) => m.get(owner)?.get(key) || before,
|
|
1298
|
+
),
|
|
1299
|
+
values = variants.map((v) => (v.present ? v.value : null)),
|
|
1300
|
+
value = join(values, branches[0][0]);
|
|
1301
|
+
if (!value && values.some((v) => hasEvidence(v)))
|
|
1302
|
+
diagnose(
|
|
1303
|
+
"flow-ambiguous-binding",
|
|
1304
|
+
file,
|
|
1305
|
+
branches[0][0],
|
|
1306
|
+
"Branch assignments do not prove one receiver",
|
|
1307
|
+
);
|
|
1308
|
+
put(owner, key, value);
|
|
1309
|
+
}
|
|
1310
|
+
};
|
|
1311
|
+
|
|
1312
|
+
const visit = (node, scope, guards, depth = 0) => {
|
|
1313
|
+
if (!node) return;
|
|
1314
|
+
if (depth > 512) {
|
|
1315
|
+
diagnostics.push({
|
|
1316
|
+
kind: "analysis-depth-limit",
|
|
1317
|
+
file,
|
|
1318
|
+
line: node.loc?.start.line,
|
|
1319
|
+
});
|
|
1320
|
+
return;
|
|
1321
|
+
}
|
|
1322
|
+
if (node.type === "ReturnStatement") {
|
|
1323
|
+
visit(node.argument, scope, guards, depth + 1);
|
|
1324
|
+
frames.at(-1)?.returns.push(resolve(node.argument, scope, guards));
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
1327
|
+
if (node.type === "Program")
|
|
1328
|
+
return statementList(node.body, scope, guards, depth);
|
|
1329
|
+
if (node.type === "BlockStatement")
|
|
1330
|
+
return statementList(
|
|
1331
|
+
node.body,
|
|
1332
|
+
new Scope(scope, ++scopeId),
|
|
1333
|
+
guards,
|
|
1334
|
+
depth,
|
|
1335
|
+
);
|
|
1336
|
+
if (functions.has(node.type))
|
|
1337
|
+
return visitFunction(node, scope, guards, depth);
|
|
1338
|
+
if (node.type === "VariableDeclaration") {
|
|
1339
|
+
for (const d of node.declarations) {
|
|
1340
|
+
declarationOwners.set(
|
|
1341
|
+
d.start,
|
|
1342
|
+
node.kind === "var" ? scope.functionScope() : scope,
|
|
1343
|
+
);
|
|
1344
|
+
visit(d, scope, guards, depth + 1);
|
|
1345
|
+
}
|
|
1346
|
+
return;
|
|
1347
|
+
}
|
|
1348
|
+
if (node.type === "VariableDeclarator") {
|
|
1349
|
+
if (
|
|
1350
|
+
isEntry &&
|
|
1351
|
+
scope === root &&
|
|
1352
|
+
node.id.type === "Identifier" &&
|
|
1353
|
+
applyNames.has(node.id.name) &&
|
|
1354
|
+
functions.has(node.init?.type)
|
|
1355
|
+
)
|
|
1356
|
+
visitFunction(
|
|
1357
|
+
node.init,
|
|
1358
|
+
scope,
|
|
1359
|
+
guards,
|
|
1360
|
+
depth + 1,
|
|
1361
|
+
new Map(
|
|
1362
|
+
node.init.params[0] ? [[node.init.params[0].start, false]] : [],
|
|
1363
|
+
),
|
|
1364
|
+
);
|
|
1365
|
+
else visit(node.init, scope, guards, depth + 1);
|
|
1366
|
+
const value = resolve(node.init, scope, guards);
|
|
1367
|
+
bind(
|
|
1368
|
+
node.id,
|
|
1369
|
+
value,
|
|
1370
|
+
declarationOwners.get(node.start) || scope,
|
|
1371
|
+
node.init?.type === "Identifier" ? node.init.name : null,
|
|
1372
|
+
);
|
|
1373
|
+
if (node.id.type === "Identifier") {
|
|
1374
|
+
const field = node.id.name;
|
|
1375
|
+
if (injectNames.has(field) || /^inject(?:Deps|Scope|s)?$/.test(field))
|
|
1376
|
+
for (const s of literalNames(node.init, scope))
|
|
1377
|
+
dependency(
|
|
1378
|
+
s,
|
|
1379
|
+
scope === root && isEntry && injectNames.has(field)
|
|
1380
|
+
? "required-inject"
|
|
1381
|
+
: "descriptor-inject",
|
|
1382
|
+
node,
|
|
1383
|
+
);
|
|
1384
|
+
if (provideNames.has(field))
|
|
1385
|
+
for (const s of literalNames(node.init, scope)) {
|
|
1386
|
+
provided.add(s);
|
|
1387
|
+
mention(s);
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
return;
|
|
1391
|
+
}
|
|
1392
|
+
const invalidate = (target) => {
|
|
1393
|
+
const value = resolve(target, scope, guards),
|
|
1394
|
+
overwritten = methodKey(target, scope);
|
|
1395
|
+
for (const set of guardSets)
|
|
1396
|
+
for (const key of [...set])
|
|
1397
|
+
if (
|
|
1398
|
+
overwritten === key ||
|
|
1399
|
+
(value?.kind === "service" && key.startsWith(value.service + "."))
|
|
1400
|
+
) {
|
|
1401
|
+
set.delete(key);
|
|
1402
|
+
if (!set.size) guardSets.delete(set);
|
|
1403
|
+
}
|
|
1404
|
+
};
|
|
1405
|
+
const targetOf = (target) => {
|
|
1406
|
+
if (target.type === "Identifier")
|
|
1407
|
+
return {
|
|
1408
|
+
owner: scope.find(target.name) || scope,
|
|
1409
|
+
key: target.name,
|
|
1410
|
+
old: scope.get(target.name),
|
|
1411
|
+
};
|
|
1412
|
+
if (members.has(target.type)) {
|
|
1413
|
+
visit(target.object, scope, guards, depth + 1);
|
|
1414
|
+
if (target.computed) visit(target.property, scope, guards, depth + 1);
|
|
1415
|
+
const object = transparent(
|
|
1416
|
+
resolve(target.object, scope, guards),
|
|
1417
|
+
target,
|
|
1418
|
+
),
|
|
1419
|
+
key = keyFor(target, scope, guards);
|
|
1420
|
+
return {
|
|
1421
|
+
object,
|
|
1422
|
+
key,
|
|
1423
|
+
old: key === null ? null : readField(object, key, target),
|
|
1424
|
+
};
|
|
1425
|
+
}
|
|
1426
|
+
return {};
|
|
1427
|
+
};
|
|
1428
|
+
const writeTarget = (target, slot, value) => {
|
|
1429
|
+
invalidate(target);
|
|
1430
|
+
if (slot.owner) return bind(target, value, slot.owner);
|
|
1431
|
+
const state = stateFor(slot.object);
|
|
1432
|
+
if (!state) return;
|
|
1433
|
+
if (slot.key === null || slot.key === undefined) {
|
|
1434
|
+
taint(slot.object);
|
|
1435
|
+
diagnose(
|
|
1436
|
+
"flow-dynamic-mutation",
|
|
1437
|
+
file,
|
|
1438
|
+
node,
|
|
1439
|
+
"Unknown write key may replace existing fields",
|
|
1440
|
+
);
|
|
1441
|
+
} else {
|
|
1442
|
+
writeField(state, slot.key, value);
|
|
1443
|
+
if (["context", "service"].includes(slot.object.kind))
|
|
1444
|
+
diagnose(
|
|
1445
|
+
"flow-service-mutation",
|
|
1446
|
+
file,
|
|
1447
|
+
node,
|
|
1448
|
+
"Context/service member was replaced locally",
|
|
1449
|
+
);
|
|
1450
|
+
}
|
|
1451
|
+
};
|
|
1452
|
+
if (node.type === "SequenceExpression") {
|
|
1453
|
+
if (hasEffect(node, scope)) return;
|
|
1454
|
+
remember(node, scope, null);
|
|
1455
|
+
let value = null;
|
|
1456
|
+
for (const expression of node.expressions) {
|
|
1457
|
+
visit(expression, scope, guards, depth + 1);
|
|
1458
|
+
value = resolve(expression, scope, guards);
|
|
1459
|
+
}
|
|
1460
|
+
remember(node, scope, value);
|
|
1461
|
+
return;
|
|
1462
|
+
}
|
|
1463
|
+
if (node.type === "AwaitExpression") {
|
|
1464
|
+
if (hasEffect(node, scope)) return;
|
|
1465
|
+
remember(node, scope, null);
|
|
1466
|
+
visit(node.argument, scope, guards, depth + 1);
|
|
1467
|
+
const value = resolve(node.argument, scope, guards);
|
|
1468
|
+
if (value?.kind === "promise" && hasEvidence(value.value))
|
|
1469
|
+
diagnose(
|
|
1470
|
+
"flow-async-return",
|
|
1471
|
+
file,
|
|
1472
|
+
node,
|
|
1473
|
+
"Awaited factory value is not expanded",
|
|
1474
|
+
);
|
|
1475
|
+
for (const set of guardSets) set.clear();
|
|
1476
|
+
guardSets.clear();
|
|
1477
|
+
return;
|
|
1478
|
+
}
|
|
1479
|
+
if (node.type === "UnaryExpression" && node.operator === "delete") {
|
|
1480
|
+
if (hasEffect(node, scope)) return;
|
|
1481
|
+
remember(node, scope, null);
|
|
1482
|
+
const slot = targetOf(node.argument);
|
|
1483
|
+
writeTarget(node.argument, slot, null);
|
|
1484
|
+
return;
|
|
1485
|
+
}
|
|
1486
|
+
if (
|
|
1487
|
+
node.type === "AssignmentExpression" ||
|
|
1488
|
+
node.type === "UpdateExpression"
|
|
1489
|
+
) {
|
|
1490
|
+
if (hasEffect(node, scope)) return;
|
|
1491
|
+
remember(node, scope, null);
|
|
1492
|
+
const target =
|
|
1493
|
+
node.type === "AssignmentExpression" ? node.left : node.argument;
|
|
1494
|
+
const slot = targetOf(target);
|
|
1495
|
+
let value = null;
|
|
1496
|
+
if (node.type === "AssignmentExpression") {
|
|
1497
|
+
visit(node.right, scope, guards, depth + 1);
|
|
1498
|
+
const right = resolve(node.right, scope, guards);
|
|
1499
|
+
if (node.operator === "=") value = right;
|
|
1500
|
+
else if (
|
|
1501
|
+
node.operator === "+=" &&
|
|
1502
|
+
slot.old?.kind === "constant" &&
|
|
1503
|
+
right?.kind === "constant"
|
|
1504
|
+
) {
|
|
1505
|
+
const result = slot.old.value + right.value;
|
|
1506
|
+
if (typeof result !== "string" || result.length <= 8192)
|
|
1507
|
+
value = { kind: "constant", value: result };
|
|
1508
|
+
}
|
|
1509
|
+
} else if (slot.old?.kind === "constant")
|
|
1510
|
+
value = {
|
|
1511
|
+
kind: "constant",
|
|
1512
|
+
value: Number(slot.old.value) + (node.operator === "++" ? 1 : -1),
|
|
1513
|
+
};
|
|
1514
|
+
if (!value && node.operator !== "=")
|
|
1515
|
+
diagnose(
|
|
1516
|
+
"flow-mutation-value",
|
|
1517
|
+
file,
|
|
1518
|
+
node,
|
|
1519
|
+
"Mutation does not establish a constant value",
|
|
1520
|
+
);
|
|
1521
|
+
writeTarget(target, slot, value);
|
|
1522
|
+
remember(
|
|
1523
|
+
node,
|
|
1524
|
+
scope,
|
|
1525
|
+
node.type === "UpdateExpression" && !node.prefix ? slot.old : value,
|
|
1526
|
+
);
|
|
1527
|
+
if (node.type === "AssignmentExpression" && slot.key === "inject")
|
|
1528
|
+
for (const name of literalNames(node.right, scope))
|
|
1529
|
+
dependency(name, "descriptor-inject", node);
|
|
1530
|
+
if (node.type === "AssignmentExpression" && slot.key === "provide")
|
|
1531
|
+
for (const name of literalNames(node.right, scope)) {
|
|
1532
|
+
provided.add(name);
|
|
1533
|
+
mention(name);
|
|
1534
|
+
}
|
|
1535
|
+
return;
|
|
1536
|
+
}
|
|
1537
|
+
if (node.type === "ObjectProperty" && keyName(node) === "inject")
|
|
1538
|
+
for (const s of literalNames(node.value, scope))
|
|
1539
|
+
dependency(s, "descriptor-inject", node);
|
|
1540
|
+
if (node.type === "CatchClause") {
|
|
1541
|
+
const caught = new Scope(scope, ++scopeId);
|
|
1542
|
+
bind(node.param, null, caught);
|
|
1543
|
+
visit(node.body, caught, guards, depth + 1);
|
|
1544
|
+
return;
|
|
1545
|
+
}
|
|
1546
|
+
if (node.type === "SwitchStatement") {
|
|
1547
|
+
visit(node.discriminant, scope, guards, depth + 1);
|
|
1548
|
+
diagnose(
|
|
1549
|
+
"flow-control-join",
|
|
1550
|
+
file,
|
|
1551
|
+
node,
|
|
1552
|
+
"Switch paths are conservatively joined",
|
|
1553
|
+
);
|
|
1554
|
+
const child = new Scope(scope, ++scopeId);
|
|
1555
|
+
predeclare(
|
|
1556
|
+
node.cases.flatMap((c) => c.consequent),
|
|
1557
|
+
child,
|
|
1558
|
+
);
|
|
1559
|
+
for (const c of node.cases)
|
|
1560
|
+
if (c.test)
|
|
1561
|
+
branch(
|
|
1562
|
+
child,
|
|
1563
|
+
[
|
|
1564
|
+
[c.test, guardSet(guards)],
|
|
1565
|
+
[null, guardSet(guards)],
|
|
1566
|
+
],
|
|
1567
|
+
depth,
|
|
1568
|
+
);
|
|
1569
|
+
branch(
|
|
1570
|
+
child,
|
|
1571
|
+
node.cases
|
|
1572
|
+
.map((c, index) => [
|
|
1573
|
+
{
|
|
1574
|
+
type: "BlockStatement",
|
|
1575
|
+
body: node.cases.slice(index).flatMap((c) => c.consequent),
|
|
1576
|
+
loc: c.loc,
|
|
1577
|
+
start: c.start,
|
|
1578
|
+
},
|
|
1579
|
+
guardSet(guards),
|
|
1580
|
+
])
|
|
1581
|
+
.concat([[null, guardSet(guards)]]),
|
|
1582
|
+
depth,
|
|
1583
|
+
);
|
|
1584
|
+
return;
|
|
1585
|
+
}
|
|
1586
|
+
if (node.type === "TryStatement") {
|
|
1587
|
+
diagnose(
|
|
1588
|
+
"flow-control-join",
|
|
1589
|
+
file,
|
|
1590
|
+
node,
|
|
1591
|
+
"Exception paths are conservatively joined",
|
|
1592
|
+
);
|
|
1593
|
+
// Collect and join writes before a catch reads them: an exception can
|
|
1594
|
+
// occur before or after any assignment in the try block.
|
|
1595
|
+
branch(
|
|
1596
|
+
scope,
|
|
1597
|
+
[
|
|
1598
|
+
[node.block, guardSet(guards)],
|
|
1599
|
+
[null, guardSet(guards)],
|
|
1600
|
+
],
|
|
1601
|
+
depth,
|
|
1602
|
+
);
|
|
1603
|
+
if (node.handler)
|
|
1604
|
+
branch(
|
|
1605
|
+
scope,
|
|
1606
|
+
[
|
|
1607
|
+
[node.handler, guardSet(guards)],
|
|
1608
|
+
[null, guardSet(guards)],
|
|
1609
|
+
],
|
|
1610
|
+
depth,
|
|
1611
|
+
);
|
|
1612
|
+
visit(node.finalizer, scope, guards, depth + 1);
|
|
1613
|
+
return;
|
|
1614
|
+
}
|
|
1615
|
+
if (node.type === "IfStatement") {
|
|
1616
|
+
visit(node.test, scope, guards, depth + 1);
|
|
1617
|
+
branch(
|
|
1618
|
+
scope,
|
|
1619
|
+
[
|
|
1620
|
+
[
|
|
1621
|
+
node.consequent,
|
|
1622
|
+
merged(guards, conditionGuards(node.test, scope, true)),
|
|
1623
|
+
],
|
|
1624
|
+
[
|
|
1625
|
+
node.alternate,
|
|
1626
|
+
merged(guards, conditionGuards(node.test, scope, false)),
|
|
1627
|
+
],
|
|
1628
|
+
],
|
|
1629
|
+
depth,
|
|
1630
|
+
);
|
|
1631
|
+
return;
|
|
1632
|
+
}
|
|
1633
|
+
if (node.type === "ConditionalExpression") {
|
|
1634
|
+
visit(node.test, scope, guards, depth + 1);
|
|
1635
|
+
branch(
|
|
1636
|
+
scope,
|
|
1637
|
+
[
|
|
1638
|
+
[
|
|
1639
|
+
node.consequent,
|
|
1640
|
+
merged(guards, conditionGuards(node.test, scope, true)),
|
|
1641
|
+
],
|
|
1642
|
+
[
|
|
1643
|
+
node.alternate,
|
|
1644
|
+
merged(guards, conditionGuards(node.test, scope, false)),
|
|
1645
|
+
],
|
|
1646
|
+
],
|
|
1647
|
+
depth,
|
|
1648
|
+
);
|
|
1649
|
+
return;
|
|
1650
|
+
}
|
|
1651
|
+
if (
|
|
1652
|
+
[
|
|
1653
|
+
"WhileStatement",
|
|
1654
|
+
"DoWhileStatement",
|
|
1655
|
+
"ForStatement",
|
|
1656
|
+
"ForInStatement",
|
|
1657
|
+
"ForOfStatement",
|
|
1658
|
+
].includes(node.type)
|
|
1659
|
+
) {
|
|
1660
|
+
visit(node.init || node.left, scope, guards, depth + 1);
|
|
1661
|
+
visit(node.test || node.right, scope, guards, depth + 1);
|
|
1662
|
+
branch(
|
|
1663
|
+
scope,
|
|
1664
|
+
[
|
|
1665
|
+
[node.body, guardSet(guards), node.update],
|
|
1666
|
+
[null, guardSet(guards)],
|
|
1667
|
+
],
|
|
1668
|
+
depth,
|
|
1669
|
+
);
|
|
1670
|
+
return;
|
|
1671
|
+
}
|
|
1672
|
+
if (node.type === "LogicalExpression") {
|
|
1673
|
+
visit(node.left, scope, guards, depth + 1);
|
|
1674
|
+
branch(
|
|
1675
|
+
scope,
|
|
1676
|
+
[
|
|
1677
|
+
[
|
|
1678
|
+
node.right,
|
|
1679
|
+
merged(
|
|
1680
|
+
guards,
|
|
1681
|
+
conditionGuards(node.left, scope, node.operator === "&&"),
|
|
1682
|
+
),
|
|
1683
|
+
],
|
|
1684
|
+
[null, guardSet(guards)],
|
|
1685
|
+
],
|
|
1686
|
+
depth,
|
|
1687
|
+
);
|
|
1688
|
+
return;
|
|
1689
|
+
}
|
|
1690
|
+
if (calls.has(node.type)) {
|
|
1691
|
+
recordCall(node, scope, guards);
|
|
1692
|
+
resolve(node, scope, guards);
|
|
1693
|
+
let target = resolve(node.callee, scope, guards);
|
|
1694
|
+
if (target?.kind === "namespace")
|
|
1695
|
+
target = lookupExport(target.target, "default");
|
|
1696
|
+
if (
|
|
1697
|
+
!target ||
|
|
1698
|
+
!["function", "method", "service"].includes(target.kind)
|
|
1699
|
+
) {
|
|
1700
|
+
const args = node.arguments.map((a) => resolve(a, scope, guards));
|
|
1701
|
+
if (args.some((v) => hasEvidence(v)))
|
|
1702
|
+
diagnose(
|
|
1703
|
+
"flow-unknown-target",
|
|
1704
|
+
file,
|
|
1705
|
+
node,
|
|
1706
|
+
"Context/service passed to an unresolved call",
|
|
1707
|
+
);
|
|
1708
|
+
for (const arg of args) taint(arg);
|
|
1709
|
+
}
|
|
1710
|
+
if (
|
|
1711
|
+
members.has(node.callee?.type) &&
|
|
1712
|
+
resolve(node.callee.object, scope)?.kind === "context"
|
|
1713
|
+
) {
|
|
1714
|
+
const operation = keyFor(node.callee, scope, guards);
|
|
1715
|
+
if (
|
|
1716
|
+
operation === "get" &&
|
|
1717
|
+
constant(node.arguments[0], scope, guards) !== null
|
|
1718
|
+
)
|
|
1719
|
+
dependency(
|
|
1720
|
+
constant(node.arguments[0], scope, guards),
|
|
1721
|
+
node.arguments[1]?.value === false ||
|
|
1722
|
+
node.arguments[1]?.value === 0
|
|
1723
|
+
? "optional-read"
|
|
1724
|
+
: "read",
|
|
1725
|
+
node,
|
|
1726
|
+
resolve(node.callee.object, scope)?.inferred === true,
|
|
1727
|
+
);
|
|
1728
|
+
if (
|
|
1729
|
+
operation === "provide" &&
|
|
1730
|
+
constant(node.arguments[0], scope, guards) !== null
|
|
1731
|
+
) {
|
|
1732
|
+
provided.add(constant(node.arguments[0], scope, guards));
|
|
1733
|
+
mention(constant(node.arguments[0], scope, guards));
|
|
1734
|
+
}
|
|
1735
|
+
if (operation === "inject") {
|
|
1736
|
+
const injected = literalNames(node.arguments[0], scope);
|
|
1737
|
+
for (const s of injected)
|
|
1738
|
+
dependency(
|
|
1739
|
+
s,
|
|
1740
|
+
"scoped-inject",
|
|
1741
|
+
node,
|
|
1742
|
+
resolve(node.callee.object, scope)?.inferred === true,
|
|
1743
|
+
);
|
|
1744
|
+
const callback = node.arguments.find((a) => functions.has(a?.type));
|
|
1745
|
+
visit(node.callee, scope, guards, depth + 1);
|
|
1746
|
+
for (const arg of node.arguments)
|
|
1747
|
+
if (arg !== callback) visit(arg, scope, guards, depth + 1);
|
|
1748
|
+
if (callback)
|
|
1749
|
+
visitFunction(
|
|
1750
|
+
callback,
|
|
1751
|
+
scope,
|
|
1752
|
+
guards,
|
|
1753
|
+
depth + 1,
|
|
1754
|
+
new Map(
|
|
1755
|
+
callback.params[0]
|
|
1756
|
+
? [
|
|
1757
|
+
[
|
|
1758
|
+
callback.params[0].start,
|
|
1759
|
+
resolve(node.callee.object, scope)?.inferred === true,
|
|
1760
|
+
],
|
|
1761
|
+
]
|
|
1762
|
+
: [],
|
|
1763
|
+
),
|
|
1764
|
+
);
|
|
1765
|
+
return;
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
if (calls.has(node.type)) return;
|
|
1770
|
+
for (const child of childNodes(node))
|
|
1771
|
+
visit(child, scope, guards, depth + 1);
|
|
1772
|
+
};
|
|
1773
|
+
for (const n of program.body) {
|
|
1774
|
+
if (n.type === "ExportDefaultDeclaration")
|
|
1775
|
+
module.exports.set("default", { node: n.declaration });
|
|
1776
|
+
if (n.type === "ExportNamedDeclaration") {
|
|
1777
|
+
const d = n.declaration;
|
|
1778
|
+
if (d?.id) module.exports.set(d.id.name, { local: d.id.name });
|
|
1779
|
+
if (d?.type === "VariableDeclaration")
|
|
1780
|
+
for (const v of d.declarations)
|
|
1781
|
+
for (const name of namesIn(v.id))
|
|
1782
|
+
module.exports.set(name, { local: name });
|
|
1783
|
+
for (const spec of n.specifiers || [])
|
|
1784
|
+
module.exports.set(
|
|
1785
|
+
spec.exported.name ?? spec.exported.value,
|
|
1786
|
+
n.source
|
|
1787
|
+
? {
|
|
1788
|
+
source: n.source.value,
|
|
1789
|
+
name: spec.local.name ?? spec.local.value,
|
|
1790
|
+
}
|
|
1791
|
+
: { local: spec.local.name },
|
|
1792
|
+
);
|
|
1793
|
+
}
|
|
1794
|
+
if (n.type === "ExportAllDeclaration") module.stars.push(n.source.value);
|
|
1795
|
+
}
|
|
1796
|
+
module.resolveExport = (entry) =>
|
|
1797
|
+
entry.local
|
|
1798
|
+
? resolve({ type: "Identifier", name: entry.local }, root)
|
|
1799
|
+
: resolve(entry.node, root);
|
|
1800
|
+
module.cjs = () => cjsModule.fields.get("exports");
|
|
1801
|
+
module.entry = () => {
|
|
1802
|
+
if (!isEntry) return;
|
|
1803
|
+
const apply = lookupExport(file, "apply");
|
|
1804
|
+
if (apply?.kind !== "function") return;
|
|
1805
|
+
const guards = guardSet();
|
|
1806
|
+
frames.push({ executing: true, guards });
|
|
1807
|
+
try {
|
|
1808
|
+
apply.invoke([{ kind: "context", inferred: false }], guards, sides);
|
|
1809
|
+
} finally {
|
|
1810
|
+
frames.pop();
|
|
1811
|
+
}
|
|
1812
|
+
};
|
|
1813
|
+
module.initialize = () => {
|
|
1814
|
+
if (module.initialized || module.initializing) return;
|
|
1815
|
+
module.initializing = true;
|
|
1816
|
+
try {
|
|
1817
|
+
visit(program, root, new Set());
|
|
1818
|
+
} catch (error) {
|
|
1819
|
+
diagnose("analysis-error", file, null, error.message);
|
|
1820
|
+
} finally {
|
|
1821
|
+
module.initializing = false;
|
|
1822
|
+
module.initialized = true;
|
|
1823
|
+
}
|
|
1824
|
+
};
|
|
1825
|
+
module.scan = (entryOnly) => {
|
|
1826
|
+
module.initialize();
|
|
1827
|
+
for (const deferred of module.deferred)
|
|
1828
|
+
if (deferred.entry === entryOnly) {
|
|
1829
|
+
currentDeferred = deferred.node;
|
|
1830
|
+
try {
|
|
1831
|
+
deferred.run();
|
|
1832
|
+
} catch (error) {
|
|
1833
|
+
diagnose("analysis-error", file, deferred.node, error.message);
|
|
1834
|
+
} finally {
|
|
1835
|
+
currentDeferred = null;
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
if (!entryOnly) subContexts.push({ file, names: [...heads].sort() });
|
|
1839
|
+
};
|
|
1840
|
+
}
|
|
1841
|
+
for (const module of modules.values()) module.initialize();
|
|
1842
|
+
for (const module of modules.values()) module.entry();
|
|
1843
|
+
for (const pass of [true, false])
|
|
1844
|
+
for (const module of modules.values()) module.scan(pass);
|
|
1845
|
+
// A helper may be seen both standalone and through proved callers. Preserve
|
|
1846
|
+
// each service/side, prefer proved evidence, and keep every unguarded path.
|
|
1847
|
+
const unique = new Map();
|
|
1848
|
+
for (const site of sites) {
|
|
1849
|
+
const key = [
|
|
1850
|
+
site.file,
|
|
1851
|
+
site.start,
|
|
1852
|
+
site.service,
|
|
1853
|
+
site.method,
|
|
1854
|
+
site.sides.join(","),
|
|
1855
|
+
].join(":");
|
|
1856
|
+
const old = unique.get(key);
|
|
1857
|
+
if (!old) unique.set(key, site);
|
|
1858
|
+
else {
|
|
1859
|
+
old.inferredReceiver = old.inferredReceiver && site.inferredReceiver;
|
|
1860
|
+
old.guarded = old.guarded && site.guarded;
|
|
1861
|
+
old.capabilityGuarded = old.capabilityGuarded && site.capabilityGuarded;
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
sites.splice(0, sites.length, ...unique.values());
|
|
1865
|
+
for (const site of sites) if (!site.guarded) site.guardKind = "none";
|
|
1866
|
+
const uniqueBindings = new Map();
|
|
1867
|
+
for (const binding of bindings) {
|
|
1868
|
+
const key = [
|
|
1869
|
+
binding.file,
|
|
1870
|
+
binding.start,
|
|
1871
|
+
binding.variable,
|
|
1872
|
+
binding.service,
|
|
1873
|
+
binding.form,
|
|
1874
|
+
binding.from ?? "",
|
|
1875
|
+
].join(":");
|
|
1876
|
+
if (!uniqueBindings.has(key)) uniqueBindings.set(key, binding);
|
|
1877
|
+
}
|
|
1878
|
+
bindings.splice(0, bindings.length, ...uniqueBindings.values());
|
|
1879
|
+
const guardedPairs = [],
|
|
1880
|
+
unguardedPairs = [];
|
|
1881
|
+
for (const [svc, methods] of callMap)
|
|
1882
|
+
for (const method of methods) {
|
|
1883
|
+
const relevant = sites.filter(
|
|
1884
|
+
(s) => s.service === svc && s.method === method,
|
|
1885
|
+
);
|
|
1886
|
+
(relevant.every((s) => s.guarded) ? guardedPairs : unguardedPairs).push(
|
|
1887
|
+
`${svc}.${method}`,
|
|
1888
|
+
);
|
|
1889
|
+
}
|
|
1890
|
+
sites.sort(
|
|
1891
|
+
(a, b) =>
|
|
1892
|
+
(a.file < b.file ? -1 : a.file > b.file ? 1 : 0) || a.start - b.start,
|
|
1893
|
+
);
|
|
1894
|
+
return {
|
|
1895
|
+
services,
|
|
1896
|
+
declared,
|
|
1897
|
+
softDeclared,
|
|
1898
|
+
addressable,
|
|
1899
|
+
calls: callMap,
|
|
1900
|
+
serviceFiles,
|
|
1901
|
+
sites,
|
|
1902
|
+
provided,
|
|
1903
|
+
bindings,
|
|
1904
|
+
dependencies,
|
|
1905
|
+
diagnostics,
|
|
1906
|
+
subContexts,
|
|
1907
|
+
ambiguousAliases: [],
|
|
1908
|
+
shadowedBare: [],
|
|
1909
|
+
unresolvedBindings: [],
|
|
1910
|
+
guardedPairs: guardedPairs.sort(),
|
|
1911
|
+
unguardedPairs: unguardedPairs.sort(),
|
|
1912
|
+
};
|
|
1913
|
+
}
|