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.
@@ -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
+ }