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