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