modality-ts 0.0.4 → 0.0.6

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.
@@ -2,6 +2,9 @@ import * as ts from "typescript";
2
2
  import { effectReads, effectWrites } from "modality-ts/kernel";
3
3
  export * from "./pipeline/index.js";
4
4
  export * from "./spi/index.js";
5
+ function emptyContextBindings() {
6
+ return { vars: [], setters: new Map(), hookReturns: new Map() };
7
+ }
5
8
  function setterBindingFromDecl(decl) {
6
9
  const localMatch = /^local:([^.]+)\.(.+)$/.exec(decl.id);
7
10
  const atomMatch = /^atom:(.+)$/.exec(decl.id);
@@ -12,6 +15,183 @@ function setterBindingFromDecl(decl) {
12
15
  domain: decl.domain
13
16
  };
14
17
  }
18
+ function discoverContextBindings(source, fileName, route, typeAliases) {
19
+ const bindings = emptyContextBindings();
20
+ const providerValues = new Map();
21
+ const visitProvider = (node, componentName) => {
22
+ const component = componentNameFor(node) ?? componentName;
23
+ const localSetters = new Map();
24
+ if ((ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node)) && component && node.body && ts.isBlock(node.body)) {
25
+ for (const statement of node.body.statements) {
26
+ if (!ts.isVariableStatement(statement))
27
+ continue;
28
+ for (const declaration of statement.declarationList.declarations) {
29
+ if (!ts.isArrayBindingPattern(declaration.name) || !declaration.initializer || !isUseStateCall(declaration.initializer))
30
+ continue;
31
+ const stateName = declaration.name.elements[0];
32
+ const setterName = declaration.name.elements[1];
33
+ if (!setterName || !ts.isBindingElement(stateName) || !ts.isIdentifier(stateName.name) || !ts.isBindingElement(setterName) || !ts.isIdentifier(setterName.name))
34
+ continue;
35
+ const domain = inferUseStateDomain(declaration.initializer, typeAliases);
36
+ const varId = `local:${component}.${stateName.name.text}`;
37
+ const setter = { varId, component, stateName: stateName.name.text, domain };
38
+ localSetters.set(setterName.name.text, setter);
39
+ }
40
+ }
41
+ for (const statement of node.body.statements) {
42
+ if (!ts.isVariableStatement(statement))
43
+ continue;
44
+ for (const declaration of statement.declarationList.declarations) {
45
+ if (!ts.isIdentifier(declaration.name) || !declaration.initializer)
46
+ continue;
47
+ const setter = setterAliasBinding(declaration.initializer, localSetters);
48
+ if (setter)
49
+ localSetters.set(declaration.name.text, setter);
50
+ }
51
+ }
52
+ }
53
+ if (component && localSetters.size > 0 && node.getText(source).includes(".Provider")) {
54
+ const fields = providerValueFields(node, localSetters);
55
+ if (fields.size > 0) {
56
+ providerValues.set(component, fields);
57
+ for (const setter of fields.values())
58
+ bindings.setters.set(setter.stateName, setter);
59
+ }
60
+ }
61
+ ts.forEachChild(node, (child) => visitProvider(child, component));
62
+ };
63
+ visitProvider(source, undefined);
64
+ const providerFieldMaps = [...providerValues.values()];
65
+ const visitHook = (node) => {
66
+ const name = customHookDeclarationName(node);
67
+ if (name && (ts.isFunctionDeclaration(node) || (ts.isVariableDeclaration(node) && node.initializer && isExtractableHandler(node.initializer)))) {
68
+ const hook = ts.isFunctionDeclaration(node) ? node : node.initializer;
69
+ if (hookUsesContext(hook) && providerFieldMaps.length > 0) {
70
+ const merged = new Map();
71
+ for (const map of providerFieldMaps)
72
+ for (const [field, setter] of map)
73
+ merged.set(field, setter);
74
+ bindings.hookReturns.set(name, merged);
75
+ }
76
+ }
77
+ ts.forEachChild(node, visitHook);
78
+ };
79
+ visitHook(source);
80
+ return bindings;
81
+ }
82
+ function providerValueFields(node, localSetters) {
83
+ const fields = new Map();
84
+ const visit = (candidate) => {
85
+ if (ts.isJsxAttribute(candidate) && ts.isIdentifier(candidate.name) && candidate.name.text === "value" && candidate.initializer && ts.isJsxExpression(candidate.initializer)) {
86
+ const value = providerValueObject(node, candidate.initializer.expression);
87
+ if (value) {
88
+ for (const property of value.properties) {
89
+ if (!ts.isShorthandPropertyAssignment(property) && !ts.isPropertyAssignment(property))
90
+ continue;
91
+ const name = ts.isShorthandPropertyAssignment(property) ? property.name.text : propertyName(property.name);
92
+ const expr = ts.isShorthandPropertyAssignment(property) ? property.name : property.initializer;
93
+ if (!name || !ts.isIdentifier(expr))
94
+ continue;
95
+ const setter = localSetters.get(expr.text);
96
+ if (setter)
97
+ fields.set(name, setter);
98
+ }
99
+ }
100
+ }
101
+ ts.forEachChild(candidate, visit);
102
+ };
103
+ visit(node);
104
+ return fields;
105
+ }
106
+ function setterAliasBinding(expression, localSetters) {
107
+ const callback = useCallbackFunction(expression);
108
+ if (!callback || callback.parameters.length !== 1 || !ts.isIdentifier(callback.parameters[0].name))
109
+ return undefined;
110
+ const parameter = callback.parameters[0].name.text;
111
+ const call = firstCallInFunction(callback);
112
+ if (!call || !ts.isIdentifier(call.expression) || call.arguments.length !== 1 || !ts.isIdentifier(call.arguments[0]) || call.arguments[0].text !== parameter)
113
+ return undefined;
114
+ return localSetters.get(call.expression.text);
115
+ }
116
+ function useCallbackFunction(expression) {
117
+ if (!ts.isCallExpression(expression) || !ts.isIdentifier(expression.expression) || expression.expression.text !== "useCallback")
118
+ return undefined;
119
+ const first = expression.arguments[0];
120
+ return first && isExtractableHandler(first) ? first : undefined;
121
+ }
122
+ function firstCallInFunction(fn) {
123
+ if (!ts.isBlock(fn.body))
124
+ return ts.isCallExpression(fn.body) ? fn.body : undefined;
125
+ for (const statement of fn.body.statements) {
126
+ if (ts.isExpressionStatement(statement) && ts.isCallExpression(statement.expression))
127
+ return statement.expression;
128
+ }
129
+ return undefined;
130
+ }
131
+ function providerValueObject(scope, expression) {
132
+ if (!expression)
133
+ return undefined;
134
+ if (ts.isObjectLiteralExpression(expression))
135
+ return expression;
136
+ if (!ts.isIdentifier(expression))
137
+ return undefined;
138
+ const declaration = variableDeclarationIn(scope, expression.text);
139
+ if (!declaration?.initializer || !ts.isCallExpression(declaration.initializer))
140
+ return undefined;
141
+ if (!ts.isIdentifier(declaration.initializer.expression) || declaration.initializer.expression.text !== "useMemo")
142
+ return undefined;
143
+ const callback = declaration.initializer.arguments[0];
144
+ if (!callback || !isExtractableHandler(callback))
145
+ return undefined;
146
+ if (ts.isObjectLiteralExpression(callback.body))
147
+ return callback.body;
148
+ if (ts.isParenthesizedExpression(callback.body) && ts.isObjectLiteralExpression(callback.body.expression))
149
+ return callback.body.expression;
150
+ return undefined;
151
+ }
152
+ function variableDeclarationIn(scope, name) {
153
+ let found;
154
+ const visit = (node) => {
155
+ if (found)
156
+ return;
157
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.name.text === name) {
158
+ found = node;
159
+ return;
160
+ }
161
+ ts.forEachChild(node, visit);
162
+ };
163
+ visit(scope);
164
+ return found;
165
+ }
166
+ function hookUsesContext(hook) {
167
+ let found = false;
168
+ const visit = (node) => {
169
+ if (found)
170
+ return;
171
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === "useContext") {
172
+ found = true;
173
+ return;
174
+ }
175
+ ts.forEachChild(node, visit);
176
+ };
177
+ visit(hook);
178
+ return found;
179
+ }
180
+ function bindContextHookObjectDeclaration(node, contextBindings, setters) {
181
+ if (!ts.isVariableDeclaration(node) || !ts.isObjectBindingPattern(node.name) || !node.initializer || !ts.isCallExpression(node.initializer) || !ts.isIdentifier(node.initializer.expression))
182
+ return;
183
+ const hook = contextBindings.hookReturns.get(node.initializer.expression.text);
184
+ if (!hook)
185
+ return;
186
+ for (const element of node.name.elements) {
187
+ if (!ts.isIdentifier(element.name))
188
+ continue;
189
+ const property = element.propertyName && ts.isIdentifier(element.propertyName) ? element.propertyName.text : element.name.text;
190
+ const setter = hook.get(property);
191
+ if (setter)
192
+ setters.set(element.name.text, setter);
193
+ }
194
+ }
15
195
  export function inferDomainFromTypeNode(node, typeAliases = new Map()) {
16
196
  if (!node)
17
197
  return { kind: "tokens", count: 1 };
@@ -48,12 +228,15 @@ export function extractUseStateSkeleton(sourceText, options = {}) {
48
228
  const transitions = [];
49
229
  const warnings = [];
50
230
  const route = options.route ?? "/";
231
+ const routePatterns = options.routePatterns ?? [];
51
232
  const effectApis = new Set(options.effectApis ?? []);
52
233
  const sourcePlugins = options.sourcePlugins ?? [];
53
234
  const routerPlugin = options.routerPlugin;
54
235
  const setters = new Map();
236
+ const contextBindings = discoverContextBindings(source, fileName, route, typeAliases);
55
237
  const globalTaints = new Set();
56
238
  const components = componentDeclarations(source);
239
+ const providerComponents = providerComponentNames(source);
57
240
  const customHooks = customHookDeclarations(source);
58
241
  const statefulListComponents = detectStatefulListComponents(source, components);
59
242
  const reportedStatefulListComponents = new Set();
@@ -66,6 +249,12 @@ export function extractUseStateSkeleton(sourceText, options = {}) {
66
249
  setters.set(channel.symbolName, setterBindingFromDecl(decl));
67
250
  }
68
251
  }
252
+ for (const decl of contextBindings.vars) {
253
+ if (!vars.some((candidate) => candidate.id === decl.id))
254
+ vars.push(decl);
255
+ }
256
+ for (const [symbolName, setter] of contextBindings.setters)
257
+ setters.set(symbolName, setter);
69
258
  const handlers = new Map();
70
259
  const visit = (node, componentName) => {
71
260
  if (!componentName && isCustomHookDeclaration(node))
@@ -76,16 +265,20 @@ export function extractUseStateSkeleton(sourceText, options = {}) {
76
265
  if (handler)
77
266
  handlers.set(node.name.text, handler);
78
267
  }
268
+ if (ts.isFunctionDeclaration(node) && node.name && isExtractableHandler(node)) {
269
+ handlers.set(node.name.text, node);
270
+ }
79
271
  if (ts.isVariableDeclaration(node) && node.initializer && isUseReducerCall(node.initializer)) {
80
272
  warnings.push({ message: `Unsupported useReducer ${nextComponent ?? "Anonymous"}.useReducer`, ...lineAndColumn(source, node) });
81
273
  }
274
+ bindContextHookObjectDeclaration(node, contextBindings, setters);
82
275
  if (ts.isVariableDeclaration(node) && nextComponent && inlineCustomHookState(source, fileName, node, customHooks, vars, setters, nextComponent, route)) {
83
276
  return;
84
277
  }
85
278
  const customHook = calledCustomHook(node, new Set(customHooks.keys()));
86
279
  if (customHook && nextComponent) {
87
280
  const key = `${nextComponent}.${customHook}`;
88
- if (!reportedCustomHooks.has(key)) {
281
+ if (!contextBindings.hookReturns.has(customHook) && !reportedCustomHooks.has(key)) {
89
282
  reportedCustomHooks.add(key);
90
283
  warnings.push({ message: `Unextractable custom hook ${key}`, ...lineAndColumn(source, node) });
91
284
  }
@@ -110,7 +303,7 @@ export function extractUseStateSkeleton(sourceText, options = {}) {
110
303
  id: varId,
111
304
  domain,
112
305
  origin: { file: fileName, ...lineAndColumn(source, node) },
113
- scope: { kind: "route-local", route },
306
+ scope: providerComponents.has(component) ? { kind: "global" } : { kind: "route-local", route },
114
307
  initial: initialValueForUseState(node.initializer, domain)
115
308
  });
116
309
  }
@@ -123,6 +316,9 @@ export function extractUseStateSkeleton(sourceText, options = {}) {
123
316
  warnings.push({ message: "Unsupported useState binding pattern", ...lineAndColumn(source, node) });
124
317
  }
125
318
  }
319
+ const link = linkNavigationTransition(source, fileName, node, nextComponent ?? "Anonymous", routePatterns);
320
+ if (link)
321
+ transitions.push(link);
126
322
  const refTaint = refSetterTaint(node, setters);
127
323
  if (refTaint) {
128
324
  const key = `Global taint ${refTaint.varId}`;
@@ -170,7 +366,7 @@ export function extractUseStateSkeleton(sourceText, options = {}) {
170
366
  renderGuardFor(node, setters, warnings, source, nextComponent ?? "Anonymous", guardLocals),
171
367
  disabledGuardFor(node, setters, warnings, source, nextComponent ?? "Anonymous", guardLocals)
172
368
  ]);
173
- const extracted = transitionsFromJsxAttribute(source, fileName, node, setters, handlers, nextComponent ?? "Anonymous", effectApis, options.asyncOutcomes ?? {}, sourcePlugins, routerPlugin, guard, warnings);
369
+ const extracted = transitionsFromJsxAttribute(source, fileName, node, setters, handlers, nextComponent ?? "Anonymous", effectApis, options.asyncOutcomes ?? {}, sourcePlugins, routerPlugin, guard, routePatterns, contextBindings, warnings);
174
370
  transitions.push(...extracted);
175
371
  if (extracted.length === 0 && !forwardsComponentProp(node, handlers, components.get(nextComponent ?? "")) && !handlerSchedulesModeledTimer(node, handlers, setters)) {
176
372
  warnings.push({ message: `Unextractable handler ${nextComponent ?? "Anonymous"}.${node.name.text}`, ...lineAndColumn(source, node) });
@@ -179,7 +375,7 @@ export function extractUseStateSkeleton(sourceText, options = {}) {
179
375
  if (ts.isCallExpression(node) && isUseEffectCall(node)) {
180
376
  const extracted = transitionsFromUseEffect(source, fileName, node, setters, nextComponent ?? "Anonymous");
181
377
  transitions.push(...extracted);
182
- if (extracted.length === 0 && useEffectWritesModeledState(node, setters)) {
378
+ if (extracted.length === 0 && useEffectWritesModeledState(node, setters) && !providerComponents.has(nextComponent ?? "")) {
183
379
  warnings.push({ message: `Unextractable effect ${nextComponent ?? "Anonymous"}.useEffect`, ...lineAndColumn(source, node) });
184
380
  }
185
381
  }
@@ -394,7 +590,7 @@ function isUseEffectCall(node) {
394
590
  return ts.isIdentifier(node.expression) && node.expression.text === "useEffect";
395
591
  }
396
592
  function isExtractableHandler(node) {
397
- return ts.isArrowFunction(node) || ts.isFunctionExpression(node);
593
+ return ts.isArrowFunction(node) || ts.isFunctionExpression(node) || (ts.isFunctionDeclaration(node) && Boolean(node.body));
398
594
  }
399
595
  function extractableHandlerInitializer(node) {
400
596
  if (isExtractableHandler(node))
@@ -447,7 +643,7 @@ function transitionsFromTimerCall(source, fileName, node, setters, component) {
447
643
  if (!summaries || summaries.length === 0)
448
644
  return [];
449
645
  const effects = summaries.map((summary) => summary.effect);
450
- const writes = uniqueStrings(effects.map((effect) => effect.var));
646
+ const writes = uniqueStrings(effects.flatMap(effectWriteVars));
451
647
  const suffix = writes.map((id) => stateNameForVar(id, setters) ?? safeId(id)).join("_") || "callback";
452
648
  return [{
453
649
  id: `${component}.${name}.${suffix}`,
@@ -504,7 +700,7 @@ function handlerSchedulesModeledTimer(attribute, handlers, setters) {
504
700
  visit(handler.body);
505
701
  return found;
506
702
  }
507
- function transitionsFromJsxAttribute(source, fileName, node, setters, handlers, component, effectApis, asyncOutcomes, sourcePlugins, routerPlugin, disabledGuard, warnings) {
703
+ function transitionsFromJsxAttribute(source, fileName, node, setters, handlers, component, effectApis, asyncOutcomes, sourcePlugins, routerPlugin, disabledGuard, routePatterns, contextBindings, warnings) {
508
704
  if (!node.initializer)
509
705
  return [];
510
706
  const expression = ts.isJsxExpression(node.initializer) ? node.initializer.expression : undefined;
@@ -515,7 +711,7 @@ function transitionsFromJsxAttribute(source, fileName, node, setters, handlers,
515
711
  return [];
516
712
  const attr = node.name.text;
517
713
  const locator = locatorForEventAttribute(node);
518
- return tagStableIdKey(transitionsFromResolvedHandler(source, fileName, node, attr, handler, setters, handlers, component, effectApis, asyncOutcomes, sourcePlugins, routerPlugin, disabledGuard, locator, warnings), handler);
714
+ return tagStableIdKey(transitionsFromResolvedHandler(source, fileName, node, attr, handler, setters, handlers, component, effectApis, asyncOutcomes, sourcePlugins, routerPlugin, disabledGuard, locator, routePatterns, contextBindings, warnings), handler);
519
715
  }
520
716
  function transitionsFromComponentPropAttribute(source, fileName, node, setters, handlers, components, component, effectApis, asyncOutcomes, sourcePlugins, routerPlugin, warnings) {
521
717
  if (!node.initializer || !ts.isIdentifier(node.name))
@@ -526,7 +722,7 @@ function transitionsFromComponentPropAttribute(source, fileName, node, setters,
526
722
  const callee = components.get(tag);
527
723
  if (!callee)
528
724
  return [];
529
- const trigger = componentPropTrigger(source, callee, node.name.text, setters, warnings);
725
+ const trigger = componentPropTrigger(source, callee, node.name.text, setters, warnings) ?? transparentComponentPropTrigger(callee, node.name.text);
530
726
  if (!trigger)
531
727
  return [];
532
728
  const expression = ts.isJsxExpression(node.initializer) ? node.initializer.expression : undefined;
@@ -538,7 +734,7 @@ function transitionsFromComponentPropAttribute(source, fileName, node, setters,
538
734
  renderGuardFor(node, setters, warnings, source, component, guardLocals),
539
735
  disabledGuardFor(node, setters, warnings, source, component, guardLocals)
540
736
  ]);
541
- return tagStableIdKey(transitionsFromResolvedHandler(source, fileName, node, trigger.attr, handler, setters, handlers, component, effectApis, asyncOutcomes, sourcePlugins, routerPlugin, combineParsedGuards([trigger.guard, callerGuard]), trigger.locator, warnings), handler);
737
+ return tagStableIdKey(transitionsFromResolvedHandler(source, fileName, node, trigger.attr, handler, setters, handlers, component, effectApis, asyncOutcomes, sourcePlugins, routerPlugin, combineParsedGuards([trigger.guard, callerGuard]), trigger.locator, [], emptyContextBindings(), warnings), handler);
542
738
  }
543
739
  function transitionsFromBoundedListAttribute(source, fileName, node, setters, handlers, component, listInfo) {
544
740
  if (!node.initializer || !ts.isIdentifier(node.name))
@@ -598,8 +794,8 @@ function normalizedAstKey(node) {
598
794
  .replace(/\/\/.*$/gm, "")
599
795
  .replace(/\s+/g, "");
600
796
  }
601
- function transitionsFromResolvedHandler(source, fileName, node, attr, handler, setters, handlers, component, effectApis, asyncOutcomes, sourcePlugins, routerPlugin, disabledGuard, locator, warnings) {
602
- const asyncTransitions = transitionsFromAsyncHandler(source, fileName, attr, handler, setters, component, effectApis, asyncOutcomes, locator, warnings);
797
+ function transitionsFromResolvedHandler(source, fileName, node, attr, handler, setters, handlers, component, effectApis, asyncOutcomes, sourcePlugins, routerPlugin, disabledGuard, locator, routePatterns, contextBindings, warnings) {
798
+ const asyncTransitions = transitionsFromAsyncHandler(source, fileName, attr, handler, setters, component, effectApis, asyncOutcomes, locator, routePatterns, warnings);
603
799
  if (asyncTransitions.length > 0)
604
800
  return applyParsedGuard(asyncTransitions, disabledGuard);
605
801
  const conditionalTransition = conditionalTransitionFromHandler(source, fileName, node, attr, handler, setters, component, locator);
@@ -611,13 +807,13 @@ function transitionsFromResolvedHandler(source, fileName, node, attr, handler, s
611
807
  const sequentialTransition = sequentialTransitionFromHandler(source, fileName, node, attr, handler, setters, handlers, component, locator);
612
808
  if (sequentialTransition)
613
809
  return applyParsedGuard([sequentialTransition], disabledGuard);
614
- const summary = callSummaryFromHandler(handler, setters);
810
+ const summary = callSummaryFromHandler(handler, setters, componentScopeLocalsFor(node, setters, contextBindings));
615
811
  if (!summary)
616
812
  return [];
617
813
  const inlined = inlinedHelperCall(summary.call, handlers, setters);
618
814
  const inlinedCall = inlined?.call ?? summary.call;
619
815
  const locals = inlined?.locals ?? summary.locals;
620
- const navigation = navigationTransition(source, fileName, node, attr, component, inlinedCall, locator, routerPlugin);
816
+ const navigation = navigationTransition(source, fileName, node, attr, component, inlinedCall, locator, routerPlugin, routePatterns);
621
817
  if (navigation)
622
818
  return applyParsedGuard([navigation], disabledGuard);
623
819
  const pluginWrite = pluginWriteTransition(source, fileName, node, attr, component, inlinedCall, setters, locals, sourcePlugins, locator);
@@ -626,6 +822,9 @@ function transitionsFromResolvedHandler(source, fileName, node, attr, handler, s
626
822
  const swrMutate = swrMutateTransition(source, fileName, node, attr, component, inlinedCall, locator);
627
823
  if (swrMutate)
628
824
  return applyParsedGuard([swrMutate], disabledGuard);
825
+ const noop = noopCallTransition(source, fileName, node, attr, component, inlinedCall, locator);
826
+ if (noop)
827
+ return applyParsedGuard([noop], disabledGuard);
629
828
  const setterCall = setterCallFrom(inlinedCall, setters);
630
829
  if (!setterCall) {
631
830
  const escaped = escapedSetters(inlinedCall, setters, locals);
@@ -674,7 +873,7 @@ function sequentialTransitionFromHandler(source, fileName, node, attr, handler,
674
873
  if (summaries.length <= 1)
675
874
  return undefined;
676
875
  const effects = summaries.map((summary) => summary.effect);
677
- const writes = uniqueStrings(effects.map((effect) => effect.var));
876
+ const writes = uniqueStrings(effects.flatMap(effectWriteVars));
678
877
  return {
679
878
  id: `${component}.${attr}.${writes.map((id) => stateNameForVar(id, setters) ?? safeId(id)).join("_")}.seq`,
680
879
  cls: "user",
@@ -1055,6 +1254,28 @@ function componentGuardLocalsFor(attribute, setters) {
1055
1254
  }
1056
1255
  return locals;
1057
1256
  }
1257
+ function componentScopeLocalsFor(attribute, setters, contextBindings) {
1258
+ const body = enclosingFunctionBody(attribute);
1259
+ if (!body)
1260
+ return new Map();
1261
+ const locals = new Map();
1262
+ for (const statement of body.statements) {
1263
+ if (statement.pos > attribute.pos)
1264
+ break;
1265
+ if (ts.isReturnStatement(statement))
1266
+ break;
1267
+ bindConstStatement(statement, setters, locals, true);
1268
+ for (const declaration of variableDeclarations(statement)) {
1269
+ bindContextHookObjectDeclaration(declaration, contextBindings, setters);
1270
+ }
1271
+ }
1272
+ return locals;
1273
+ }
1274
+ function variableDeclarations(node) {
1275
+ if (!ts.isVariableStatement(node))
1276
+ return [];
1277
+ return [...node.declarationList.declarations];
1278
+ }
1058
1279
  function enclosingFunctionBody(node) {
1059
1280
  let current = node;
1060
1281
  while (current) {
@@ -1161,6 +1382,25 @@ function swrMutateTransition(source, fileName, node, attr, component, call, loca
1161
1382
  confidence: "exact"
1162
1383
  };
1163
1384
  }
1385
+ function noopCallTransition(source, fileName, node, attr, component, call, locator) {
1386
+ const name = callName(call.expression) ?? call.expression.getText(source);
1387
+ if (!isKnownPureUiCall(name))
1388
+ return undefined;
1389
+ return {
1390
+ id: `${component}.${attr}.${safeId(name)}.noop`,
1391
+ cls: "user",
1392
+ label: labelForEvent(attr, locator),
1393
+ source: [{ file: fileName, ...lineAndColumn(source, node) }],
1394
+ guard: { kind: "lit", value: true },
1395
+ effect: { kind: "seq", effects: [] },
1396
+ reads: [],
1397
+ writes: [],
1398
+ confidence: "exact"
1399
+ };
1400
+ }
1401
+ function isKnownPureUiCall(name) {
1402
+ return name.endsWith(".click") || name === "confirm" || name === "navigator.clipboard.writeText" || name.endsWith(".writeText");
1403
+ }
1164
1404
  function bindConstStatement(statement, setters, locals, partialBoolean = false) {
1165
1405
  if (!ts.isVariableStatement(statement))
1166
1406
  return false;
@@ -1184,8 +1424,8 @@ function inlinedHelperCall(call, handlers, setters) {
1184
1424
  const helper = handlers.get(call.expression.text);
1185
1425
  return helper ? callSummaryFromHandler(helper, setters) : undefined;
1186
1426
  }
1187
- function navigationTransition(source, fileName, node, attr, component, call, locator, routerPlugin) {
1188
- const navigation = navigationCall(call, routerPlugin);
1427
+ function navigationTransition(source, fileName, node, attr, component, call, locator, routerPlugin, routePatterns = []) {
1428
+ const navigation = navigationCall(call, routerPlugin, routePatterns);
1189
1429
  if (!navigation)
1190
1430
  return undefined;
1191
1431
  const routeId = navigation.to ? safeId(navigation.to) : "back";
@@ -1209,7 +1449,7 @@ function navigationTransition(source, fileName, node, attr, component, call, loc
1209
1449
  confidence: "exact"
1210
1450
  };
1211
1451
  }
1212
- function navigationCall(call, routerPlugin) {
1452
+ function navigationCall(call, routerPlugin, routePatterns = []) {
1213
1453
  const name = callName(call.expression);
1214
1454
  if (!name)
1215
1455
  return undefined;
@@ -1217,11 +1457,11 @@ function navigationCall(call, routerPlugin) {
1217
1457
  if (pluginNavigation && pluginNavigation !== "unsupported")
1218
1458
  return pluginNavigation;
1219
1459
  if (name === "navigate" && call.arguments.length === 1) {
1220
- const to = literalValue(call.arguments[0]);
1460
+ const to = routeTargetValue(call.arguments[0], routePatterns);
1221
1461
  return typeof to === "string" ? { mode: "push", to } : undefined;
1222
1462
  }
1223
1463
  if ((name.endsWith(".push") || name.endsWith(".replace")) && call.arguments.length === 1) {
1224
- const to = literalValue(call.arguments[0]);
1464
+ const to = routeTargetValue(call.arguments[0], routePatterns);
1225
1465
  if (typeof to !== "string")
1226
1466
  return undefined;
1227
1467
  return { mode: name.endsWith(".replace") ? "replace" : "push", to };
@@ -1231,6 +1471,68 @@ function navigationCall(call, routerPlugin) {
1231
1471
  }
1232
1472
  return undefined;
1233
1473
  }
1474
+ function linkNavigationTransition(source, fileName, node, component, routePatterns) {
1475
+ if ((!ts.isJsxOpeningElement(node) && !ts.isJsxSelfClosingElement(node)) || node.tagName.getText(source) !== "Link")
1476
+ return undefined;
1477
+ const toAttr = node.attributes.properties.find((property) => ts.isJsxAttribute(property) && ts.isIdentifier(property.name) && property.name.text === "to");
1478
+ if (!toAttr)
1479
+ return undefined;
1480
+ const to = jsxRouteTarget(toAttr, routePatterns);
1481
+ if (!to)
1482
+ return undefined;
1483
+ return {
1484
+ id: `${component}.Link.navigate.${safeId(to)}`,
1485
+ cls: "nav",
1486
+ label: { kind: "navigate", mode: "push", to },
1487
+ source: [{ file: fileName, ...lineAndColumn(source, toAttr) }],
1488
+ guard: { kind: "lit", value: true },
1489
+ effect: { kind: "navigate", mode: "push", to: { kind: "lit", value: to } },
1490
+ reads: ["sys:route", "sys:history"],
1491
+ writes: ["sys:route", "sys:history"],
1492
+ confidence: "exact"
1493
+ };
1494
+ }
1495
+ function jsxRouteTarget(attribute, routePatterns) {
1496
+ if (!attribute.initializer)
1497
+ return undefined;
1498
+ if (ts.isStringLiteral(attribute.initializer))
1499
+ return normalizeRouteTarget(attribute.initializer.text, routePatterns);
1500
+ if (!ts.isJsxExpression(attribute.initializer) || !attribute.initializer.expression)
1501
+ return undefined;
1502
+ return routeTargetValue(attribute.initializer.expression, routePatterns);
1503
+ }
1504
+ function routeTargetValue(expression, routePatterns) {
1505
+ if (!expression)
1506
+ return undefined;
1507
+ const literal = literalValue(expression);
1508
+ if (typeof literal === "string")
1509
+ return normalizeRouteTarget(literal, routePatterns);
1510
+ if (ts.isNoSubstitutionTemplateLiteral(expression))
1511
+ return normalizeRouteTarget(expression.text, routePatterns);
1512
+ if (ts.isTemplateExpression(expression)) {
1513
+ const pattern = templateRoutePattern(expression);
1514
+ return pattern ? normalizeRouteTarget(pattern, routePatterns) : undefined;
1515
+ }
1516
+ return undefined;
1517
+ }
1518
+ function templateRoutePattern(expression) {
1519
+ let value = expression.head.text;
1520
+ for (const span of expression.templateSpans)
1521
+ value += ":param" + span.literal.text;
1522
+ return value;
1523
+ }
1524
+ function normalizeRouteTarget(target, routePatterns) {
1525
+ const slash = target.startsWith("/") ? target : `/${target}`;
1526
+ const matched = routePatterns.find((pattern) => routePatternMatches(pattern, slash));
1527
+ return matched ?? slash.replace(/\/:param(?=\/|$)/g, "/:id");
1528
+ }
1529
+ function routePatternMatches(pattern, target) {
1530
+ const left = pattern.replace(/^\/+/, "").split("/");
1531
+ const right = target.replace(/^\/+/, "").split("/");
1532
+ if (left.length !== right.length)
1533
+ return false;
1534
+ return left.every((part, index) => part.startsWith(":") || part === "*" || part === right[index] || right[index] === ":param");
1535
+ }
1234
1536
  function escapedSetters(call, setters, locals = new Map()) {
1235
1537
  return call.arguments
1236
1538
  .filter(ts.isIdentifier)
@@ -1562,6 +1864,25 @@ function componentPropTrigger(source, component, propName, setters, warnings) {
1562
1864
  visit(component);
1563
1865
  return trigger;
1564
1866
  }
1867
+ function transparentComponentPropTrigger(component, propName) {
1868
+ if (!isForwardablePropName(propName) || !componentSpreadsPropsToElement(component))
1869
+ return undefined;
1870
+ return { attr: propName };
1871
+ }
1872
+ function componentSpreadsPropsToElement(component) {
1873
+ let found = false;
1874
+ const visit = (node) => {
1875
+ if (found)
1876
+ return;
1877
+ if ((ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) && node.attributes.properties.some(ts.isJsxSpreadAttribute)) {
1878
+ found = true;
1879
+ return;
1880
+ }
1881
+ ts.forEachChild(node, visit);
1882
+ };
1883
+ visit(component);
1884
+ return found;
1885
+ }
1565
1886
  function forwardsComponentProp(node, handlers, component) {
1566
1887
  if (!component || !node.initializer)
1567
1888
  return false;
@@ -1709,7 +2030,7 @@ function jsxTagName(attribute) {
1709
2030
  return undefined;
1710
2031
  return ts.isIdentifier(parent.tagName) ? parent.tagName.text : undefined;
1711
2032
  }
1712
- function transitionsFromAsyncHandler(source, fileName, attr, expression, setters, component, effectApis, asyncOutcomes, locator, warnings) {
2033
+ function transitionsFromAsyncHandler(source, fileName, attr, expression, setters, component, effectApis, asyncOutcomes, locator, routePatterns, warnings) {
1713
2034
  if (!ts.isBlock(expression.body))
1714
2035
  return [];
1715
2036
  const statements = expression.body.statements;
@@ -1739,14 +2060,16 @@ function transitionsFromAsyncHandler(source, fileName, attr, expression, setters
1739
2060
  const successSummaries = summarizeAsyncSegment(successStatements, setters);
1740
2061
  const catchSummaries = tryStatement?.catchClause ? summarizeAsyncSegment(tryStatement.catchClause.block.statements, setters) : [];
1741
2062
  const preEffects = preSummaries.map((summary) => summary.effect);
1742
- const successEffects = successSummaries.map((summary) => summary.effect);
1743
- const catchEffects = catchSummaries.map((summary) => summary.effect);
2063
+ const finallySummaries = tryStatement?.finallyBlock ? summarizeAsyncSegment(tryStatement.finallyBlock.statements, setters) : [];
2064
+ const finallyEffects = finallySummaries.map((summary) => summary.effect);
2065
+ const successEffects = [...successSummaries.map((summary) => summary.effect), ...finallyEffects];
2066
+ const catchEffects = [...catchSummaries.map((summary) => summary.effect), ...finallyEffects];
1744
2067
  const preReads = uniqueStrings([...preSummaries.flatMap((summary) => summary.reads), ...opArgs.reads]);
1745
- const successReads = uniqueStrings(successSummaries.flatMap((summary) => summary.reads));
1746
- const catchReads = uniqueStrings(catchSummaries.flatMap((summary) => summary.reads));
2068
+ const successReads = uniqueStrings([...successSummaries.flatMap((summary) => summary.reads), ...finallySummaries.flatMap((summary) => summary.reads)]);
2069
+ const catchReads = uniqueStrings([...catchSummaries.flatMap((summary) => summary.reads), ...finallySummaries.flatMap((summary) => summary.reads)]);
1747
2070
  if (successEffects.length === 0 && catchEffects.length === 0)
1748
2071
  return [];
1749
- const writes = [...new Set([...preEffects, ...successEffects, ...catchEffects].map((effect) => effect.var))];
2072
+ const writes = uniqueStrings([...preEffects, ...successEffects, ...catchEffects].flatMap(effectWriteVars));
1750
2073
  const baseId = `${component}.${attr}.${op}`;
1751
2074
  for (const read of uniqueStrings([...successReads, ...catchReads])) {
1752
2075
  warnings.push({ message: `Stale-read risk ${baseId}:${read}`, ...lineAndColumn(source, awaitStatement) });
@@ -1760,7 +2083,7 @@ function transitionsFromAsyncHandler(source, fileName, attr, expression, setters
1760
2083
  guard: { kind: "lit", value: true },
1761
2084
  effect: { kind: "seq", effects: [...preEffects, { kind: "enqueue", op, continuation: `${baseId}.cont`, args: opArgs.args }] },
1762
2085
  reads: preReads,
1763
- writes: [...new Set([...preEffects.map((effect) => effect.var), "sys:pending"])],
2086
+ writes: uniqueStrings([...preEffects.flatMap(effectWriteVars), "sys:pending"]),
1764
2087
  confidence: confidenceForEffects(preEffects)
1765
2088
  };
1766
2089
  const success = {
@@ -1771,12 +2094,13 @@ function transitionsFromAsyncHandler(source, fileName, attr, expression, setters
1771
2094
  guard: pendingIs(op),
1772
2095
  effect: { kind: "seq", effects: [{ kind: "dequeue", index: 0 }, ...successEffects] },
1773
2096
  reads: uniqueStrings(["sys:pending", ...successReads]),
1774
- writes: ["sys:pending", ...successEffects.map((effect) => effect.var)],
2097
+ writes: [...new Set(["sys:pending", ...successEffects.flatMap(effectWriteVars)])],
1775
2098
  confidence: confidenceForEffects(successEffects)
1776
2099
  };
1777
- const transitions = [enqueue, success];
2100
+ const successNavigate = firstNavigationInStatements(successStatements, routePatterns);
2101
+ const transitions = [enqueue, successNavigate ? appendEffect(success, navigationEffect(successNavigate)) : success];
1778
2102
  if (catchEffects.length > 0 || asyncOutcomes[op]?.error !== undefined) {
1779
- transitions.push({
2103
+ const errorTransition = {
1780
2104
  id: `${baseId}.error`,
1781
2105
  cls: "env",
1782
2106
  label: { kind: "resolve", op, outcome: "error" },
@@ -1784,9 +2108,10 @@ function transitionsFromAsyncHandler(source, fileName, attr, expression, setters
1784
2108
  guard: pendingIs(op),
1785
2109
  effect: { kind: "seq", effects: [{ kind: "dequeue", index: 0 }, ...catchEffects] },
1786
2110
  reads: uniqueStrings(["sys:pending", ...catchReads]),
1787
- writes: ["sys:pending", ...catchEffects.map((effect) => effect.var)],
2111
+ writes: [...new Set(["sys:pending", ...catchEffects.flatMap(effectWriteVars)])],
1788
2112
  confidence: confidenceForEffects(catchEffects)
1789
- });
2113
+ };
2114
+ transitions.push(errorTransition);
1790
2115
  }
1791
2116
  else {
1792
2117
  warnings.push({ message: `Unhandled rejection ${baseId}`, ...lineAndColumn(source, awaitStatement) });
@@ -1838,7 +2163,7 @@ function transitionsFromSequentialAwait(source, fileName, attr, expression, firs
1838
2163
  guard: promiseAllGuard(promiseAllOps),
1839
2164
  effect: { kind: "seq", effects: [...promiseAllOps.map((_, index) => ({ kind: "dequeue", index })).reverse(), ...tailEffects] },
1840
2165
  reads: uniqueStrings(["sys:pending", ...tailReads]),
1841
- writes: uniqueStrings(["sys:pending", ...tailEffects.map((effect) => effect.var)]),
2166
+ writes: uniqueStrings(["sys:pending", ...tailEffects.flatMap(effectWriteVars)]),
1842
2167
  confidence: confidenceForEffects(tailEffects)
1843
2168
  }
1844
2169
  : {
@@ -1849,7 +2174,7 @@ function transitionsFromSequentialAwait(source, fileName, attr, expression, firs
1849
2174
  guard: pendingIs(secondOp),
1850
2175
  effect: { kind: "seq", effects: [{ kind: "dequeue", index: 0 }, ...tailEffects] },
1851
2176
  reads: uniqueStrings(["sys:pending", ...tailReads]),
1852
- writes: uniqueStrings(["sys:pending", ...tailEffects.map((effect) => effect.var)]),
2177
+ writes: uniqueStrings(["sys:pending", ...tailEffects.flatMap(effectWriteVars)]),
1853
2178
  confidence: confidenceForEffects(tailEffects)
1854
2179
  };
1855
2180
  const transitions = [
@@ -1861,7 +2186,7 @@ function transitionsFromSequentialAwait(source, fileName, attr, expression, firs
1861
2186
  guard: { kind: "lit", value: true },
1862
2187
  effect: { kind: "seq", effects: [...preEffects, { kind: "enqueue", op: firstOp, continuation: `${firstBaseId}.cont`, args: {} }] },
1863
2188
  reads: preReads,
1864
- writes: uniqueStrings([...preEffects.map((effect) => effect.var), "sys:pending"]),
2189
+ writes: uniqueStrings([...preEffects.flatMap(effectWriteVars), "sys:pending"]),
1865
2190
  confidence: confidenceForEffects(preEffects)
1866
2191
  },
1867
2192
  {
@@ -1872,7 +2197,7 @@ function transitionsFromSequentialAwait(source, fileName, attr, expression, firs
1872
2197
  guard: pendingIs(firstOp),
1873
2198
  effect: { kind: "seq", effects: [{ kind: "dequeue", index: 0 }, ...betweenEffects, ...secondEnqueueEffects] },
1874
2199
  reads: uniqueStrings(["sys:pending", ...betweenReads]),
1875
- writes: uniqueStrings(["sys:pending", ...betweenEffects.map((effect) => effect.var)]),
2200
+ writes: uniqueStrings(["sys:pending", ...betweenEffects.flatMap(effectWriteVars)]),
1876
2201
  confidence: confidenceForEffects(betweenEffects)
1877
2202
  },
1878
2203
  secondSuccess
@@ -1965,14 +2290,14 @@ function transitionsFromUseEffect(source, fileName, node, setters, component) {
1965
2290
  const guards = assignEffects.map((effect) => ({ kind: "neq", args: [{ kind: "read", var: effect.var }, effect.expr] }));
1966
2291
  const guard = guards.length > 0 ? guards.slice(1).reduce((acc, next) => andGuard(acc, next), guards[0]) : { kind: "lit", value: true };
1967
2292
  transitions.push({
1968
- id: `${component}.useEffect.${effects.map((effect) => effect.var.split(".").at(-1) ?? effect.var).join("_")}`,
2293
+ id: `${component}.useEffect.${effects.flatMap(effectWriteVars).map((varId) => varId.split(".").at(-1) ?? varId).join("_")}`,
1969
2294
  cls: "internal",
1970
2295
  label: { kind: "internal", text: `${component}.useEffect` },
1971
2296
  source: [{ file: fileName, ...lineAndColumn(source, node) }],
1972
2297
  guard,
1973
2298
  effect: effects.length === 1 ? effects[0] : { kind: "seq", effects },
1974
- reads: uniqueStrings([...deps, ...effectReads, ...effects.map((effect) => effect.var)]),
1975
- writes: [...new Set(effects.map((effect) => effect.var))],
2299
+ reads: uniqueStrings([...deps, ...effectReads, ...effects.flatMap(effectWriteVars)]),
2300
+ writes: uniqueStrings(effects.flatMap(effectWriteVars)),
1976
2301
  confidence: effects.some((effect) => effect.kind === "havoc") ? "over-approx" : "exact",
1977
2302
  triggeredBy: deps
1978
2303
  });
@@ -1981,14 +2306,14 @@ function transitionsFromUseEffect(source, fileName, node, setters, component) {
1981
2306
  const cleanupEffects = cleanup.map((summary) => summary.effect);
1982
2307
  const cleanupReads = uniqueStrings(cleanup.flatMap((summary) => summary.reads));
1983
2308
  transitions.push({
1984
- id: `${component}.useEffect.cleanup.${cleanupEffects.map((effect) => effect.var.split(".").at(-1) ?? effect.var).join("_")}`,
2309
+ id: `${component}.useEffect.cleanup.${cleanupEffects.flatMap(effectWriteVars).map((varId) => varId.split(".").at(-1) ?? varId).join("_")}`,
1985
2310
  cls: "internal",
1986
2311
  label: { kind: "internal", text: `${component}.useEffect.cleanup` },
1987
2312
  source: [{ file: fileName, ...lineAndColumn(source, node) }],
1988
2313
  guard: { kind: "lit", value: true },
1989
2314
  effect: cleanupEffects.length === 1 ? cleanupEffects[0] : { kind: "seq", effects: cleanupEffects },
1990
2315
  reads: cleanupReads,
1991
- writes: [...new Set(cleanupEffects.map((effect) => effect.var))],
2316
+ writes: uniqueStrings(cleanupEffects.flatMap(effectWriteVars)),
1992
2317
  confidence: "over-approx",
1993
2318
  triggeredBy: deps
1994
2319
  });
@@ -2415,7 +2740,7 @@ function containsAwaitedEffect(statements, effectApis) {
2415
2740
  }
2416
2741
  if (insideAwait && ts.isCallExpression(node)) {
2417
2742
  const name = callName(node.expression);
2418
- if (name && effectApis.has(name)) {
2743
+ if (name && effectApis.has(effectOpForCall(name, node))) {
2419
2744
  found = true;
2420
2745
  return;
2421
2746
  }
@@ -2430,13 +2755,94 @@ function awaitedOp(statement, effectApis) {
2430
2755
  return awaitedCall(statement, effectApis)?.op;
2431
2756
  }
2432
2757
  function awaitedCall(statement, effectApis) {
2433
- if (!ts.isExpressionStatement(statement))
2758
+ const awaitExpression = awaitedCallExpressionInStatement(statement);
2759
+ if (!awaitExpression)
2434
2760
  return undefined;
2435
- const expression = statement.expression;
2436
- if (!ts.isAwaitExpression(expression) || !ts.isCallExpression(expression.expression))
2761
+ const name = callName(awaitExpression.expression);
2762
+ if (!name)
2763
+ return undefined;
2764
+ const op = effectOpForCall(name, awaitExpression);
2765
+ if (!effectApis.has(op))
2437
2766
  return undefined;
2438
- const name = callName(expression.expression.expression);
2439
- return name && effectApis.has(name) ? { op: name, call: expression.expression } : undefined;
2767
+ return { op, call: awaitExpression };
2768
+ }
2769
+ function awaitedCallExpressionInStatement(statement) {
2770
+ if (ts.isExpressionStatement(statement) && ts.isAwaitExpression(statement.expression) && ts.isCallExpression(statement.expression.expression)) {
2771
+ return statement.expression.expression;
2772
+ }
2773
+ if (ts.isVariableStatement(statement)) {
2774
+ for (const declaration of statement.declarationList.declarations) {
2775
+ if (declaration.initializer &&
2776
+ ts.isAwaitExpression(declaration.initializer) &&
2777
+ ts.isCallExpression(declaration.initializer.expression) &&
2778
+ callName(declaration.initializer.expression.expression) === "fetch") {
2779
+ return declaration.initializer.expression;
2780
+ }
2781
+ }
2782
+ }
2783
+ return undefined;
2784
+ }
2785
+ function effectOpForCall(name, call) {
2786
+ if (name !== "fetch")
2787
+ return name;
2788
+ const url = fetchUrl(call.arguments[0]);
2789
+ const method = fetchMethod(call.arguments[1]) ?? "GET";
2790
+ return url ? `${method} ${url}` : "fetch";
2791
+ }
2792
+ function fetchUrl(argument) {
2793
+ if (!argument)
2794
+ return undefined;
2795
+ if (ts.isStringLiteral(argument) || ts.isNoSubstitutionTemplateLiteral(argument))
2796
+ return normalizeFetchUrl(argument.text);
2797
+ if (ts.isTemplateExpression(argument)) {
2798
+ const pattern = templateRoutePattern(argument);
2799
+ return pattern ? normalizeFetchUrl(pattern.replace(/\/:param(?=\/|$)/g, "/:id")) : undefined;
2800
+ }
2801
+ return undefined;
2802
+ }
2803
+ function normalizeFetchUrl(value) {
2804
+ return value.startsWith("/") ? value : `/${value}`;
2805
+ }
2806
+ function fetchMethod(argument) {
2807
+ if (!argument || !ts.isObjectLiteralExpression(argument))
2808
+ return undefined;
2809
+ const method = argument.properties.find((property) => ts.isPropertyAssignment(property) && propertyName(property.name) === "method");
2810
+ const value = method ? literalValue(method.initializer) : undefined;
2811
+ return typeof value === "string" ? value.toUpperCase() : undefined;
2812
+ }
2813
+ function firstNavigationInStatements(statements, routePatterns) {
2814
+ for (const statement of statements) {
2815
+ let found;
2816
+ const visit = (node) => {
2817
+ if (found)
2818
+ return;
2819
+ if (ts.isCallExpression(node))
2820
+ found = navigationCall(node, undefined, routePatterns);
2821
+ ts.forEachChild(node, visit);
2822
+ };
2823
+ visit(statement);
2824
+ if (found)
2825
+ return found;
2826
+ }
2827
+ return undefined;
2828
+ }
2829
+ function navigationEffect(navigation) {
2830
+ return {
2831
+ kind: "navigate",
2832
+ mode: navigation.mode,
2833
+ ...(navigation.to ? { to: { kind: "lit", value: navigation.to } } : {})
2834
+ };
2835
+ }
2836
+ function appendEffect(transition, effect) {
2837
+ const current = transition.effect.kind === "seq" ? transition.effect.effects : [transition.effect];
2838
+ const writes = uniqueStrings([...transition.writes, ...effectWriteVars(effect)]);
2839
+ const reads = uniqueStrings([...transition.reads, ...(effect.kind === "navigate" ? ["sys:route", "sys:history"] : [])]);
2840
+ return {
2841
+ ...transition,
2842
+ effect: { kind: "seq", effects: [...current, effect] },
2843
+ reads,
2844
+ writes
2845
+ };
2440
2846
  }
2441
2847
  function effectCallArgs(call, setters, locals) {
2442
2848
  const first = call.arguments[0];
@@ -2485,7 +2891,7 @@ function promiseAllAwaitOps(statement, effectApis) {
2485
2891
  function callName(expression) {
2486
2892
  if (ts.isIdentifier(expression))
2487
2893
  return expression.text;
2488
- if (ts.isPropertyAccessExpression(expression))
2894
+ if (ts.isPropertyAccessExpression(expression) || ts.isPropertyAccessChain(expression))
2489
2895
  return `${callName(expression.expression) ?? expression.expression.getText()}.${expression.name.text}`;
2490
2896
  return undefined;
2491
2897
  }
@@ -2502,6 +2908,17 @@ function componentNameFor(node) {
2502
2908
  return node.name.text;
2503
2909
  return undefined;
2504
2910
  }
2911
+ function providerComponentNames(source) {
2912
+ const names = new Set();
2913
+ const visit = (node) => {
2914
+ const name = componentNameFor(node);
2915
+ if (name && node.getText(source).includes(".Provider"))
2916
+ names.add(name);
2917
+ ts.forEachChild(node, visit);
2918
+ };
2919
+ visit(source);
2920
+ return names;
2921
+ }
2505
2922
  function startsUppercase(value) {
2506
2923
  return /^[A-Z]/.test(value);
2507
2924
  }