modality-ts 0.0.2 → 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.
@@ -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,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,CA2I3H"}
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"}
@@ -68,8 +68,10 @@ export function extractUseStateSkeleton(sourceText, options = {}) {
68
68
  if (!componentName && isCustomHookDeclaration(node))
69
69
  return;
70
70
  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);
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);
73
75
  }
74
76
  if (ts.isVariableDeclaration(node) && node.initializer && isUseReducerCall(node.initializer)) {
75
77
  warnings.push({ message: `Unsupported useReducer ${nextComponent ?? "Anonymous"}.useReducer`, ...lineAndColumn(source, node) });
@@ -391,6 +393,15 @@ function isUseEffectCall(node) {
391
393
  function isExtractableHandler(node) {
392
394
  return ts.isArrowFunction(node) || ts.isFunctionExpression(node);
393
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
+ }
394
405
  function refSetterTaint(node, setters) {
395
406
  if (ts.isVariableDeclaration(node) && node.initializer && isUseRefCall(node.initializer)) {
396
407
  const arg = node.initializer.arguments[0];
@@ -594,7 +605,7 @@ function transitionsFromResolvedHandler(source, fileName, node, attr, handler, s
594
605
  const loopTransitions = loopWriteTransitions(source, fileName, node, attr, handler, setters, component, locator);
595
606
  if (loopTransitions.length > 0)
596
607
  return applyParsedGuard(loopTransitions, disabledGuard);
597
- const sequentialTransition = sequentialTransitionFromHandler(source, fileName, node, attr, handler, setters, component, locator);
608
+ const sequentialTransition = sequentialTransitionFromHandler(source, fileName, node, attr, handler, setters, handlers, component, locator);
598
609
  if (sequentialTransition)
599
610
  return applyParsedGuard([sequentialTransition], disabledGuard);
600
611
  const summary = callSummaryFromHandler(handler, setters);
@@ -606,6 +617,9 @@ function transitionsFromResolvedHandler(source, fileName, node, attr, handler, s
606
617
  const navigation = navigationTransition(source, fileName, node, attr, component, inlinedCall, locator);
607
618
  if (navigation)
608
619
  return applyParsedGuard([navigation], disabledGuard);
620
+ const swrMutate = swrMutateTransition(source, fileName, node, attr, component, inlinedCall, locator);
621
+ if (swrMutate)
622
+ return applyParsedGuard([swrMutate], disabledGuard);
609
623
  const setterCall = setterCallFrom(inlinedCall, setters);
610
624
  if (!setterCall) {
611
625
  const escaped = escapedSetters(inlinedCall, setters);
@@ -633,7 +647,7 @@ function transitionsFromResolvedHandler(source, fileName, node, attr, handler, s
633
647
  confidence: "exact"
634
648
  }], disabledGuard);
635
649
  }
636
- function sequentialTransitionFromHandler(source, fileName, node, attr, handler, setters, component, locator) {
650
+ function sequentialTransitionFromHandler(source, fileName, node, attr, handler, setters, handlers, component, locator) {
637
651
  if (!ts.isBlock(handler.body))
638
652
  return undefined;
639
653
  const locals = new Map();
@@ -641,6 +655,11 @@ function sequentialTransitionFromHandler(source, fileName, node, attr, handler,
641
655
  for (const statement of handler.body.statements) {
642
656
  if (bindConstStatement(statement, setters, locals))
643
657
  continue;
658
+ const helper = helperSummariesFromStatement(statement, handlers, setters);
659
+ if (helper) {
660
+ summaries.push(...helper);
661
+ continue;
662
+ }
644
663
  const summary = summarizeSetterStatement(statement, setters, locals);
645
664
  if (!summary)
646
665
  return undefined;
@@ -662,6 +681,23 @@ function sequentialTransitionFromHandler(source, fileName, node, attr, handler,
662
681
  confidence: effects.some((effect) => effect.kind === "havoc") ? "over-approx" : "exact"
663
682
  };
664
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
+ }
665
701
  function loopWriteTransitions(source, fileName, node, attr, handler, setters, component, locator) {
666
702
  if (!ts.isBlock(handler.body))
667
703
  return [];
@@ -727,6 +763,11 @@ function havocSetterTransition(source, fileName, node, attr, component, setter,
727
763
  };
728
764
  }
729
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
+ }
730
771
  if ((ts.isArrowFunction(argument) || ts.isFunctionExpression(argument)) && argument.parameters.length === 1 && ts.isIdentifier(argument.parameters[0].name)) {
731
772
  if (ts.isBlock(argument.body))
732
773
  return undefined;
@@ -734,11 +775,42 @@ function setterArgumentExpr(argument, setter, setters, locals) {
734
775
  }
735
776
  return valueExpr(argument, setters, locals);
736
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
+ }
737
809
  function valueExpr(expression, setters, locals) {
738
810
  const value = literalValue(expression);
739
811
  if (value !== undefined)
740
812
  return { expr: { kind: "lit", value }, reads: [] };
741
- if (ts.isIdentifier(expression) || ts.isPropertyAccessExpression(expression))
813
+ if (ts.isIdentifier(expression) || isPropertyAccessLike(expression))
742
814
  return modeledReadExpr(expression, setters, locals);
743
815
  if (ts.isPrefixUnaryExpression(expression) && expression.operator === ts.SyntaxKind.ExclamationToken) {
744
816
  const parsed = booleanExpr(expression.operand, setters, locals);
@@ -747,7 +819,7 @@ function valueExpr(expression, setters, locals) {
747
819
  if (ts.isParenthesizedExpression(expression))
748
820
  return valueExpr(expression.expression, setters, locals);
749
821
  if (ts.isBinaryExpression(expression) && expression.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken) {
750
- return nullishOptionalReadExpr(expression, setters);
822
+ return nullishOptionalReadExpr(expression, setters, locals);
751
823
  }
752
824
  if (ts.isConditionalExpression(expression)) {
753
825
  const condition = booleanExpr(expression.condition, setters, locals);
@@ -791,22 +863,25 @@ function objectSpreadUpdateExpr(expression, setters, locals) {
791
863
  }
792
864
  return current;
793
865
  }
794
- function nullishOptionalReadExpr(expression, setters) {
795
- if (literalValue(expression.right) !== null)
866
+ function nullishOptionalReadExpr(expression, setters, locals = new Map()) {
867
+ const fallback = literalValue(expression.right);
868
+ if (fallback === undefined)
796
869
  return undefined;
797
870
  const read = optionalReadPath(expression.left);
798
871
  if (!read?.optional || read.path.length === 0)
799
872
  return undefined;
800
- 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);
801
875
  if (!varId)
802
876
  return undefined;
877
+ const basePath = local?.expr.kind === "read" ? local.expr.path ?? [] : [];
803
878
  return {
804
879
  expr: {
805
880
  kind: "cond",
806
881
  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 }
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] }
810
885
  ]
811
886
  },
812
887
  reads: [varId]
@@ -815,7 +890,7 @@ function nullishOptionalReadExpr(expression, setters) {
815
890
  function optionalReadPath(expression) {
816
891
  if (ts.isIdentifier(expression))
817
892
  return { base: expression.text, path: [], optional: false };
818
- if (ts.isPropertyAccessExpression(expression)) {
893
+ if (isPropertyAccessLike(expression)) {
819
894
  const base = optionalReadPath(expression.expression);
820
895
  if (!base)
821
896
  return undefined;
@@ -891,9 +966,13 @@ function modeledReadExpr(expression, setters, locals) {
891
966
  reads: local.reads
892
967
  };
893
968
  }
894
- const stateVar = stateVarForName(base, setters);
969
+ const setter = setterForName(base, setters);
970
+ const stateVar = setter?.varId;
895
971
  if (!stateVar)
896
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
+ }
897
976
  return {
898
977
  expr: { kind: "read", var: stateVar, ...(segments.length > 0 ? { path: segments } : {}) },
899
978
  reads: [stateVar]
@@ -980,6 +1059,8 @@ function callSummaryFromHandler(handler, setters, initialLocals = new Map()) {
980
1059
  const body = handler.body;
981
1060
  if (ts.isCallExpression(body))
982
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) };
983
1064
  if (ts.isBlock(body)) {
984
1065
  const locals = new Map(initialLocals);
985
1066
  for (let index = 0; index < body.statements.length; index += 1) {
@@ -987,12 +1068,29 @@ function callSummaryFromHandler(handler, setters, initialLocals = new Map()) {
987
1068
  const isLast = index === body.statements.length - 1;
988
1069
  if (isLast && ts.isExpressionStatement(statement) && ts.isCallExpression(statement.expression))
989
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 };
990
1073
  if (!bindConstStatement(statement, setters, locals))
991
1074
  return undefined;
992
1075
  }
993
1076
  }
994
1077
  return undefined;
995
1078
  }
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
+ }
996
1094
  function bindConstStatement(statement, setters, locals, partialBoolean = false) {
997
1095
  if (!ts.isVariableStatement(statement))
998
1096
  return false;
@@ -1989,7 +2087,7 @@ function parseGuardExpression(expression, setters, locals = new Map()) {
1989
2087
  return { expr: { kind: "lit", value: true }, reads: [] };
1990
2088
  if (expression.kind === ts.SyntaxKind.FalseKeyword)
1991
2089
  return { expr: { kind: "lit", value: false }, reads: [] };
1992
- if (ts.isIdentifier(expression) || ts.isPropertyAccessExpression(expression))
2090
+ if (ts.isIdentifier(expression) || isPropertyAccessLike(expression))
1993
2091
  return valueExpr(expression, setters, locals);
1994
2092
  if (ts.isPrefixUnaryExpression(expression) && expression.operator === ts.SyntaxKind.ExclamationToken) {
1995
2093
  const parsed = parseGuardExpression(expression.operand, setters, locals);
@@ -2034,7 +2132,7 @@ function parseGuardOperand(expression, setters, locals = new Map()) {
2034
2132
  const value = literalValue(expression);
2035
2133
  if (value !== undefined)
2036
2134
  return { expr: { kind: "lit", value }, reads: [] };
2037
- if (ts.isIdentifier(expression) || ts.isPropertyAccessExpression(expression))
2135
+ if (ts.isIdentifier(expression) || isPropertyAccessLike(expression))
2038
2136
  return valueExpr(expression, setters, locals);
2039
2137
  return parseGuardExpression(expression, setters, locals);
2040
2138
  }
@@ -2050,7 +2148,32 @@ function parseConjunctiveGuardExpression(expression, setters, locals = new Map()
2050
2148
  return parseGuardExpression(expression, setters, locals);
2051
2149
  }
2052
2150
  function stateVarForName(name, setters) {
2053
- 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;
2054
2177
  }
2055
2178
  function andGuard(left, right) {
2056
2179
  if (isTrueLiteral(left))
@@ -2149,12 +2272,15 @@ function isInputValueExpression(node, parameter) {
2149
2272
  function propertyAccessPath(node) {
2150
2273
  if (ts.isIdentifier(node))
2151
2274
  return [node.text];
2152
- if (ts.isPropertyAccessExpression(node)) {
2275
+ if (isPropertyAccessLike(node)) {
2153
2276
  const base = propertyAccessPath(node.expression);
2154
2277
  return base ? [...base, node.name.text] : undefined;
2155
2278
  }
2156
2279
  return undefined;
2157
2280
  }
2281
+ function isPropertyAccessLike(node) {
2282
+ return ts.isPropertyAccessExpression(node) || ts.isPropertyAccessChain(node);
2283
+ }
2158
2284
  function valueClassForDomain(domain) {
2159
2285
  if (domain.kind === "enum")
2160
2286
  return domain.values.join("|") || "enum";