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.
- package/dist/checker/search/eval.d.ts.map +1 -1
- package/dist/checker/search/eval.js +49 -1
- package/dist/checker/search/eval.js.map +1 -1
- package/dist/checker/search/index.js +26 -7
- package/dist/checker/search/index.js.map +1 -1
- package/dist/extraction/index.d.ts +4 -2
- package/dist/extraction/index.d.ts.map +1 -1
- package/dist/extraction/index.js +246 -39
- package/dist/extraction/index.js.map +1 -1
- package/dist/extraction/pipeline/index.d.ts +2 -0
- package/dist/extraction/pipeline/index.d.ts.map +1 -1
- package/dist/extraction/pipeline/index.js +4 -2
- package/dist/extraction/pipeline/index.js.map +1 -1
- package/dist/harness/index.d.ts +29 -2
- package/dist/harness/index.d.ts.map +1 -1
- package/dist/harness/index.js +79 -9
- package/dist/harness/index.js.map +1 -1
- package/dist/kernel/overlay/index.d.ts +16 -1
- package/dist/kernel/overlay/index.d.ts.map +1 -1
- package/dist/kernel/overlay/index.js +56 -1
- package/dist/kernel/overlay/index.js.map +1 -1
- package/dist/kernel/report/types.d.ts +4 -0
- package/dist/kernel/report/types.d.ts.map +1 -1
- package/dist/modality/cli.js +15 -9
- package/dist/modality/cli.js.map +1 -1
- package/dist/modality/codegen/replay-test.d.ts.map +1 -1
- package/dist/modality/codegen/replay-test.js +13 -4
- package/dist/modality/codegen/replay-test.js.map +1 -1
- package/dist/modality/features/ci/command.d.ts +2 -0
- package/dist/modality/features/ci/command.d.ts.map +1 -1
- package/dist/modality/features/ci/command.js +2 -0
- package/dist/modality/features/ci/command.js.map +1 -1
- package/dist/modality/features/conform/command.d.ts +2 -0
- package/dist/modality/features/conform/command.d.ts.map +1 -1
- package/dist/modality/features/conform/command.js +91 -6
- package/dist/modality/features/conform/command.js.map +1 -1
- package/dist/modality/features/export/command.d.ts +29 -1
- package/dist/modality/features/export/command.d.ts.map +1 -1
- package/dist/modality/features/export/command.js +82 -1
- package/dist/modality/features/export/command.js.map +1 -1
- package/dist/modality/features/export/index.d.ts +2 -2
- package/dist/modality/features/export/index.d.ts.map +1 -1
- package/dist/modality/features/export/index.js +1 -1
- package/dist/modality/features/export/index.js.map +1 -1
- package/dist/modality/features/extract/command.d.ts.map +1 -1
- package/dist/modality/features/extract/command.js +75 -7
- package/dist/modality/features/extract/command.js.map +1 -1
- package/dist/modality/features/replay/command.d.ts +2 -0
- package/dist/modality/features/replay/command.d.ts.map +1 -1
- package/dist/modality/features/replay/command.js +89 -6
- package/dist/modality/features/replay/command.js.map +1 -1
- package/dist/modality/overlay.d.ts +2 -1
- package/dist/modality/overlay.d.ts.map +1 -1
- package/dist/modality/overlay.js +13 -2
- package/dist/modality/overlay.js.map +1 -1
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +22 -2
- package/dist/runtime/index.js.map +1 -1
- package/dist/sources/jotai/index.d.ts.map +1 -1
- package/dist/sources/jotai/index.js +32 -9
- package/dist/sources/jotai/index.js.map +1 -1
- package/dist/sources/swr/index.d.ts +5 -1
- package/dist/sources/swr/index.d.ts.map +1 -1
- package/dist/sources/swr/index.js +112 -12
- package/dist/sources/swr/index.js.map +1 -1
- package/package.json +2 -2
package/dist/extraction/index.js
CHANGED
|
@@ -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
|
|
72
|
-
|
|
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) ||
|
|
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
|
-
|
|
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
|
|
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:
|
|
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 (
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2107
|
+
return combineParsedGuards(guards);
|
|
1929
2108
|
}
|
|
1930
|
-
return
|
|
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) ||
|
|
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) ||
|
|
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
|
|
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 (
|
|
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";
|