modality-ts 0.0.2 → 0.0.4

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.
Files changed (66) hide show
  1. package/dist/checker/search/eval.d.ts.map +1 -1
  2. package/dist/checker/search/eval.js +49 -1
  3. package/dist/checker/search/eval.js.map +1 -1
  4. package/dist/checker/search/index.js +26 -7
  5. package/dist/checker/search/index.js.map +1 -1
  6. package/dist/extraction/index.d.ts +4 -2
  7. package/dist/extraction/index.d.ts.map +1 -1
  8. package/dist/extraction/index.js +246 -39
  9. package/dist/extraction/index.js.map +1 -1
  10. package/dist/extraction/pipeline/index.d.ts +2 -0
  11. package/dist/extraction/pipeline/index.d.ts.map +1 -1
  12. package/dist/extraction/pipeline/index.js +4 -2
  13. package/dist/extraction/pipeline/index.js.map +1 -1
  14. package/dist/harness/index.d.ts +29 -2
  15. package/dist/harness/index.d.ts.map +1 -1
  16. package/dist/harness/index.js +79 -9
  17. package/dist/harness/index.js.map +1 -1
  18. package/dist/kernel/overlay/index.d.ts +16 -1
  19. package/dist/kernel/overlay/index.d.ts.map +1 -1
  20. package/dist/kernel/overlay/index.js +56 -1
  21. package/dist/kernel/overlay/index.js.map +1 -1
  22. package/dist/kernel/report/types.d.ts +4 -0
  23. package/dist/kernel/report/types.d.ts.map +1 -1
  24. package/dist/modality/cli.js +15 -9
  25. package/dist/modality/cli.js.map +1 -1
  26. package/dist/modality/codegen/replay-test.d.ts.map +1 -1
  27. package/dist/modality/codegen/replay-test.js +13 -4
  28. package/dist/modality/codegen/replay-test.js.map +1 -1
  29. package/dist/modality/features/ci/command.d.ts +2 -0
  30. package/dist/modality/features/ci/command.d.ts.map +1 -1
  31. package/dist/modality/features/ci/command.js +2 -0
  32. package/dist/modality/features/ci/command.js.map +1 -1
  33. package/dist/modality/features/conform/command.d.ts +2 -0
  34. package/dist/modality/features/conform/command.d.ts.map +1 -1
  35. package/dist/modality/features/conform/command.js +91 -6
  36. package/dist/modality/features/conform/command.js.map +1 -1
  37. package/dist/modality/features/export/command.d.ts +29 -1
  38. package/dist/modality/features/export/command.d.ts.map +1 -1
  39. package/dist/modality/features/export/command.js +82 -1
  40. package/dist/modality/features/export/command.js.map +1 -1
  41. package/dist/modality/features/export/index.d.ts +2 -2
  42. package/dist/modality/features/export/index.d.ts.map +1 -1
  43. package/dist/modality/features/export/index.js +1 -1
  44. package/dist/modality/features/export/index.js.map +1 -1
  45. package/dist/modality/features/extract/command.d.ts.map +1 -1
  46. package/dist/modality/features/extract/command.js +75 -7
  47. package/dist/modality/features/extract/command.js.map +1 -1
  48. package/dist/modality/features/replay/command.d.ts +2 -0
  49. package/dist/modality/features/replay/command.d.ts.map +1 -1
  50. package/dist/modality/features/replay/command.js +89 -6
  51. package/dist/modality/features/replay/command.js.map +1 -1
  52. package/dist/modality/overlay.d.ts +2 -1
  53. package/dist/modality/overlay.d.ts.map +1 -1
  54. package/dist/modality/overlay.js +13 -2
  55. package/dist/modality/overlay.js.map +1 -1
  56. package/dist/runtime/index.d.ts.map +1 -1
  57. package/dist/runtime/index.js +22 -2
  58. package/dist/runtime/index.js.map +1 -1
  59. package/dist/sources/jotai/index.d.ts.map +1 -1
  60. package/dist/sources/jotai/index.js +32 -9
  61. package/dist/sources/jotai/index.js.map +1 -1
  62. package/dist/sources/swr/index.d.ts +5 -1
  63. package/dist/sources/swr/index.d.ts.map +1 -1
  64. package/dist/sources/swr/index.js +112 -12
  65. package/dist/sources/swr/index.js.map +1 -1
  66. package/package.json +2 -2
@@ -1,4 +1,5 @@
1
1
  import * as ts from "typescript";
2
+ import { effectReads, effectWrites } from "modality-ts/kernel";
2
3
  export * from "./pipeline/index.js";
3
4
  export * from "./spi/index.js";
4
5
  function setterBindingFromDecl(decl) {
@@ -48,6 +49,8 @@ export function extractUseStateSkeleton(sourceText, options = {}) {
48
49
  const warnings = [];
49
50
  const route = options.route ?? "/";
50
51
  const effectApis = new Set(options.effectApis ?? []);
52
+ const sourcePlugins = options.sourcePlugins ?? [];
53
+ const routerPlugin = options.routerPlugin;
51
54
  const setters = new Map();
52
55
  const globalTaints = new Set();
53
56
  const components = componentDeclarations(source);
@@ -68,8 +71,10 @@ export function extractUseStateSkeleton(sourceText, options = {}) {
68
71
  if (!componentName && isCustomHookDeclaration(node))
69
72
  return;
70
73
  const nextComponent = componentNameFor(node) ?? componentName;
71
- if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && isExtractableHandler(node.initializer)) {
72
- handlers.set(node.name.text, node.initializer);
74
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) {
75
+ const handler = extractableHandlerInitializer(node.initializer);
76
+ if (handler)
77
+ handlers.set(node.name.text, handler);
73
78
  }
74
79
  if (ts.isVariableDeclaration(node) && node.initializer && isUseReducerCall(node.initializer)) {
75
80
  warnings.push({ message: `Unsupported useReducer ${nextComponent ?? "Anonymous"}.useReducer`, ...lineAndColumn(source, node) });
@@ -135,7 +140,7 @@ export function extractUseStateSkeleton(sourceText, options = {}) {
135
140
  }
136
141
  }
137
142
  if (ts.isJsxAttribute(node) && ts.isIdentifier(node.name) && node.initializer && isForwardablePropName(node.name.text) && !isIntrinsicJsxAttribute(node)) {
138
- const extracted = transitionsFromComponentPropAttribute(source, fileName, node, setters, handlers, components, nextComponent ?? "Anonymous", effectApis, options.asyncOutcomes ?? {}, warnings);
143
+ const extracted = transitionsFromComponentPropAttribute(source, fileName, node, setters, handlers, components, nextComponent ?? "Anonymous", effectApis, options.asyncOutcomes ?? {}, sourcePlugins, routerPlugin, warnings);
139
144
  transitions.push(...extracted);
140
145
  if (extracted.length === 0) {
141
146
  warnings.push({ message: `Unextractable handler ${nextComponent ?? "Anonymous"}.${node.name.text}`, ...lineAndColumn(source, node) });
@@ -165,7 +170,7 @@ export function extractUseStateSkeleton(sourceText, options = {}) {
165
170
  renderGuardFor(node, setters, warnings, source, nextComponent ?? "Anonymous", guardLocals),
166
171
  disabledGuardFor(node, setters, warnings, source, nextComponent ?? "Anonymous", guardLocals)
167
172
  ]);
168
- const extracted = transitionsFromJsxAttribute(source, fileName, node, setters, handlers, nextComponent ?? "Anonymous", effectApis, options.asyncOutcomes ?? {}, guard, warnings);
173
+ const extracted = transitionsFromJsxAttribute(source, fileName, node, setters, handlers, nextComponent ?? "Anonymous", effectApis, options.asyncOutcomes ?? {}, sourcePlugins, routerPlugin, guard, warnings);
169
174
  transitions.push(...extracted);
170
175
  if (extracted.length === 0 && !forwardsComponentProp(node, handlers, components.get(nextComponent ?? "")) && !handlerSchedulesModeledTimer(node, handlers, setters)) {
171
176
  warnings.push({ message: `Unextractable handler ${nextComponent ?? "Anonymous"}.${node.name.text}`, ...lineAndColumn(source, node) });
@@ -391,6 +396,15 @@ function isUseEffectCall(node) {
391
396
  function isExtractableHandler(node) {
392
397
  return ts.isArrowFunction(node) || ts.isFunctionExpression(node);
393
398
  }
399
+ function extractableHandlerInitializer(node) {
400
+ if (isExtractableHandler(node))
401
+ return node;
402
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === "useCallback") {
403
+ const callback = node.arguments[0];
404
+ return callback && isExtractableHandler(callback) ? callback : undefined;
405
+ }
406
+ return undefined;
407
+ }
394
408
  function refSetterTaint(node, setters) {
395
409
  if (ts.isVariableDeclaration(node) && node.initializer && isUseRefCall(node.initializer)) {
396
410
  const arg = node.initializer.arguments[0];
@@ -490,7 +504,7 @@ function handlerSchedulesModeledTimer(attribute, handlers, setters) {
490
504
  visit(handler.body);
491
505
  return found;
492
506
  }
493
- function transitionsFromJsxAttribute(source, fileName, node, setters, handlers, component, effectApis, asyncOutcomes, disabledGuard, warnings) {
507
+ function transitionsFromJsxAttribute(source, fileName, node, setters, handlers, component, effectApis, asyncOutcomes, sourcePlugins, routerPlugin, disabledGuard, warnings) {
494
508
  if (!node.initializer)
495
509
  return [];
496
510
  const expression = ts.isJsxExpression(node.initializer) ? node.initializer.expression : undefined;
@@ -501,9 +515,9 @@ function transitionsFromJsxAttribute(source, fileName, node, setters, handlers,
501
515
  return [];
502
516
  const attr = node.name.text;
503
517
  const locator = locatorForEventAttribute(node);
504
- return tagStableIdKey(transitionsFromResolvedHandler(source, fileName, node, attr, handler, setters, handlers, component, effectApis, asyncOutcomes, disabledGuard, locator, warnings), handler);
518
+ return tagStableIdKey(transitionsFromResolvedHandler(source, fileName, node, attr, handler, setters, handlers, component, effectApis, asyncOutcomes, sourcePlugins, routerPlugin, disabledGuard, locator, warnings), handler);
505
519
  }
506
- function transitionsFromComponentPropAttribute(source, fileName, node, setters, handlers, components, component, effectApis, asyncOutcomes, warnings) {
520
+ function transitionsFromComponentPropAttribute(source, fileName, node, setters, handlers, components, component, effectApis, asyncOutcomes, sourcePlugins, routerPlugin, warnings) {
507
521
  if (!node.initializer || !ts.isIdentifier(node.name))
508
522
  return [];
509
523
  const tag = jsxTagName(node);
@@ -524,7 +538,7 @@ function transitionsFromComponentPropAttribute(source, fileName, node, setters,
524
538
  renderGuardFor(node, setters, warnings, source, component, guardLocals),
525
539
  disabledGuardFor(node, setters, warnings, source, component, guardLocals)
526
540
  ]);
527
- return tagStableIdKey(transitionsFromResolvedHandler(source, fileName, node, trigger.attr, handler, setters, handlers, component, effectApis, asyncOutcomes, combineParsedGuards([trigger.guard, callerGuard]), trigger.locator, warnings), handler);
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);
528
542
  }
529
543
  function transitionsFromBoundedListAttribute(source, fileName, node, setters, handlers, component, listInfo) {
530
544
  if (!node.initializer || !ts.isIdentifier(node.name))
@@ -584,7 +598,7 @@ function normalizedAstKey(node) {
584
598
  .replace(/\/\/.*$/gm, "")
585
599
  .replace(/\s+/g, "");
586
600
  }
587
- function transitionsFromResolvedHandler(source, fileName, node, attr, handler, setters, handlers, component, effectApis, asyncOutcomes, disabledGuard, locator, warnings) {
601
+ function transitionsFromResolvedHandler(source, fileName, node, attr, handler, setters, handlers, component, effectApis, asyncOutcomes, sourcePlugins, routerPlugin, disabledGuard, locator, warnings) {
588
602
  const asyncTransitions = transitionsFromAsyncHandler(source, fileName, attr, handler, setters, component, effectApis, asyncOutcomes, locator, warnings);
589
603
  if (asyncTransitions.length > 0)
590
604
  return applyParsedGuard(asyncTransitions, disabledGuard);
@@ -594,7 +608,7 @@ function transitionsFromResolvedHandler(source, fileName, node, attr, handler, s
594
608
  const loopTransitions = loopWriteTransitions(source, fileName, node, attr, handler, setters, component, locator);
595
609
  if (loopTransitions.length > 0)
596
610
  return applyParsedGuard(loopTransitions, disabledGuard);
597
- const sequentialTransition = sequentialTransitionFromHandler(source, fileName, node, attr, handler, setters, component, locator);
611
+ const sequentialTransition = sequentialTransitionFromHandler(source, fileName, node, attr, handler, setters, handlers, component, locator);
598
612
  if (sequentialTransition)
599
613
  return applyParsedGuard([sequentialTransition], disabledGuard);
600
614
  const summary = callSummaryFromHandler(handler, setters);
@@ -603,12 +617,18 @@ function transitionsFromResolvedHandler(source, fileName, node, attr, handler, s
603
617
  const inlined = inlinedHelperCall(summary.call, handlers, setters);
604
618
  const inlinedCall = inlined?.call ?? summary.call;
605
619
  const locals = inlined?.locals ?? summary.locals;
606
- const navigation = navigationTransition(source, fileName, node, attr, component, inlinedCall, locator);
620
+ const navigation = navigationTransition(source, fileName, node, attr, component, inlinedCall, locator, routerPlugin);
607
621
  if (navigation)
608
622
  return applyParsedGuard([navigation], disabledGuard);
623
+ const pluginWrite = pluginWriteTransition(source, fileName, node, attr, component, inlinedCall, setters, locals, sourcePlugins, locator);
624
+ if (pluginWrite)
625
+ return applyParsedGuard([pluginWrite], disabledGuard);
626
+ const swrMutate = swrMutateTransition(source, fileName, node, attr, component, inlinedCall, locator);
627
+ if (swrMutate)
628
+ return applyParsedGuard([swrMutate], disabledGuard);
609
629
  const setterCall = setterCallFrom(inlinedCall, setters);
610
630
  if (!setterCall) {
611
- const escaped = escapedSetters(inlinedCall, setters);
631
+ const escaped = escapedSetters(inlinedCall, setters, locals);
612
632
  if (escaped.length === 0)
613
633
  return [];
614
634
  return applyParsedGuard(escapedSetterTransitions(source, fileName, node, attr, component, escaped, locator), disabledGuard);
@@ -633,7 +653,7 @@ function transitionsFromResolvedHandler(source, fileName, node, attr, handler, s
633
653
  confidence: "exact"
634
654
  }], disabledGuard);
635
655
  }
636
- function sequentialTransitionFromHandler(source, fileName, node, attr, handler, setters, component, locator) {
656
+ function sequentialTransitionFromHandler(source, fileName, node, attr, handler, setters, handlers, component, locator) {
637
657
  if (!ts.isBlock(handler.body))
638
658
  return undefined;
639
659
  const locals = new Map();
@@ -641,6 +661,11 @@ function sequentialTransitionFromHandler(source, fileName, node, attr, handler,
641
661
  for (const statement of handler.body.statements) {
642
662
  if (bindConstStatement(statement, setters, locals))
643
663
  continue;
664
+ const helper = helperSummariesFromStatement(statement, handlers, setters);
665
+ if (helper) {
666
+ summaries.push(...helper);
667
+ continue;
668
+ }
644
669
  const summary = summarizeSetterStatement(statement, setters, locals);
645
670
  if (!summary)
646
671
  return undefined;
@@ -662,6 +687,23 @@ function sequentialTransitionFromHandler(source, fileName, node, attr, handler,
662
687
  confidence: effects.some((effect) => effect.kind === "havoc") ? "over-approx" : "exact"
663
688
  };
664
689
  }
690
+ function helperSummariesFromStatement(statement, handlers, setters) {
691
+ if (!ts.isExpressionStatement(statement) || !ts.isCallExpression(statement.expression) || !ts.isIdentifier(statement.expression.expression))
692
+ return undefined;
693
+ const helper = handlers.get(statement.expression.expression.text);
694
+ if (!helper || !ts.isBlock(helper.body))
695
+ return undefined;
696
+ const locals = new Map();
697
+ const summaries = [];
698
+ for (const child of helper.body.statements) {
699
+ if (bindConstStatement(child, setters, locals))
700
+ continue;
701
+ const summary = summarizeSetterStatement(child, setters, locals);
702
+ if (summary)
703
+ summaries.push(summary);
704
+ }
705
+ return summaries.length > 0 ? summaries : undefined;
706
+ }
665
707
  function loopWriteTransitions(source, fileName, node, attr, handler, setters, component, locator) {
666
708
  if (!ts.isBlock(handler.body))
667
709
  return [];
@@ -727,6 +769,11 @@ function havocSetterTransition(source, fileName, node, attr, component, setter,
727
769
  };
728
770
  }
729
771
  function setterArgumentExpr(argument, setter, setters, locals) {
772
+ if (ts.isObjectLiteralExpression(argument)) {
773
+ const object = objectLiteralAssignmentExpr(argument, setter.domain, setters, locals);
774
+ if (object)
775
+ return object;
776
+ }
730
777
  if ((ts.isArrowFunction(argument) || ts.isFunctionExpression(argument)) && argument.parameters.length === 1 && ts.isIdentifier(argument.parameters[0].name)) {
731
778
  if (ts.isBlock(argument.body))
732
779
  return undefined;
@@ -734,11 +781,42 @@ function setterArgumentExpr(argument, setter, setters, locals) {
734
781
  }
735
782
  return valueExpr(argument, setters, locals);
736
783
  }
784
+ function objectLiteralAssignmentExpr(expression, domain, setters, locals) {
785
+ const value = {};
786
+ const reads = new Set();
787
+ const fields = domain.kind === "record" ? domain.fields : domain.kind === "tagged" ? taggedFieldsForObject(expression, domain) : {};
788
+ for (const property of expression.properties) {
789
+ if (!ts.isPropertyAssignment(property))
790
+ return undefined;
791
+ const name = propertyName(property.name);
792
+ if (!name)
793
+ return undefined;
794
+ const literal = literalValue(property.initializer);
795
+ if (literal !== undefined) {
796
+ value[name] = literal;
797
+ continue;
798
+ }
799
+ const bound = valueExpr(property.initializer, setters, locals);
800
+ if (bound?.expr.kind === "lit") {
801
+ value[name] = bound.expr.value;
802
+ bound.reads.forEach((read) => reads.add(read));
803
+ continue;
804
+ }
805
+ value[name] = firstValue(fields[name] ?? { kind: "tokens", count: 1 });
806
+ }
807
+ return { expr: { kind: "lit", value }, reads: [...reads] };
808
+ }
809
+ function taggedFieldsForObject(expression, domain) {
810
+ const tagProperty = expression.properties.find((property) => ts.isPropertyAssignment(property) && propertyName(property.name) === domain.tag);
811
+ const tag = tagProperty ? literalValue(tagProperty.initializer) : undefined;
812
+ const variant = typeof tag === "string" ? domain.variants[tag] : undefined;
813
+ return variant?.kind === "record" ? variant.fields : {};
814
+ }
737
815
  function valueExpr(expression, setters, locals) {
738
816
  const value = literalValue(expression);
739
817
  if (value !== undefined)
740
818
  return { expr: { kind: "lit", value }, reads: [] };
741
- if (ts.isIdentifier(expression) || ts.isPropertyAccessExpression(expression))
819
+ if (ts.isIdentifier(expression) || isPropertyAccessLike(expression))
742
820
  return modeledReadExpr(expression, setters, locals);
743
821
  if (ts.isPrefixUnaryExpression(expression) && expression.operator === ts.SyntaxKind.ExclamationToken) {
744
822
  const parsed = booleanExpr(expression.operand, setters, locals);
@@ -746,8 +824,12 @@ function valueExpr(expression, setters, locals) {
746
824
  }
747
825
  if (ts.isParenthesizedExpression(expression))
748
826
  return valueExpr(expression.expression, setters, locals);
827
+ if (ts.isBinaryExpression(expression) &&
828
+ (expression.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken || expression.operatorToken.kind === ts.SyntaxKind.BarBarToken)) {
829
+ return booleanExpr(expression, setters, locals);
830
+ }
749
831
  if (ts.isBinaryExpression(expression) && expression.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken) {
750
- return nullishOptionalReadExpr(expression, setters);
832
+ return nullishOptionalReadExpr(expression, setters, locals);
751
833
  }
752
834
  if (ts.isConditionalExpression(expression)) {
753
835
  const condition = booleanExpr(expression.condition, setters, locals);
@@ -791,22 +873,25 @@ function objectSpreadUpdateExpr(expression, setters, locals) {
791
873
  }
792
874
  return current;
793
875
  }
794
- function nullishOptionalReadExpr(expression, setters) {
795
- if (literalValue(expression.right) !== null)
876
+ function nullishOptionalReadExpr(expression, setters, locals = new Map()) {
877
+ const fallback = literalValue(expression.right);
878
+ if (fallback === undefined)
796
879
  return undefined;
797
880
  const read = optionalReadPath(expression.left);
798
881
  if (!read?.optional || read.path.length === 0)
799
882
  return undefined;
800
- const varId = stateVarForName(read.base, setters);
883
+ const local = locals.get(read.base);
884
+ const varId = local?.expr.kind === "read" ? local.expr.var : stateVarForName(read.base, setters);
801
885
  if (!varId)
802
886
  return undefined;
887
+ const basePath = local?.expr.kind === "read" ? local.expr.path ?? [] : [];
803
888
  return {
804
889
  expr: {
805
890
  kind: "cond",
806
891
  args: [
807
- { kind: "eq", args: [{ kind: "read", var: varId }, { kind: "lit", value: null }] },
808
- { kind: "lit", value: null },
809
- { kind: "read", var: varId, path: read.path }
892
+ { kind: "eq", args: [{ kind: "read", var: varId, ...(basePath.length > 0 ? { path: basePath } : {}) }, { kind: "lit", value: null }] },
893
+ { kind: "lit", value: fallback },
894
+ { kind: "read", var: varId, path: [...basePath, ...read.path] }
810
895
  ]
811
896
  },
812
897
  reads: [varId]
@@ -815,7 +900,7 @@ function nullishOptionalReadExpr(expression, setters) {
815
900
  function optionalReadPath(expression) {
816
901
  if (ts.isIdentifier(expression))
817
902
  return { base: expression.text, path: [], optional: false };
818
- if (ts.isPropertyAccessExpression(expression)) {
903
+ if (isPropertyAccessLike(expression)) {
819
904
  const base = optionalReadPath(expression.expression);
820
905
  if (!base)
821
906
  return undefined;
@@ -891,9 +976,13 @@ function modeledReadExpr(expression, setters, locals) {
891
976
  reads: local.reads
892
977
  };
893
978
  }
894
- const stateVar = stateVarForName(base, setters);
979
+ const setter = setterForName(base, setters);
980
+ const stateVar = setter?.varId;
895
981
  if (!stateVar)
896
982
  return undefined;
983
+ if (setter.domain.kind === "tagged" && segments.length > 0 && segments[0] !== setter.domain.tag) {
984
+ return { expr: { kind: "lit", value: firstValue(taggedPathDomain(setter.domain, segments) ?? { kind: "tokens", count: 1 }) }, reads: [] };
985
+ }
897
986
  return {
898
987
  expr: { kind: "read", var: stateVar, ...(segments.length > 0 ? { path: segments } : {}) },
899
988
  reads: [stateVar]
@@ -980,6 +1069,8 @@ function callSummaryFromHandler(handler, setters, initialLocals = new Map()) {
980
1069
  const body = handler.body;
981
1070
  if (ts.isCallExpression(body))
982
1071
  return { call: body, locals: new Map(initialLocals) };
1072
+ if (ts.isVoidExpression(body) && ts.isCallExpression(body.expression))
1073
+ return { call: body.expression, locals: new Map(initialLocals) };
983
1074
  if (ts.isBlock(body)) {
984
1075
  const locals = new Map(initialLocals);
985
1076
  for (let index = 0; index < body.statements.length; index += 1) {
@@ -987,12 +1078,89 @@ function callSummaryFromHandler(handler, setters, initialLocals = new Map()) {
987
1078
  const isLast = index === body.statements.length - 1;
988
1079
  if (isLast && ts.isExpressionStatement(statement) && ts.isCallExpression(statement.expression))
989
1080
  return { call: statement.expression, locals };
1081
+ if (isLast && ts.isExpressionStatement(statement) && ts.isVoidExpression(statement.expression) && ts.isCallExpression(statement.expression.expression))
1082
+ return { call: statement.expression.expression, locals };
990
1083
  if (!bindConstStatement(statement, setters, locals))
991
1084
  return undefined;
992
1085
  }
993
1086
  }
994
1087
  return undefined;
995
1088
  }
1089
+ function pluginWriteTransition(source, fileName, node, attr, component, call, setters, locals, sourcePlugins, locator) {
1090
+ const callee = callName(call.expression);
1091
+ if (!callee)
1092
+ return undefined;
1093
+ const ctx = {
1094
+ read: (name, path) => {
1095
+ const local = locals.get(name);
1096
+ if (local?.expr.kind === "read") {
1097
+ return { kind: "read", var: local.expr.var, path: [...(local.expr.path ?? []), ...(path ?? [])] };
1098
+ }
1099
+ const varId = stateVarForName(name, setters) ?? name;
1100
+ return { kind: "read", var: varId, ...(path && path.length > 0 ? { path } : {}) };
1101
+ },
1102
+ locator
1103
+ };
1104
+ const callSite = {
1105
+ callee,
1106
+ arguments: call.arguments.map(callArgumentValue),
1107
+ source: { file: fileName, ...lineAndColumn(source, call) }
1108
+ };
1109
+ for (const plugin of sourcePlugins) {
1110
+ const summary = plugin.summarizeWrite?.(callSite, ctx);
1111
+ if (!summary || summary === "unsupported")
1112
+ continue;
1113
+ const reads = [...effectReads(summary)].sort();
1114
+ const writes = [...effectWrites(summary)].sort();
1115
+ return {
1116
+ id: `${component}.${attr}.${safeId(plugin.id)}.${safeId(callee)}`,
1117
+ cls: "user",
1118
+ label: labelForEvent(attr, locator),
1119
+ source: [{ file: fileName, ...lineAndColumn(source, node) }],
1120
+ guard: { kind: "lit", value: true },
1121
+ effect: summary,
1122
+ reads,
1123
+ writes,
1124
+ confidence: "exact"
1125
+ };
1126
+ }
1127
+ return undefined;
1128
+ }
1129
+ function callArgumentValue(argument) {
1130
+ const literal = literalValue(argument);
1131
+ if (literal !== undefined)
1132
+ return literal;
1133
+ if (ts.isIdentifier(argument))
1134
+ return argument.text;
1135
+ if (ts.isObjectLiteralExpression(argument)) {
1136
+ const fields = {};
1137
+ for (const property of argument.properties) {
1138
+ if (!ts.isPropertyAssignment(property))
1139
+ return argument.getText();
1140
+ const name = propertyName(property.name);
1141
+ if (!name)
1142
+ return argument.getText();
1143
+ fields[name] = callArgumentValue(property.initializer);
1144
+ }
1145
+ return fields;
1146
+ }
1147
+ return argument.getText();
1148
+ }
1149
+ function swrMutateTransition(source, fileName, node, attr, component, call, locator) {
1150
+ if (!ts.isIdentifier(call.expression) || call.expression.text !== "mutate")
1151
+ return undefined;
1152
+ return {
1153
+ id: `${component}.${attr}.mutate`,
1154
+ cls: "user",
1155
+ label: labelForEvent(attr, locator),
1156
+ source: [{ file: fileName, ...lineAndColumn(source, node) }],
1157
+ guard: { kind: "lit", value: true },
1158
+ effect: { kind: "seq", effects: [] },
1159
+ reads: [],
1160
+ writes: [],
1161
+ confidence: "exact"
1162
+ };
1163
+ }
996
1164
  function bindConstStatement(statement, setters, locals, partialBoolean = false) {
997
1165
  if (!ts.isVariableStatement(statement))
998
1166
  return false;
@@ -1001,7 +1169,8 @@ function bindConstStatement(statement, setters, locals, partialBoolean = false)
1001
1169
  for (const declaration of statement.declarationList.declarations) {
1002
1170
  if (!ts.isIdentifier(declaration.name) || !declaration.initializer)
1003
1171
  return false;
1004
- const binding = valueExpr(declaration.initializer, setters, locals) ??
1172
+ const setterAlias = ts.isIdentifier(declaration.initializer) ? setters.get(declaration.initializer.text) ?? locals.get(declaration.initializer.text)?.setter : undefined;
1173
+ const binding = setterAlias ? { expr: { kind: "lit", value: null }, reads: [], setter: setterAlias } : valueExpr(declaration.initializer, setters, locals) ??
1005
1174
  (partialBoolean ? parseConjunctiveGuardExpression(declaration.initializer, setters, locals) : booleanExpr(declaration.initializer, setters, locals));
1006
1175
  if (!binding)
1007
1176
  return false;
@@ -1015,8 +1184,8 @@ function inlinedHelperCall(call, handlers, setters) {
1015
1184
  const helper = handlers.get(call.expression.text);
1016
1185
  return helper ? callSummaryFromHandler(helper, setters) : undefined;
1017
1186
  }
1018
- function navigationTransition(source, fileName, node, attr, component, call, locator) {
1019
- const navigation = navigationCall(call);
1187
+ function navigationTransition(source, fileName, node, attr, component, call, locator, routerPlugin) {
1188
+ const navigation = navigationCall(call, routerPlugin);
1020
1189
  if (!navigation)
1021
1190
  return undefined;
1022
1191
  const routeId = navigation.to ? safeId(navigation.to) : "back";
@@ -1040,10 +1209,13 @@ function navigationTransition(source, fileName, node, attr, component, call, loc
1040
1209
  confidence: "exact"
1041
1210
  };
1042
1211
  }
1043
- function navigationCall(call) {
1212
+ function navigationCall(call, routerPlugin) {
1044
1213
  const name = callName(call.expression);
1045
1214
  if (!name)
1046
1215
  return undefined;
1216
+ const pluginNavigation = routerPlugin?.navigationCall(name, call.arguments.map(callArgumentValue));
1217
+ if (pluginNavigation && pluginNavigation !== "unsupported")
1218
+ return pluginNavigation;
1047
1219
  if (name === "navigate" && call.arguments.length === 1) {
1048
1220
  const to = literalValue(call.arguments[0]);
1049
1221
  return typeof to === "string" ? { mode: "push", to } : undefined;
@@ -1059,10 +1231,10 @@ function navigationCall(call) {
1059
1231
  }
1060
1232
  return undefined;
1061
1233
  }
1062
- function escapedSetters(call, setters) {
1234
+ function escapedSetters(call, setters, locals = new Map()) {
1063
1235
  return call.arguments
1064
1236
  .filter(ts.isIdentifier)
1065
- .map((arg) => setters.get(arg.text))
1237
+ .map((arg) => setters.get(arg.text) ?? locals.get(arg.text)?.setter)
1066
1238
  .filter((setter) => Boolean(setter));
1067
1239
  }
1068
1240
  function escapedSetterTransitions(source, fileName, node, attr, component, setters, locator) {
@@ -1892,6 +2064,7 @@ function renderGuardFor(eventAttribute, setters, warnings, source, component, lo
1892
2064
  const element = jsxElementForAttribute(eventAttribute);
1893
2065
  if (!element)
1894
2066
  return undefined;
2067
+ const guards = [];
1895
2068
  let current = element;
1896
2069
  while (current.parent) {
1897
2070
  const parent = current.parent;
@@ -1903,7 +2076,9 @@ function renderGuardFor(eventAttribute, setters, warnings, source, component, lo
1903
2076
  warnings.push({ message: `Unsupported render guard ${component}.${eventAttribute.name.getText(source)}`, ...lineAndColumn(source, parent.left) });
1904
2077
  return undefined;
1905
2078
  }
1906
- return parsed;
2079
+ guards.push(parsed);
2080
+ current = parent;
2081
+ continue;
1907
2082
  }
1908
2083
  if (ts.isConditionalExpression(parent) && parent.whenTrue === current) {
1909
2084
  const parsed = parseConjunctiveGuardExpression(parent.condition, setters, locals);
@@ -1911,7 +2086,9 @@ function renderGuardFor(eventAttribute, setters, warnings, source, component, lo
1911
2086
  warnings.push({ message: `Unsupported render guard ${component}.${eventAttribute.name.getText(source)}`, ...lineAndColumn(source, parent.condition) });
1912
2087
  return undefined;
1913
2088
  }
1914
- return parsed;
2089
+ guards.push(parsed);
2090
+ current = parent;
2091
+ continue;
1915
2092
  }
1916
2093
  if (ts.isConditionalExpression(parent) && parent.whenFalse === current) {
1917
2094
  const parsed = parseConjunctiveGuardExpression(parent.condition, setters, locals);
@@ -1919,15 +2096,17 @@ function renderGuardFor(eventAttribute, setters, warnings, source, component, lo
1919
2096
  warnings.push({ message: `Unsupported render guard ${component}.${eventAttribute.name.getText(source)}`, ...lineAndColumn(source, parent.condition) });
1920
2097
  return undefined;
1921
2098
  }
1922
- return { expr: { kind: "not", args: [parsed.expr] }, reads: parsed.reads };
2099
+ guards.push({ expr: { kind: "not", args: [parsed.expr] }, reads: parsed.reads });
2100
+ current = parent;
2101
+ continue;
1923
2102
  }
1924
- if (ts.isParenthesizedExpression(parent) || ts.isJsxExpression(parent)) {
2103
+ if (ts.isParenthesizedExpression(parent) || ts.isJsxExpression(parent) || ts.isJsxElement(parent) || ts.isJsxFragment(parent)) {
1925
2104
  current = parent;
1926
2105
  continue;
1927
2106
  }
1928
- return undefined;
2107
+ return combineParsedGuards(guards);
1929
2108
  }
1930
- return undefined;
2109
+ return combineParsedGuards(guards);
1931
2110
  }
1932
2111
  function jsxElementForAttribute(attribute) {
1933
2112
  const attrs = attribute.parent;
@@ -1989,7 +2168,7 @@ function parseGuardExpression(expression, setters, locals = new Map()) {
1989
2168
  return { expr: { kind: "lit", value: true }, reads: [] };
1990
2169
  if (expression.kind === ts.SyntaxKind.FalseKeyword)
1991
2170
  return { expr: { kind: "lit", value: false }, reads: [] };
1992
- if (ts.isIdentifier(expression) || ts.isPropertyAccessExpression(expression))
2171
+ if (ts.isIdentifier(expression) || isPropertyAccessLike(expression))
1993
2172
  return valueExpr(expression, setters, locals);
1994
2173
  if (ts.isPrefixUnaryExpression(expression) && expression.operator === ts.SyntaxKind.ExclamationToken) {
1995
2174
  const parsed = parseGuardExpression(expression.operand, setters, locals);
@@ -2034,7 +2213,7 @@ function parseGuardOperand(expression, setters, locals = new Map()) {
2034
2213
  const value = literalValue(expression);
2035
2214
  if (value !== undefined)
2036
2215
  return { expr: { kind: "lit", value }, reads: [] };
2037
- if (ts.isIdentifier(expression) || ts.isPropertyAccessExpression(expression))
2216
+ if (ts.isIdentifier(expression) || isPropertyAccessLike(expression))
2038
2217
  return valueExpr(expression, setters, locals);
2039
2218
  return parseGuardExpression(expression, setters, locals);
2040
2219
  }
@@ -2050,7 +2229,32 @@ function parseConjunctiveGuardExpression(expression, setters, locals = new Map()
2050
2229
  return parseGuardExpression(expression, setters, locals);
2051
2230
  }
2052
2231
  function stateVarForName(name, setters) {
2053
- return [...setters.values()].find((setter) => setter.stateName === name)?.varId;
2232
+ return setterForName(name, setters)?.varId;
2233
+ }
2234
+ function setterForName(name, setters) {
2235
+ return setters.get(name) ?? [...setters.values()].find((setter) => setter.stateName === name);
2236
+ }
2237
+ function taggedPathDomain(domain, path) {
2238
+ const [field, ...rest] = path;
2239
+ if (!field)
2240
+ return domain;
2241
+ const variants = Object.values(domain.variants).filter((variant) => variant.kind === "record");
2242
+ const fieldDomains = variants.map((variant) => variant.fields[field]).filter((candidate) => Boolean(candidate));
2243
+ if (fieldDomains.length === 0)
2244
+ return undefined;
2245
+ const first = fieldDomains[0];
2246
+ if (rest.length === 0)
2247
+ return first;
2248
+ return first.kind === "record" ? domainAtRecordPath(first, rest) : undefined;
2249
+ }
2250
+ function domainAtRecordPath(domain, path) {
2251
+ const [field, ...rest] = path;
2252
+ if (!field)
2253
+ return domain;
2254
+ const next = domain.fields[field];
2255
+ if (!next || rest.length === 0)
2256
+ return next;
2257
+ return next.kind === "record" ? domainAtRecordPath(next, rest) : undefined;
2054
2258
  }
2055
2259
  function andGuard(left, right) {
2056
2260
  if (isTrueLiteral(left))
@@ -2149,12 +2353,15 @@ function isInputValueExpression(node, parameter) {
2149
2353
  function propertyAccessPath(node) {
2150
2354
  if (ts.isIdentifier(node))
2151
2355
  return [node.text];
2152
- if (ts.isPropertyAccessExpression(node)) {
2356
+ if (isPropertyAccessLike(node)) {
2153
2357
  const base = propertyAccessPath(node.expression);
2154
2358
  return base ? [...base, node.name.text] : undefined;
2155
2359
  }
2156
2360
  return undefined;
2157
2361
  }
2362
+ function isPropertyAccessLike(node) {
2363
+ return ts.isPropertyAccessExpression(node) || ts.isPropertyAccessChain(node);
2364
+ }
2158
2365
  function valueClassForDomain(domain) {
2159
2366
  if (domain.kind === "enum")
2160
2367
  return domain.values.join("|") || "enum";