modality-ts 0.0.1 → 0.0.3

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.
@@ -25,7 +25,7 @@ export interface UseStateExtractionResult {
25
25
  export interface ExtractedModelSkeleton extends UseStateExtractionResult {
26
26
  transitions: Transition[];
27
27
  }
28
- export declare function inferDomainFromTypeNode(node: ts.TypeNode | undefined): AbstractDomain;
28
+ export declare function inferDomainFromTypeNode(node: ts.TypeNode | undefined, typeAliases?: ReadonlyMap<string, ts.TypeNode>): AbstractDomain;
29
29
  export declare function extractUseStateVars(sourceText: string, options?: UseStateExtractionOptions): UseStateExtractionResult;
30
30
  export declare function extractUseStateSkeleton(sourceText: string, options?: UseStateExtractionOptions): ExtractedModelSkeleton;
31
31
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/extraction/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,YAAY,CAAC;AACjC,OAAO,KAAK,EAAE,cAAc,EAA6B,YAAY,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AACrH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAEnD,cAAc,qBAAqB,CAAC;AACpC,cAAc,gBAAgB,CAAC;AAE/B,MAAM,WAAW,yBAAyB;IACxC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC/B,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,OAAO,EAAE,KAAK,CAAC;QAAC,KAAK,CAAC,EAAE,KAAK,CAAA;KAAE,CAAC,CAAC;IAClE,SAAS,CAAC,EAAE,SAAS,YAAY,EAAE,CAAC;IACpC,aAAa,CAAC,EAAE,SAAS,YAAY,EAAE,CAAC;CACzC;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,YAAY,EAAE,CAAC;IACrB,QAAQ,EAAE,iBAAiB,EAAE,CAAC;CAC/B;AAED,MAAM,WAAW,sBAAuB,SAAQ,wBAAwB;IACtE,WAAW,EAAE,UAAU,EAAE,CAAC;CAC3B;AA0CD,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,EAAE,CAAC,QAAQ,GAAG,SAAS,GAAG,cAAc,CAuBrF;AAED,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,GAAE,yBAA8B,GAAG,wBAAwB,CAEzH;AAED,wBAAgB,uBAAuB,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,GAAE,yBAA8B,GAAG,sBAAsB,CAyI3H"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/extraction/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,YAAY,CAAC;AACjC,OAAO,KAAK,EAAE,cAAc,EAA6B,YAAY,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AACrH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAEnD,cAAc,qBAAqB,CAAC;AACpC,cAAc,gBAAgB,CAAC;AAE/B,MAAM,WAAW,yBAAyB;IACxC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC/B,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,OAAO,EAAE,KAAK,CAAC;QAAC,KAAK,CAAC,EAAE,KAAK,CAAA;KAAE,CAAC,CAAC;IAClE,SAAS,CAAC,EAAE,SAAS,YAAY,EAAE,CAAC;IACpC,aAAa,CAAC,EAAE,SAAS,YAAY,EAAE,CAAC;CACzC;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,YAAY,EAAE,CAAC;IACrB,QAAQ,EAAE,iBAAiB,EAAE,CAAC;CAC/B;AAED,MAAM,WAAW,sBAAuB,SAAQ,wBAAwB;IACtE,WAAW,EAAE,UAAU,EAAE,CAAC;CAC3B;AA0CD,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,EAAE,CAAC,QAAQ,GAAG,SAAS,EAAE,WAAW,GAAE,WAAW,CAAC,MAAM,EAAE,EAAE,CAAC,QAAQ,CAAa,GAAG,cAAc,CAuBhJ;AAED,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,GAAE,yBAA8B,GAAG,wBAAwB,CAEzH;AAED,wBAAgB,uBAAuB,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,GAAE,yBAA8B,GAAG,sBAAsB,CA4I3H"}
@@ -11,7 +11,7 @@ function setterBindingFromDecl(decl) {
11
11
  domain: decl.domain
12
12
  };
13
13
  }
14
- export function inferDomainFromTypeNode(node) {
14
+ export function inferDomainFromTypeNode(node, typeAliases = new Map()) {
15
15
  if (!node)
16
16
  return { kind: "tokens", count: 1 };
17
17
  switch (node.kind) {
@@ -25,13 +25,13 @@ export function inferDomainFromTypeNode(node) {
25
25
  case ts.SyntaxKind.LiteralType:
26
26
  return domainFromLiteralType(node);
27
27
  case ts.SyntaxKind.UnionType:
28
- return domainFromUnion(node);
28
+ return domainFromUnion(node, typeAliases);
29
29
  case ts.SyntaxKind.TypeLiteral:
30
30
  return domainFromTypeLiteral(node);
31
31
  case ts.SyntaxKind.ArrayType:
32
32
  return { kind: "lengthCat" };
33
33
  case ts.SyntaxKind.TypeReference:
34
- return domainFromTypeReference(node);
34
+ return domainFromTypeReference(node, typeAliases);
35
35
  default:
36
36
  return { kind: "tokens", count: 1 };
37
37
  }
@@ -42,6 +42,7 @@ export function extractUseStateVars(sourceText, options = {}) {
42
42
  export function extractUseStateSkeleton(sourceText, options = {}) {
43
43
  const fileName = options.fileName ?? "App.tsx";
44
44
  const source = ts.createSourceFile(fileName, sourceText, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
45
+ const typeAliases = typeAliasDeclarations(source);
45
46
  const vars = options.stateVars ? [...options.stateVars] : [];
46
47
  const transitions = [];
47
48
  const warnings = [];
@@ -67,8 +68,10 @@ export function extractUseStateSkeleton(sourceText, options = {}) {
67
68
  if (!componentName && isCustomHookDeclaration(node))
68
69
  return;
69
70
  const nextComponent = componentNameFor(node) ?? componentName;
70
- if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && isExtractableHandler(node.initializer)) {
71
- handlers.set(node.name.text, node.initializer);
71
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) {
72
+ const handler = extractableHandlerInitializer(node.initializer);
73
+ if (handler)
74
+ handlers.set(node.name.text, handler);
72
75
  }
73
76
  if (ts.isVariableDeclaration(node) && node.initializer && isUseReducerCall(node.initializer)) {
74
77
  warnings.push({ message: `Unsupported useReducer ${nextComponent ?? "Anonymous"}.useReducer`, ...lineAndColumn(source, node) });
@@ -96,7 +99,7 @@ export function extractUseStateSkeleton(sourceText, options = {}) {
96
99
  const stateName = node.name.elements[0];
97
100
  const setterName = node.name.elements[1];
98
101
  if (ts.isBindingElement(stateName) && ts.isIdentifier(stateName.name)) {
99
- const domain = inferUseStateDomain(node.initializer);
102
+ const domain = inferUseStateDomain(node.initializer, typeAliases);
100
103
  const component = nextComponent ?? "Anonymous";
101
104
  const varId = `local:${component}.${stateName.name.text}`;
102
105
  if (!options.stateVars) {
@@ -159,9 +162,10 @@ export function extractUseStateSkeleton(sourceText, options = {}) {
159
162
  ts.forEachChild(node, (child) => visit(child, nextComponent));
160
163
  return;
161
164
  }
165
+ const guardLocals = componentGuardLocalsFor(node, setters);
162
166
  const guard = combineParsedGuards([
163
- renderGuardFor(node, setters, warnings, source, nextComponent ?? "Anonymous"),
164
- disabledGuardFor(node, setters, warnings, source, nextComponent ?? "Anonymous")
167
+ renderGuardFor(node, setters, warnings, source, nextComponent ?? "Anonymous", guardLocals),
168
+ disabledGuardFor(node, setters, warnings, source, nextComponent ?? "Anonymous", guardLocals)
165
169
  ]);
166
170
  const extracted = transitionsFromJsxAttribute(source, fileName, node, setters, handlers, nextComponent ?? "Anonymous", effectApis, options.asyncOutcomes ?? {}, guard, warnings);
167
171
  transitions.push(...extracted);
@@ -223,10 +227,10 @@ function shortHash(value) {
223
227
  }
224
228
  return (hash >>> 0).toString(36).padStart(6, "0").slice(0, 6);
225
229
  }
226
- function inferUseStateDomain(call) {
230
+ function inferUseStateDomain(call, typeAliases = new Map()) {
227
231
  const typeArg = call.typeArguments?.[0];
228
232
  if (typeArg)
229
- return inferDomainFromTypeNode(typeArg);
233
+ return inferDomainFromTypeNode(typeArg, typeAliases);
230
234
  const initial = call.arguments[0];
231
235
  if (!initial)
232
236
  return { kind: "tokens", count: 1 };
@@ -289,10 +293,10 @@ function domainFromLiteralType(node) {
289
293
  return { kind: "option", inner: { kind: "tokens", count: 1 } };
290
294
  return { kind: "tokens", count: 1 };
291
295
  }
292
- function domainFromUnion(node) {
296
+ function domainFromUnion(node, typeAliases = new Map()) {
293
297
  const nonNull = node.types.filter((part) => part.kind !== ts.SyntaxKind.UndefinedKeyword && !(ts.isLiteralTypeNode(part) && part.literal.kind === ts.SyntaxKind.NullKeyword));
294
298
  if (nonNull.length !== node.types.length && nonNull.length > 0) {
295
- return { kind: "option", inner: nonNull.length === 1 ? inferDomainFromTypeNode(nonNull[0]) : domainFromUnionMembers(nonNull) };
299
+ return { kind: "option", inner: nonNull.length === 1 ? inferDomainFromTypeNode(nonNull[0], typeAliases) : domainFromUnionMembers(nonNull) };
296
300
  }
297
301
  return domainFromUnionMembers(node.types);
298
302
  }
@@ -353,14 +357,27 @@ function domainFromTypeLiteral(node, omitField) {
353
357
  }
354
358
  return { kind: "record", fields };
355
359
  }
356
- function domainFromTypeReference(node) {
360
+ function domainFromTypeReference(node, typeAliases = new Map()) {
357
361
  const name = node.typeName.getText();
362
+ const alias = typeAliases.get(name);
363
+ if (alias)
364
+ return inferDomainFromTypeNode(alias, typeAliases);
358
365
  if ((name === "Array" || name === "ReadonlyArray") && node.typeArguments?.length === 1)
359
366
  return { kind: "lengthCat" };
360
367
  if (name === "Record")
361
368
  return { kind: "tokens", count: 1 };
362
369
  return { kind: "tokens", count: 1 };
363
370
  }
371
+ function typeAliasDeclarations(source) {
372
+ const aliases = new Map();
373
+ const visit = (node) => {
374
+ if (ts.isTypeAliasDeclaration(node) && ts.isIdentifier(node.name))
375
+ aliases.set(node.name.text, node.type);
376
+ ts.forEachChild(node, visit);
377
+ };
378
+ visit(source);
379
+ return aliases;
380
+ }
364
381
  function isUseStateCall(node) {
365
382
  return ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === "useState";
366
383
  }
@@ -376,6 +393,15 @@ function isUseEffectCall(node) {
376
393
  function isExtractableHandler(node) {
377
394
  return ts.isArrowFunction(node) || ts.isFunctionExpression(node);
378
395
  }
396
+ function extractableHandlerInitializer(node) {
397
+ if (isExtractableHandler(node))
398
+ return node;
399
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === "useCallback") {
400
+ const callback = node.arguments[0];
401
+ return callback && isExtractableHandler(callback) ? callback : undefined;
402
+ }
403
+ return undefined;
404
+ }
379
405
  function refSetterTaint(node, setters) {
380
406
  if (ts.isVariableDeclaration(node) && node.initializer && isUseRefCall(node.initializer)) {
381
407
  const arg = node.initializer.arguments[0];
@@ -504,9 +530,10 @@ function transitionsFromComponentPropAttribute(source, fileName, node, setters,
504
530
  const handler = handlerExpression(expression, handlers);
505
531
  if (!handler)
506
532
  return [];
533
+ const guardLocals = componentGuardLocalsFor(node, setters);
507
534
  const callerGuard = combineParsedGuards([
508
- renderGuardFor(node, setters, warnings, source, component),
509
- disabledGuardFor(node, setters, warnings, source, component)
535
+ renderGuardFor(node, setters, warnings, source, component, guardLocals),
536
+ disabledGuardFor(node, setters, warnings, source, component, guardLocals)
510
537
  ]);
511
538
  return tagStableIdKey(transitionsFromResolvedHandler(source, fileName, node, trigger.attr, handler, setters, handlers, component, effectApis, asyncOutcomes, combineParsedGuards([trigger.guard, callerGuard]), trigger.locator, warnings), handler);
512
539
  }
@@ -578,7 +605,7 @@ function transitionsFromResolvedHandler(source, fileName, node, attr, handler, s
578
605
  const loopTransitions = loopWriteTransitions(source, fileName, node, attr, handler, setters, component, locator);
579
606
  if (loopTransitions.length > 0)
580
607
  return applyParsedGuard(loopTransitions, disabledGuard);
581
- const sequentialTransition = sequentialTransitionFromHandler(source, fileName, node, attr, handler, setters, component, locator);
608
+ const sequentialTransition = sequentialTransitionFromHandler(source, fileName, node, attr, handler, setters, handlers, component, locator);
582
609
  if (sequentialTransition)
583
610
  return applyParsedGuard([sequentialTransition], disabledGuard);
584
611
  const summary = callSummaryFromHandler(handler, setters);
@@ -590,6 +617,9 @@ function transitionsFromResolvedHandler(source, fileName, node, attr, handler, s
590
617
  const navigation = navigationTransition(source, fileName, node, attr, component, inlinedCall, locator);
591
618
  if (navigation)
592
619
  return applyParsedGuard([navigation], disabledGuard);
620
+ const swrMutate = swrMutateTransition(source, fileName, node, attr, component, inlinedCall, locator);
621
+ if (swrMutate)
622
+ return applyParsedGuard([swrMutate], disabledGuard);
593
623
  const setterCall = setterCallFrom(inlinedCall, setters);
594
624
  if (!setterCall) {
595
625
  const escaped = escapedSetters(inlinedCall, setters);
@@ -617,7 +647,7 @@ function transitionsFromResolvedHandler(source, fileName, node, attr, handler, s
617
647
  confidence: "exact"
618
648
  }], disabledGuard);
619
649
  }
620
- function sequentialTransitionFromHandler(source, fileName, node, attr, handler, setters, component, locator) {
650
+ function sequentialTransitionFromHandler(source, fileName, node, attr, handler, setters, handlers, component, locator) {
621
651
  if (!ts.isBlock(handler.body))
622
652
  return undefined;
623
653
  const locals = new Map();
@@ -625,6 +655,11 @@ function sequentialTransitionFromHandler(source, fileName, node, attr, handler,
625
655
  for (const statement of handler.body.statements) {
626
656
  if (bindConstStatement(statement, setters, locals))
627
657
  continue;
658
+ const helper = helperSummariesFromStatement(statement, handlers, setters);
659
+ if (helper) {
660
+ summaries.push(...helper);
661
+ continue;
662
+ }
628
663
  const summary = summarizeSetterStatement(statement, setters, locals);
629
664
  if (!summary)
630
665
  return undefined;
@@ -646,6 +681,23 @@ function sequentialTransitionFromHandler(source, fileName, node, attr, handler,
646
681
  confidence: effects.some((effect) => effect.kind === "havoc") ? "over-approx" : "exact"
647
682
  };
648
683
  }
684
+ function helperSummariesFromStatement(statement, handlers, setters) {
685
+ if (!ts.isExpressionStatement(statement) || !ts.isCallExpression(statement.expression) || !ts.isIdentifier(statement.expression.expression))
686
+ return undefined;
687
+ const helper = handlers.get(statement.expression.expression.text);
688
+ if (!helper || !ts.isBlock(helper.body))
689
+ return undefined;
690
+ const locals = new Map();
691
+ const summaries = [];
692
+ for (const child of helper.body.statements) {
693
+ if (bindConstStatement(child, setters, locals))
694
+ continue;
695
+ const summary = summarizeSetterStatement(child, setters, locals);
696
+ if (summary)
697
+ summaries.push(summary);
698
+ }
699
+ return summaries.length > 0 ? summaries : undefined;
700
+ }
649
701
  function loopWriteTransitions(source, fileName, node, attr, handler, setters, component, locator) {
650
702
  if (!ts.isBlock(handler.body))
651
703
  return [];
@@ -711,6 +763,11 @@ function havocSetterTransition(source, fileName, node, attr, component, setter,
711
763
  };
712
764
  }
713
765
  function setterArgumentExpr(argument, setter, setters, locals) {
766
+ if (ts.isObjectLiteralExpression(argument)) {
767
+ const object = objectLiteralAssignmentExpr(argument, setter.domain, setters, locals);
768
+ if (object)
769
+ return object;
770
+ }
714
771
  if ((ts.isArrowFunction(argument) || ts.isFunctionExpression(argument)) && argument.parameters.length === 1 && ts.isIdentifier(argument.parameters[0].name)) {
715
772
  if (ts.isBlock(argument.body))
716
773
  return undefined;
@@ -718,11 +775,42 @@ function setterArgumentExpr(argument, setter, setters, locals) {
718
775
  }
719
776
  return valueExpr(argument, setters, locals);
720
777
  }
778
+ function objectLiteralAssignmentExpr(expression, domain, setters, locals) {
779
+ const value = {};
780
+ const reads = new Set();
781
+ const fields = domain.kind === "record" ? domain.fields : domain.kind === "tagged" ? taggedFieldsForObject(expression, domain) : {};
782
+ for (const property of expression.properties) {
783
+ if (!ts.isPropertyAssignment(property))
784
+ return undefined;
785
+ const name = propertyName(property.name);
786
+ if (!name)
787
+ return undefined;
788
+ const literal = literalValue(property.initializer);
789
+ if (literal !== undefined) {
790
+ value[name] = literal;
791
+ continue;
792
+ }
793
+ const bound = valueExpr(property.initializer, setters, locals);
794
+ if (bound?.expr.kind === "lit") {
795
+ value[name] = bound.expr.value;
796
+ bound.reads.forEach((read) => reads.add(read));
797
+ continue;
798
+ }
799
+ value[name] = firstValue(fields[name] ?? { kind: "tokens", count: 1 });
800
+ }
801
+ return { expr: { kind: "lit", value }, reads: [...reads] };
802
+ }
803
+ function taggedFieldsForObject(expression, domain) {
804
+ const tagProperty = expression.properties.find((property) => ts.isPropertyAssignment(property) && propertyName(property.name) === domain.tag);
805
+ const tag = tagProperty ? literalValue(tagProperty.initializer) : undefined;
806
+ const variant = typeof tag === "string" ? domain.variants[tag] : undefined;
807
+ return variant?.kind === "record" ? variant.fields : {};
808
+ }
721
809
  function valueExpr(expression, setters, locals) {
722
810
  const value = literalValue(expression);
723
811
  if (value !== undefined)
724
812
  return { expr: { kind: "lit", value }, reads: [] };
725
- if (ts.isIdentifier(expression) || ts.isPropertyAccessExpression(expression))
813
+ if (ts.isIdentifier(expression) || isPropertyAccessLike(expression))
726
814
  return modeledReadExpr(expression, setters, locals);
727
815
  if (ts.isPrefixUnaryExpression(expression) && expression.operator === ts.SyntaxKind.ExclamationToken) {
728
816
  const parsed = booleanExpr(expression.operand, setters, locals);
@@ -731,7 +819,7 @@ function valueExpr(expression, setters, locals) {
731
819
  if (ts.isParenthesizedExpression(expression))
732
820
  return valueExpr(expression.expression, setters, locals);
733
821
  if (ts.isBinaryExpression(expression) && expression.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken) {
734
- return nullishOptionalReadExpr(expression, setters);
822
+ return nullishOptionalReadExpr(expression, setters, locals);
735
823
  }
736
824
  if (ts.isConditionalExpression(expression)) {
737
825
  const condition = booleanExpr(expression.condition, setters, locals);
@@ -775,22 +863,25 @@ function objectSpreadUpdateExpr(expression, setters, locals) {
775
863
  }
776
864
  return current;
777
865
  }
778
- function nullishOptionalReadExpr(expression, setters) {
779
- if (literalValue(expression.right) !== null)
866
+ function nullishOptionalReadExpr(expression, setters, locals = new Map()) {
867
+ const fallback = literalValue(expression.right);
868
+ if (fallback === undefined)
780
869
  return undefined;
781
870
  const read = optionalReadPath(expression.left);
782
871
  if (!read?.optional || read.path.length === 0)
783
872
  return undefined;
784
- const varId = stateVarForName(read.base, setters);
873
+ const local = locals.get(read.base);
874
+ const varId = local?.expr.kind === "read" ? local.expr.var : stateVarForName(read.base, setters);
785
875
  if (!varId)
786
876
  return undefined;
877
+ const basePath = local?.expr.kind === "read" ? local.expr.path ?? [] : [];
787
878
  return {
788
879
  expr: {
789
880
  kind: "cond",
790
881
  args: [
791
- { kind: "eq", args: [{ kind: "read", var: varId }, { kind: "lit", value: null }] },
792
- { kind: "lit", value: null },
793
- { kind: "read", var: varId, path: read.path }
882
+ { kind: "eq", args: [{ kind: "read", var: varId, ...(basePath.length > 0 ? { path: basePath } : {}) }, { kind: "lit", value: null }] },
883
+ { kind: "lit", value: fallback },
884
+ { kind: "read", var: varId, path: [...basePath, ...read.path] }
794
885
  ]
795
886
  },
796
887
  reads: [varId]
@@ -799,7 +890,7 @@ function nullishOptionalReadExpr(expression, setters) {
799
890
  function optionalReadPath(expression) {
800
891
  if (ts.isIdentifier(expression))
801
892
  return { base: expression.text, path: [], optional: false };
802
- if (ts.isPropertyAccessExpression(expression)) {
893
+ if (isPropertyAccessLike(expression)) {
803
894
  const base = optionalReadPath(expression.expression);
804
895
  if (!base)
805
896
  return undefined;
@@ -875,9 +966,13 @@ function modeledReadExpr(expression, setters, locals) {
875
966
  reads: local.reads
876
967
  };
877
968
  }
878
- const stateVar = stateVarForName(base, setters);
969
+ const setter = setterForName(base, setters);
970
+ const stateVar = setter?.varId;
879
971
  if (!stateVar)
880
972
  return undefined;
973
+ if (setter.domain.kind === "tagged" && segments.length > 0 && segments[0] !== setter.domain.tag) {
974
+ return { expr: { kind: "lit", value: firstValue(taggedPathDomain(setter.domain, segments) ?? { kind: "tokens", count: 1 }) }, reads: [] };
975
+ }
881
976
  return {
882
977
  expr: { kind: "read", var: stateVar, ...(segments.length > 0 ? { path: segments } : {}) },
883
978
  reads: [stateVar]
@@ -936,10 +1031,36 @@ function effectWriteVars(effect) {
936
1031
  function stateNameForVar(varId, setters) {
937
1032
  return [...setters.values()].find((setter) => setter.varId === varId)?.stateName;
938
1033
  }
1034
+ function componentGuardLocalsFor(attribute, setters) {
1035
+ const body = enclosingFunctionBody(attribute);
1036
+ if (!body)
1037
+ return new Map();
1038
+ const locals = new Map();
1039
+ for (const statement of body.statements) {
1040
+ if (statement.pos > attribute.pos)
1041
+ break;
1042
+ if (ts.isReturnStatement(statement))
1043
+ break;
1044
+ bindConstStatement(statement, setters, locals, true);
1045
+ }
1046
+ return locals;
1047
+ }
1048
+ function enclosingFunctionBody(node) {
1049
+ let current = node;
1050
+ while (current) {
1051
+ if ((ts.isFunctionDeclaration(current) || ts.isFunctionExpression(current) || ts.isArrowFunction(current)) && current.body && ts.isBlock(current.body)) {
1052
+ return current.body;
1053
+ }
1054
+ current = current.parent;
1055
+ }
1056
+ return undefined;
1057
+ }
939
1058
  function callSummaryFromHandler(handler, setters, initialLocals = new Map()) {
940
1059
  const body = handler.body;
941
1060
  if (ts.isCallExpression(body))
942
1061
  return { call: body, locals: new Map(initialLocals) };
1062
+ if (ts.isVoidExpression(body) && ts.isCallExpression(body.expression))
1063
+ return { call: body.expression, locals: new Map(initialLocals) };
943
1064
  if (ts.isBlock(body)) {
944
1065
  const locals = new Map(initialLocals);
945
1066
  for (let index = 0; index < body.statements.length; index += 1) {
@@ -947,13 +1068,30 @@ function callSummaryFromHandler(handler, setters, initialLocals = new Map()) {
947
1068
  const isLast = index === body.statements.length - 1;
948
1069
  if (isLast && ts.isExpressionStatement(statement) && ts.isCallExpression(statement.expression))
949
1070
  return { call: statement.expression, locals };
1071
+ if (isLast && ts.isExpressionStatement(statement) && ts.isVoidExpression(statement.expression) && ts.isCallExpression(statement.expression.expression))
1072
+ return { call: statement.expression.expression, locals };
950
1073
  if (!bindConstStatement(statement, setters, locals))
951
1074
  return undefined;
952
1075
  }
953
1076
  }
954
1077
  return undefined;
955
1078
  }
956
- function bindConstStatement(statement, setters, locals) {
1079
+ function swrMutateTransition(source, fileName, node, attr, component, call, locator) {
1080
+ if (!ts.isIdentifier(call.expression) || call.expression.text !== "mutate")
1081
+ return undefined;
1082
+ return {
1083
+ id: `${component}.${attr}.mutate`,
1084
+ cls: "user",
1085
+ label: labelForEvent(attr, locator),
1086
+ source: [{ file: fileName, ...lineAndColumn(source, node) }],
1087
+ guard: { kind: "lit", value: true },
1088
+ effect: { kind: "seq", effects: [] },
1089
+ reads: [],
1090
+ writes: [],
1091
+ confidence: "exact"
1092
+ };
1093
+ }
1094
+ function bindConstStatement(statement, setters, locals, partialBoolean = false) {
957
1095
  if (!ts.isVariableStatement(statement))
958
1096
  return false;
959
1097
  if ((ts.getCombinedNodeFlags(statement.declarationList) & ts.NodeFlags.Const) === 0)
@@ -961,7 +1099,8 @@ function bindConstStatement(statement, setters, locals) {
961
1099
  for (const declaration of statement.declarationList.declarations) {
962
1100
  if (!ts.isIdentifier(declaration.name) || !declaration.initializer)
963
1101
  return false;
964
- const binding = valueExpr(declaration.initializer, setters, locals);
1102
+ const binding = valueExpr(declaration.initializer, setters, locals) ??
1103
+ (partialBoolean ? parseConjunctiveGuardExpression(declaration.initializer, setters, locals) : booleanExpr(declaration.initializer, setters, locals));
965
1104
  if (!binding)
966
1105
  return false;
967
1106
  locals.set(declaration.name.text, binding);
@@ -1847,7 +1986,7 @@ function combineParsedGuards(guards) {
1847
1986
  reads: [...new Set(parsed.flatMap((guard) => guard.reads))]
1848
1987
  };
1849
1988
  }
1850
- function renderGuardFor(eventAttribute, setters, warnings, source, component) {
1989
+ function renderGuardFor(eventAttribute, setters, warnings, source, component, locals = new Map()) {
1851
1990
  const element = jsxElementForAttribute(eventAttribute);
1852
1991
  if (!element)
1853
1992
  return undefined;
@@ -1857,7 +1996,7 @@ function renderGuardFor(eventAttribute, setters, warnings, source, component) {
1857
1996
  if (ts.isBinaryExpression(parent) &&
1858
1997
  parent.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken &&
1859
1998
  parent.right === current) {
1860
- const parsed = parseGuardExpression(parent.left, setters);
1999
+ const parsed = parseConjunctiveGuardExpression(parent.left, setters, locals);
1861
2000
  if (!parsed) {
1862
2001
  warnings.push({ message: `Unsupported render guard ${component}.${eventAttribute.name.getText(source)}`, ...lineAndColumn(source, parent.left) });
1863
2002
  return undefined;
@@ -1865,7 +2004,7 @@ function renderGuardFor(eventAttribute, setters, warnings, source, component) {
1865
2004
  return parsed;
1866
2005
  }
1867
2006
  if (ts.isConditionalExpression(parent) && parent.whenTrue === current) {
1868
- const parsed = parseGuardExpression(parent.condition, setters);
2007
+ const parsed = parseConjunctiveGuardExpression(parent.condition, setters, locals);
1869
2008
  if (!parsed) {
1870
2009
  warnings.push({ message: `Unsupported render guard ${component}.${eventAttribute.name.getText(source)}`, ...lineAndColumn(source, parent.condition) });
1871
2010
  return undefined;
@@ -1873,7 +2012,7 @@ function renderGuardFor(eventAttribute, setters, warnings, source, component) {
1873
2012
  return parsed;
1874
2013
  }
1875
2014
  if (ts.isConditionalExpression(parent) && parent.whenFalse === current) {
1876
- const parsed = parseGuardExpression(parent.condition, setters);
2015
+ const parsed = parseConjunctiveGuardExpression(parent.condition, setters, locals);
1877
2016
  if (!parsed) {
1878
2017
  warnings.push({ message: `Unsupported render guard ${component}.${eventAttribute.name.getText(source)}`, ...lineAndColumn(source, parent.condition) });
1879
2018
  return undefined;
@@ -1897,50 +2036,73 @@ function jsxElementForAttribute(attribute) {
1897
2036
  return element.parent;
1898
2037
  return ts.isJsxSelfClosingElement(element) ? element : undefined;
1899
2038
  }
1900
- function disabledGuardFor(eventAttribute, setters, warnings, source, component) {
2039
+ function disabledGuardFor(eventAttribute, setters, warnings, source, component, locals = new Map()) {
1901
2040
  const attrs = eventAttribute.parent;
1902
2041
  if (!ts.isJsxAttributes(attrs))
1903
2042
  return undefined;
1904
- const disabled = attrs.properties.find((property) => ts.isJsxAttribute(property) && ts.isIdentifier(property.name) && (property.name.text === "disabled" || property.name.text === "aria-disabled"));
2043
+ const disabled = attrs.properties.find((property) => ts.isJsxAttribute(property) && ts.isIdentifier(property.name) && (property.name.text === "disabled" || property.name.text === "aria-disabled")) ?? submitButtonDisabledAttribute(eventAttribute);
1905
2044
  if (!disabled)
1906
2045
  return undefined;
1907
- const parsed = jsxAttributeBoolean(disabled, setters);
2046
+ const parsed = jsxAttributeBoolean(disabled, setters, locals);
1908
2047
  if (!parsed) {
1909
2048
  warnings.push({ message: `Unsupported disabled guard ${component}.${eventAttribute.name.getText(source)}`, ...lineAndColumn(source, disabled) });
1910
2049
  return undefined;
1911
2050
  }
1912
2051
  return { expr: { kind: "not", args: [parsed.expr] }, reads: parsed.reads };
1913
2052
  }
1914
- function jsxAttributeBoolean(attribute, setters) {
2053
+ function submitButtonDisabledAttribute(eventAttribute) {
2054
+ if (!ts.isIdentifier(eventAttribute.name) || eventAttribute.name.text !== "onSubmit")
2055
+ return undefined;
2056
+ const element = jsxElementForAttribute(eventAttribute);
2057
+ if (!element || !ts.isJsxElement(element))
2058
+ return undefined;
2059
+ let found;
2060
+ const visit = (node) => {
2061
+ if (found)
2062
+ return;
2063
+ if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
2064
+ const tag = ts.isIdentifier(node.tagName) ? node.tagName.text : undefined;
2065
+ if (tag === "button" && stringAttribute(node.attributes, "type") === "submit") {
2066
+ found = node.attributes.properties.find((property) => ts.isJsxAttribute(property) && ts.isIdentifier(property.name) && (property.name.text === "disabled" || property.name.text === "aria-disabled"));
2067
+ if (found)
2068
+ return;
2069
+ }
2070
+ }
2071
+ ts.forEachChild(node, visit);
2072
+ };
2073
+ visit(element);
2074
+ return found;
2075
+ }
2076
+ function jsxAttributeBoolean(attribute, setters, locals = new Map()) {
1915
2077
  if (!attribute.initializer)
1916
2078
  return { expr: { kind: "lit", value: true }, reads: [] };
1917
2079
  if (ts.isStringLiteral(attribute.initializer))
1918
2080
  return { expr: { kind: "lit", value: attribute.initializer.text === "true" }, reads: [] };
1919
2081
  if (!ts.isJsxExpression(attribute.initializer) || !attribute.initializer.expression)
1920
2082
  return undefined;
1921
- return parseGuardExpression(attribute.initializer.expression, setters);
2083
+ return parseConjunctiveGuardExpression(attribute.initializer.expression, setters, locals);
1922
2084
  }
1923
- function parseGuardExpression(expression, setters) {
2085
+ function parseGuardExpression(expression, setters, locals = new Map()) {
1924
2086
  if (expression.kind === ts.SyntaxKind.TrueKeyword)
1925
2087
  return { expr: { kind: "lit", value: true }, reads: [] };
1926
2088
  if (expression.kind === ts.SyntaxKind.FalseKeyword)
1927
2089
  return { expr: { kind: "lit", value: false }, reads: [] };
1928
- if (ts.isIdentifier(expression) || ts.isPropertyAccessExpression(expression))
1929
- return valueExpr(expression, setters, new Map());
2090
+ if (ts.isIdentifier(expression) || isPropertyAccessLike(expression))
2091
+ return valueExpr(expression, setters, locals);
1930
2092
  if (ts.isPrefixUnaryExpression(expression) && expression.operator === ts.SyntaxKind.ExclamationToken) {
1931
- const parsed = parseGuardExpression(expression.operand, setters);
2093
+ const parsed = parseGuardExpression(expression.operand, setters, locals);
1932
2094
  return parsed ? { expr: { kind: "not", args: [parsed.expr] }, reads: parsed.reads } : undefined;
1933
2095
  }
1934
2096
  if (ts.isParenthesizedExpression(expression))
1935
- return parseGuardExpression(expression.expression, setters);
2097
+ return parseGuardExpression(expression.expression, setters, locals);
1936
2098
  if (ts.isBinaryExpression(expression))
1937
- return parseBinaryGuardExpression(expression, setters);
2099
+ return parseBinaryGuardExpression(expression, setters, locals);
1938
2100
  return undefined;
1939
2101
  }
1940
- function parseBinaryGuardExpression(expression, setters) {
2102
+ function parseBinaryGuardExpression(expression, setters, locals = new Map()) {
1941
2103
  if (expression.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken || expression.operatorToken.kind === ts.SyntaxKind.BarBarToken) {
1942
- const left = parseGuardExpression(expression.left, setters);
1943
- const right = parseGuardExpression(expression.right, setters);
2104
+ const left = parseGuardExpression(expression.left, setters, locals);
2105
+ const right = parseGuardExpression(expression.right, setters, locals);
1944
2106
  if (!left || !right)
1945
2107
  return undefined;
1946
2108
  return {
@@ -1952,8 +2114,8 @@ function parseBinaryGuardExpression(expression, setters) {
1952
2114
  expression.operatorToken.kind === ts.SyntaxKind.EqualsEqualsToken ||
1953
2115
  expression.operatorToken.kind === ts.SyntaxKind.ExclamationEqualsEqualsToken ||
1954
2116
  expression.operatorToken.kind === ts.SyntaxKind.ExclamationEqualsToken) {
1955
- const left = parseGuardOperand(expression.left, setters);
1956
- const right = parseGuardOperand(expression.right, setters);
2117
+ const left = parseGuardOperand(expression.left, setters, locals);
2118
+ const right = parseGuardOperand(expression.right, setters, locals);
1957
2119
  if (!left || !right)
1958
2120
  return undefined;
1959
2121
  return {
@@ -1966,16 +2128,52 @@ function parseBinaryGuardExpression(expression, setters) {
1966
2128
  }
1967
2129
  return undefined;
1968
2130
  }
1969
- function parseGuardOperand(expression, setters) {
2131
+ function parseGuardOperand(expression, setters, locals = new Map()) {
1970
2132
  const value = literalValue(expression);
1971
2133
  if (value !== undefined)
1972
2134
  return { expr: { kind: "lit", value }, reads: [] };
1973
- if (ts.isIdentifier(expression) || ts.isPropertyAccessExpression(expression))
1974
- return valueExpr(expression, setters, new Map());
1975
- return parseGuardExpression(expression, setters);
2135
+ if (ts.isIdentifier(expression) || isPropertyAccessLike(expression))
2136
+ return valueExpr(expression, setters, locals);
2137
+ return parseGuardExpression(expression, setters, locals);
2138
+ }
2139
+ function parseConjunctiveGuardExpression(expression, setters, locals = new Map()) {
2140
+ if (ts.isParenthesizedExpression(expression))
2141
+ return parseConjunctiveGuardExpression(expression.expression, setters, locals);
2142
+ if (ts.isBinaryExpression(expression) && expression.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) {
2143
+ return combineParsedGuards([
2144
+ parseConjunctiveGuardExpression(expression.left, setters, locals),
2145
+ parseConjunctiveGuardExpression(expression.right, setters, locals)
2146
+ ]);
2147
+ }
2148
+ return parseGuardExpression(expression, setters, locals);
1976
2149
  }
1977
2150
  function stateVarForName(name, setters) {
1978
- return [...setters.values()].find((setter) => setter.stateName === name)?.varId;
2151
+ return setterForName(name, setters)?.varId;
2152
+ }
2153
+ function setterForName(name, setters) {
2154
+ return setters.get(name) ?? [...setters.values()].find((setter) => setter.stateName === name);
2155
+ }
2156
+ function taggedPathDomain(domain, path) {
2157
+ const [field, ...rest] = path;
2158
+ if (!field)
2159
+ return domain;
2160
+ const variants = Object.values(domain.variants).filter((variant) => variant.kind === "record");
2161
+ const fieldDomains = variants.map((variant) => variant.fields[field]).filter((candidate) => Boolean(candidate));
2162
+ if (fieldDomains.length === 0)
2163
+ return undefined;
2164
+ const first = fieldDomains[0];
2165
+ if (rest.length === 0)
2166
+ return first;
2167
+ return first.kind === "record" ? domainAtRecordPath(first, rest) : undefined;
2168
+ }
2169
+ function domainAtRecordPath(domain, path) {
2170
+ const [field, ...rest] = path;
2171
+ if (!field)
2172
+ return domain;
2173
+ const next = domain.fields[field];
2174
+ if (!next || rest.length === 0)
2175
+ return next;
2176
+ return next.kind === "record" ? domainAtRecordPath(next, rest) : undefined;
1979
2177
  }
1980
2178
  function andGuard(left, right) {
1981
2179
  if (isTrueLiteral(left))
@@ -2074,12 +2272,15 @@ function isInputValueExpression(node, parameter) {
2074
2272
  function propertyAccessPath(node) {
2075
2273
  if (ts.isIdentifier(node))
2076
2274
  return [node.text];
2077
- if (ts.isPropertyAccessExpression(node)) {
2275
+ if (isPropertyAccessLike(node)) {
2078
2276
  const base = propertyAccessPath(node.expression);
2079
2277
  return base ? [...base, node.name.text] : undefined;
2080
2278
  }
2081
2279
  return undefined;
2082
2280
  }
2281
+ function isPropertyAccessLike(node) {
2282
+ return ts.isPropertyAccessExpression(node) || ts.isPropertyAccessChain(node);
2283
+ }
2083
2284
  function valueClassForDomain(domain) {
2084
2285
  if (domain.kind === "enum")
2085
2286
  return domain.values.join("|") || "enum";