@gtsx/core 0.0.1 → 0.0.2
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/analyzer.d.ts +26 -0
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +2218 -136
- package/dist/analyzer.js.map +1 -1
- package/dist/boundary-rect.js +5 -2
- package/dist/boundary-rect.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +19 -9
- package/dist/cli.js.map +1 -1
- package/dist/config-model.d.ts +1 -1
- package/dist/config-model.d.ts.map +1 -1
- package/dist/config-model.js +1 -1
- package/dist/config-model.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/init.js +2 -2
- package/dist/init.js.map +1 -1
- package/dist/project-index.d.ts.map +1 -1
- package/dist/project-index.js +130 -18
- package/dist/project-index.js.map +1 -1
- package/dist/project-scope.d.ts.map +1 -1
- package/dist/project-scope.js +2 -6
- package/dist/project-scope.js.map +1 -1
- package/dist/react-transform.d.ts.map +1 -1
- package/dist/react-transform.js +142 -29
- package/dist/react-transform.js.map +1 -1
- package/dist/runtime.d.ts +6 -3
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +9 -3
- package/dist/runtime.js.map +1 -1
- package/dist/types.d.ts +14 -5
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/analyzer.js
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import { resolve } from "node:path";
|
|
1
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
3
3
|
import ts from "typescript";
|
|
4
|
+
export function createGTSXAnalysisCache(sourceFilesByPath = new Map()) {
|
|
5
|
+
return {
|
|
6
|
+
componentDependencyBindingsByPath: new Map(),
|
|
7
|
+
exportedComponentTargetsByPath: new Map(),
|
|
8
|
+
importedGTSXPathByKey: new Map(),
|
|
9
|
+
importedScopeHookNamesByPath: new Map(),
|
|
10
|
+
providerSummariesByPath: new Map(),
|
|
11
|
+
scopeHookProviderNamesByPath: new Map(),
|
|
12
|
+
sourceFilesByPath,
|
|
13
|
+
topLevelFunctionLikeBodiesByPath: new Map(),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
4
16
|
export function analyzeEntry(options) {
|
|
5
17
|
const entryCoordinate = parseEntryCoordinate(options.entry);
|
|
6
18
|
const entryPath = resolve(options.cwd, entryCoordinate.file);
|
|
@@ -22,11 +34,30 @@ export function analyzeEntry(options) {
|
|
|
22
34
|
],
|
|
23
35
|
};
|
|
24
36
|
}
|
|
25
|
-
const
|
|
26
|
-
|
|
37
|
+
const sourceFile = sourceFileForAbsolutePath(entryPath, options.cache);
|
|
38
|
+
if (!sourceFile) {
|
|
39
|
+
return {
|
|
40
|
+
entry: options.entry,
|
|
41
|
+
mode: "unknown",
|
|
42
|
+
defaultExport: false,
|
|
43
|
+
cases: [],
|
|
44
|
+
providers: {},
|
|
45
|
+
diagnostics: [
|
|
46
|
+
{
|
|
47
|
+
stage: "contract-extraction",
|
|
48
|
+
code: "entry-not-found",
|
|
49
|
+
message: `GTSX entry does not exist: ${options.entry}`,
|
|
50
|
+
file: options.entry,
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
};
|
|
54
|
+
}
|
|
27
55
|
const componentExportName = getComponentExportName(sourceFile, entryCoordinate.exportName);
|
|
28
|
-
const scopeHookNames =
|
|
29
|
-
|
|
56
|
+
const scopeHookNames = new Set([
|
|
57
|
+
...getScopeHookNames(sourceFile),
|
|
58
|
+
...getImportedScopeHookNames(sourceFile, entryPath, options.cwd, options.cache),
|
|
59
|
+
]);
|
|
60
|
+
const providerCases = Object.fromEntries(getGProviderSummariesForFile(sourceFile, entryPath, options.cwd, options.cache));
|
|
30
61
|
const componentAssignments = [];
|
|
31
62
|
const scopeAssignments = [];
|
|
32
63
|
for (const statement of sourceFile.statements) {
|
|
@@ -64,12 +95,20 @@ export function analyzeEntry(options) {
|
|
|
64
95
|
file: options.entry,
|
|
65
96
|
});
|
|
66
97
|
}
|
|
67
|
-
for (const
|
|
98
|
+
for (const violation of getNonGTSXHookCalls(sourceFile, componentExportName, scopeHookNames, {
|
|
99
|
+
cwd: options.cwd,
|
|
100
|
+
entryPath,
|
|
101
|
+
cache: options.cache,
|
|
102
|
+
sourceFilesByPath: options.cache?.sourceFilesByPath ?? new Map([[entryPath, sourceFile]]),
|
|
103
|
+
visitedComponents: new Set(),
|
|
104
|
+
})) {
|
|
68
105
|
diagnostics.push({
|
|
69
106
|
stage: "contract-extraction",
|
|
70
107
|
code: "non-gtsx-hook",
|
|
71
|
-
message:
|
|
72
|
-
|
|
108
|
+
message: violation.file === normalizeProjectPath(relative(options.cwd, entryPath))
|
|
109
|
+
? `GTSX components may only call GTSX hooks; found "${violation.hookName}" in "${violation.componentName}". Wrap production hooks with createGScopeHook(...).`
|
|
110
|
+
: `GTSX components may only call GTSX hooks; found "${violation.hookName}" in dependency "${violation.file}#${violation.componentName}". Wrap production hooks with createGScopeHook(...).`,
|
|
111
|
+
file: violation.file,
|
|
73
112
|
});
|
|
74
113
|
}
|
|
75
114
|
}
|
|
@@ -90,6 +129,7 @@ export function analyzeEntry(options) {
|
|
|
90
129
|
});
|
|
91
130
|
}
|
|
92
131
|
const componentCases = componentAssignments.flatMap((assignment) => assignment.cases);
|
|
132
|
+
const componentStaticCases = componentAssignments.flatMap((assignment) => assignment.staticCases);
|
|
93
133
|
const mode = componentCases.length === 0
|
|
94
134
|
? "unknown"
|
|
95
135
|
: componentCases.some((testCase) => testCase.kind === "scope")
|
|
@@ -98,6 +138,14 @@ export function analyzeEntry(options) {
|
|
|
98
138
|
const selectedCases = mode === "scope"
|
|
99
139
|
? componentCases.map((testCase) => ({ ...testCase, kind: "scope" }))
|
|
100
140
|
: componentCases.map((testCase) => ({ ...testCase, kind: "pure" }));
|
|
141
|
+
const importedNames = getImportedNames(sourceFile);
|
|
142
|
+
for (const testCase of selectedCases) {
|
|
143
|
+
for (const providerName of testCase.providers ?? []) {
|
|
144
|
+
if (!providerCases[providerName] && importedNames.has(providerName)) {
|
|
145
|
+
providerCases[providerName] = { name: providerName, cases: [] };
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
101
149
|
if (selectedCases.length === 0) {
|
|
102
150
|
diagnostics.push({
|
|
103
151
|
stage: "contract-extraction",
|
|
@@ -108,6 +156,31 @@ export function analyzeEntry(options) {
|
|
|
108
156
|
}
|
|
109
157
|
for (const testCase of selectedCases) {
|
|
110
158
|
validateProviderSelections(testCase, providerCases, diagnostics, options.entry);
|
|
159
|
+
validateProviderVariantSelections(testCase, providerCases, diagnostics, options.entry);
|
|
160
|
+
}
|
|
161
|
+
if (componentExportName) {
|
|
162
|
+
const consumedProviderNames = getGProviderConsumers(sourceFile, componentExportName, {
|
|
163
|
+
cwd: options.cwd,
|
|
164
|
+
entryPath,
|
|
165
|
+
cache: options.cache,
|
|
166
|
+
sourceFilesByPath: options.cache?.sourceFilesByPath ?? new Map([[entryPath, sourceFile]]),
|
|
167
|
+
visitedComponents: new Set(),
|
|
168
|
+
});
|
|
169
|
+
validateProviderVariantCoverage(consumedProviderNames, providerCases, selectedCases, diagnostics, options.entry);
|
|
170
|
+
validateJSXTreeCaseReachability(sourceFile, componentExportName, scopeHookNames, componentStaticCases, diagnostics, options.entry, {
|
|
171
|
+
cwd: options.cwd,
|
|
172
|
+
entryPath,
|
|
173
|
+
cache: options.cache,
|
|
174
|
+
sourceFilesByPath: options.cache?.sourceFilesByPath ?? new Map([[entryPath, sourceFile]]),
|
|
175
|
+
visitedComponents: new Set(),
|
|
176
|
+
});
|
|
177
|
+
validateProviderProjectionWarnings(sourceFile, componentExportName, scopeHookNames, providerCases, diagnostics, options.entry, {
|
|
178
|
+
cwd: options.cwd,
|
|
179
|
+
entryPath,
|
|
180
|
+
cache: options.cache,
|
|
181
|
+
sourceFilesByPath: options.cache?.sourceFilesByPath ?? new Map([[entryPath, sourceFile]]),
|
|
182
|
+
visitedComponents: new Set(),
|
|
183
|
+
});
|
|
111
184
|
}
|
|
112
185
|
return {
|
|
113
186
|
entry: options.entry,
|
|
@@ -145,6 +218,13 @@ function getDefaultExportName(sourceFile) {
|
|
|
145
218
|
if (ts.isExportAssignment(statement) && ts.isIdentifier(statement.expression)) {
|
|
146
219
|
return statement.expression.text;
|
|
147
220
|
}
|
|
221
|
+
if (ts.isExportDeclaration(statement) && !statement.moduleSpecifier && statement.exportClause && ts.isNamedExports(statement.exportClause)) {
|
|
222
|
+
for (const element of statement.exportClause.elements) {
|
|
223
|
+
if (element.name.text === "default") {
|
|
224
|
+
return element.propertyName?.text ?? element.name.text;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
148
228
|
}
|
|
149
229
|
return undefined;
|
|
150
230
|
}
|
|
@@ -155,6 +235,20 @@ function getNamedExportName(sourceFile, exportName) {
|
|
|
155
235
|
hasModifier(statement, ts.SyntaxKind.ExportKeyword)) {
|
|
156
236
|
return exportName;
|
|
157
237
|
}
|
|
238
|
+
if (ts.isVariableStatement(statement) && hasModifier(statement, ts.SyntaxKind.ExportKeyword)) {
|
|
239
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
240
|
+
if (ts.isIdentifier(declaration.name) && declaration.name.text === exportName && getFunctionLikeBody(sourceFile, exportName)) {
|
|
241
|
+
return exportName;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (ts.isExportDeclaration(statement) && !statement.moduleSpecifier && statement.exportClause && ts.isNamedExports(statement.exportClause)) {
|
|
246
|
+
for (const element of statement.exportClause.elements) {
|
|
247
|
+
if (element.name.text === exportName) {
|
|
248
|
+
return element.propertyName?.text ?? element.name.text;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
158
252
|
}
|
|
159
253
|
return undefined;
|
|
160
254
|
}
|
|
@@ -165,6 +259,18 @@ function getFirstNamedExportName(sourceFile) {
|
|
|
165
259
|
hasModifier(statement, ts.SyntaxKind.ExportKeyword)) {
|
|
166
260
|
return statement.name.text;
|
|
167
261
|
}
|
|
262
|
+
if (ts.isVariableStatement(statement) && hasModifier(statement, ts.SyntaxKind.ExportKeyword)) {
|
|
263
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
264
|
+
if (ts.isIdentifier(declaration.name) && getFunctionLikeBody(sourceFile, declaration.name.text)) {
|
|
265
|
+
return declaration.name.text;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (ts.isExportDeclaration(statement) && !statement.moduleSpecifier && statement.exportClause && ts.isNamedExports(statement.exportClause)) {
|
|
270
|
+
const element = statement.exportClause.elements[0];
|
|
271
|
+
if (element)
|
|
272
|
+
return element.propertyName?.text ?? element.name.text;
|
|
273
|
+
}
|
|
168
274
|
}
|
|
169
275
|
return undefined;
|
|
170
276
|
}
|
|
@@ -176,186 +282,2159 @@ function getScopeHookNames(sourceFile) {
|
|
|
176
282
|
for (const declaration of statement.declarationList.declarations) {
|
|
177
283
|
if (!ts.isIdentifier(declaration.name) || !declaration.initializer)
|
|
178
284
|
continue;
|
|
179
|
-
if (isCreateGScopeCall(declaration.initializer)) {
|
|
285
|
+
if (isCreateGScopeCall(unwrapExpression(declaration.initializer))) {
|
|
180
286
|
names.add(declaration.name.text);
|
|
181
287
|
}
|
|
182
288
|
}
|
|
183
289
|
}
|
|
184
290
|
return names;
|
|
185
291
|
}
|
|
186
|
-
function
|
|
187
|
-
const
|
|
292
|
+
function getScopeHookProviderNamesForFile(sourceFile, filePath, cache) {
|
|
293
|
+
const cached = cache?.scopeHookProviderNamesByPath.get(filePath);
|
|
294
|
+
if (cached)
|
|
295
|
+
return cached;
|
|
296
|
+
const hookProviders = new Map();
|
|
188
297
|
for (const statement of sourceFile.statements) {
|
|
189
298
|
if (!ts.isVariableStatement(statement))
|
|
190
299
|
continue;
|
|
191
300
|
for (const declaration of statement.declarationList.declarations) {
|
|
192
301
|
if (!ts.isIdentifier(declaration.name) || !declaration.initializer)
|
|
193
302
|
continue;
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
303
|
+
const initializer = unwrapExpression(declaration.initializer);
|
|
304
|
+
if (!isCreateGScopeCall(initializer))
|
|
305
|
+
continue;
|
|
306
|
+
const providersExpression = initializer.arguments[1];
|
|
307
|
+
if (!providersExpression)
|
|
308
|
+
continue;
|
|
309
|
+
const providerNames = readProviderNameList(providersExpression, sourceFile);
|
|
310
|
+
if (providerNames.length > 0)
|
|
311
|
+
hookProviders.set(declaration.name.text, providerNames);
|
|
197
312
|
}
|
|
198
313
|
}
|
|
199
|
-
|
|
314
|
+
cache?.scopeHookProviderNamesByPath.set(filePath, hookProviders);
|
|
315
|
+
return hookProviders;
|
|
200
316
|
}
|
|
201
|
-
function
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
317
|
+
function readProviderNameList(expression, sourceFile, visited = new Set()) {
|
|
318
|
+
const value = unwrapExpression(expression);
|
|
319
|
+
if (ts.isArrayLiteralExpression(value)) {
|
|
320
|
+
return value.elements.flatMap((element) => {
|
|
321
|
+
const provider = unwrapExpression(element);
|
|
322
|
+
return ts.isIdentifier(provider) ? [provider.text] : [];
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
if (ts.isIdentifier(value)) {
|
|
326
|
+
if (visited.has(value.text))
|
|
327
|
+
return [];
|
|
328
|
+
visited.add(value.text);
|
|
329
|
+
const initializer = topLevelVariableInitializer(sourceFile, value.text);
|
|
330
|
+
return initializer ? readProviderNameList(initializer, sourceFile, visited) : [];
|
|
331
|
+
}
|
|
332
|
+
return [];
|
|
333
|
+
}
|
|
334
|
+
function topLevelVariableInitializer(sourceFile, name) {
|
|
335
|
+
for (const statement of sourceFile.statements) {
|
|
336
|
+
if (!ts.isVariableStatement(statement))
|
|
337
|
+
continue;
|
|
338
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
339
|
+
if (ts.isIdentifier(declaration.name) && declaration.name.text === name)
|
|
340
|
+
return declaration.initializer;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return undefined;
|
|
344
|
+
}
|
|
345
|
+
function getGProviderSummaries(sourceFile) {
|
|
346
|
+
const providers = new Map();
|
|
347
|
+
for (const statement of sourceFile.statements) {
|
|
348
|
+
if (!ts.isVariableStatement(statement))
|
|
349
|
+
continue;
|
|
350
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
351
|
+
if (!ts.isIdentifier(declaration.name) || !declaration.initializer)
|
|
352
|
+
continue;
|
|
353
|
+
const initializer = unwrapExpression(declaration.initializer);
|
|
354
|
+
if (!isCreateGProviderCall(initializer))
|
|
355
|
+
continue;
|
|
356
|
+
const variants = readCreateGProviderVariants(initializer);
|
|
357
|
+
providers.set(declaration.name.text, {
|
|
358
|
+
name: declaration.name.text,
|
|
359
|
+
cases: [],
|
|
360
|
+
...(variants && variants.length > 0 ? { variants } : {}),
|
|
212
361
|
});
|
|
213
362
|
}
|
|
214
363
|
}
|
|
364
|
+
return providers;
|
|
215
365
|
}
|
|
216
|
-
function
|
|
217
|
-
const
|
|
218
|
-
if (
|
|
219
|
-
return
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
if (
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
366
|
+
function getGProviderSummariesForFile(sourceFile, filePath, cwd, cache) {
|
|
367
|
+
const cached = cache?.providerSummariesByPath.get(filePath);
|
|
368
|
+
if (cached)
|
|
369
|
+
return cached;
|
|
370
|
+
const providers = new Map(getGProviderSummaries(sourceFile));
|
|
371
|
+
cache?.providerSummariesByPath.set(filePath, providers);
|
|
372
|
+
for (const statement of sourceFile.statements) {
|
|
373
|
+
if (!ts.isImportDeclaration(statement) || !statement.importClause || !ts.isStringLiteral(statement.moduleSpecifier))
|
|
374
|
+
continue;
|
|
375
|
+
const targetPath = resolveImportedGTSXPath(filePath, cwd, statement.moduleSpecifier.text, cache);
|
|
376
|
+
if (!targetPath)
|
|
377
|
+
continue;
|
|
378
|
+
const targetSource = sourceFileForAbsolutePath(targetPath, cache);
|
|
379
|
+
if (!targetSource)
|
|
380
|
+
continue;
|
|
381
|
+
const targetProviders = getGProviderSummariesForFile(targetSource, targetPath, cwd, cache);
|
|
382
|
+
const importClause = statement.importClause;
|
|
383
|
+
if (importClause.name) {
|
|
384
|
+
const importedDefault = targetProviders.get("default");
|
|
385
|
+
if (importedDefault) {
|
|
386
|
+
providers.set(importClause.name.text, { ...importedDefault, name: importClause.name.text });
|
|
230
387
|
}
|
|
231
|
-
|
|
232
|
-
|
|
388
|
+
}
|
|
389
|
+
const namedBindings = importClause.namedBindings;
|
|
390
|
+
if (!namedBindings || !ts.isNamedImports(namedBindings))
|
|
391
|
+
continue;
|
|
392
|
+
for (const element of namedBindings.elements) {
|
|
393
|
+
const importedName = element.propertyName?.text ?? element.name.text;
|
|
394
|
+
const importedProvider = targetProviders.get(importedName);
|
|
395
|
+
if (importedProvider) {
|
|
396
|
+
providers.set(element.name.text, { ...importedProvider, name: element.name.text });
|
|
233
397
|
}
|
|
234
398
|
}
|
|
235
|
-
ts.forEachChild(node, visit);
|
|
236
399
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
400
|
+
return providers;
|
|
401
|
+
}
|
|
402
|
+
function readCreateGProviderVariants(expression) {
|
|
403
|
+
const options = expression.arguments[1] ? unwrapExpression(expression.arguments[1]) : undefined;
|
|
404
|
+
if (!options || !ts.isObjectLiteralExpression(options))
|
|
405
|
+
return undefined;
|
|
406
|
+
const variantsProperty = options.properties.find((property) => ts.isPropertyAssignment(property) && getStaticPropertyName(property.name) === "variants");
|
|
407
|
+
if (!variantsProperty)
|
|
408
|
+
return undefined;
|
|
409
|
+
const variantsValue = unwrapExpression(variantsProperty.initializer);
|
|
410
|
+
if (!ts.isArrayLiteralExpression(variantsValue))
|
|
411
|
+
return undefined;
|
|
412
|
+
return variantsValue.elements.flatMap((element) => {
|
|
413
|
+
const variant = unwrapExpression(element);
|
|
414
|
+
return ts.isStringLiteral(variant) ? [variant.text] : [];
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
function getImportedScopeHookNames(sourceFile, entryPath, cwd, cache) {
|
|
418
|
+
const cached = cache?.importedScopeHookNamesByPath.get(entryPath);
|
|
419
|
+
if (cached)
|
|
420
|
+
return cached;
|
|
421
|
+
const names = new Set();
|
|
422
|
+
for (const statement of sourceFile.statements) {
|
|
423
|
+
if (!ts.isImportDeclaration(statement))
|
|
424
|
+
continue;
|
|
425
|
+
const clause = statement.importClause;
|
|
426
|
+
if (!clause || !ts.isStringLiteral(statement.moduleSpecifier))
|
|
427
|
+
continue;
|
|
428
|
+
const targetPath = resolveImportedGTSXPath(entryPath, cwd, statement.moduleSpecifier.text, cache);
|
|
429
|
+
if (!targetPath)
|
|
430
|
+
continue;
|
|
431
|
+
const targetSource = sourceFileForAbsolutePath(targetPath, cache);
|
|
432
|
+
if (!targetSource)
|
|
433
|
+
continue;
|
|
434
|
+
const exportedScopeHookNames = getExportedScopeHookNames(targetSource);
|
|
435
|
+
if (clause.name && exportedScopeHookNames.has("default") && isHookName(clause.name.text)) {
|
|
436
|
+
names.add(clause.name.text);
|
|
437
|
+
}
|
|
438
|
+
const namedBindings = clause.namedBindings;
|
|
439
|
+
if (!namedBindings || !ts.isNamedImports(namedBindings))
|
|
440
|
+
continue;
|
|
441
|
+
for (const element of namedBindings.elements) {
|
|
442
|
+
const importedName = element.propertyName?.text ?? element.name.text;
|
|
443
|
+
if (exportedScopeHookNames.has(importedName) && isHookName(element.name.text)) {
|
|
444
|
+
names.add(element.name.text);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
245
447
|
}
|
|
448
|
+
cache?.importedScopeHookNamesByPath.set(entryPath, names);
|
|
449
|
+
return names;
|
|
246
450
|
}
|
|
247
|
-
function
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
function visit(node) {
|
|
257
|
-
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {
|
|
258
|
-
if (scopeHookNames.has(node.expression.text)) {
|
|
259
|
-
hookNames.add(node.expression.text);
|
|
451
|
+
function getExportedScopeHookNames(sourceFile) {
|
|
452
|
+
const localScopeHookNames = getScopeHookNames(sourceFile);
|
|
453
|
+
const exportedScopeHookNames = new Set();
|
|
454
|
+
for (const statement of sourceFile.statements) {
|
|
455
|
+
if (ts.isVariableStatement(statement) && hasModifier(statement, ts.SyntaxKind.ExportKeyword)) {
|
|
456
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
457
|
+
if (ts.isIdentifier(declaration.name) && localScopeHookNames.has(declaration.name.text)) {
|
|
458
|
+
exportedScopeHookNames.add(declaration.name.text);
|
|
459
|
+
}
|
|
260
460
|
}
|
|
261
|
-
|
|
262
|
-
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
if (ts.isExportAssignment(statement) && ts.isIdentifier(statement.expression) && localScopeHookNames.has(statement.expression.text)) {
|
|
464
|
+
exportedScopeHookNames.add("default");
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
if (!ts.isExportDeclaration(statement) || statement.moduleSpecifier || !statement.exportClause || !ts.isNamedExports(statement.exportClause)) {
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
for (const element of statement.exportClause.elements) {
|
|
471
|
+
const localName = element.propertyName?.text ?? element.name.text;
|
|
472
|
+
if (localScopeHookNames.has(localName)) {
|
|
473
|
+
exportedScopeHookNames.add(element.name.text);
|
|
263
474
|
}
|
|
264
475
|
}
|
|
265
|
-
ts.forEachChild(node, visit);
|
|
266
476
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
477
|
+
return exportedScopeHookNames;
|
|
478
|
+
}
|
|
479
|
+
function resolveImportedGTSXPath(entryPath, cwd, specifier, cache) {
|
|
480
|
+
const cacheKey = `${entryPath}\0${specifier}`;
|
|
481
|
+
const cached = cache?.importedGTSXPathByKey.get(cacheKey);
|
|
482
|
+
if (cached !== undefined)
|
|
483
|
+
return cached ?? undefined;
|
|
484
|
+
const basePath = resolveImportBasePath(entryPath, cwd, specifier);
|
|
485
|
+
if (!basePath) {
|
|
486
|
+
cache?.importedGTSXPathByKey.set(cacheKey, null);
|
|
487
|
+
return undefined;
|
|
275
488
|
}
|
|
489
|
+
const visited = new Set();
|
|
490
|
+
const resolved = resolveImportedGTSXPathFromBase(basePath, cwd, visited, cache);
|
|
491
|
+
cache?.importedGTSXPathByKey.set(cacheKey, resolved ?? null);
|
|
492
|
+
return resolved;
|
|
276
493
|
}
|
|
277
|
-
function
|
|
278
|
-
|
|
494
|
+
function resolveImportedGTSXPathFromBase(basePath, cwd, visited, cache) {
|
|
495
|
+
for (const candidate of importedGTSXPathCandidates(basePath)) {
|
|
496
|
+
if (isFile(candidate))
|
|
497
|
+
return candidate;
|
|
498
|
+
}
|
|
499
|
+
for (const barrelCandidate of importedTSXBarrelCandidates(basePath)) {
|
|
500
|
+
if (!isFile(barrelCandidate) || visited.has(barrelCandidate))
|
|
501
|
+
continue;
|
|
502
|
+
visited.add(barrelCandidate);
|
|
503
|
+
const barrelSource = sourceFileForAbsolutePath(barrelCandidate, cache);
|
|
504
|
+
if (!barrelSource)
|
|
505
|
+
continue;
|
|
506
|
+
for (const statement of barrelSource.statements) {
|
|
507
|
+
if (!ts.isExportDeclaration(statement) || !statement.moduleSpecifier || !ts.isStringLiteral(statement.moduleSpecifier))
|
|
508
|
+
continue;
|
|
509
|
+
const nextBasePath = resolveImportBasePath(barrelCandidate, cwd, statement.moduleSpecifier.text);
|
|
510
|
+
const resolvedPath = nextBasePath ? resolveImportedGTSXPathFromBase(nextBasePath, cwd, visited, cache) : undefined;
|
|
511
|
+
if (resolvedPath)
|
|
512
|
+
return resolvedPath;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
return undefined;
|
|
279
516
|
}
|
|
280
|
-
function
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
if (
|
|
284
|
-
|
|
517
|
+
function resolveImportBasePath(entryPath, cwd, specifier) {
|
|
518
|
+
if (specifier.startsWith("@/")) {
|
|
519
|
+
const direct = resolve(cwd, specifier.slice(2));
|
|
520
|
+
if (importedGTSXPathCandidates(direct).some((candidate) => isFile(candidate)) ||
|
|
521
|
+
importedTSXBarrelCandidates(direct).some((candidate) => isFile(candidate))) {
|
|
522
|
+
return direct;
|
|
285
523
|
}
|
|
524
|
+
return resolve(cwd, "src", specifier.slice(2));
|
|
286
525
|
}
|
|
287
|
-
|
|
526
|
+
if (specifier.startsWith("."))
|
|
527
|
+
return resolve(dirname(entryPath), specifier);
|
|
528
|
+
return undefined;
|
|
288
529
|
}
|
|
289
|
-
function
|
|
290
|
-
|
|
530
|
+
function importedGTSXPathCandidates(basePath) {
|
|
531
|
+
const extensionCandidate = /\.(?:tsx|ts|jsx|js)$/.test(basePath) ? basePath.replace(/\.(?:tsx|ts|jsx|js)$/, ".g.tsx") : undefined;
|
|
532
|
+
const extensionTypeCandidate = /\.(?:tsx|ts|jsx|js)$/.test(basePath) ? basePath.replace(/\.(?:tsx|ts|jsx|js)$/, ".g.ts") : undefined;
|
|
533
|
+
const candidates = [
|
|
534
|
+
basePath.endsWith(".g.tsx") ? basePath : undefined,
|
|
535
|
+
basePath.endsWith(".g.ts") ? basePath : undefined,
|
|
536
|
+
basePath.endsWith(".g") ? `${basePath}.tsx` : undefined,
|
|
537
|
+
basePath.endsWith(".g") ? `${basePath}.ts` : undefined,
|
|
538
|
+
`${basePath}.g.tsx`,
|
|
539
|
+
`${basePath}.g.ts`,
|
|
540
|
+
extensionCandidate,
|
|
541
|
+
extensionTypeCandidate,
|
|
542
|
+
join(basePath, "index.g.tsx"),
|
|
543
|
+
join(basePath, "index.g.ts"),
|
|
544
|
+
];
|
|
545
|
+
return [...new Set(candidates.filter((candidate) => Boolean(candidate)))];
|
|
291
546
|
}
|
|
292
|
-
function
|
|
293
|
-
|
|
294
|
-
return
|
|
295
|
-
const expression = statement.expression;
|
|
296
|
-
if (!ts.isBinaryExpression(expression) || expression.operatorToken.kind !== ts.SyntaxKind.EqualsToken) {
|
|
297
|
-
return undefined;
|
|
547
|
+
function isFile(path) {
|
|
548
|
+
try {
|
|
549
|
+
return statSync(path).isFile();
|
|
298
550
|
}
|
|
299
|
-
|
|
300
|
-
return
|
|
551
|
+
catch {
|
|
552
|
+
return false;
|
|
301
553
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
554
|
+
}
|
|
555
|
+
function importedTSXBarrelCandidates(basePath) {
|
|
556
|
+
const candidates = [
|
|
557
|
+
basePath.endsWith(".tsx") ? basePath : undefined,
|
|
558
|
+
`${basePath}.tsx`,
|
|
559
|
+
join(basePath, "index.tsx"),
|
|
560
|
+
];
|
|
561
|
+
return [...new Set(candidates.filter((candidate) => Boolean(candidate)))];
|
|
562
|
+
}
|
|
563
|
+
function getImportedNames(sourceFile) {
|
|
564
|
+
const names = new Set();
|
|
565
|
+
for (const statement of sourceFile.statements) {
|
|
566
|
+
if (!ts.isImportDeclaration(statement))
|
|
567
|
+
continue;
|
|
568
|
+
const clause = statement.importClause;
|
|
569
|
+
if (!clause)
|
|
570
|
+
continue;
|
|
571
|
+
if (clause.name) {
|
|
572
|
+
names.add(clause.name.text);
|
|
573
|
+
}
|
|
574
|
+
const namedBindings = clause.namedBindings;
|
|
575
|
+
if (!namedBindings || !ts.isNamedImports(namedBindings))
|
|
576
|
+
continue;
|
|
577
|
+
for (const element of namedBindings.elements) {
|
|
578
|
+
names.add(element.name.text);
|
|
579
|
+
}
|
|
313
580
|
}
|
|
314
|
-
return
|
|
315
|
-
targetName: expression.left.expression.text,
|
|
316
|
-
cases: readCasesObject(casesExpression, sourceFile, diagnostics),
|
|
317
|
-
};
|
|
581
|
+
return names;
|
|
318
582
|
}
|
|
319
|
-
function
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
583
|
+
function validateProviderSelections(testCase, providerCases, diagnostics, file) {
|
|
584
|
+
if (!testCase.providers)
|
|
585
|
+
return;
|
|
586
|
+
for (const providerName of testCase.providers) {
|
|
587
|
+
if (!providerCases[providerName]) {
|
|
323
588
|
diagnostics.push({
|
|
324
589
|
stage: "contract-extraction",
|
|
325
|
-
code: "
|
|
326
|
-
message: "
|
|
327
|
-
file
|
|
590
|
+
code: "missing-provider",
|
|
591
|
+
message: `Case "${testCase.name}" selects unknown provider "${providerName}".`,
|
|
592
|
+
file,
|
|
593
|
+
caseName: testCase.name,
|
|
328
594
|
});
|
|
329
|
-
continue;
|
|
330
595
|
}
|
|
331
|
-
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
function validateProviderVariantSelections(testCase, providerCases, diagnostics, file) {
|
|
599
|
+
if (!testCase.providerVariants)
|
|
600
|
+
return;
|
|
601
|
+
for (const [providerName, selection] of Object.entries(testCase.providerVariants)) {
|
|
602
|
+
const variants = providerVariantSelectionValues(selection);
|
|
603
|
+
const provider = providerCases[providerName];
|
|
604
|
+
if (!provider) {
|
|
605
|
+
diagnostics.push({
|
|
606
|
+
stage: "contract-extraction",
|
|
607
|
+
code: "missing-provider",
|
|
608
|
+
message: `Case "${testCase.name}" marks unknown provider "${providerName}" variants "${variants.join(", ")}".`,
|
|
609
|
+
file,
|
|
610
|
+
caseName: testCase.name,
|
|
611
|
+
});
|
|
332
612
|
continue;
|
|
333
|
-
|
|
334
|
-
if (!
|
|
613
|
+
}
|
|
614
|
+
if (!provider.variants || provider.variants.length === 0) {
|
|
335
615
|
diagnostics.push({
|
|
336
616
|
stage: "contract-extraction",
|
|
337
|
-
code: "
|
|
338
|
-
message: "
|
|
339
|
-
file
|
|
617
|
+
code: "missing-provider-variants",
|
|
618
|
+
message: `Case "${testCase.name}" marks provider "${providerName}" variants "${variants.join(", ")}", but "${providerName}" does not declare variants.`,
|
|
619
|
+
file,
|
|
620
|
+
caseName: testCase.name,
|
|
340
621
|
});
|
|
341
622
|
continue;
|
|
342
623
|
}
|
|
343
|
-
const
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
624
|
+
const unknownVariants = variants.filter((variant) => !provider.variants?.includes(variant));
|
|
625
|
+
if (unknownVariants.length > 0) {
|
|
626
|
+
diagnostics.push({
|
|
627
|
+
stage: "contract-extraction",
|
|
628
|
+
code: "unknown-provider-variant",
|
|
629
|
+
message: `Case "${testCase.name}" marks unknown "${providerName}" variants "${unknownVariants.join(", ")}". Expected one of: ${provider.variants.join(", ")}.`,
|
|
630
|
+
file,
|
|
631
|
+
caseName: testCase.name,
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
function validateProviderVariantCoverage(consumedProviderNames, providerCases, selectedCases, diagnostics, file) {
|
|
637
|
+
for (const providerName of consumedProviderNames) {
|
|
638
|
+
const provider = providerCases[providerName];
|
|
639
|
+
if (!provider?.variants || provider.variants.length === 0)
|
|
640
|
+
continue;
|
|
641
|
+
const coveredVariants = new Set(selectedCases.flatMap((testCase) => {
|
|
642
|
+
const selection = testCase.providerVariants?.[providerName];
|
|
643
|
+
return selection ? providerVariantSelectionValues(selection) : [];
|
|
644
|
+
}));
|
|
645
|
+
const missingVariants = provider.variants.filter((variant) => !coveredVariants.has(variant));
|
|
646
|
+
if (missingVariants.length === 0)
|
|
647
|
+
continue;
|
|
648
|
+
diagnostics.push({
|
|
649
|
+
stage: "contract-extraction",
|
|
650
|
+
code: "missing-provider-variant-cases",
|
|
651
|
+
message: `GTSX entry consumes provider "${providerName}" but its cases do not cover variants: ${missingVariants.join(", ")}.`,
|
|
652
|
+
file,
|
|
350
653
|
});
|
|
351
654
|
}
|
|
352
|
-
return cases;
|
|
353
655
|
}
|
|
354
|
-
function
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
656
|
+
function providerVariantSelectionValues(selection) {
|
|
657
|
+
return Array.isArray(selection) ? selection : [selection];
|
|
658
|
+
}
|
|
659
|
+
function validateJSXTreeCaseReachability(sourceFile, componentName, scopeHookNames, staticCases, diagnostics, file, context) {
|
|
660
|
+
if (staticCases.length === 0)
|
|
661
|
+
return;
|
|
662
|
+
const component = getFunctionLikeDeclaration(sourceFile, componentName);
|
|
663
|
+
if (!component?.body)
|
|
664
|
+
return;
|
|
665
|
+
const branchContext = createJSXBranchAnalysisContext(sourceFile, component, scopeHookNames);
|
|
666
|
+
const defaultValues = defaultStaticValuesForFunctionLike(component);
|
|
667
|
+
const caseFacts = staticCases.map((testCase) => ({
|
|
668
|
+
...testCase,
|
|
669
|
+
values: new Map([...defaultValues, ...testCase.values]),
|
|
670
|
+
}));
|
|
671
|
+
const dependencies = reachableJSXDependenciesForComponent(sourceFile, componentName, branchContext, context);
|
|
672
|
+
const reported = new Set();
|
|
673
|
+
for (const dependency of dependencies) {
|
|
674
|
+
const opaque = firstOpaqueJSXBranchPredicate(dependency.condition);
|
|
675
|
+
if (opaque) {
|
|
676
|
+
const key = `opaque:${dependency.tagName}:${opaque.text}`;
|
|
677
|
+
if (reported.has(key))
|
|
678
|
+
continue;
|
|
679
|
+
reported.add(key);
|
|
680
|
+
diagnostics.push({
|
|
681
|
+
stage: "contract-extraction",
|
|
682
|
+
code: "opaque-jsx-control-flow",
|
|
683
|
+
message: `JSX dependency <${dependency.tagName}> is controlled by an opaque expression "${opaque.text}". Use props, GTSX context, or GScope values directly so cases can cover the tree structure.`,
|
|
684
|
+
file,
|
|
685
|
+
});
|
|
686
|
+
continue;
|
|
687
|
+
}
|
|
688
|
+
const evaluations = caseFacts.map((testCase) => evaluateJSXBranchPredicate(dependency.condition, testCase.values));
|
|
689
|
+
const coveredCaseNames = caseFacts
|
|
690
|
+
.filter((_testCase, index) => evaluations[index] === true)
|
|
691
|
+
.map((testCase) => testCase.name);
|
|
692
|
+
if (coveredCaseNames.length > 0)
|
|
693
|
+
continue;
|
|
694
|
+
const unknownCaseNames = caseFacts
|
|
695
|
+
.filter((_testCase, index) => evaluations[index] === "unknown")
|
|
696
|
+
.map((testCase) => testCase.name);
|
|
697
|
+
if (unknownCaseNames.length > 0) {
|
|
698
|
+
const key = `unknown:${dependency.tagName}:${formatJSXBranchPredicate(dependency.condition)}`;
|
|
699
|
+
if (reported.has(key))
|
|
700
|
+
continue;
|
|
701
|
+
reported.add(key);
|
|
702
|
+
diagnostics.push({
|
|
703
|
+
stage: "contract-extraction",
|
|
704
|
+
code: "unknown-jsx-branch-coverage",
|
|
705
|
+
message: `JSX dependency <${dependency.tagName}> is controlled by "${formatJSXBranchPredicate(dependency.condition)}", but case values are not static enough to prove coverage. Unknown cases: ${unknownCaseNames.join(", ")}.`,
|
|
706
|
+
file,
|
|
707
|
+
});
|
|
708
|
+
continue;
|
|
709
|
+
}
|
|
710
|
+
const key = `uncovered:${dependency.tagName}:${formatJSXBranchPredicate(dependency.condition)}`;
|
|
711
|
+
if (reported.has(key))
|
|
712
|
+
continue;
|
|
713
|
+
reported.add(key);
|
|
714
|
+
diagnostics.push({
|
|
715
|
+
stage: "contract-extraction",
|
|
716
|
+
code: "uncovered-jsx-branch",
|
|
717
|
+
message: `No case renders JSX dependency <${dependency.tagName}> behind "${formatJSXBranchPredicate(dependency.condition)}". Add a case whose props, GTSX context, or GScope values make that branch reachable.`,
|
|
718
|
+
file,
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
function validateProviderProjectionWarnings(sourceFile, componentName, scopeHookNames, providerCases, diagnostics, file, context) {
|
|
723
|
+
const warnings = providerProjectionWarningsForComponent(sourceFile, componentName, scopeHookNames, providerCases, context);
|
|
724
|
+
const reported = new Set();
|
|
725
|
+
for (const warning of warnings) {
|
|
726
|
+
const key = `${warning.target.filePath}#${warning.target.componentName}:${warning.providerName}`;
|
|
727
|
+
if (reported.has(key))
|
|
728
|
+
continue;
|
|
729
|
+
reported.add(key);
|
|
730
|
+
diagnostics.push({
|
|
731
|
+
stage: "contract-extraction",
|
|
732
|
+
severity: "warning",
|
|
733
|
+
code: "unmarked-provider-variant-projection",
|
|
734
|
+
message: `JSX dependency <${warning.tagName}> receives props derived from provider "${warning.providerName}", but its cases do not mark "${warning.providerName}" variants. If those props are environment projections, add GProviderCase markers; if the child is environment-neutral, this warning can be ignored.`,
|
|
735
|
+
file,
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
function providerProjectionWarningsForComponent(sourceFile, componentName, scopeHookNames, providerCases, context) {
|
|
740
|
+
const component = getFunctionLikeDeclaration(sourceFile, componentName);
|
|
741
|
+
if (!component?.body)
|
|
742
|
+
return [];
|
|
743
|
+
const warnings = [];
|
|
744
|
+
const branchContext = createJSXBranchAnalysisContext(sourceFile, component, scopeHookNames);
|
|
745
|
+
const helperFunctions = getTopLevelFunctionLikeBodiesForPath(sourceFile, context.entryPath, context.cache);
|
|
746
|
+
const importBindings = componentDependencyBindingsForFile(sourceFile, context.entryPath, context);
|
|
747
|
+
const visitedHelpers = new Set([componentName]);
|
|
748
|
+
visitFunctionLikeBody(component, branchContext, visitedHelpers);
|
|
749
|
+
return warnings;
|
|
750
|
+
function visitFunctionLikeBody(functionLike, currentBranchContext, currentVisitedHelpers) {
|
|
751
|
+
if (!functionLike.body)
|
|
752
|
+
return;
|
|
753
|
+
const localAliases = localComponentAliasBindingsForBody(functionLike.body);
|
|
754
|
+
visit(functionLike.body, currentBranchContext, currentVisitedHelpers, localAliases);
|
|
755
|
+
}
|
|
756
|
+
function visit(node, currentBranchContext, currentVisitedHelpers, localAliases) {
|
|
757
|
+
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && !isHookName(node.expression.text)) {
|
|
758
|
+
const helper = getFunctionLikeDeclaration(sourceFile, node.expression.text);
|
|
759
|
+
if (helper?.body && expressionContainsJSX(helper.body) && !currentVisitedHelpers.has(node.expression.text)) {
|
|
760
|
+
const helperVisited = new Set(currentVisitedHelpers);
|
|
761
|
+
helperVisited.add(node.expression.text);
|
|
762
|
+
const helperContext = bindFunctionCallArguments(helper, node.arguments, currentBranchContext);
|
|
763
|
+
visitFunctionLikeBody(helper, helperContext, helperVisited);
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
|
|
768
|
+
visitJSXOpeningLike(node, currentBranchContext, localAliases);
|
|
769
|
+
}
|
|
770
|
+
ts.forEachChild(node, (child) => visit(child, currentBranchContext, currentVisitedHelpers, localAliases));
|
|
771
|
+
}
|
|
772
|
+
function visitJSXOpeningLike(node, currentBranchContext, localAliases) {
|
|
773
|
+
const target = componentDependencyTargetForJsxTag(node.tagName, {
|
|
774
|
+
filePath: context.entryPath,
|
|
775
|
+
importBindings,
|
|
776
|
+
localAliases,
|
|
777
|
+
localComponentNames: helperFunctions,
|
|
778
|
+
});
|
|
779
|
+
if (!target)
|
|
780
|
+
return;
|
|
781
|
+
const projectedProviderNames = providerVariantNamesReferencedByJsxAttributes(node.attributes, providerCases, currentBranchContext);
|
|
782
|
+
if (projectedProviderNames.size === 0)
|
|
783
|
+
return;
|
|
784
|
+
const targetSourceFile = sourceFileForPath(target.filePath, context);
|
|
785
|
+
if (!targetSourceFile)
|
|
786
|
+
return;
|
|
787
|
+
for (const providerName of projectedProviderNames) {
|
|
788
|
+
const markerStatus = componentCasesProviderVariantMarkerStatus(targetSourceFile, target.componentName, providerName);
|
|
789
|
+
if (markerStatus !== false)
|
|
790
|
+
continue;
|
|
791
|
+
warnings.push({
|
|
792
|
+
providerName,
|
|
793
|
+
tagName: jsxTagNameText(node.tagName, sourceFile),
|
|
794
|
+
target,
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
function providerVariantNamesReferencedByJsxAttributes(attributes, providerCases, context) {
|
|
800
|
+
const providerNames = new Set();
|
|
801
|
+
for (const property of attributes.properties) {
|
|
802
|
+
const expression = ts.isJsxAttribute(property) && property.initializer
|
|
803
|
+
? ts.isJsxExpression(property.initializer)
|
|
804
|
+
? property.initializer.expression
|
|
805
|
+
: property.initializer
|
|
806
|
+
: ts.isJsxSpreadAttribute(property)
|
|
807
|
+
? property.expression
|
|
808
|
+
: undefined;
|
|
809
|
+
if (!expression)
|
|
810
|
+
continue;
|
|
811
|
+
for (const providerName of providerVariantNamesReferencedByExpression(expression, providerCases, context)) {
|
|
812
|
+
providerNames.add(providerName);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
return providerNames;
|
|
816
|
+
}
|
|
817
|
+
function providerVariantNamesReferencedByExpression(expression, providerCases, context) {
|
|
818
|
+
const providerNames = new Set();
|
|
819
|
+
visit(expression);
|
|
820
|
+
return providerNames;
|
|
821
|
+
function visit(node) {
|
|
822
|
+
if (ts.isExpression(node)) {
|
|
823
|
+
const reference = factorReferenceForExpression(node, context);
|
|
824
|
+
if (reference?.root === "context" && (providerCases[reference.providerName]?.variants?.length ?? 0) > 0) {
|
|
825
|
+
providerNames.add(reference.providerName);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
ts.forEachChild(node, visit);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
function componentCasesProviderVariantMarkerStatus(sourceFile, componentName, providerName) {
|
|
832
|
+
let hasCases = false;
|
|
833
|
+
for (const statement of sourceFile.statements) {
|
|
834
|
+
const assignment = getCasesAssignment(statement, sourceFile, []);
|
|
835
|
+
if (!assignment || assignment.targetName !== componentName)
|
|
836
|
+
continue;
|
|
837
|
+
if (assignment.cases.length === 0)
|
|
838
|
+
continue;
|
|
839
|
+
hasCases = true;
|
|
840
|
+
if (assignment.cases.some((testCase) => testCase.providerVariants?.[providerName]))
|
|
841
|
+
return true;
|
|
842
|
+
}
|
|
843
|
+
return hasCases ? false : undefined;
|
|
844
|
+
}
|
|
845
|
+
function createJSXBranchAnalysisContext(sourceFile, component, scopeHookNames) {
|
|
846
|
+
const context = {
|
|
847
|
+
expressionAliases: new Map(),
|
|
848
|
+
factorBindings: new Map(),
|
|
849
|
+
sourceFile,
|
|
850
|
+
};
|
|
851
|
+
bindFunctionParameters(component, context);
|
|
852
|
+
if (component.body && ts.isBlock(component.body)) {
|
|
853
|
+
for (const statement of component.body.statements) {
|
|
854
|
+
bindTopLevelBranchVariableStatement(statement, context, scopeHookNames);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
return context;
|
|
858
|
+
}
|
|
859
|
+
function bindFunctionParameters(component, context) {
|
|
860
|
+
const propsParameter = component.parameters[0];
|
|
861
|
+
if (!propsParameter)
|
|
862
|
+
return;
|
|
863
|
+
if (ts.isIdentifier(propsParameter.name)) {
|
|
864
|
+
context.factorBindings.set(propsParameter.name.text, { root: "props", path: [] });
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
if (ts.isObjectBindingPattern(propsParameter.name)) {
|
|
868
|
+
bindObjectBindingPattern(propsParameter.name, { root: "props", path: [] }, context);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
function bindTopLevelBranchVariableStatement(statement, context, scopeHookNames) {
|
|
872
|
+
if (!ts.isVariableStatement(statement))
|
|
873
|
+
return;
|
|
874
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
875
|
+
if (!declaration.initializer)
|
|
876
|
+
continue;
|
|
877
|
+
const initializer = unwrapExpression(declaration.initializer);
|
|
878
|
+
const scopeProvider = scopeHookProviderFromCall(initializer, scopeHookNames);
|
|
879
|
+
const contextProvider = ts.isCallExpression(initializer) ? gContextProviderNameFromCall(initializer) : undefined;
|
|
880
|
+
const rootReference = scopeProvider
|
|
881
|
+
? { root: "scope", path: [] }
|
|
882
|
+
: contextProvider
|
|
883
|
+
? { root: "context", providerName: contextProvider, path: [] }
|
|
884
|
+
: factorReferenceForExpression(initializer, context);
|
|
885
|
+
if (rootReference) {
|
|
886
|
+
if (ts.isIdentifier(declaration.name)) {
|
|
887
|
+
context.factorBindings.set(declaration.name.text, rootReference);
|
|
888
|
+
}
|
|
889
|
+
else if (ts.isObjectBindingPattern(declaration.name)) {
|
|
890
|
+
bindObjectBindingPattern(declaration.name, rootReference, context);
|
|
891
|
+
}
|
|
892
|
+
continue;
|
|
893
|
+
}
|
|
894
|
+
if (ts.isIdentifier(declaration.name) && !expressionContainsJSX(initializer)) {
|
|
895
|
+
context.expressionAliases.set(declaration.name.text, initializer);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
function bindObjectBindingPattern(pattern, rootReference, context) {
|
|
900
|
+
for (const element of pattern.elements) {
|
|
901
|
+
if (element.dotDotDotToken)
|
|
902
|
+
continue;
|
|
903
|
+
const propertyName = element.propertyName ? bindingNameText(element.propertyName) : bindingNameText(element.name);
|
|
904
|
+
if (!propertyName)
|
|
905
|
+
continue;
|
|
906
|
+
const nextReference = appendFactorReferencePath(rootReference, propertyName);
|
|
907
|
+
if (ts.isIdentifier(element.name)) {
|
|
908
|
+
context.factorBindings.set(element.name.text, nextReference);
|
|
909
|
+
}
|
|
910
|
+
else if (ts.isObjectBindingPattern(element.name)) {
|
|
911
|
+
bindObjectBindingPattern(element.name, nextReference, context);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
function bindingNameText(name) {
|
|
916
|
+
if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name))
|
|
917
|
+
return name.text;
|
|
918
|
+
return undefined;
|
|
919
|
+
}
|
|
920
|
+
function cloneJSXBranchAnalysisContext(context) {
|
|
921
|
+
return {
|
|
922
|
+
expressionAliases: new Map(context.expressionAliases),
|
|
923
|
+
factorBindings: new Map(context.factorBindings),
|
|
924
|
+
sourceFile: context.sourceFile,
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
function bindCallbackItemParameter(callback, sourceReference, context) {
|
|
928
|
+
const callbackContext = cloneJSXBranchAnalysisContext(context);
|
|
929
|
+
const itemReference = appendFactorReferencePath(sourceReference, "number");
|
|
930
|
+
const parameter = callback.parameters[0];
|
|
931
|
+
if (!parameter)
|
|
932
|
+
return callbackContext;
|
|
933
|
+
if (ts.isIdentifier(parameter.name)) {
|
|
934
|
+
callbackContext.factorBindings.set(parameter.name.text, itemReference);
|
|
935
|
+
}
|
|
936
|
+
else if (ts.isObjectBindingPattern(parameter.name)) {
|
|
937
|
+
bindObjectBindingPattern(parameter.name, itemReference, callbackContext);
|
|
938
|
+
}
|
|
939
|
+
return callbackContext;
|
|
940
|
+
}
|
|
941
|
+
function bindFunctionCallArguments(functionLike, args, context) {
|
|
942
|
+
const callContext = cloneJSXBranchAnalysisContext(context);
|
|
943
|
+
for (let index = 0; index < functionLike.parameters.length; index += 1) {
|
|
944
|
+
const parameter = functionLike.parameters[index];
|
|
945
|
+
const argument = args[index];
|
|
946
|
+
if (!parameter || !argument)
|
|
947
|
+
continue;
|
|
948
|
+
const argumentReference = factorReferenceForExpression(argument, context);
|
|
949
|
+
if (argumentReference) {
|
|
950
|
+
if (ts.isIdentifier(parameter.name)) {
|
|
951
|
+
callContext.factorBindings.set(parameter.name.text, argumentReference);
|
|
952
|
+
}
|
|
953
|
+
else if (ts.isObjectBindingPattern(parameter.name)) {
|
|
954
|
+
bindObjectBindingPattern(parameter.name, argumentReference, callContext);
|
|
955
|
+
}
|
|
956
|
+
continue;
|
|
957
|
+
}
|
|
958
|
+
if (ts.isIdentifier(parameter.name) && !expressionContainsJSX(argument)) {
|
|
959
|
+
callContext.expressionAliases.set(parameter.name.text, argument);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
return callContext;
|
|
963
|
+
}
|
|
964
|
+
function nonEmptyCollectionPredicate(reference) {
|
|
965
|
+
return {
|
|
966
|
+
kind: "relation",
|
|
967
|
+
operator: ">",
|
|
968
|
+
ref: appendFactorReferencePath(reference, "length"),
|
|
969
|
+
value: { kind: "number", value: 0 },
|
|
970
|
+
};
|
|
971
|
+
}
|
|
972
|
+
function opaqueJSXBranchPredicate(expression, context) {
|
|
973
|
+
return {
|
|
974
|
+
kind: "opaque",
|
|
975
|
+
reason: "JSX callback source is not a direct props/context/scope expression",
|
|
976
|
+
text: expression.getText(context.sourceFile),
|
|
977
|
+
};
|
|
978
|
+
}
|
|
979
|
+
function jsxAttributeFactorReference(attributes, context) {
|
|
980
|
+
for (const property of attributes.properties) {
|
|
981
|
+
if (!ts.isJsxAttribute(property) || !property.initializer)
|
|
982
|
+
continue;
|
|
983
|
+
if (!ts.isIdentifier(property.name))
|
|
984
|
+
continue;
|
|
985
|
+
if (!new Set(["items", "rows", "data", "options"]).has(property.name.text))
|
|
986
|
+
continue;
|
|
987
|
+
if (!ts.isJsxExpression(property.initializer) || !property.initializer.expression)
|
|
988
|
+
continue;
|
|
989
|
+
const reference = factorReferenceForExpression(property.initializer.expression, context);
|
|
990
|
+
if (reference)
|
|
991
|
+
return reference;
|
|
992
|
+
}
|
|
993
|
+
return undefined;
|
|
994
|
+
}
|
|
995
|
+
function defaultStaticValuesForFunctionLike(component) {
|
|
996
|
+
const values = new Map();
|
|
997
|
+
const propsParameter = component.parameters[0];
|
|
998
|
+
if (!propsParameter || !ts.isObjectBindingPattern(propsParameter.name))
|
|
999
|
+
return values;
|
|
1000
|
+
collectBindingDefaults(propsParameter.name, { root: "props", path: [] }, values);
|
|
1001
|
+
return values;
|
|
1002
|
+
}
|
|
1003
|
+
function collectBindingDefaults(pattern, rootReference, values) {
|
|
1004
|
+
for (const element of pattern.elements) {
|
|
1005
|
+
if (element.dotDotDotToken)
|
|
1006
|
+
continue;
|
|
1007
|
+
const propertyName = element.propertyName ? bindingNameText(element.propertyName) : bindingNameText(element.name);
|
|
1008
|
+
if (!propertyName)
|
|
1009
|
+
continue;
|
|
1010
|
+
const nextReference = appendFactorReferencePath(rootReference, propertyName);
|
|
1011
|
+
if (element.initializer) {
|
|
1012
|
+
const value = readStaticBranchValue(element.initializer);
|
|
1013
|
+
if (value)
|
|
1014
|
+
values.set(factorReferenceKey(nextReference), value);
|
|
1015
|
+
}
|
|
1016
|
+
if (ts.isObjectBindingPattern(element.name)) {
|
|
1017
|
+
collectBindingDefaults(element.name, nextReference, values);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
function scopeHookProviderFromCall(expression, scopeHookNames) {
|
|
1022
|
+
return ts.isCallExpression(expression) && ts.isIdentifier(expression.expression) && scopeHookNames.has(expression.expression.text)
|
|
1023
|
+
? expression.expression.text
|
|
1024
|
+
: undefined;
|
|
1025
|
+
}
|
|
1026
|
+
function reachableJSXDependenciesForComponent(sourceFile, componentName, branchContext, context) {
|
|
1027
|
+
const dependencies = [];
|
|
1028
|
+
const componentBody = getFunctionLikeBody(sourceFile, componentName);
|
|
1029
|
+
if (!componentBody)
|
|
1030
|
+
return dependencies;
|
|
1031
|
+
const helperFunctions = getTopLevelFunctionLikeBodiesForPath(sourceFile, context.entryPath, context.cache);
|
|
1032
|
+
const localComponentNames = helperFunctions;
|
|
1033
|
+
const importBindings = componentDependencyBindingsForFile(sourceFile, context.entryPath, context);
|
|
1034
|
+
const localAliases = localComponentAliasBindingsForBody(componentBody);
|
|
1035
|
+
const always = { kind: "always" };
|
|
1036
|
+
const visitedHelpers = new Set([componentName]);
|
|
1037
|
+
if (ts.isBlock(componentBody)) {
|
|
1038
|
+
for (const statement of componentBody.statements)
|
|
1039
|
+
visitStatement(statement, always, branchContext, visitedHelpers);
|
|
1040
|
+
}
|
|
1041
|
+
else {
|
|
1042
|
+
visitExpression(componentBody, always, branchContext, visitedHelpers);
|
|
1043
|
+
}
|
|
1044
|
+
return dependencies;
|
|
1045
|
+
function visitStatement(statement, condition, currentBranchContext, currentVisitedHelpers) {
|
|
1046
|
+
if (ts.isBlock(statement)) {
|
|
1047
|
+
for (const child of statement.statements)
|
|
1048
|
+
visitStatement(child, condition, currentBranchContext, currentVisitedHelpers);
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1051
|
+
if (ts.isReturnStatement(statement)) {
|
|
1052
|
+
if (statement.expression)
|
|
1053
|
+
visitExpression(statement.expression, condition, currentBranchContext, currentVisitedHelpers);
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
1056
|
+
if (ts.isIfStatement(statement)) {
|
|
1057
|
+
const predicate = parseJSXBranchPredicate(statement.expression, currentBranchContext);
|
|
1058
|
+
visitStatementOrBlock(statement.thenStatement, andJSXBranchPredicates(condition, predicate), currentBranchContext, currentVisitedHelpers);
|
|
1059
|
+
if (statement.elseStatement) {
|
|
1060
|
+
visitStatementOrBlock(statement.elseStatement, andJSXBranchPredicates(condition, notJSXBranchPredicate(predicate)), currentBranchContext, currentVisitedHelpers);
|
|
1061
|
+
}
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
if (ts.isExpressionStatement(statement)) {
|
|
1065
|
+
visitExpression(statement.expression, condition, currentBranchContext, currentVisitedHelpers);
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
if (nodeContainsJSX(statement)) {
|
|
1069
|
+
visitOpaqueJSXSubtree(statement, condition, currentBranchContext, currentVisitedHelpers);
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
function visitStatementOrBlock(statement, condition, currentBranchContext, currentVisitedHelpers) {
|
|
1073
|
+
if (ts.isBlock(statement)) {
|
|
1074
|
+
for (const child of statement.statements)
|
|
1075
|
+
visitStatement(child, condition, currentBranchContext, currentVisitedHelpers);
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
visitStatement(statement, condition, currentBranchContext, currentVisitedHelpers);
|
|
1079
|
+
}
|
|
1080
|
+
function visitExpression(expression, condition, currentBranchContext, currentVisitedHelpers) {
|
|
1081
|
+
const value = unwrapExpression(expression);
|
|
1082
|
+
if (ts.isJsxElement(value)) {
|
|
1083
|
+
visitJSXOpeningLike(value.openingElement, condition, currentBranchContext, currentVisitedHelpers);
|
|
1084
|
+
for (const child of value.children)
|
|
1085
|
+
visitJSXChild(child, condition, currentBranchContext, currentVisitedHelpers);
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
1088
|
+
if (ts.isJsxSelfClosingElement(value)) {
|
|
1089
|
+
visitJSXOpeningLike(value, condition, currentBranchContext, currentVisitedHelpers);
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
if (ts.isJsxFragment(value)) {
|
|
1093
|
+
for (const child of value.children)
|
|
1094
|
+
visitJSXChild(child, condition, currentBranchContext, currentVisitedHelpers);
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
if (ts.isConditionalExpression(value)) {
|
|
1098
|
+
const predicate = parseJSXBranchPredicate(value.condition, currentBranchContext);
|
|
1099
|
+
visitExpression(value.whenTrue, andJSXBranchPredicates(condition, predicate), currentBranchContext, currentVisitedHelpers);
|
|
1100
|
+
visitExpression(value.whenFalse, andJSXBranchPredicates(condition, notJSXBranchPredicate(predicate)), currentBranchContext, currentVisitedHelpers);
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
if (ts.isBinaryExpression(value) && value.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) {
|
|
1104
|
+
const predicate = parseJSXBranchPredicate(value.left, currentBranchContext);
|
|
1105
|
+
visitExpression(value.right, andJSXBranchPredicates(condition, predicate), currentBranchContext, currentVisitedHelpers);
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
if (ts.isBinaryExpression(value) && value.operatorToken.kind === ts.SyntaxKind.BarBarToken) {
|
|
1109
|
+
const predicate = parseJSXBranchPredicate(value.left, currentBranchContext);
|
|
1110
|
+
visitExpression(value.right, andJSXBranchPredicates(condition, notJSXBranchPredicate(predicate)), currentBranchContext, currentVisitedHelpers);
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
if (ts.isArrowFunction(value) || ts.isFunctionExpression(value)) {
|
|
1114
|
+
visitFunctionLikeBody(value, condition, currentBranchContext, currentVisitedHelpers);
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
if (ts.isCallExpression(value) && visitJSXProducingCall(value, condition, currentBranchContext, currentVisitedHelpers)) {
|
|
1118
|
+
return;
|
|
1119
|
+
}
|
|
1120
|
+
ts.forEachChild(value, (child) => {
|
|
1121
|
+
if (isExpressionWithPossibleJSX(child))
|
|
1122
|
+
visitExpression(child, condition, currentBranchContext, currentVisitedHelpers);
|
|
1123
|
+
});
|
|
1124
|
+
}
|
|
1125
|
+
function visitFunctionLikeBody(functionLike, condition, currentBranchContext, currentVisitedHelpers) {
|
|
1126
|
+
if (!functionLike.body)
|
|
1127
|
+
return;
|
|
1128
|
+
if (ts.isBlock(functionLike.body)) {
|
|
1129
|
+
for (const statement of functionLike.body.statements)
|
|
1130
|
+
visitStatement(statement, condition, currentBranchContext, currentVisitedHelpers);
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
1133
|
+
visitExpression(functionLike.body, condition, currentBranchContext, currentVisitedHelpers);
|
|
1134
|
+
}
|
|
1135
|
+
function visitJSXProducingCall(call, condition, currentBranchContext, currentVisitedHelpers) {
|
|
1136
|
+
if (ts.isPropertyAccessExpression(call.expression) && call.expression.name.text === "map") {
|
|
1137
|
+
const callback = call.arguments[0] ? unwrapExpression(call.arguments[0]) : undefined;
|
|
1138
|
+
if (callback && (ts.isArrowFunction(callback) || ts.isFunctionExpression(callback))) {
|
|
1139
|
+
const sourceReference = factorReferenceForExpression(call.expression.expression, currentBranchContext);
|
|
1140
|
+
const callbackCondition = sourceReference
|
|
1141
|
+
? andJSXBranchPredicates(condition, nonEmptyCollectionPredicate(sourceReference))
|
|
1142
|
+
: andJSXBranchPredicates(condition, opaqueJSXBranchPredicate(call.expression.expression, currentBranchContext));
|
|
1143
|
+
const callbackContext = sourceReference
|
|
1144
|
+
? bindCallbackItemParameter(callback, sourceReference, currentBranchContext)
|
|
1145
|
+
: cloneJSXBranchAnalysisContext(currentBranchContext);
|
|
1146
|
+
visitFunctionLikeBody(callback, callbackCondition, callbackContext, currentVisitedHelpers);
|
|
1147
|
+
return true;
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
if (ts.isIdentifier(call.expression)) {
|
|
1151
|
+
const helper = getFunctionLikeDeclaration(sourceFile, call.expression.text);
|
|
1152
|
+
if (helper?.body && expressionContainsJSX(helper.body) && !currentVisitedHelpers.has(call.expression.text)) {
|
|
1153
|
+
const helperVisited = new Set(currentVisitedHelpers);
|
|
1154
|
+
helperVisited.add(call.expression.text);
|
|
1155
|
+
const helperContext = bindFunctionCallArguments(helper, call.arguments, currentBranchContext);
|
|
1156
|
+
visitFunctionLikeBody(helper, condition, helperContext, helperVisited);
|
|
1157
|
+
return true;
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
let handled = false;
|
|
1161
|
+
for (const argument of call.arguments) {
|
|
1162
|
+
if (!expressionContainsJSX(argument))
|
|
1163
|
+
continue;
|
|
1164
|
+
visitExpression(argument, condition, currentBranchContext, currentVisitedHelpers);
|
|
1165
|
+
handled = true;
|
|
1166
|
+
}
|
|
1167
|
+
return handled;
|
|
1168
|
+
}
|
|
1169
|
+
function visitOpaqueJSXSubtree(node, condition, currentBranchContext, currentVisitedHelpers) {
|
|
1170
|
+
const opaqueCondition = andJSXBranchPredicates(condition, {
|
|
1171
|
+
kind: "opaque",
|
|
1172
|
+
reason: "JSX is produced from unsupported statement-level control flow",
|
|
1173
|
+
text: summarizeNodeText(node, currentBranchContext.sourceFile),
|
|
1174
|
+
});
|
|
1175
|
+
visit(node);
|
|
1176
|
+
function visit(current) {
|
|
1177
|
+
if (ts.isJsxElement(current)) {
|
|
1178
|
+
visitJSXOpeningLike(current.openingElement, opaqueCondition, currentBranchContext, currentVisitedHelpers);
|
|
1179
|
+
for (const child of current.children)
|
|
1180
|
+
visit(child);
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
1183
|
+
if (ts.isJsxSelfClosingElement(current)) {
|
|
1184
|
+
visitJSXOpeningLike(current, opaqueCondition, currentBranchContext, currentVisitedHelpers);
|
|
1185
|
+
return;
|
|
1186
|
+
}
|
|
1187
|
+
if (ts.isJsxFragment(current)) {
|
|
1188
|
+
for (const child of current.children)
|
|
1189
|
+
visit(child);
|
|
1190
|
+
return;
|
|
1191
|
+
}
|
|
1192
|
+
ts.forEachChild(current, visit);
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
function visitJSXChild(child, condition, currentBranchContext, currentVisitedHelpers) {
|
|
1196
|
+
if (ts.isJsxText(child))
|
|
1197
|
+
return;
|
|
1198
|
+
if (ts.isJsxExpression(child)) {
|
|
1199
|
+
if (child.expression)
|
|
1200
|
+
visitExpression(child.expression, condition, currentBranchContext, currentVisitedHelpers);
|
|
1201
|
+
return;
|
|
1202
|
+
}
|
|
1203
|
+
visitExpression(child, condition, currentBranchContext, currentVisitedHelpers);
|
|
1204
|
+
}
|
|
1205
|
+
function visitJSXOpeningLike(node, condition, currentBranchContext, currentVisitedHelpers) {
|
|
1206
|
+
const target = componentDependencyTargetForJsxTag(node.tagName, {
|
|
1207
|
+
filePath: context.entryPath,
|
|
1208
|
+
importBindings,
|
|
1209
|
+
localAliases,
|
|
1210
|
+
localComponentNames,
|
|
1211
|
+
});
|
|
1212
|
+
if (target) {
|
|
1213
|
+
dependencies.push({
|
|
1214
|
+
condition,
|
|
1215
|
+
tagName: jsxTagNameText(node.tagName, sourceFile),
|
|
1216
|
+
target,
|
|
1217
|
+
});
|
|
1218
|
+
}
|
|
1219
|
+
visitJSXAttributes(node.attributes, condition, currentBranchContext, currentVisitedHelpers);
|
|
1220
|
+
}
|
|
1221
|
+
function visitJSXAttributes(attributes, condition, currentBranchContext, currentVisitedHelpers) {
|
|
1222
|
+
const itemSource = jsxAttributeFactorReference(attributes, currentBranchContext);
|
|
1223
|
+
for (const property of attributes.properties) {
|
|
1224
|
+
if (!ts.isJsxAttribute(property) || !property.initializer)
|
|
1225
|
+
continue;
|
|
1226
|
+
const initializer = property.initializer;
|
|
1227
|
+
const expression = ts.isJsxExpression(initializer) ? initializer.expression : initializer;
|
|
1228
|
+
if (!expression || !expressionContainsJSX(expression))
|
|
1229
|
+
continue;
|
|
1230
|
+
const value = unwrapExpression(expression);
|
|
1231
|
+
if ((ts.isArrowFunction(value) || ts.isFunctionExpression(value)) && value.parameters.length > 0) {
|
|
1232
|
+
const callbackCondition = itemSource
|
|
1233
|
+
? andJSXBranchPredicates(condition, nonEmptyCollectionPredicate(itemSource))
|
|
1234
|
+
: andJSXBranchPredicates(condition, opaqueJSXBranchPredicate(value, currentBranchContext));
|
|
1235
|
+
const callbackContext = itemSource ? bindCallbackItemParameter(value, itemSource, currentBranchContext) : currentBranchContext;
|
|
1236
|
+
visitFunctionLikeBody(value, callbackCondition, callbackContext, currentVisitedHelpers);
|
|
1237
|
+
continue;
|
|
1238
|
+
}
|
|
1239
|
+
visitExpression(expression, condition, currentBranchContext, currentVisitedHelpers);
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
function isExpressionWithPossibleJSX(node) {
|
|
1244
|
+
return ts.isExpression(node) && nodeContainsJSX(node);
|
|
1245
|
+
}
|
|
1246
|
+
function expressionContainsJSX(node) {
|
|
1247
|
+
return nodeContainsJSX(node);
|
|
1248
|
+
}
|
|
1249
|
+
function nodeContainsJSX(node) {
|
|
1250
|
+
let found = false;
|
|
1251
|
+
visit(node);
|
|
1252
|
+
return found;
|
|
1253
|
+
function visit(current) {
|
|
1254
|
+
if (found)
|
|
1255
|
+
return;
|
|
1256
|
+
if (ts.isJsxElement(current) || ts.isJsxSelfClosingElement(current) || ts.isJsxFragment(current)) {
|
|
1257
|
+
found = true;
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
ts.forEachChild(current, visit);
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
function summarizeNodeText(node, sourceFile) {
|
|
1264
|
+
const text = node.getText(sourceFile).replace(/\s+/g, " ").trim();
|
|
1265
|
+
return text.length > 140 ? `${text.slice(0, 137)}...` : text;
|
|
1266
|
+
}
|
|
1267
|
+
function parseJSXBranchPredicate(expression, context, resolvingAliases = new Set()) {
|
|
1268
|
+
const value = unwrapExpression(expression);
|
|
1269
|
+
if (value.kind === ts.SyntaxKind.TrueKeyword)
|
|
1270
|
+
return { kind: "static", value: true };
|
|
1271
|
+
if (value.kind === ts.SyntaxKind.FalseKeyword)
|
|
1272
|
+
return { kind: "static", value: false };
|
|
1273
|
+
if (ts.isPrefixUnaryExpression(value) && value.operator === ts.SyntaxKind.ExclamationToken) {
|
|
1274
|
+
return notJSXBranchPredicate(parseJSXBranchPredicate(value.operand, context, resolvingAliases));
|
|
1275
|
+
}
|
|
1276
|
+
if (ts.isBinaryExpression(value)) {
|
|
1277
|
+
if (value.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) {
|
|
1278
|
+
return andJSXBranchPredicates(parseJSXBranchPredicate(value.left, context, resolvingAliases), parseJSXBranchPredicate(value.right, context, resolvingAliases));
|
|
1279
|
+
}
|
|
1280
|
+
if (value.operatorToken.kind === ts.SyntaxKind.BarBarToken) {
|
|
1281
|
+
return orJSXBranchPredicates(parseJSXBranchPredicate(value.left, context, resolvingAliases), parseJSXBranchPredicate(value.right, context, resolvingAliases));
|
|
1282
|
+
}
|
|
1283
|
+
const equalityPredicate = parseBinaryComparisonPredicate(value, context, resolvingAliases);
|
|
1284
|
+
if (equalityPredicate)
|
|
1285
|
+
return equalityPredicate;
|
|
1286
|
+
}
|
|
1287
|
+
if (ts.isCallExpression(value) &&
|
|
1288
|
+
ts.isIdentifier(value.expression) &&
|
|
1289
|
+
value.expression.text === "Boolean" &&
|
|
1290
|
+
value.arguments.length === 1 &&
|
|
1291
|
+
value.arguments[0]) {
|
|
1292
|
+
return parseJSXBranchPredicate(value.arguments[0], context, resolvingAliases);
|
|
1293
|
+
}
|
|
1294
|
+
if (ts.isIdentifier(value)) {
|
|
1295
|
+
const alias = context.expressionAliases.get(value.text);
|
|
1296
|
+
if (alias && !resolvingAliases.has(value.text)) {
|
|
1297
|
+
resolvingAliases.add(value.text);
|
|
1298
|
+
const predicate = parseJSXBranchPredicate(alias, context, resolvingAliases);
|
|
1299
|
+
resolvingAliases.delete(value.text);
|
|
1300
|
+
return predicate;
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
const reference = factorReferenceForExpression(value, context);
|
|
1304
|
+
if (reference)
|
|
1305
|
+
return { kind: "truthy", ref: reference };
|
|
1306
|
+
const staticValue = readStaticBranchValue(value);
|
|
1307
|
+
const staticTruthy = staticValue ? staticBranchValueTruthy(staticValue) : undefined;
|
|
1308
|
+
if (staticTruthy !== undefined)
|
|
1309
|
+
return { kind: "static", value: staticTruthy };
|
|
1310
|
+
return {
|
|
1311
|
+
kind: "opaque",
|
|
1312
|
+
reason: "condition is not a direct props/context/scope expression",
|
|
1313
|
+
text: value.getText(context.sourceFile),
|
|
1314
|
+
};
|
|
1315
|
+
}
|
|
1316
|
+
function parseBinaryComparisonPredicate(expression, context, resolvingAliases) {
|
|
1317
|
+
const leftReference = factorReferenceForExpression(expression.left, context);
|
|
1318
|
+
const rightReference = factorReferenceForExpression(expression.right, context);
|
|
1319
|
+
const leftValue = readStaticBranchValue(expression.left);
|
|
1320
|
+
const rightValue = readStaticBranchValue(expression.right);
|
|
1321
|
+
const equalityOperators = new Set([
|
|
1322
|
+
ts.SyntaxKind.EqualsEqualsEqualsToken,
|
|
1323
|
+
ts.SyntaxKind.EqualsEqualsToken,
|
|
1324
|
+
ts.SyntaxKind.ExclamationEqualsEqualsToken,
|
|
1325
|
+
ts.SyntaxKind.ExclamationEqualsToken,
|
|
1326
|
+
]);
|
|
1327
|
+
if (equalityOperators.has(expression.operatorToken.kind)) {
|
|
1328
|
+
if (leftReference && rightReference) {
|
|
1329
|
+
const predicate = { kind: "equals-ref", left: leftReference, right: rightReference };
|
|
1330
|
+
return expression.operatorToken.kind === ts.SyntaxKind.ExclamationEqualsEqualsToken ||
|
|
1331
|
+
expression.operatorToken.kind === ts.SyntaxKind.ExclamationEqualsToken
|
|
1332
|
+
? notJSXBranchPredicate(predicate)
|
|
1333
|
+
: predicate;
|
|
1334
|
+
}
|
|
1335
|
+
const reference = leftReference ?? rightReference;
|
|
1336
|
+
const staticValue = leftReference ? rightValue : rightReference ? leftValue : undefined;
|
|
1337
|
+
if (!reference || !staticValue)
|
|
1338
|
+
return undefined;
|
|
1339
|
+
const predicate = { kind: "equals", ref: reference, value: staticValue };
|
|
1340
|
+
return expression.operatorToken.kind === ts.SyntaxKind.ExclamationEqualsEqualsToken ||
|
|
1341
|
+
expression.operatorToken.kind === ts.SyntaxKind.ExclamationEqualsToken
|
|
1342
|
+
? notJSXBranchPredicate(predicate)
|
|
1343
|
+
: predicate;
|
|
1344
|
+
}
|
|
1345
|
+
const relationOperator = relationOperatorText(expression.operatorToken.kind);
|
|
1346
|
+
if (relationOperator) {
|
|
1347
|
+
if (leftReference && rightValue)
|
|
1348
|
+
return { kind: "relation", operator: relationOperator, ref: leftReference, value: rightValue };
|
|
1349
|
+
if (rightReference && leftValue) {
|
|
1350
|
+
const invertedOperator = invertRelationOperator(relationOperator);
|
|
1351
|
+
return { kind: "relation", operator: invertedOperator, ref: rightReference, value: leftValue };
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
return undefined;
|
|
1355
|
+
}
|
|
1356
|
+
function relationOperatorText(kind) {
|
|
1357
|
+
if (kind === ts.SyntaxKind.LessThanToken)
|
|
1358
|
+
return "<";
|
|
1359
|
+
if (kind === ts.SyntaxKind.LessThanEqualsToken)
|
|
1360
|
+
return "<=";
|
|
1361
|
+
if (kind === ts.SyntaxKind.GreaterThanToken)
|
|
1362
|
+
return ">";
|
|
1363
|
+
if (kind === ts.SyntaxKind.GreaterThanEqualsToken)
|
|
1364
|
+
return ">=";
|
|
1365
|
+
return undefined;
|
|
1366
|
+
}
|
|
1367
|
+
function invertRelationOperator(operator) {
|
|
1368
|
+
if (operator === "<")
|
|
1369
|
+
return ">";
|
|
1370
|
+
if (operator === "<=")
|
|
1371
|
+
return ">=";
|
|
1372
|
+
if (operator === ">")
|
|
1373
|
+
return "<";
|
|
1374
|
+
return "<=";
|
|
1375
|
+
}
|
|
1376
|
+
function factorReferenceForExpression(expression, context) {
|
|
1377
|
+
const value = unwrapExpression(expression);
|
|
1378
|
+
if (ts.isIdentifier(value)) {
|
|
1379
|
+
const direct = context.factorBindings.get(value.text);
|
|
1380
|
+
if (direct)
|
|
1381
|
+
return direct;
|
|
1382
|
+
const alias = context.expressionAliases.get(value.text);
|
|
1383
|
+
return alias ? factorReferenceForExpression(alias, context) : undefined;
|
|
1384
|
+
}
|
|
1385
|
+
if (ts.isPropertyAccessExpression(value)) {
|
|
1386
|
+
const base = factorReferenceForExpression(value.expression, context);
|
|
1387
|
+
return base ? appendFactorReferencePath(base, value.name.text) : undefined;
|
|
1388
|
+
}
|
|
1389
|
+
if (ts.isElementAccessExpression(value) && ts.isStringLiteralLike(value.argumentExpression)) {
|
|
1390
|
+
const base = factorReferenceForExpression(value.expression, context);
|
|
1391
|
+
return base ? appendFactorReferencePath(base, value.argumentExpression.text) : undefined;
|
|
1392
|
+
}
|
|
1393
|
+
if (ts.isElementAccessExpression(value) && ts.isNumericLiteral(value.argumentExpression)) {
|
|
1394
|
+
const base = factorReferenceForExpression(value.expression, context);
|
|
1395
|
+
return base ? appendFactorReferencePath(base, "number") : undefined;
|
|
1396
|
+
}
|
|
1397
|
+
return undefined;
|
|
1398
|
+
}
|
|
1399
|
+
function appendFactorReferencePath(reference, propertyName) {
|
|
1400
|
+
if (reference.root === "context") {
|
|
1401
|
+
return { root: "context", providerName: reference.providerName, path: [...reference.path, propertyName] };
|
|
1402
|
+
}
|
|
1403
|
+
return { root: reference.root, path: [...reference.path, propertyName] };
|
|
1404
|
+
}
|
|
1405
|
+
function andJSXBranchPredicates(left, right) {
|
|
1406
|
+
if (left.kind === "always")
|
|
1407
|
+
return right;
|
|
1408
|
+
if (right.kind === "always")
|
|
1409
|
+
return left;
|
|
1410
|
+
if (left.kind === "static" && left.value)
|
|
1411
|
+
return right;
|
|
1412
|
+
if (right.kind === "static" && right.value)
|
|
1413
|
+
return left;
|
|
1414
|
+
if (left.kind === "static" && !left.value)
|
|
1415
|
+
return left;
|
|
1416
|
+
if (right.kind === "static" && !right.value)
|
|
1417
|
+
return right;
|
|
1418
|
+
return {
|
|
1419
|
+
kind: "and",
|
|
1420
|
+
predicates: [
|
|
1421
|
+
...(left.kind === "and" ? left.predicates : [left]),
|
|
1422
|
+
...(right.kind === "and" ? right.predicates : [right]),
|
|
1423
|
+
],
|
|
1424
|
+
};
|
|
1425
|
+
}
|
|
1426
|
+
function orJSXBranchPredicates(left, right) {
|
|
1427
|
+
if (left.kind === "static" && left.value)
|
|
1428
|
+
return left;
|
|
1429
|
+
if (right.kind === "static" && right.value)
|
|
1430
|
+
return right;
|
|
1431
|
+
if (left.kind === "static" && !left.value)
|
|
1432
|
+
return right;
|
|
1433
|
+
if (right.kind === "static" && !right.value)
|
|
1434
|
+
return left;
|
|
1435
|
+
return {
|
|
1436
|
+
kind: "or",
|
|
1437
|
+
predicates: [
|
|
1438
|
+
...(left.kind === "or" ? left.predicates : [left]),
|
|
1439
|
+
...(right.kind === "or" ? right.predicates : [right]),
|
|
1440
|
+
],
|
|
1441
|
+
};
|
|
1442
|
+
}
|
|
1443
|
+
function notJSXBranchPredicate(predicate) {
|
|
1444
|
+
if (predicate.kind === "static")
|
|
1445
|
+
return { kind: "static", value: !predicate.value };
|
|
1446
|
+
if (predicate.kind === "not")
|
|
1447
|
+
return predicate.predicate;
|
|
1448
|
+
return { kind: "not", predicate };
|
|
1449
|
+
}
|
|
1450
|
+
function firstOpaqueJSXBranchPredicate(predicate) {
|
|
1451
|
+
if (predicate.kind === "opaque")
|
|
1452
|
+
return predicate;
|
|
1453
|
+
if (predicate.kind === "not")
|
|
1454
|
+
return firstOpaqueJSXBranchPredicate(predicate.predicate);
|
|
1455
|
+
if (predicate.kind === "and" || predicate.kind === "or") {
|
|
1456
|
+
for (const child of predicate.predicates) {
|
|
1457
|
+
const opaque = firstOpaqueJSXBranchPredicate(child);
|
|
1458
|
+
if (opaque)
|
|
1459
|
+
return opaque;
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
return undefined;
|
|
1463
|
+
}
|
|
1464
|
+
function evaluateJSXBranchPredicate(predicate, values) {
|
|
1465
|
+
if (predicate.kind === "always")
|
|
1466
|
+
return true;
|
|
1467
|
+
if (predicate.kind === "static")
|
|
1468
|
+
return predicate.value;
|
|
1469
|
+
if (predicate.kind === "opaque")
|
|
1470
|
+
return "unknown";
|
|
1471
|
+
if (predicate.kind === "truthy") {
|
|
1472
|
+
return evaluateStaticBranchValueTruthy(staticBranchValueForReference(values, predicate.ref) ?? { kind: "undefined" });
|
|
1473
|
+
}
|
|
1474
|
+
if (predicate.kind === "equals") {
|
|
1475
|
+
return evaluateSameStaticBranchValue(staticBranchValueForReference(values, predicate.ref) ?? { kind: "undefined" }, predicate.value);
|
|
1476
|
+
}
|
|
1477
|
+
if (predicate.kind === "equals-ref") {
|
|
1478
|
+
return evaluateSameStaticBranchValue(staticBranchValueForReference(values, predicate.left) ?? { kind: "undefined" }, staticBranchValueForReference(values, predicate.right) ?? { kind: "undefined" });
|
|
1479
|
+
}
|
|
1480
|
+
if (predicate.kind === "relation") {
|
|
1481
|
+
return evaluateStaticBranchRelation(staticBranchValueForReference(values, predicate.ref) ?? { kind: "undefined" }, predicate.operator, predicate.value);
|
|
1482
|
+
}
|
|
1483
|
+
if (predicate.kind === "not") {
|
|
1484
|
+
return evaluateNegatedJSXBranchPredicate(predicate.predicate, values);
|
|
1485
|
+
}
|
|
1486
|
+
if (predicate.kind === "and") {
|
|
1487
|
+
let unknown = false;
|
|
1488
|
+
for (const child of predicate.predicates) {
|
|
1489
|
+
const value = evaluateJSXBranchPredicate(child, values);
|
|
1490
|
+
if (value === false)
|
|
1491
|
+
return false;
|
|
1492
|
+
if (value === "unknown")
|
|
1493
|
+
unknown = true;
|
|
1494
|
+
}
|
|
1495
|
+
return unknown ? "unknown" : true;
|
|
1496
|
+
}
|
|
1497
|
+
if (predicate.kind === "or") {
|
|
1498
|
+
let unknown = false;
|
|
1499
|
+
for (const child of predicate.predicates) {
|
|
1500
|
+
const value = evaluateJSXBranchPredicate(child, values);
|
|
1501
|
+
if (value === true)
|
|
1502
|
+
return true;
|
|
1503
|
+
if (value === "unknown")
|
|
1504
|
+
unknown = true;
|
|
1505
|
+
}
|
|
1506
|
+
return unknown ? "unknown" : false;
|
|
1507
|
+
}
|
|
1508
|
+
return "unknown";
|
|
1509
|
+
}
|
|
1510
|
+
function evaluateNegatedJSXBranchPredicate(predicate, values) {
|
|
1511
|
+
if (predicate.kind === "truthy") {
|
|
1512
|
+
const value = staticBranchValueForReference(values, predicate.ref) ?? { kind: "undefined" };
|
|
1513
|
+
if (value.kind === "oneOf") {
|
|
1514
|
+
let unknown = false;
|
|
1515
|
+
for (const option of value.values) {
|
|
1516
|
+
const result = evaluateStaticBranchValueTruthy(option);
|
|
1517
|
+
if (result === false)
|
|
1518
|
+
return true;
|
|
1519
|
+
if (result === "unknown")
|
|
1520
|
+
unknown = true;
|
|
1521
|
+
}
|
|
1522
|
+
return unknown ? "unknown" : false;
|
|
1523
|
+
}
|
|
1524
|
+
const truthy = evaluateStaticBranchValueTruthy(value);
|
|
1525
|
+
return truthy === "unknown" ? "unknown" : !truthy;
|
|
1526
|
+
}
|
|
1527
|
+
if (predicate.kind === "equals") {
|
|
1528
|
+
const value = staticBranchValueForReference(values, predicate.ref) ?? { kind: "undefined" };
|
|
1529
|
+
if (value.kind === "oneOf") {
|
|
1530
|
+
let unknown = false;
|
|
1531
|
+
for (const option of value.values) {
|
|
1532
|
+
const result = evaluateSameStaticBranchValue(option, predicate.value);
|
|
1533
|
+
if (result === false)
|
|
1534
|
+
return true;
|
|
1535
|
+
if (result === "unknown")
|
|
1536
|
+
unknown = true;
|
|
1537
|
+
}
|
|
1538
|
+
return unknown ? "unknown" : false;
|
|
1539
|
+
}
|
|
1540
|
+
const result = evaluateSameStaticBranchValue(value, predicate.value);
|
|
1541
|
+
return result === "unknown" ? "unknown" : !result;
|
|
1542
|
+
}
|
|
1543
|
+
if (predicate.kind === "equals-ref") {
|
|
1544
|
+
const left = staticBranchValueForReference(values, predicate.left) ?? { kind: "undefined" };
|
|
1545
|
+
const right = staticBranchValueForReference(values, predicate.right) ?? { kind: "undefined" };
|
|
1546
|
+
if (left.kind === "oneOf") {
|
|
1547
|
+
let unknown = false;
|
|
1548
|
+
for (const option of left.values) {
|
|
1549
|
+
const result = evaluateSameStaticBranchValue(option, right);
|
|
1550
|
+
if (result === false)
|
|
1551
|
+
return true;
|
|
1552
|
+
if (result === "unknown")
|
|
1553
|
+
unknown = true;
|
|
1554
|
+
}
|
|
1555
|
+
return unknown ? "unknown" : false;
|
|
1556
|
+
}
|
|
1557
|
+
if (right.kind === "oneOf") {
|
|
1558
|
+
let unknown = false;
|
|
1559
|
+
for (const option of right.values) {
|
|
1560
|
+
const result = evaluateSameStaticBranchValue(left, option);
|
|
1561
|
+
if (result === false)
|
|
1562
|
+
return true;
|
|
1563
|
+
if (result === "unknown")
|
|
1564
|
+
unknown = true;
|
|
1565
|
+
}
|
|
1566
|
+
return unknown ? "unknown" : false;
|
|
1567
|
+
}
|
|
1568
|
+
const result = evaluateSameStaticBranchValue(left, right);
|
|
1569
|
+
return result === "unknown" ? "unknown" : !result;
|
|
1570
|
+
}
|
|
1571
|
+
if (predicate.kind === "relation") {
|
|
1572
|
+
const value = staticBranchValueForReference(values, predicate.ref) ?? { kind: "undefined" };
|
|
1573
|
+
if (value.kind === "oneOf") {
|
|
1574
|
+
let unknown = false;
|
|
1575
|
+
for (const option of value.values) {
|
|
1576
|
+
const result = evaluateStaticBranchRelation(option, predicate.operator, predicate.value);
|
|
1577
|
+
if (result === false)
|
|
1578
|
+
return true;
|
|
1579
|
+
if (result === "unknown")
|
|
1580
|
+
unknown = true;
|
|
1581
|
+
}
|
|
1582
|
+
return unknown ? "unknown" : false;
|
|
1583
|
+
}
|
|
1584
|
+
const result = evaluateStaticBranchRelation(value, predicate.operator, predicate.value);
|
|
1585
|
+
return result === "unknown" ? "unknown" : !result;
|
|
1586
|
+
}
|
|
1587
|
+
if (predicate.kind === "and")
|
|
1588
|
+
return evaluateJSXBranchPredicate({ kind: "or", predicates: predicate.predicates.map(notJSXBranchPredicate) }, values);
|
|
1589
|
+
if (predicate.kind === "or")
|
|
1590
|
+
return evaluateJSXBranchPredicate({ kind: "and", predicates: predicate.predicates.map(notJSXBranchPredicate) }, values);
|
|
1591
|
+
const value = evaluateJSXBranchPredicate(predicate, values);
|
|
1592
|
+
return value === "unknown" ? "unknown" : !value;
|
|
1593
|
+
}
|
|
1594
|
+
function staticBranchValueForReference(values, reference) {
|
|
1595
|
+
const key = factorReferenceKey(reference);
|
|
1596
|
+
const exact = values.get(key);
|
|
1597
|
+
if (exact)
|
|
1598
|
+
return exact;
|
|
1599
|
+
const parts = key.split(".");
|
|
1600
|
+
while (parts.length > 1) {
|
|
1601
|
+
parts.pop();
|
|
1602
|
+
const ancestor = values.get(parts.join("."));
|
|
1603
|
+
if (ancestor && staticBranchValueIncludesUnknown(ancestor))
|
|
1604
|
+
return { kind: "unknown" };
|
|
1605
|
+
}
|
|
1606
|
+
return undefined;
|
|
1607
|
+
}
|
|
1608
|
+
function staticBranchValueIncludesUnknown(value) {
|
|
1609
|
+
return value.kind === "unknown" || (value.kind === "oneOf" && value.values.some(staticBranchValueIncludesUnknown));
|
|
1610
|
+
}
|
|
1611
|
+
function evaluateStaticBranchValueTruthy(value) {
|
|
1612
|
+
if (value.kind === "unknown")
|
|
1613
|
+
return "unknown";
|
|
1614
|
+
if (value.kind === "oneOf") {
|
|
1615
|
+
let unknown = false;
|
|
1616
|
+
for (const option of value.values) {
|
|
1617
|
+
const result = evaluateStaticBranchValueTruthy(option);
|
|
1618
|
+
if (result === true)
|
|
1619
|
+
return true;
|
|
1620
|
+
if (result === "unknown")
|
|
1621
|
+
unknown = true;
|
|
1622
|
+
}
|
|
1623
|
+
return unknown ? "unknown" : false;
|
|
1624
|
+
}
|
|
1625
|
+
return staticBranchValueTruthy(value);
|
|
1626
|
+
}
|
|
1627
|
+
function evaluateSameStaticBranchValue(left, right) {
|
|
1628
|
+
if (left.kind === "unknown" || right.kind === "unknown")
|
|
1629
|
+
return "unknown";
|
|
1630
|
+
if (left.kind === "oneOf") {
|
|
1631
|
+
let unknown = false;
|
|
1632
|
+
for (const option of left.values) {
|
|
1633
|
+
const result = evaluateSameStaticBranchValue(option, right);
|
|
1634
|
+
if (result === true)
|
|
1635
|
+
return true;
|
|
1636
|
+
if (result === "unknown")
|
|
1637
|
+
unknown = true;
|
|
1638
|
+
}
|
|
1639
|
+
return unknown ? "unknown" : false;
|
|
1640
|
+
}
|
|
1641
|
+
if (right.kind === "oneOf") {
|
|
1642
|
+
let unknown = false;
|
|
1643
|
+
for (const option of right.values) {
|
|
1644
|
+
const result = evaluateSameStaticBranchValue(left, option);
|
|
1645
|
+
if (result === true)
|
|
1646
|
+
return true;
|
|
1647
|
+
if (result === "unknown")
|
|
1648
|
+
unknown = true;
|
|
1649
|
+
}
|
|
1650
|
+
return unknown ? "unknown" : false;
|
|
1651
|
+
}
|
|
1652
|
+
return sameStaticBranchValue(left, right);
|
|
1653
|
+
}
|
|
1654
|
+
function staticBranchValueTruthy(value) {
|
|
1655
|
+
if (value.kind === "unknown")
|
|
1656
|
+
return false;
|
|
1657
|
+
if (value.kind === "oneOf")
|
|
1658
|
+
return value.values.some(staticBranchValueTruthy);
|
|
1659
|
+
if (value.kind === "undefined" || value.kind === "null")
|
|
1660
|
+
return false;
|
|
1661
|
+
if (value.kind === "boolean")
|
|
1662
|
+
return value.value;
|
|
1663
|
+
if (value.kind === "number")
|
|
1664
|
+
return value.value !== 0 && !Number.isNaN(value.value);
|
|
1665
|
+
if (value.kind === "string")
|
|
1666
|
+
return value.value.length > 0;
|
|
1667
|
+
if (value.kind === "array" || value.kind === "object" || value.kind === "truthy")
|
|
1668
|
+
return true;
|
|
1669
|
+
return false;
|
|
1670
|
+
}
|
|
1671
|
+
function sameStaticBranchValue(left, right) {
|
|
1672
|
+
if (left.kind === "oneOf")
|
|
1673
|
+
return left.values.some((value) => sameStaticBranchValue(value, right));
|
|
1674
|
+
if (right.kind === "oneOf")
|
|
1675
|
+
return right.values.some((value) => sameStaticBranchValue(left, value));
|
|
1676
|
+
if (left.kind === "undefined" && right.kind === "undefined")
|
|
1677
|
+
return true;
|
|
1678
|
+
if (left.kind === "null" && right.kind === "null")
|
|
1679
|
+
return true;
|
|
1680
|
+
if (left.kind === "boolean" && right.kind === "boolean")
|
|
1681
|
+
return left.value === right.value;
|
|
1682
|
+
if (left.kind === "number" && right.kind === "number")
|
|
1683
|
+
return left.value === right.value;
|
|
1684
|
+
if (left.kind === "string" && right.kind === "string")
|
|
1685
|
+
return left.value === right.value;
|
|
1686
|
+
if (left.kind === "array" && right.kind === "array")
|
|
1687
|
+
return left.length === right.length;
|
|
1688
|
+
return false;
|
|
1689
|
+
}
|
|
1690
|
+
function evaluateStaticBranchRelation(left, operator, right) {
|
|
1691
|
+
if (left.kind === "unknown" || right.kind === "unknown")
|
|
1692
|
+
return "unknown";
|
|
1693
|
+
if (left.kind === "oneOf") {
|
|
1694
|
+
let unknown = false;
|
|
1695
|
+
for (const value of left.values) {
|
|
1696
|
+
const result = evaluateStaticBranchRelation(value, operator, right);
|
|
1697
|
+
if (result === true)
|
|
1698
|
+
return true;
|
|
1699
|
+
if (result === "unknown")
|
|
1700
|
+
unknown = true;
|
|
1701
|
+
}
|
|
1702
|
+
return unknown ? "unknown" : false;
|
|
1703
|
+
}
|
|
1704
|
+
if (right.kind === "oneOf") {
|
|
1705
|
+
let unknown = false;
|
|
1706
|
+
for (const value of right.values) {
|
|
1707
|
+
const result = evaluateStaticBranchRelation(left, operator, value);
|
|
1708
|
+
if (result === true)
|
|
1709
|
+
return true;
|
|
1710
|
+
if (result === "unknown")
|
|
1711
|
+
unknown = true;
|
|
1712
|
+
}
|
|
1713
|
+
return unknown ? "unknown" : false;
|
|
1714
|
+
}
|
|
1715
|
+
const leftNumber = staticBranchValueNumber(left);
|
|
1716
|
+
const rightNumber = staticBranchValueNumber(right);
|
|
1717
|
+
if (leftNumber === undefined || rightNumber === undefined)
|
|
1718
|
+
return "unknown";
|
|
1719
|
+
if (operator === "<")
|
|
1720
|
+
return leftNumber < rightNumber;
|
|
1721
|
+
if (operator === "<=")
|
|
1722
|
+
return leftNumber <= rightNumber;
|
|
1723
|
+
if (operator === ">")
|
|
1724
|
+
return leftNumber > rightNumber;
|
|
1725
|
+
return leftNumber >= rightNumber;
|
|
1726
|
+
}
|
|
1727
|
+
function staticBranchValueNumber(value) {
|
|
1728
|
+
if (value.kind === "number")
|
|
1729
|
+
return value.value;
|
|
1730
|
+
if (value.kind === "array")
|
|
1731
|
+
return value.length;
|
|
1732
|
+
return undefined;
|
|
1733
|
+
}
|
|
1734
|
+
function factorReferenceKey(reference) {
|
|
1735
|
+
if (reference.root === "context")
|
|
1736
|
+
return ["context", reference.providerName, ...reference.path].join(".");
|
|
1737
|
+
return [reference.root, ...reference.path].join(".");
|
|
1738
|
+
}
|
|
1739
|
+
function formatJSXBranchPredicate(predicate) {
|
|
1740
|
+
if (predicate.kind === "always")
|
|
1741
|
+
return "always";
|
|
1742
|
+
if (predicate.kind === "static")
|
|
1743
|
+
return String(predicate.value);
|
|
1744
|
+
if (predicate.kind === "truthy")
|
|
1745
|
+
return factorReferenceKey(predicate.ref);
|
|
1746
|
+
if (predicate.kind === "equals")
|
|
1747
|
+
return `${factorReferenceKey(predicate.ref)} === ${formatStaticBranchValue(predicate.value)}`;
|
|
1748
|
+
if (predicate.kind === "equals-ref")
|
|
1749
|
+
return `${factorReferenceKey(predicate.left)} === ${factorReferenceKey(predicate.right)}`;
|
|
1750
|
+
if (predicate.kind === "relation") {
|
|
1751
|
+
return `${factorReferenceKey(predicate.ref)} ${predicate.operator} ${formatStaticBranchValue(predicate.value)}`;
|
|
1752
|
+
}
|
|
1753
|
+
if (predicate.kind === "not")
|
|
1754
|
+
return `!(${formatJSXBranchPredicate(predicate.predicate)})`;
|
|
1755
|
+
if (predicate.kind === "and")
|
|
1756
|
+
return predicate.predicates.map(formatJSXBranchPredicate).join(" && ");
|
|
1757
|
+
if (predicate.kind === "or")
|
|
1758
|
+
return predicate.predicates.map(formatJSXBranchPredicate).join(" || ");
|
|
1759
|
+
return predicate.text;
|
|
1760
|
+
}
|
|
1761
|
+
function formatStaticBranchValue(value) {
|
|
1762
|
+
if (value.kind === "boolean")
|
|
1763
|
+
return String(value.value);
|
|
1764
|
+
if (value.kind === "number")
|
|
1765
|
+
return String(value.value);
|
|
1766
|
+
if (value.kind === "string")
|
|
1767
|
+
return JSON.stringify(value.value);
|
|
1768
|
+
if (value.kind === "array")
|
|
1769
|
+
return `array(length=${value.length})`;
|
|
1770
|
+
if (value.kind === "oneOf")
|
|
1771
|
+
return `oneOf(${value.values.map(formatStaticBranchValue).join(", ")})`;
|
|
1772
|
+
return value.kind;
|
|
1773
|
+
}
|
|
1774
|
+
function jsxTagNameText(tagName, sourceFile) {
|
|
1775
|
+
if (ts.isIdentifier(tagName))
|
|
1776
|
+
return tagName.text;
|
|
1777
|
+
return tagName.getText(sourceFile);
|
|
1778
|
+
}
|
|
1779
|
+
function getNonGTSXHookCalls(sourceFile, componentName, scopeHookNames, context) {
|
|
1780
|
+
const violations = new Map();
|
|
1781
|
+
visitComponent(sourceFile, context.entryPath, componentName);
|
|
1782
|
+
return [...violations.values()];
|
|
1783
|
+
function visitComponent(currentSourceFile, currentPath, currentComponentName) {
|
|
1784
|
+
const componentKey = `${currentPath}#${currentComponentName}`;
|
|
1785
|
+
if (context.visitedComponents.has(componentKey))
|
|
1786
|
+
return;
|
|
1787
|
+
context.visitedComponents.add(componentKey);
|
|
1788
|
+
const componentBody = getFunctionLikeBody(currentSourceFile, currentComponentName);
|
|
1789
|
+
if (!componentBody)
|
|
1790
|
+
return;
|
|
1791
|
+
const currentScopeHookNames = currentPath === context.entryPath
|
|
1792
|
+
? scopeHookNames
|
|
1793
|
+
: new Set([
|
|
1794
|
+
...getScopeHookNames(currentSourceFile),
|
|
1795
|
+
...getImportedScopeHookNames(currentSourceFile, currentPath, context.cwd, context.cache),
|
|
1796
|
+
]);
|
|
1797
|
+
const helperFunctions = getTopLevelFunctionLikeBodiesForPath(currentSourceFile, currentPath, context.cache);
|
|
1798
|
+
const visitedHelpers = new Set([currentComponentName]);
|
|
1799
|
+
const localComponentNames = helperFunctions;
|
|
1800
|
+
const importBindings = componentDependencyBindingsForFile(currentSourceFile, currentPath, context);
|
|
1801
|
+
const file = normalizeProjectPath(relative(context.cwd, currentPath));
|
|
1802
|
+
visit(componentBody, localComponentAliasBindingsForBody(componentBody));
|
|
1803
|
+
function visit(node, localAliases) {
|
|
1804
|
+
if (ts.isCallExpression(node)) {
|
|
1805
|
+
const hookName = getHookCallName(node, currentSourceFile);
|
|
1806
|
+
if (hookName) {
|
|
1807
|
+
if (!isAllowedGTSXHookCall(node, hookName, currentScopeHookNames)) {
|
|
1808
|
+
const key = `${file}#${currentComponentName}:${hookName}`;
|
|
1809
|
+
violations.set(key, { hookName, componentName: currentComponentName, file });
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
else if (ts.isIdentifier(node.expression)) {
|
|
1813
|
+
visitHelper(node.expression.text);
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
|
|
1817
|
+
visitJSXDependency(node.tagName, localAliases);
|
|
1818
|
+
}
|
|
1819
|
+
ts.forEachChild(node, (child) => visit(child, localAliases));
|
|
1820
|
+
}
|
|
1821
|
+
function visitHelper(functionName) {
|
|
1822
|
+
if (visitedHelpers.has(functionName))
|
|
1823
|
+
return;
|
|
1824
|
+
const helperBody = helperFunctions.get(functionName);
|
|
1825
|
+
if (!helperBody)
|
|
1826
|
+
return;
|
|
1827
|
+
visitedHelpers.add(functionName);
|
|
1828
|
+
visit(helperBody, localComponentAliasBindingsForBody(helperBody));
|
|
1829
|
+
}
|
|
1830
|
+
function visitJSXDependency(tagName, localAliases) {
|
|
1831
|
+
const target = componentDependencyTargetForJsxTag(tagName, {
|
|
1832
|
+
filePath: currentPath,
|
|
1833
|
+
importBindings,
|
|
1834
|
+
localAliases,
|
|
1835
|
+
localComponentNames,
|
|
1836
|
+
});
|
|
1837
|
+
if (!target)
|
|
1838
|
+
return;
|
|
1839
|
+
const targetSourceFile = sourceFileForPath(target.filePath, context);
|
|
1840
|
+
if (!targetSourceFile)
|
|
1841
|
+
return;
|
|
1842
|
+
visitComponent(targetSourceFile, target.filePath, target.componentName);
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
function getGProviderConsumers(sourceFile, componentName, context) {
|
|
1847
|
+
const providers = new Set();
|
|
1848
|
+
visitComponent(sourceFile, context.entryPath, componentName);
|
|
1849
|
+
return providers;
|
|
1850
|
+
function visitComponent(currentSourceFile, currentPath, currentComponentName) {
|
|
1851
|
+
const componentKey = `${currentPath}#${currentComponentName}`;
|
|
1852
|
+
if (context.visitedComponents.has(componentKey))
|
|
1853
|
+
return;
|
|
1854
|
+
context.visitedComponents.add(componentKey);
|
|
1855
|
+
const componentBody = getFunctionLikeBody(currentSourceFile, currentComponentName);
|
|
1856
|
+
if (!componentBody)
|
|
1857
|
+
return;
|
|
1858
|
+
const scopeHookProviderNames = getScopeHookProviderNamesForFile(currentSourceFile, currentPath, context.cache);
|
|
1859
|
+
const helperFunctions = getTopLevelFunctionLikeBodiesForPath(currentSourceFile, currentPath, context.cache);
|
|
1860
|
+
const visitedHelpers = new Set([currentComponentName]);
|
|
1861
|
+
const localComponentNames = helperFunctions;
|
|
1862
|
+
const importBindings = componentDependencyBindingsForFile(currentSourceFile, currentPath, context);
|
|
1863
|
+
visit(componentBody, localComponentAliasBindingsForBody(componentBody));
|
|
1864
|
+
function visit(node, localAliases) {
|
|
1865
|
+
if (ts.isCallExpression(node)) {
|
|
1866
|
+
const contextProvider = gContextProviderNameFromCall(node);
|
|
1867
|
+
if (contextProvider) {
|
|
1868
|
+
providers.add(contextProvider);
|
|
1869
|
+
}
|
|
1870
|
+
else if (ts.isIdentifier(node.expression) && scopeHookProviderNames.has(node.expression.text)) {
|
|
1871
|
+
for (const providerName of scopeHookProviderNames.get(node.expression.text) ?? [])
|
|
1872
|
+
providers.add(providerName);
|
|
1873
|
+
}
|
|
1874
|
+
else if (ts.isIdentifier(node.expression) && !isHookName(node.expression.text)) {
|
|
1875
|
+
visitHelper(node.expression.text);
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
|
|
1879
|
+
visitJSXDependency(node.tagName, localAliases);
|
|
1880
|
+
}
|
|
1881
|
+
ts.forEachChild(node, (child) => visit(child, localAliases));
|
|
1882
|
+
}
|
|
1883
|
+
function visitHelper(functionName) {
|
|
1884
|
+
if (visitedHelpers.has(functionName))
|
|
1885
|
+
return;
|
|
1886
|
+
const helperBody = helperFunctions.get(functionName);
|
|
1887
|
+
if (!helperBody)
|
|
1888
|
+
return;
|
|
1889
|
+
visitedHelpers.add(functionName);
|
|
1890
|
+
visit(helperBody, localComponentAliasBindingsForBody(helperBody));
|
|
1891
|
+
}
|
|
1892
|
+
function visitJSXDependency(tagName, localAliases) {
|
|
1893
|
+
const target = componentDependencyTargetForJsxTag(tagName, {
|
|
1894
|
+
filePath: currentPath,
|
|
1895
|
+
importBindings,
|
|
1896
|
+
localAliases,
|
|
1897
|
+
localComponentNames,
|
|
1898
|
+
});
|
|
1899
|
+
if (!target)
|
|
1900
|
+
return;
|
|
1901
|
+
const targetSourceFile = sourceFileForPath(target.filePath, context);
|
|
1902
|
+
if (!targetSourceFile)
|
|
1903
|
+
return;
|
|
1904
|
+
visitComponent(targetSourceFile, target.filePath, target.componentName);
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
function gContextProviderNameFromCall(node) {
|
|
1909
|
+
if (!ts.isIdentifier(node.expression))
|
|
1910
|
+
return undefined;
|
|
1911
|
+
if (node.expression.text !== "useGContext" && node.expression.text !== "useGContextUpdate")
|
|
1912
|
+
return undefined;
|
|
1913
|
+
const provider = node.arguments[0] ? unwrapExpression(node.arguments[0]) : undefined;
|
|
1914
|
+
return provider && ts.isIdentifier(provider) ? provider.text : undefined;
|
|
1915
|
+
}
|
|
1916
|
+
function localComponentAliasBindingsForBody(body) {
|
|
1917
|
+
const aliases = new Map();
|
|
1918
|
+
if (!ts.isBlock(body))
|
|
1919
|
+
return aliases;
|
|
1920
|
+
for (const statement of body.statements) {
|
|
1921
|
+
if (!ts.isVariableStatement(statement))
|
|
1922
|
+
continue;
|
|
1923
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
1924
|
+
if (!ts.isIdentifier(declaration.name) || !declaration.initializer)
|
|
1925
|
+
continue;
|
|
1926
|
+
const initializer = unwrapExpression(declaration.initializer);
|
|
1927
|
+
if (ts.isIdentifier(initializer)) {
|
|
1928
|
+
aliases.set(declaration.name.text, initializer.text);
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
return aliases;
|
|
1933
|
+
}
|
|
1934
|
+
function getFunctionLikeBody(sourceFile, functionName) {
|
|
1935
|
+
return getFunctionLikeDeclaration(sourceFile, functionName)?.body;
|
|
1936
|
+
}
|
|
1937
|
+
function getFunctionLikeDeclaration(sourceFile, functionName) {
|
|
1938
|
+
for (const statement of sourceFile.statements) {
|
|
1939
|
+
if (ts.isFunctionDeclaration(statement) && statement.name?.text === functionName) {
|
|
1940
|
+
return statement;
|
|
1941
|
+
}
|
|
1942
|
+
if (!ts.isVariableStatement(statement))
|
|
1943
|
+
continue;
|
|
1944
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
1945
|
+
if (!ts.isIdentifier(declaration.name) || declaration.name.text !== functionName || !declaration.initializer)
|
|
1946
|
+
continue;
|
|
1947
|
+
const initializer = unwrapExpression(declaration.initializer);
|
|
1948
|
+
if (ts.isArrowFunction(initializer) || ts.isFunctionExpression(initializer)) {
|
|
1949
|
+
return initializer;
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
return undefined;
|
|
1954
|
+
}
|
|
1955
|
+
function getTopLevelFunctionLikeBodies(sourceFile) {
|
|
1956
|
+
const functions = new Map();
|
|
1957
|
+
for (const statement of sourceFile.statements) {
|
|
1958
|
+
if (ts.isFunctionDeclaration(statement) && statement.name && statement.body) {
|
|
1959
|
+
functions.set(statement.name.text, statement.body);
|
|
1960
|
+
continue;
|
|
1961
|
+
}
|
|
1962
|
+
if (!ts.isVariableStatement(statement))
|
|
1963
|
+
continue;
|
|
1964
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
1965
|
+
if (!ts.isIdentifier(declaration.name) || !declaration.initializer)
|
|
1966
|
+
continue;
|
|
1967
|
+
const initializer = unwrapExpression(declaration.initializer);
|
|
1968
|
+
if (ts.isArrowFunction(initializer) || ts.isFunctionExpression(initializer)) {
|
|
1969
|
+
functions.set(declaration.name.text, initializer.body);
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
return functions;
|
|
1974
|
+
}
|
|
1975
|
+
function getTopLevelFunctionLikeBodiesForPath(sourceFile, filePath, cache) {
|
|
1976
|
+
const cached = cache?.topLevelFunctionLikeBodiesByPath.get(filePath);
|
|
1977
|
+
if (cached)
|
|
1978
|
+
return cached;
|
|
1979
|
+
const functions = getTopLevelFunctionLikeBodies(sourceFile);
|
|
1980
|
+
cache?.topLevelFunctionLikeBodiesByPath.set(filePath, functions);
|
|
1981
|
+
return functions;
|
|
1982
|
+
}
|
|
1983
|
+
function componentDependencyBindingsForFile(sourceFile, filePath, context) {
|
|
1984
|
+
const cached = context.cache?.componentDependencyBindingsByPath.get(filePath);
|
|
1985
|
+
if (cached)
|
|
1986
|
+
return cached;
|
|
1987
|
+
const bindings = {
|
|
1988
|
+
names: new Map(),
|
|
1989
|
+
namespaces: new Map(),
|
|
1990
|
+
};
|
|
1991
|
+
for (const statement of sourceFile.statements) {
|
|
1992
|
+
if (!ts.isImportDeclaration(statement) || !statement.importClause)
|
|
1993
|
+
continue;
|
|
1994
|
+
if (!ts.isStringLiteral(statement.moduleSpecifier))
|
|
1995
|
+
continue;
|
|
1996
|
+
const targetPath = resolveImportedGTSXPath(filePath, context.cwd, statement.moduleSpecifier.text, context.cache);
|
|
1997
|
+
if (!targetPath)
|
|
1998
|
+
continue;
|
|
1999
|
+
const targetSourceFile = sourceFileForPath(targetPath, context);
|
|
2000
|
+
if (!targetSourceFile)
|
|
2001
|
+
continue;
|
|
2002
|
+
const importClause = statement.importClause;
|
|
2003
|
+
if (importClause.name) {
|
|
2004
|
+
const componentName = getComponentExportName(targetSourceFile, "default");
|
|
2005
|
+
if (componentName) {
|
|
2006
|
+
bindings.names.set(importClause.name.text, { filePath: targetPath, componentName });
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
if (!importClause.namedBindings)
|
|
2010
|
+
continue;
|
|
2011
|
+
if (ts.isNamedImports(importClause.namedBindings)) {
|
|
2012
|
+
for (const element of importClause.namedBindings.elements) {
|
|
2013
|
+
const importedName = element.propertyName?.text ?? element.name.text;
|
|
2014
|
+
const componentName = getComponentExportName(targetSourceFile, importedName);
|
|
2015
|
+
if (componentName) {
|
|
2016
|
+
bindings.names.set(element.name.text, { filePath: targetPath, componentName });
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
else if (ts.isNamespaceImport(importClause.namedBindings)) {
|
|
2021
|
+
const namespaceExports = exportedComponentTargetsForFile(targetSourceFile, targetPath, context.cache);
|
|
2022
|
+
if (namespaceExports.size > 0) {
|
|
2023
|
+
bindings.namespaces.set(importClause.namedBindings.name.text, namespaceExports);
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
context.cache?.componentDependencyBindingsByPath.set(filePath, bindings);
|
|
2028
|
+
return bindings;
|
|
2029
|
+
}
|
|
2030
|
+
function sourceFileForPath(filePath, context) {
|
|
2031
|
+
const cached = context.sourceFilesByPath.get(filePath);
|
|
2032
|
+
if (cached)
|
|
2033
|
+
return cached;
|
|
2034
|
+
const sourceFile = sourceFileForAbsolutePath(filePath, context.cache);
|
|
2035
|
+
if (!sourceFile)
|
|
2036
|
+
return undefined;
|
|
2037
|
+
context.sourceFilesByPath.set(filePath, sourceFile);
|
|
2038
|
+
return sourceFile;
|
|
2039
|
+
}
|
|
2040
|
+
function sourceFileForAbsolutePath(filePath, cache) {
|
|
2041
|
+
const cached = cache?.sourceFilesByPath.get(filePath);
|
|
2042
|
+
if (cached)
|
|
2043
|
+
return cached;
|
|
2044
|
+
if (!isFile(filePath))
|
|
2045
|
+
return undefined;
|
|
2046
|
+
const sourceFile = ts.createSourceFile(filePath, readFileSync(filePath, "utf8"), ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
|
|
2047
|
+
cache?.sourceFilesByPath.set(filePath, sourceFile);
|
|
2048
|
+
return sourceFile;
|
|
2049
|
+
}
|
|
2050
|
+
function exportedComponentTargetsForFile(sourceFile, filePath, cache) {
|
|
2051
|
+
const cached = cache?.exportedComponentTargetsByPath.get(filePath);
|
|
2052
|
+
if (cached)
|
|
2053
|
+
return cached;
|
|
2054
|
+
const targets = new Map();
|
|
2055
|
+
const defaultName = getComponentExportName(sourceFile, "default");
|
|
2056
|
+
if (defaultName) {
|
|
2057
|
+
targets.set("default", { filePath, componentName: defaultName });
|
|
2058
|
+
}
|
|
2059
|
+
for (const statement of sourceFile.statements) {
|
|
2060
|
+
if ((ts.isFunctionDeclaration(statement) || ts.isClassDeclaration(statement)) && statement.name) {
|
|
2061
|
+
if (hasModifier(statement, ts.SyntaxKind.ExportKeyword)) {
|
|
2062
|
+
targets.set(statement.name.text, { filePath, componentName: statement.name.text });
|
|
2063
|
+
}
|
|
2064
|
+
continue;
|
|
2065
|
+
}
|
|
2066
|
+
if (ts.isVariableStatement(statement) && hasModifier(statement, ts.SyntaxKind.ExportKeyword)) {
|
|
2067
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
2068
|
+
if (ts.isIdentifier(declaration.name) && getFunctionLikeBody(sourceFile, declaration.name.text)) {
|
|
2069
|
+
targets.set(declaration.name.text, { filePath, componentName: declaration.name.text });
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
continue;
|
|
2073
|
+
}
|
|
2074
|
+
if (!ts.isExportDeclaration(statement) || statement.moduleSpecifier || !statement.exportClause || !ts.isNamedExports(statement.exportClause)) {
|
|
2075
|
+
continue;
|
|
2076
|
+
}
|
|
2077
|
+
for (const element of statement.exportClause.elements) {
|
|
2078
|
+
const localName = element.propertyName?.text ?? element.name.text;
|
|
2079
|
+
if (getFunctionLikeBody(sourceFile, localName)) {
|
|
2080
|
+
targets.set(element.name.text, { filePath, componentName: localName });
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
cache?.exportedComponentTargetsByPath.set(filePath, targets);
|
|
2085
|
+
return targets;
|
|
2086
|
+
}
|
|
2087
|
+
function componentDependencyTargetForJsxTag(tagName, input) {
|
|
2088
|
+
if (ts.isIdentifier(tagName)) {
|
|
2089
|
+
return componentDependencyTargetForIdentifier(tagName.text, input);
|
|
2090
|
+
}
|
|
2091
|
+
if (ts.isPropertyAccessExpression(tagName) && ts.isIdentifier(tagName.expression)) {
|
|
2092
|
+
return input.importBindings.namespaces.get(tagName.expression.text)?.get(tagName.name.text);
|
|
2093
|
+
}
|
|
2094
|
+
return undefined;
|
|
2095
|
+
}
|
|
2096
|
+
function componentDependencyTargetForIdentifier(name, input) {
|
|
2097
|
+
if (!isComponentName(name))
|
|
2098
|
+
return undefined;
|
|
2099
|
+
let currentName = name;
|
|
2100
|
+
const visited = new Set();
|
|
2101
|
+
while (!visited.has(currentName)) {
|
|
2102
|
+
visited.add(currentName);
|
|
2103
|
+
const aliasTarget = input.localAliases?.get(currentName);
|
|
2104
|
+
if (aliasTarget) {
|
|
2105
|
+
currentName = aliasTarget;
|
|
2106
|
+
continue;
|
|
2107
|
+
}
|
|
2108
|
+
if (input.localComponentNames?.has(currentName)) {
|
|
2109
|
+
return { filePath: input.filePath, componentName: currentName };
|
|
2110
|
+
}
|
|
2111
|
+
const importTarget = input.importBindings.names.get(currentName);
|
|
2112
|
+
if (importTarget)
|
|
2113
|
+
return importTarget;
|
|
2114
|
+
return undefined;
|
|
2115
|
+
}
|
|
2116
|
+
return undefined;
|
|
2117
|
+
}
|
|
2118
|
+
function getHookCallName(node, sourceFile) {
|
|
2119
|
+
if (ts.isIdentifier(node.expression) && isHookName(node.expression.text)) {
|
|
2120
|
+
return node.expression.text;
|
|
2121
|
+
}
|
|
2122
|
+
if (ts.isPropertyAccessExpression(node.expression) && isHookName(node.expression.name.text)) {
|
|
2123
|
+
return node.expression.getText(sourceFile);
|
|
2124
|
+
}
|
|
2125
|
+
return undefined;
|
|
2126
|
+
}
|
|
2127
|
+
function isAllowedGTSXHookCall(node, hookName, scopeHookNames) {
|
|
2128
|
+
if (ts.isIdentifier(node.expression)) {
|
|
2129
|
+
return hookName === "useGContext" || hookName === "useGContextUpdate" || scopeHookNames.has(hookName);
|
|
2130
|
+
}
|
|
2131
|
+
if (ts.isPropertyAccessExpression(node.expression)) {
|
|
2132
|
+
return node.expression.name.text === "useUpdate";
|
|
2133
|
+
}
|
|
2134
|
+
return false;
|
|
2135
|
+
}
|
|
2136
|
+
function getGScopeHookCalls(sourceFile, componentName, scopeHookNames) {
|
|
2137
|
+
const component = getFunctionDeclaration(sourceFile, componentName);
|
|
2138
|
+
if (!component?.body)
|
|
2139
|
+
return [];
|
|
2140
|
+
const helperFunctions = getTopLevelFunctionDeclarations(sourceFile);
|
|
2141
|
+
const visitedHelpers = new Set([componentName]);
|
|
2142
|
+
const hookNames = new Set();
|
|
2143
|
+
visit(component.body);
|
|
2144
|
+
return [...hookNames];
|
|
2145
|
+
function visit(node) {
|
|
2146
|
+
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {
|
|
2147
|
+
if (scopeHookNames.has(node.expression.text)) {
|
|
2148
|
+
hookNames.add(node.expression.text);
|
|
2149
|
+
}
|
|
2150
|
+
else if (!isHookName(node.expression.text)) {
|
|
2151
|
+
visitHelper(node.expression.text);
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
ts.forEachChild(node, visit);
|
|
2155
|
+
}
|
|
2156
|
+
function visitHelper(functionName) {
|
|
2157
|
+
if (visitedHelpers.has(functionName))
|
|
2158
|
+
return;
|
|
2159
|
+
const helper = helperFunctions.get(functionName);
|
|
2160
|
+
if (!helper?.body)
|
|
2161
|
+
return;
|
|
2162
|
+
visitedHelpers.add(functionName);
|
|
2163
|
+
visit(helper.body);
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
function getFunctionDeclaration(sourceFile, functionName) {
|
|
2167
|
+
return sourceFile.statements.find((statement) => ts.isFunctionDeclaration(statement) && statement.name?.text === functionName);
|
|
2168
|
+
}
|
|
2169
|
+
function getTopLevelFunctionDeclarations(sourceFile) {
|
|
2170
|
+
const functions = new Map();
|
|
2171
|
+
for (const statement of sourceFile.statements) {
|
|
2172
|
+
if (ts.isFunctionDeclaration(statement) && statement.name) {
|
|
2173
|
+
functions.set(statement.name.text, statement);
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
return functions;
|
|
2177
|
+
}
|
|
2178
|
+
function isHookName(name) {
|
|
2179
|
+
return name === "use" || /^use[A-Z0-9_]/.test(name);
|
|
2180
|
+
}
|
|
2181
|
+
function isComponentName(name) {
|
|
2182
|
+
return /^[A-Z]/.test(name);
|
|
2183
|
+
}
|
|
2184
|
+
function jsxTagIdentifier(tagName) {
|
|
2185
|
+
if (ts.isIdentifier(tagName))
|
|
2186
|
+
return tagName.text;
|
|
2187
|
+
return undefined;
|
|
2188
|
+
}
|
|
2189
|
+
function getCasesAssignment(statement, sourceFile, diagnostics) {
|
|
2190
|
+
if (!ts.isExpressionStatement(statement))
|
|
2191
|
+
return undefined;
|
|
2192
|
+
const expression = statement.expression;
|
|
2193
|
+
if (!ts.isBinaryExpression(expression) || expression.operatorToken.kind !== ts.SyntaxKind.EqualsToken) {
|
|
2194
|
+
return undefined;
|
|
2195
|
+
}
|
|
2196
|
+
if (!ts.isPropertyAccessExpression(expression.left) || expression.left.name.text !== "cases") {
|
|
2197
|
+
return undefined;
|
|
2198
|
+
}
|
|
2199
|
+
if (!ts.isIdentifier(expression.left.expression))
|
|
2200
|
+
return undefined;
|
|
2201
|
+
const casesExpression = unwrapExpression(expression.right);
|
|
2202
|
+
if (!ts.isObjectLiteralExpression(casesExpression)) {
|
|
2203
|
+
diagnostics.push({
|
|
2204
|
+
stage: "contract-extraction",
|
|
2205
|
+
code: "malformed-cases",
|
|
2206
|
+
message: "GTSX cases must be a statically enumerable object literal.",
|
|
2207
|
+
file: sourceFile.fileName,
|
|
2208
|
+
});
|
|
2209
|
+
return { targetName: expression.left.expression.text, cases: [], staticCases: [] };
|
|
2210
|
+
}
|
|
2211
|
+
return {
|
|
2212
|
+
targetName: expression.left.expression.text,
|
|
2213
|
+
...readCasesObject(casesExpression, sourceFile, diagnostics),
|
|
2214
|
+
};
|
|
2215
|
+
}
|
|
2216
|
+
function readCasesObject(objectLiteral, sourceFile, diagnostics) {
|
|
2217
|
+
const cases = [];
|
|
2218
|
+
const staticCases = [];
|
|
2219
|
+
for (const property of objectLiteral.properties) {
|
|
2220
|
+
if (ts.isSpreadAssignment(property)) {
|
|
2221
|
+
diagnostics.push({
|
|
2222
|
+
stage: "contract-extraction",
|
|
2223
|
+
code: "malformed-cases",
|
|
2224
|
+
message: "GTSX cases do not support spread composition in the first implementation.",
|
|
2225
|
+
file: sourceFile.fileName,
|
|
2226
|
+
});
|
|
2227
|
+
continue;
|
|
2228
|
+
}
|
|
2229
|
+
if (!ts.isPropertyAssignment(property))
|
|
2230
|
+
continue;
|
|
2231
|
+
const caseName = getStaticPropertyName(property.name);
|
|
2232
|
+
if (!caseName) {
|
|
2233
|
+
diagnostics.push({
|
|
2234
|
+
stage: "contract-extraction",
|
|
2235
|
+
code: "non-static-case-key",
|
|
2236
|
+
message: "GTSX case keys must be statically enumerable object literal keys.",
|
|
2237
|
+
file: sourceFile.fileName,
|
|
2238
|
+
});
|
|
2239
|
+
continue;
|
|
2240
|
+
}
|
|
2241
|
+
const providerVariants = readProviderVariantMarkers(property.initializer);
|
|
2242
|
+
const caseValue = unwrapExpression(property.initializer);
|
|
2243
|
+
const providers = ts.isObjectLiteralExpression(caseValue) ? readProviderSelections(caseValue) : undefined;
|
|
2244
|
+
const kind = ts.isObjectLiteralExpression(caseValue) && hasStaticProperty(caseValue, "scope") ? "scope" : "pure";
|
|
2245
|
+
cases.push({
|
|
2246
|
+
kind,
|
|
2247
|
+
name: caseName,
|
|
2248
|
+
...(providerVariants && Object.keys(providerVariants).length > 0 ? { providerVariants } : {}),
|
|
2249
|
+
...(providers && Object.keys(providers).length > 0 ? { providers } : {}),
|
|
2250
|
+
});
|
|
2251
|
+
staticCases.push(readCaseStaticFacts(caseName, caseValue, providerVariants));
|
|
2252
|
+
}
|
|
2253
|
+
return { cases, staticCases };
|
|
2254
|
+
}
|
|
2255
|
+
function readProviderVariantMarkers(expression) {
|
|
2256
|
+
if (ts.isSatisfiesExpression(expression))
|
|
2257
|
+
return readProviderVariantMarkersFromType(expression.type);
|
|
2258
|
+
if (ts.isAsExpression(expression) || ts.isParenthesizedExpression(expression))
|
|
2259
|
+
return readProviderVariantMarkers(expression.expression);
|
|
2260
|
+
return undefined;
|
|
2261
|
+
}
|
|
2262
|
+
function readCaseStaticFacts(caseName, caseValue, providerVariants) {
|
|
2263
|
+
const values = new Map();
|
|
2264
|
+
if (ts.isObjectLiteralExpression(caseValue)) {
|
|
2265
|
+
const props = objectLiteralPropertyExpression(caseValue, "props");
|
|
2266
|
+
if (props)
|
|
2267
|
+
flattenStaticObjectExpression(props, "props", values);
|
|
2268
|
+
const scope = objectLiteralPropertyExpression(caseValue, "scope");
|
|
2269
|
+
if (scope)
|
|
2270
|
+
flattenStaticObjectExpression(scope, "scope", values);
|
|
2271
|
+
const providers = objectLiteralPropertyExpression(caseValue, "providers");
|
|
2272
|
+
if (providers)
|
|
2273
|
+
flattenProviderStaticValues(providers, values);
|
|
2274
|
+
}
|
|
2275
|
+
for (const [providerName, selection] of Object.entries(providerVariants ?? {})) {
|
|
2276
|
+
const variants = providerVariantSelectionValues(selection);
|
|
2277
|
+
values.set(`context.${providerName}.variant`, variants.length === 1
|
|
2278
|
+
? { kind: "string", value: variants[0] }
|
|
2279
|
+
: { kind: "oneOf", values: variants.map((variant) => ({ kind: "string", value: variant })) });
|
|
2280
|
+
}
|
|
2281
|
+
return {
|
|
2282
|
+
name: caseName,
|
|
2283
|
+
...(providerVariants && Object.keys(providerVariants).length > 0 ? { providerVariants } : {}),
|
|
2284
|
+
values,
|
|
2285
|
+
};
|
|
2286
|
+
}
|
|
2287
|
+
function objectLiteralPropertyExpression(objectLiteral, propertyName) {
|
|
2288
|
+
const property = objectLiteral.properties.find((candidate) => ts.isPropertyAssignment(candidate) && getStaticPropertyName(candidate.name) === propertyName);
|
|
2289
|
+
return property ? unwrapExpression(property.initializer) : undefined;
|
|
2290
|
+
}
|
|
2291
|
+
function flattenProviderStaticValues(expression, values) {
|
|
2292
|
+
const providersValue = unwrapExpression(expression);
|
|
2293
|
+
if (!ts.isArrayLiteralExpression(providersValue))
|
|
2294
|
+
return;
|
|
2295
|
+
for (const element of providersValue.elements) {
|
|
2296
|
+
const entry = unwrapExpression(element);
|
|
2297
|
+
if (!ts.isArrayLiteralExpression(entry))
|
|
2298
|
+
continue;
|
|
2299
|
+
const providerExpression = entry.elements[0] ? unwrapExpression(entry.elements[0]) : undefined;
|
|
2300
|
+
const valueExpression = entry.elements[1] ? unwrapExpression(entry.elements[1]) : undefined;
|
|
2301
|
+
if (!providerExpression || !ts.isIdentifier(providerExpression) || !valueExpression)
|
|
2302
|
+
continue;
|
|
2303
|
+
flattenStaticObjectExpression(valueExpression, `context.${providerExpression.text}`, values);
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
function flattenStaticObjectExpression(expression, prefix, values) {
|
|
2307
|
+
const value = unwrapExpression(expression);
|
|
2308
|
+
const staticValue = readStaticBranchValue(value);
|
|
2309
|
+
if (staticValue) {
|
|
2310
|
+
setStaticBranchValue(values, prefix, staticValue);
|
|
2311
|
+
if (staticValue.kind === "array")
|
|
2312
|
+
setStaticBranchValue(values, `${prefix}.length`, { kind: "number", value: staticValue.length });
|
|
2313
|
+
if (staticValue.kind === "string")
|
|
2314
|
+
setStaticBranchValue(values, `${prefix}.length`, { kind: "number", value: staticValue.value.length });
|
|
2315
|
+
}
|
|
2316
|
+
if (ts.isArrayLiteralExpression(value)) {
|
|
2317
|
+
for (const element of value.elements) {
|
|
2318
|
+
if (ts.isSpreadElement(element)) {
|
|
2319
|
+
setStaticBranchValue(values, `${prefix}.number`, { kind: "unknown" });
|
|
2320
|
+
continue;
|
|
2321
|
+
}
|
|
2322
|
+
flattenStaticObjectExpression(element, `${prefix}.number`, values);
|
|
2323
|
+
}
|
|
2324
|
+
return;
|
|
2325
|
+
}
|
|
2326
|
+
if (!ts.isObjectLiteralExpression(value)) {
|
|
2327
|
+
if (!staticValue)
|
|
2328
|
+
setStaticBranchValue(values, prefix, { kind: "unknown" });
|
|
2329
|
+
return;
|
|
2330
|
+
}
|
|
2331
|
+
for (const property of value.properties) {
|
|
2332
|
+
if (ts.isSpreadAssignment(property)) {
|
|
2333
|
+
setStaticBranchValue(values, prefix, { kind: "unknown" });
|
|
2334
|
+
continue;
|
|
2335
|
+
}
|
|
2336
|
+
if (!ts.isPropertyAssignment(property))
|
|
2337
|
+
continue;
|
|
2338
|
+
const propertyName = getStaticPropertyName(property.name);
|
|
2339
|
+
if (!propertyName)
|
|
2340
|
+
continue;
|
|
2341
|
+
flattenStaticObjectExpression(property.initializer, `${prefix}.${propertyName}`, values);
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
function setStaticBranchValue(values, key, value) {
|
|
2345
|
+
const existing = values.get(key);
|
|
2346
|
+
if (!existing) {
|
|
2347
|
+
values.set(key, value);
|
|
2348
|
+
return;
|
|
2349
|
+
}
|
|
2350
|
+
if (sameStaticBranchValue(existing, value))
|
|
2351
|
+
return;
|
|
2352
|
+
values.set(key, { kind: "oneOf", values: [...staticBranchValueOptions(existing), value] });
|
|
2353
|
+
}
|
|
2354
|
+
function staticBranchValueOptions(value) {
|
|
2355
|
+
return value.kind === "oneOf" ? value.values : [value];
|
|
2356
|
+
}
|
|
2357
|
+
function readStaticBranchValue(expression) {
|
|
2358
|
+
const value = unwrapExpression(expression);
|
|
2359
|
+
if (value.kind === ts.SyntaxKind.TrueKeyword)
|
|
2360
|
+
return { kind: "boolean", value: true };
|
|
2361
|
+
if (value.kind === ts.SyntaxKind.FalseKeyword)
|
|
2362
|
+
return { kind: "boolean", value: false };
|
|
2363
|
+
if (value.kind === ts.SyntaxKind.NullKeyword)
|
|
2364
|
+
return { kind: "null" };
|
|
2365
|
+
if (ts.isIdentifier(value) && value.text === "undefined")
|
|
2366
|
+
return { kind: "undefined" };
|
|
2367
|
+
if (ts.isStringLiteral(value) || ts.isNoSubstitutionTemplateLiteral(value))
|
|
2368
|
+
return { kind: "string", value: value.text };
|
|
2369
|
+
if (ts.isNumericLiteral(value))
|
|
2370
|
+
return { kind: "number", value: Number(value.text) };
|
|
2371
|
+
if (ts.isArrayLiteralExpression(value)) {
|
|
2372
|
+
if (value.elements.some((element) => ts.isSpreadElement(element)))
|
|
2373
|
+
return undefined;
|
|
2374
|
+
return { kind: "array", length: value.elements.length };
|
|
2375
|
+
}
|
|
2376
|
+
if (ts.isObjectLiteralExpression(value))
|
|
2377
|
+
return { kind: "object" };
|
|
2378
|
+
if (ts.isArrowFunction(value) || ts.isFunctionExpression(value))
|
|
2379
|
+
return { kind: "truthy" };
|
|
2380
|
+
if (ts.isPrefixUnaryExpression(value) && value.operator === ts.SyntaxKind.ExclamationToken) {
|
|
2381
|
+
const inner = readStaticBranchValue(value.operand);
|
|
2382
|
+
const truthy = inner ? staticBranchValueTruthy(inner) : undefined;
|
|
2383
|
+
return truthy === undefined ? undefined : { kind: "boolean", value: !truthy };
|
|
2384
|
+
}
|
|
2385
|
+
return undefined;
|
|
2386
|
+
}
|
|
2387
|
+
function readProviderVariantMarkersFromType(typeNode) {
|
|
2388
|
+
const variants = new Map();
|
|
2389
|
+
visit(typeNode);
|
|
2390
|
+
return Object.fromEntries([...variants.entries()].map(([providerName, providerVariants]) => {
|
|
2391
|
+
const values = [...providerVariants];
|
|
2392
|
+
return [providerName, values.length === 1 ? values[0] : values];
|
|
2393
|
+
}));
|
|
2394
|
+
function visit(node) {
|
|
2395
|
+
if (ts.isIntersectionTypeNode(node) || ts.isUnionTypeNode(node)) {
|
|
2396
|
+
for (const child of node.types)
|
|
2397
|
+
visit(child);
|
|
2398
|
+
return;
|
|
2399
|
+
}
|
|
2400
|
+
if (ts.isParenthesizedTypeNode(node)) {
|
|
2401
|
+
visit(node.type);
|
|
2402
|
+
return;
|
|
2403
|
+
}
|
|
2404
|
+
if (!ts.isTypeReferenceNode(node))
|
|
2405
|
+
return;
|
|
2406
|
+
if (!ts.isIdentifier(node.typeName) || node.typeName.text !== "GProviderCase")
|
|
2407
|
+
return;
|
|
2408
|
+
const providerType = node.typeArguments?.[0];
|
|
2409
|
+
const variantType = node.typeArguments?.[1];
|
|
2410
|
+
if (!providerType || !variantType || !ts.isTypeQueryNode(providerType))
|
|
2411
|
+
return;
|
|
2412
|
+
if (!ts.isIdentifier(providerType.exprName))
|
|
2413
|
+
return;
|
|
2414
|
+
const providerVariants = readProviderVariantTypeValues(variantType);
|
|
2415
|
+
if (providerVariants.length === 0)
|
|
2416
|
+
return;
|
|
2417
|
+
const providerName = providerType.exprName.text;
|
|
2418
|
+
const current = variants.get(providerName) ?? new Set();
|
|
2419
|
+
for (const variant of providerVariants)
|
|
2420
|
+
current.add(variant);
|
|
2421
|
+
variants.set(providerName, current);
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2424
|
+
function readProviderVariantTypeValues(typeNode) {
|
|
2425
|
+
if (ts.isParenthesizedTypeNode(typeNode))
|
|
2426
|
+
return readProviderVariantTypeValues(typeNode.type);
|
|
2427
|
+
if (ts.isUnionTypeNode(typeNode))
|
|
2428
|
+
return typeNode.types.flatMap(readProviderVariantTypeValues);
|
|
2429
|
+
if (ts.isLiteralTypeNode(typeNode) && ts.isStringLiteral(typeNode.literal))
|
|
2430
|
+
return [typeNode.literal.text];
|
|
2431
|
+
return [];
|
|
2432
|
+
}
|
|
2433
|
+
function readProviderSelections(caseValue) {
|
|
2434
|
+
const providersProperty = caseValue.properties.find((property) => ts.isPropertyAssignment(property) && getStaticPropertyName(property.name) === "providers");
|
|
2435
|
+
if (!providersProperty)
|
|
2436
|
+
return undefined;
|
|
2437
|
+
const providersValue = unwrapExpression(providersProperty.initializer);
|
|
359
2438
|
if (!ts.isArrayLiteralExpression(providersValue))
|
|
360
2439
|
return undefined;
|
|
361
2440
|
const providers = [];
|
|
@@ -394,4 +2473,7 @@ function hasStaticProperty(objectLiteral, propertyName) {
|
|
|
394
2473
|
function hasModifier(node, kind) {
|
|
395
2474
|
return Boolean(ts.canHaveModifiers(node) && ts.getModifiers(node)?.some((modifier) => modifier.kind === kind));
|
|
396
2475
|
}
|
|
2476
|
+
function normalizeProjectPath(filePath) {
|
|
2477
|
+
return filePath.split(sep).join("/");
|
|
2478
|
+
}
|
|
397
2479
|
//# sourceMappingURL=analyzer.js.map
|