@brandlab-digital/conformance 1.0.0-rc.8 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.d.ts +0 -3
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +35 -93
- package/dist/cli.js.map +1 -1
- package/dist/current-release.d.ts +3 -0
- package/dist/current-release.d.ts.map +1 -0
- package/dist/current-release.js +54 -0
- package/dist/current-release.js.map +1 -0
- package/dist/project.d.ts +19 -24
- package/dist/project.d.ts.map +1 -1
- package/dist/project.js +444 -1070
- package/dist/project.js.map +1 -1
- package/dist/release-manifest.d.ts +29 -82
- package/dist/release-manifest.d.ts.map +1 -1
- package/dist/release-manifest.js +141 -348
- package/dist/release-manifest.js.map +1 -1
- package/dist/rendered-content.d.ts +36 -0
- package/dist/rendered-content.d.ts.map +1 -0
- package/dist/rendered-content.js +146 -0
- package/dist/rendered-content.js.map +1 -0
- package/dist/verification-policy.d.ts +162 -0
- package/dist/verification-policy.d.ts.map +1 -0
- package/dist/verification-policy.js +309 -0
- package/dist/verification-policy.js.map +1 -0
- package/package.json +6 -22
- package/dist/legacy-architecture-contracts.d.ts +0 -31
- package/dist/legacy-architecture-contracts.d.ts.map +0 -1
- package/dist/legacy-architecture-contracts.js +0 -49
- package/dist/legacy-architecture-contracts.js.map +0 -1
- package/dist/locations.d.ts +0 -63
- package/dist/locations.d.ts.map +0 -1
- package/dist/locations.js +0 -105
- package/dist/locations.js.map +0 -1
- package/dist/project-config-v2-contract.d.ts +0 -32
- package/dist/project-config-v2-contract.d.ts.map +0 -1
- package/dist/project-config-v2-contract.js +0 -926
- package/dist/project-config-v2-contract.js.map +0 -1
- package/dist/project-config-v3.d.ts +0 -43
- package/dist/project-config-v3.d.ts.map +0 -1
- package/dist/project-config-v3.js +0 -2876
- package/dist/project-config-v3.js.map +0 -1
- package/dist/project-config.d.ts +0 -3
- package/dist/project-config.d.ts.map +0 -1
- package/dist/project-config.js +0 -3
- package/dist/project-config.js.map +0 -1
- package/dist/release-catalog.d.ts +0 -4
- package/dist/release-catalog.d.ts.map +0 -1
- package/dist/release-catalog.js +0 -714
- package/dist/release-catalog.js.map +0 -1
- package/dist/release-contract-compatibility.d.ts +0 -97
- package/dist/release-contract-compatibility.d.ts.map +0 -1
- package/dist/release-contract-compatibility.js +0 -112
- package/dist/release-contract-compatibility.js.map +0 -1
- package/dist/schema-hygiene.d.ts +0 -26
- package/dist/schema-hygiene.d.ts.map +0 -1
- package/dist/schema-hygiene.js +0 -28
- package/dist/schema-hygiene.js.map +0 -1
- package/dist/source-safety-contracts.d.ts +0 -20
- package/dist/source-safety-contracts.d.ts.map +0 -1
- package/dist/source-safety-contracts.js +0 -60
- package/dist/source-safety-contracts.js.map +0 -1
- package/dist/vitest.d.ts +0 -110
- package/dist/vitest.d.ts.map +0 -1
- package/dist/vitest.js +0 -2678
- package/dist/vitest.js.map +0 -1
|
@@ -1,2876 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Project Config v3 inspector for Brandlab 1.0.0-rc.2 and later releases that
|
|
3
|
-
* explicitly declare project-config-v3.
|
|
4
|
-
*/
|
|
5
|
-
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
6
|
-
import { createHash } from "node:crypto";
|
|
7
|
-
import { extname, join, relative } from "node:path";
|
|
8
|
-
import * as ts from "typescript";
|
|
9
|
-
export const BRANDLAB_CONFIG_V3_RULESET_ID = "project-config-v3@1.0.0-rc.2";
|
|
10
|
-
export const BRANDLAB_CONFIG_V3_PROFILES = [
|
|
11
|
-
"next-local-service",
|
|
12
|
-
"next-root-level",
|
|
13
|
-
"next-static-legacy",
|
|
14
|
-
"legacy-php",
|
|
15
|
-
];
|
|
16
|
-
const ROUTE_FAMILIES = new Set([
|
|
17
|
-
"services",
|
|
18
|
-
"locations",
|
|
19
|
-
"service-locations",
|
|
20
|
-
"blog",
|
|
21
|
-
"projects",
|
|
22
|
-
"legal",
|
|
23
|
-
]);
|
|
24
|
-
const CANONICAL_ENTITY_ID_PATTERN = /^[a-z0-9]+(?:[&-][a-z0-9]+)*$/;
|
|
25
|
-
const CANONICAL_ENTITY_ID_MAX_LENGTH = 100;
|
|
26
|
-
const BLOG_ADAPTERS = new Set(["none", "mdx", "structured"]);
|
|
27
|
-
const ATTACHMENT_MODES = new Set(["none", "images-optional", "images-required"]);
|
|
28
|
-
const LOCATION_STRATEGIES = new Set([
|
|
29
|
-
"none",
|
|
30
|
-
"approved-service-area",
|
|
31
|
-
"physical-location",
|
|
32
|
-
"multi-location",
|
|
33
|
-
]);
|
|
34
|
-
const REVIEWS_PROVIDERS = new Set(["google-places", "project-owned"]);
|
|
35
|
-
const ROOT_CONFIG_KEYS = new Set([
|
|
36
|
-
"projectId",
|
|
37
|
-
"profile",
|
|
38
|
-
"routes",
|
|
39
|
-
"capabilities",
|
|
40
|
-
"sitemap",
|
|
41
|
-
"searchCertification",
|
|
42
|
-
"exceptions",
|
|
43
|
-
]);
|
|
44
|
-
const FORBIDDEN_GLOBAL_KEYS = new Set([
|
|
45
|
-
"installedRelease",
|
|
46
|
-
"packageScope",
|
|
47
|
-
"packageVersion",
|
|
48
|
-
"nextVersion",
|
|
49
|
-
"reactVersion",
|
|
50
|
-
"reactDomVersion",
|
|
51
|
-
"nodeVersion",
|
|
52
|
-
"typescriptVersion",
|
|
53
|
-
"packageManager",
|
|
54
|
-
"leadEvents",
|
|
55
|
-
"attachmentLimits",
|
|
56
|
-
"previewIndexing",
|
|
57
|
-
]);
|
|
58
|
-
const SOURCE_EXTENSIONS = new Set([".js", ".jsx", ".mjs", ".ts", ".tsx"]);
|
|
59
|
-
const SKIPPED_DIRECTORIES = new Set([
|
|
60
|
-
".git",
|
|
61
|
-
".next",
|
|
62
|
-
".turbo",
|
|
63
|
-
"coverage",
|
|
64
|
-
"dist",
|
|
65
|
-
"generated",
|
|
66
|
-
"node_modules",
|
|
67
|
-
]);
|
|
68
|
-
const REQUIRED_KERNEL_IMPORTS = [
|
|
69
|
-
["@brandlab-digital/site-next/analytics", "analytics"],
|
|
70
|
-
["@brandlab-digital/site-next/ui", "accessibility"],
|
|
71
|
-
];
|
|
72
|
-
const STRICT_KERNEL_USAGES = [
|
|
73
|
-
{
|
|
74
|
-
moduleName: "@brandlab-digital/site-core/platform-contract",
|
|
75
|
-
surface: "platform-contract",
|
|
76
|
-
exports: ["defineBrandlabPlatformContract"],
|
|
77
|
-
},
|
|
78
|
-
{
|
|
79
|
-
moduleName: "@brandlab-digital/site-core/business",
|
|
80
|
-
surface: "business",
|
|
81
|
-
exports: ["defineBrandlabBusiness"],
|
|
82
|
-
},
|
|
83
|
-
{
|
|
84
|
-
moduleName: "@brandlab-digital/site-core/routes",
|
|
85
|
-
surface: "routes",
|
|
86
|
-
exports: ["defineRouteCatalog"],
|
|
87
|
-
},
|
|
88
|
-
{
|
|
89
|
-
moduleName: "@brandlab-digital/site-next/metadata",
|
|
90
|
-
surface: "metadata",
|
|
91
|
-
exports: ["createRouteMetadataBuilder"],
|
|
92
|
-
frameworkOutput: {
|
|
93
|
-
owner: "metadata",
|
|
94
|
-
ruleId: "brandlab-config-v3-shared-metadata-framework-output",
|
|
95
|
-
},
|
|
96
|
-
},
|
|
97
|
-
{
|
|
98
|
-
moduleName: "@brandlab-digital/site-core/schema",
|
|
99
|
-
surface: "schema-graph",
|
|
100
|
-
exports: ["buildSchemaGraph"],
|
|
101
|
-
frameworkOutput: {
|
|
102
|
-
owner: "render",
|
|
103
|
-
ruleId: "brandlab-config-v3-shared-schema-graph-framework-output",
|
|
104
|
-
},
|
|
105
|
-
},
|
|
106
|
-
{
|
|
107
|
-
moduleName: "@brandlab-digital/site-next/schema",
|
|
108
|
-
surface: "schema",
|
|
109
|
-
exports: ["SchemaScript"],
|
|
110
|
-
frameworkOutput: {
|
|
111
|
-
owner: "render",
|
|
112
|
-
ruleId: "brandlab-config-v3-shared-schema-framework-output",
|
|
113
|
-
},
|
|
114
|
-
},
|
|
115
|
-
{
|
|
116
|
-
moduleName: "@brandlab-digital/site-next/breadcrumbs",
|
|
117
|
-
surface: "breadcrumbs",
|
|
118
|
-
exports: ["createRouteBreadcrumbOutput"],
|
|
119
|
-
frameworkOutput: {
|
|
120
|
-
owner: "render",
|
|
121
|
-
ruleId: "brandlab-config-v3-shared-breadcrumbs-framework-output",
|
|
122
|
-
},
|
|
123
|
-
},
|
|
124
|
-
{
|
|
125
|
-
moduleName: "@brandlab-digital/site-core/sitemaps",
|
|
126
|
-
surface: "sitemap-catalog",
|
|
127
|
-
exports: ["buildSitemapEntries"],
|
|
128
|
-
frameworkOutput: {
|
|
129
|
-
owner: "sitemap",
|
|
130
|
-
ruleId: "brandlab-config-v3-shared-sitemap-catalog-framework-output",
|
|
131
|
-
},
|
|
132
|
-
},
|
|
133
|
-
{
|
|
134
|
-
moduleName: "@brandlab-digital/site-next/sitemaps",
|
|
135
|
-
surface: "sitemap",
|
|
136
|
-
exports: ["sitemapXmlResponse", "sitemapIndexXmlResponse"],
|
|
137
|
-
frameworkOutput: {
|
|
138
|
-
owner: "sitemap",
|
|
139
|
-
ruleId: "brandlab-config-v3-shared-sitemap-framework-output",
|
|
140
|
-
},
|
|
141
|
-
},
|
|
142
|
-
{
|
|
143
|
-
moduleName: "@brandlab-digital/site-next/robots",
|
|
144
|
-
surface: "robots",
|
|
145
|
-
exports: ["buildRobotsMetadata"],
|
|
146
|
-
frameworkOutput: {
|
|
147
|
-
owner: "robots",
|
|
148
|
-
ruleId: "brandlab-config-v3-shared-robots-framework-output",
|
|
149
|
-
},
|
|
150
|
-
},
|
|
151
|
-
];
|
|
152
|
-
const FORBIDDEN_CANDIDATE_EXPORTS = [
|
|
153
|
-
{
|
|
154
|
-
moduleName: "@brandlab-digital/site-next/metadata",
|
|
155
|
-
surface: "metadata",
|
|
156
|
-
exports: [
|
|
157
|
-
"buildNextMetadata",
|
|
158
|
-
"buildBrandlabNextMetadata",
|
|
159
|
-
"createNextMetadataBuilder",
|
|
160
|
-
"createBrandlabNextMetadataBuilder",
|
|
161
|
-
],
|
|
162
|
-
},
|
|
163
|
-
{
|
|
164
|
-
moduleName: "@brandlab-digital/site-next/breadcrumbs",
|
|
165
|
-
surface: "breadcrumbs",
|
|
166
|
-
exports: ["BrandlabPathBreadcrumbs", "createBreadcrumbItems"],
|
|
167
|
-
},
|
|
168
|
-
{
|
|
169
|
-
moduleName: "@brandlab-digital/site-next/blog",
|
|
170
|
-
surface: "blog",
|
|
171
|
-
exports: [
|
|
172
|
-
"createBrandlabBlogAdapter",
|
|
173
|
-
"createMdxBlogAdapter",
|
|
174
|
-
"createStructuredBlogAdapter",
|
|
175
|
-
],
|
|
176
|
-
},
|
|
177
|
-
{
|
|
178
|
-
moduleName: "@brandlab-digital/site-core/schema",
|
|
179
|
-
surface: "schema",
|
|
180
|
-
exports: ["createSchema", "createSchemaGraph", "buildFaqPageSchema"],
|
|
181
|
-
},
|
|
182
|
-
];
|
|
183
|
-
const PROTECTED_LOCAL_PATTERNS = [
|
|
184
|
-
{
|
|
185
|
-
ruleId: "brandlab-config-v3-local-contact-transport",
|
|
186
|
-
surface: "contact",
|
|
187
|
-
pattern: /\b(?:new\s+Resend|resend\.emails\.send)\b/,
|
|
188
|
-
expected: "Contact delivery is owned by @brandlab-digital/site-next/contact/server.",
|
|
189
|
-
},
|
|
190
|
-
{
|
|
191
|
-
ruleId: "brandlab-config-v3-local-contact-email",
|
|
192
|
-
surface: "contact",
|
|
193
|
-
pattern: /\b(?:class|function|const|let|var)\s+(?:ContactFormEmail|renderContactEmail)\b/,
|
|
194
|
-
expected: "Contact email rendering is owned by @brandlab-digital/site-next/contact/email.",
|
|
195
|
-
},
|
|
196
|
-
{
|
|
197
|
-
ruleId: "brandlab-config-v3-local-lead-event",
|
|
198
|
-
surface: "analytics",
|
|
199
|
-
pattern: /\bgtag\s*\([^)]*["'](?:contact_form|phone_call)["']/,
|
|
200
|
-
expected: "Canonical lead events are owned by @brandlab-digital/site-next/analytics.",
|
|
201
|
-
},
|
|
202
|
-
{
|
|
203
|
-
ruleId: "brandlab-config-v3-url-derived-breadcrumbs",
|
|
204
|
-
surface: "breadcrumbs",
|
|
205
|
-
severity: "warning",
|
|
206
|
-
pattern: /\b(?:BrandlabPathBreadcrumbs|createBreadcrumbItems)\b/,
|
|
207
|
-
expected: "Breadcrumb labels and hierarchy derive from the semantic route catalog.",
|
|
208
|
-
},
|
|
209
|
-
];
|
|
210
|
-
export function inspectBrandlabConfigV3(root) {
|
|
211
|
-
const issues = [];
|
|
212
|
-
const configPath = join(root, "brandlab.config.ts");
|
|
213
|
-
const usage = collectProjectImports(root, issues);
|
|
214
|
-
const { imports, executableImports, outputOwnedImports, executableExports, outputOwnedExports, frameworkOwnedExports, } = usage;
|
|
215
|
-
if (!existsSync(configPath)) {
|
|
216
|
-
issues.push(configIssue({
|
|
217
|
-
ruleId: "brandlab-config-v3-missing",
|
|
218
|
-
expected: "A constrained brandlab.config.ts using defineBrandlabProject().",
|
|
219
|
-
actual: "brandlab.config.ts is missing.",
|
|
220
|
-
remediation: "Add the variation-only typed project configuration.",
|
|
221
|
-
path: "brandlab.config.ts",
|
|
222
|
-
}));
|
|
223
|
-
return { ...usage, config: null, issues };
|
|
224
|
-
}
|
|
225
|
-
const sourceText = readFileSync(configPath, "utf8");
|
|
226
|
-
const source = ts.createSourceFile(configPath, sourceText, ts.ScriptTarget.ES2022, true, ts.ScriptKind.TS);
|
|
227
|
-
for (const diagnostic of getParseDiagnostics(source)) {
|
|
228
|
-
const location = diagnostic.start === undefined
|
|
229
|
-
? undefined
|
|
230
|
-
: source.getLineAndCharacterOfPosition(diagnostic.start);
|
|
231
|
-
issues.push(configIssue({
|
|
232
|
-
ruleId: "brandlab-config-v3-syntax",
|
|
233
|
-
expected: "Syntactically valid TypeScript configuration.",
|
|
234
|
-
actual: ts.flattenDiagnosticMessageText(diagnostic.messageText, " "),
|
|
235
|
-
remediation: "Correct the configuration syntax without executing project code.",
|
|
236
|
-
path: "brandlab.config.ts",
|
|
237
|
-
line: location ? location.line + 1 : undefined,
|
|
238
|
-
}));
|
|
239
|
-
}
|
|
240
|
-
const factoryNames = collectFactoryImportNames(source);
|
|
241
|
-
if (factoryNames.size === 0) {
|
|
242
|
-
issues.push(configIssue({
|
|
243
|
-
ruleId: "brandlab-config-v3-factory-import",
|
|
244
|
-
expected: "defineBrandlabProject imported from @brandlab-digital/site-core/project.",
|
|
245
|
-
actual: "The supported project factory import was not found.",
|
|
246
|
-
remediation: "Import and call the shared project configuration factory.",
|
|
247
|
-
path: "brandlab.config.ts",
|
|
248
|
-
}));
|
|
249
|
-
}
|
|
250
|
-
const assignment = source.statements.find(ts.isExportAssignment);
|
|
251
|
-
const expression = assignment ? unwrapExpression(assignment.expression) : null;
|
|
252
|
-
if (!expression ||
|
|
253
|
-
!ts.isCallExpression(expression) ||
|
|
254
|
-
!ts.isIdentifier(expression.expression) ||
|
|
255
|
-
!factoryNames.has(expression.expression.text) ||
|
|
256
|
-
expression.arguments.length !== 1) {
|
|
257
|
-
issues.push(configIssue({
|
|
258
|
-
ruleId: "brandlab-config-v3-export",
|
|
259
|
-
expected: "One default export calling defineBrandlabProject() with one object literal.",
|
|
260
|
-
actual: "The default export does not match the constrained configuration contract.",
|
|
261
|
-
remediation: "Export the variation-only object directly through defineBrandlabProject().",
|
|
262
|
-
path: "brandlab.config.ts",
|
|
263
|
-
}));
|
|
264
|
-
return { ...usage, config: null, issues };
|
|
265
|
-
}
|
|
266
|
-
let value;
|
|
267
|
-
try {
|
|
268
|
-
value = literalValue(expression.arguments[0], source);
|
|
269
|
-
}
|
|
270
|
-
catch (error) {
|
|
271
|
-
const node = error instanceof ConfigLiteralError ? error.node : expression.arguments[0];
|
|
272
|
-
const location = source.getLineAndCharacterOfPosition(node.getStart(source));
|
|
273
|
-
issues.push(configIssue({
|
|
274
|
-
ruleId: "brandlab-config-v3-literal-only",
|
|
275
|
-
expected: "A static object containing only literal values, arrays and nested objects.",
|
|
276
|
-
actual: error instanceof Error ? error.message : "The configuration is not inspectable.",
|
|
277
|
-
remediation: "Remove spreads, computed values, function calls and executable expressions.",
|
|
278
|
-
path: "brandlab.config.ts",
|
|
279
|
-
line: location.line + 1,
|
|
280
|
-
}));
|
|
281
|
-
return { ...usage, config: null, issues };
|
|
282
|
-
}
|
|
283
|
-
const config = validateConfigValue(value, issues);
|
|
284
|
-
if (config) {
|
|
285
|
-
addImportContractIssues(root, config, usage, issues);
|
|
286
|
-
}
|
|
287
|
-
addProtectedImplementationIssues(root, issues);
|
|
288
|
-
return {
|
|
289
|
-
config,
|
|
290
|
-
imports,
|
|
291
|
-
executableImports,
|
|
292
|
-
outputOwnedImports,
|
|
293
|
-
executableExports,
|
|
294
|
-
outputOwnedExports,
|
|
295
|
-
frameworkOwnedExports,
|
|
296
|
-
issues,
|
|
297
|
-
};
|
|
298
|
-
}
|
|
299
|
-
function collectFactoryImportNames(source) {
|
|
300
|
-
const names = new Set();
|
|
301
|
-
for (const statement of source.statements) {
|
|
302
|
-
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)) {
|
|
303
|
-
continue;
|
|
304
|
-
}
|
|
305
|
-
if (statement.moduleSpecifier.text !== "@brandlab-digital/site-core/project")
|
|
306
|
-
continue;
|
|
307
|
-
const bindings = statement.importClause?.namedBindings;
|
|
308
|
-
if (!bindings || !ts.isNamedImports(bindings))
|
|
309
|
-
continue;
|
|
310
|
-
for (const element of bindings.elements) {
|
|
311
|
-
if ((element.propertyName ?? element.name).text === "defineBrandlabProject") {
|
|
312
|
-
names.add(element.name.text);
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
return names;
|
|
317
|
-
}
|
|
318
|
-
function unwrapExpression(expression) {
|
|
319
|
-
let current = expression;
|
|
320
|
-
while (ts.isParenthesizedExpression(current) ||
|
|
321
|
-
ts.isAsExpression(current) ||
|
|
322
|
-
ts.isSatisfiesExpression(current) ||
|
|
323
|
-
ts.isTypeAssertionExpression(current)) {
|
|
324
|
-
current = current.expression;
|
|
325
|
-
}
|
|
326
|
-
return current;
|
|
327
|
-
}
|
|
328
|
-
class ConfigLiteralError extends Error {
|
|
329
|
-
node;
|
|
330
|
-
constructor(message, node) {
|
|
331
|
-
super(message);
|
|
332
|
-
this.node = node;
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
function literalValue(node, source) {
|
|
336
|
-
const value = unwrapExpression(node);
|
|
337
|
-
if (ts.isStringLiteral(value) || ts.isNoSubstitutionTemplateLiteral(value))
|
|
338
|
-
return value.text;
|
|
339
|
-
if (ts.isNumericLiteral(value))
|
|
340
|
-
return Number(value.text);
|
|
341
|
-
if (value.kind === ts.SyntaxKind.TrueKeyword)
|
|
342
|
-
return true;
|
|
343
|
-
if (value.kind === ts.SyntaxKind.FalseKeyword)
|
|
344
|
-
return false;
|
|
345
|
-
if (value.kind === ts.SyntaxKind.NullKeyword)
|
|
346
|
-
return null;
|
|
347
|
-
if (ts.isPrefixUnaryExpression(value) && value.operator === ts.SyntaxKind.MinusToken) {
|
|
348
|
-
const operand = literalValue(value.operand, source);
|
|
349
|
-
if (typeof operand === "number")
|
|
350
|
-
return -operand;
|
|
351
|
-
}
|
|
352
|
-
if (ts.isArrayLiteralExpression(value)) {
|
|
353
|
-
return value.elements.map((element) => {
|
|
354
|
-
if (ts.isSpreadElement(element)) {
|
|
355
|
-
throw new ConfigLiteralError("Array spreads are not allowed.", element);
|
|
356
|
-
}
|
|
357
|
-
return literalValue(element, source);
|
|
358
|
-
});
|
|
359
|
-
}
|
|
360
|
-
if (ts.isObjectLiteralExpression(value)) {
|
|
361
|
-
const output = {};
|
|
362
|
-
for (const property of value.properties) {
|
|
363
|
-
if (!ts.isPropertyAssignment(property)) {
|
|
364
|
-
throw new ConfigLiteralError("Only explicit property assignments are allowed.", property);
|
|
365
|
-
}
|
|
366
|
-
const name = propertyName(property.name);
|
|
367
|
-
if (name === null) {
|
|
368
|
-
throw new ConfigLiteralError("Computed property names are not allowed.", property.name);
|
|
369
|
-
}
|
|
370
|
-
if (Object.hasOwn(output, name)) {
|
|
371
|
-
throw new ConfigLiteralError(`Duplicate property "${name}" is not allowed.`, property.name);
|
|
372
|
-
}
|
|
373
|
-
output[name] = literalValue(property.initializer, source);
|
|
374
|
-
}
|
|
375
|
-
return output;
|
|
376
|
-
}
|
|
377
|
-
throw new ConfigLiteralError(`Unsupported executable expression: ${value.getText(source).slice(0, 80)}.`, value);
|
|
378
|
-
}
|
|
379
|
-
function propertyName(name) {
|
|
380
|
-
if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) {
|
|
381
|
-
return name.text;
|
|
382
|
-
}
|
|
383
|
-
return null;
|
|
384
|
-
}
|
|
385
|
-
function validateConfigValue(value, issues) {
|
|
386
|
-
if (!isRecord(value)) {
|
|
387
|
-
issues.push(configIssue({
|
|
388
|
-
ruleId: "brandlab-config-v3-object",
|
|
389
|
-
expected: "A project variation object.",
|
|
390
|
-
actual: "The configuration value is not an object.",
|
|
391
|
-
remediation: "Pass one constrained object literal to defineBrandlabProject().",
|
|
392
|
-
path: "brandlab.config.ts",
|
|
393
|
-
}));
|
|
394
|
-
return null;
|
|
395
|
-
}
|
|
396
|
-
const validationStart = issues.length;
|
|
397
|
-
for (const key of FORBIDDEN_GLOBAL_KEYS) {
|
|
398
|
-
if (Object.hasOwn(value, key)) {
|
|
399
|
-
issues.push(configIssue({
|
|
400
|
-
ruleId: "brandlab-config-v3-global-fact",
|
|
401
|
-
expected: "Project variation only; runtime and release facts come from lockfiles and manifests.",
|
|
402
|
-
actual: `Global fact "${key}" is repeated in project configuration.`,
|
|
403
|
-
remediation: `Remove ${key} from brandlab.config.ts.`,
|
|
404
|
-
path: "brandlab.config.ts",
|
|
405
|
-
}));
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
addUnknownKeyIssues(value, ROOT_CONFIG_KEYS, FORBIDDEN_GLOBAL_KEYS, "config", issues);
|
|
409
|
-
const projectId = typeof value.projectId === "string" ? value.projectId : "";
|
|
410
|
-
const profile = typeof value.profile === "string" ? value.profile : "";
|
|
411
|
-
const routes = isRecord(value.routes) ? value.routes : null;
|
|
412
|
-
const rawFamilies = routes && Array.isArray(routes.families) ? routes.families : [];
|
|
413
|
-
const families = rawFamilies.filter((entry) => typeof entry === "string");
|
|
414
|
-
const searchCertification = isRecord(value.searchCertification)
|
|
415
|
-
? value.searchCertification
|
|
416
|
-
: null;
|
|
417
|
-
const representativeRoutes = searchCertification && isRecord(searchCertification.representativeRoutes)
|
|
418
|
-
? searchCertification.representativeRoutes
|
|
419
|
-
: null;
|
|
420
|
-
if (projectId.length > CANONICAL_ENTITY_ID_MAX_LENGTH ||
|
|
421
|
-
!CANONICAL_ENTITY_ID_PATTERN.test(projectId)) {
|
|
422
|
-
issues.push(configIssue({
|
|
423
|
-
ruleId: "brandlab-config-v3-project-id",
|
|
424
|
-
expected: `A 1–${CANONICAL_ENTITY_ID_MAX_LENGTH} character canonical lowercase projectId using letters, numbers, ampersands or hyphens.`,
|
|
425
|
-
actual: projectId ? `Invalid projectId "${projectId}".` : "projectId is missing.",
|
|
426
|
-
remediation: "Use the canonical entity identifier.",
|
|
427
|
-
path: "brandlab.config.ts",
|
|
428
|
-
}));
|
|
429
|
-
}
|
|
430
|
-
if (!BRANDLAB_CONFIG_V3_PROFILES.includes(profile)) {
|
|
431
|
-
issues.push(configIssue({
|
|
432
|
-
ruleId: "brandlab-config-v3-profile",
|
|
433
|
-
expected: `One supported profile: ${BRANDLAB_CONFIG_V3_PROFILES.join(", ")}.`,
|
|
434
|
-
actual: profile ? `Unsupported profile "${profile}".` : "profile is missing.",
|
|
435
|
-
remediation: "Select the structural profile that matches the project.",
|
|
436
|
-
path: "brandlab.config.ts",
|
|
437
|
-
}));
|
|
438
|
-
}
|
|
439
|
-
if (!routes || !Array.isArray(routes.families) || families.length !== rawFamilies.length) {
|
|
440
|
-
issues.push(configIssue({
|
|
441
|
-
ruleId: "brandlab-config-v3-routes",
|
|
442
|
-
expected: "routes.families as a literal string array.",
|
|
443
|
-
actual: "Route families are missing or invalid.",
|
|
444
|
-
remediation: "Declare only the route families the project owns.",
|
|
445
|
-
path: "brandlab.config.ts",
|
|
446
|
-
}));
|
|
447
|
-
}
|
|
448
|
-
else {
|
|
449
|
-
addUnknownKeyIssues(routes, new Set(["families", "aliases"]), new Set(), "routes", issues);
|
|
450
|
-
const unsupportedFamily = families.find((family) => !ROUTE_FAMILIES.has(family));
|
|
451
|
-
if (unsupportedFamily) {
|
|
452
|
-
issues.push(configIssue({
|
|
453
|
-
ruleId: "brandlab-config-v3-route-family",
|
|
454
|
-
expected: `Supported route families only: ${[...ROUTE_FAMILIES].join(", ")}.`,
|
|
455
|
-
actual: `Unsupported route family "${unsupportedFamily}".`,
|
|
456
|
-
remediation: "Use the closest supported route family or propose a versioned contract extension.",
|
|
457
|
-
path: "brandlab.config.ts",
|
|
458
|
-
}));
|
|
459
|
-
}
|
|
460
|
-
addDuplicateIssue(families, "brandlab-config-v3-route-family-duplicate", "routes.families", issues);
|
|
461
|
-
validateAliases(routes.aliases, issues);
|
|
462
|
-
}
|
|
463
|
-
if (!searchCertification) {
|
|
464
|
-
issues.push(configIssue({
|
|
465
|
-
ruleId: "brandlab-config-v3-search-certification",
|
|
466
|
-
expected: "A searchCertification object containing representativeRoutes.",
|
|
467
|
-
actual: "searchCertification is missing or invalid.",
|
|
468
|
-
remediation: "Declare the inspectable representative route selection.",
|
|
469
|
-
path: "brandlab.config.ts",
|
|
470
|
-
}));
|
|
471
|
-
}
|
|
472
|
-
else {
|
|
473
|
-
addUnknownKeyIssues(searchCertification, new Set(["representativeRoutes"]), new Set(), "searchCertification", issues);
|
|
474
|
-
}
|
|
475
|
-
if (!representativeRoutes || typeof representativeRoutes.home !== "string") {
|
|
476
|
-
issues.push(configIssue({
|
|
477
|
-
ruleId: "brandlab-config-v3-search-routes",
|
|
478
|
-
expected: "A literal home route under searchCertification.representativeRoutes.",
|
|
479
|
-
actual: "Representative Search Certification routes are incomplete.",
|
|
480
|
-
remediation: "Declare the smallest representative route set for this project.",
|
|
481
|
-
path: "brandlab.config.ts",
|
|
482
|
-
}));
|
|
483
|
-
}
|
|
484
|
-
else {
|
|
485
|
-
addUnknownKeyIssues(representativeRoutes, new Set(["home", "contact", "service", "location", "blog"]), new Set(), "searchCertification.representativeRoutes", issues);
|
|
486
|
-
for (const [name, path] of Object.entries(representativeRoutes)) {
|
|
487
|
-
if (typeof path !== "string" || !isRootRelativePath(path)) {
|
|
488
|
-
issues.push(configIssue({
|
|
489
|
-
ruleId: "brandlab-config-v3-search-route-path",
|
|
490
|
-
expected: `${name} as a strict root-relative route without encoded separators, dot segments, query or hash.`,
|
|
491
|
-
actual: `${name} is ${JSON.stringify(path)}.`,
|
|
492
|
-
remediation: "Use a canonical route path from the typed route catalog.",
|
|
493
|
-
path: "brandlab.config.ts",
|
|
494
|
-
}));
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
if (representativeRoutes.home !== "/") {
|
|
498
|
-
issues.push(configIssue({
|
|
499
|
-
ruleId: "brandlab-config-v3-search-home",
|
|
500
|
-
expected: 'searchCertification.representativeRoutes.home="/".',
|
|
501
|
-
actual: `home is ${JSON.stringify(representativeRoutes.home)}.`,
|
|
502
|
-
remediation: "Use the canonical root route for the representative homepage.",
|
|
503
|
-
path: "brandlab.config.ts",
|
|
504
|
-
}));
|
|
505
|
-
}
|
|
506
|
-
const familyRouteRequirements = [
|
|
507
|
-
{ families: ["services"], route: "service" },
|
|
508
|
-
{ families: ["locations", "service-locations"], route: "location" },
|
|
509
|
-
{ families: ["blog"], route: "blog" },
|
|
510
|
-
];
|
|
511
|
-
for (const requirement of familyRouteRequirements) {
|
|
512
|
-
const owningFamily = requirement.families.find((family) => families.includes(family));
|
|
513
|
-
if (owningFamily && typeof representativeRoutes[requirement.route] !== "string") {
|
|
514
|
-
issues.push(configIssue({
|
|
515
|
-
ruleId: "brandlab-config-v3-search-family-route",
|
|
516
|
-
surface: "search-certification",
|
|
517
|
-
expected: `A ${requirement.route} representative route when routes.families includes ${owningFamily}.`,
|
|
518
|
-
actual: `${owningFamily} is enabled without searchCertification.representativeRoutes.${requirement.route}.`,
|
|
519
|
-
remediation: "Bind every enabled route family to a canonical representative Search Certification route.",
|
|
520
|
-
path: "brandlab.config.ts",
|
|
521
|
-
}));
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
const capabilities = value.capabilities === undefined
|
|
526
|
-
? {}
|
|
527
|
-
: isRecord(value.capabilities)
|
|
528
|
-
? value.capabilities
|
|
529
|
-
: null;
|
|
530
|
-
if (capabilities === null) {
|
|
531
|
-
issues.push(configIssue({
|
|
532
|
-
ruleId: "brandlab-config-v3-capabilities",
|
|
533
|
-
expected: "capabilities as an optional literal object.",
|
|
534
|
-
actual: "capabilities is not an object.",
|
|
535
|
-
remediation: "Declare only supported project capability variation.",
|
|
536
|
-
path: "brandlab.config.ts",
|
|
537
|
-
}));
|
|
538
|
-
}
|
|
539
|
-
else {
|
|
540
|
-
validateCapabilities(capabilities, issues);
|
|
541
|
-
}
|
|
542
|
-
validateSitemap(value.sitemap, issues);
|
|
543
|
-
validateExceptions(value.exceptions, issues);
|
|
544
|
-
if (issues.slice(validationStart).some((issue) => issue.severity === "blocker"))
|
|
545
|
-
return null;
|
|
546
|
-
const safeCapabilities = capabilities ?? {};
|
|
547
|
-
const blog = isRecord(safeCapabilities.blog) ? safeCapabilities.blog : null;
|
|
548
|
-
const contact = isRecord(safeCapabilities.contact) ? safeCapabilities.contact : null;
|
|
549
|
-
const locations = isRecord(safeCapabilities.locations) ? safeCapabilities.locations : null;
|
|
550
|
-
const reviews = isRecord(safeCapabilities.reviews) ? safeCapabilities.reviews : null;
|
|
551
|
-
const reviewsProvider = typeof reviews?.provider === "string" ? reviews.provider : null;
|
|
552
|
-
if (reviewsProvider === "google-places" && !families.includes("legal")) {
|
|
553
|
-
issues.push(configIssue({
|
|
554
|
-
ruleId: "brandlab-config-v3-google-places-legal-family",
|
|
555
|
-
expected: "The legal route family for public Terms and Privacy surfaces.",
|
|
556
|
-
actual: "Google Places reviews are enabled without routes.families including legal.",
|
|
557
|
-
remediation: "Add typed Terms and Privacy routes before enabling Google Places reviews.",
|
|
558
|
-
path: "brandlab.config.ts",
|
|
559
|
-
}));
|
|
560
|
-
return null;
|
|
561
|
-
}
|
|
562
|
-
return {
|
|
563
|
-
projectId,
|
|
564
|
-
profile: profile,
|
|
565
|
-
routes: { families },
|
|
566
|
-
searchCertification: {
|
|
567
|
-
representativeRoutes: Object.fromEntries(Object.entries(representativeRoutes ?? {}).filter((entry) => typeof entry[1] === "string")),
|
|
568
|
-
},
|
|
569
|
-
capabilities: {
|
|
570
|
-
blogAdapter: typeof blog?.adapter === "string" ? blog.adapter : null,
|
|
571
|
-
contactEnabled: contact !== null,
|
|
572
|
-
locationStrategy: typeof locations?.strategy === "string" ? locations.strategy : null,
|
|
573
|
-
reviewsProvider,
|
|
574
|
-
},
|
|
575
|
-
};
|
|
576
|
-
}
|
|
577
|
-
function validateAliases(value, issues) {
|
|
578
|
-
if (value === undefined)
|
|
579
|
-
return;
|
|
580
|
-
if (!Array.isArray(value)) {
|
|
581
|
-
issues.push(configIssue({
|
|
582
|
-
ruleId: "brandlab-config-v3-route-aliases",
|
|
583
|
-
expected: "routes.aliases as an optional literal array.",
|
|
584
|
-
actual: "routes.aliases is not an array.",
|
|
585
|
-
remediation: "Declare each historical alias as a typed from/to record.",
|
|
586
|
-
path: "brandlab.config.ts",
|
|
587
|
-
}));
|
|
588
|
-
return;
|
|
589
|
-
}
|
|
590
|
-
const fromPaths = [];
|
|
591
|
-
const toPaths = [];
|
|
592
|
-
value.forEach((alias, index) => {
|
|
593
|
-
if (!isRecord(alias)) {
|
|
594
|
-
issues.push(configIssue({
|
|
595
|
-
ruleId: "brandlab-config-v3-route-alias",
|
|
596
|
-
expected: `routes.aliases[${index}] as an object.`,
|
|
597
|
-
actual: "The alias is not an object.",
|
|
598
|
-
remediation: "Declare from, to and optional 301 or 308 status.",
|
|
599
|
-
path: "brandlab.config.ts",
|
|
600
|
-
}));
|
|
601
|
-
return;
|
|
602
|
-
}
|
|
603
|
-
addUnknownKeyIssues(alias, new Set(["from", "to", "status"]), new Set(), `routes.aliases[${index}]`, issues);
|
|
604
|
-
const from = typeof alias.from === "string" ? alias.from : "";
|
|
605
|
-
const to = typeof alias.to === "string" ? alias.to : "";
|
|
606
|
-
if (!isRootRelativePath(from) || !isRootRelativePath(to)) {
|
|
607
|
-
issues.push(configIssue({
|
|
608
|
-
ruleId: "brandlab-config-v3-route-alias-path",
|
|
609
|
-
expected: "Strict root-relative from/to alias paths.",
|
|
610
|
-
actual: `Alias ${index} uses from=${JSON.stringify(alias.from)} and to=${JSON.stringify(alias.to)}.`,
|
|
611
|
-
remediation: "Use canonical root-relative paths without encoded separators, dot segments, query or hash.",
|
|
612
|
-
path: "brandlab.config.ts",
|
|
613
|
-
}));
|
|
614
|
-
}
|
|
615
|
-
if (alias.status !== undefined && alias.status !== 301 && alias.status !== 308) {
|
|
616
|
-
issues.push(configIssue({
|
|
617
|
-
ruleId: "brandlab-config-v3-route-alias-status",
|
|
618
|
-
expected: "Alias status 301 or 308 when present.",
|
|
619
|
-
actual: `Alias ${index} uses ${JSON.stringify(alias.status)}.`,
|
|
620
|
-
remediation: "Choose the approved permanent redirect status.",
|
|
621
|
-
path: "brandlab.config.ts",
|
|
622
|
-
}));
|
|
623
|
-
}
|
|
624
|
-
if (from && from === to) {
|
|
625
|
-
issues.push(configIssue({
|
|
626
|
-
ruleId: "brandlab-config-v3-route-alias-self",
|
|
627
|
-
expected: "An alias whose source differs from its destination.",
|
|
628
|
-
actual: `Alias ${index} redirects ${from} to itself.`,
|
|
629
|
-
remediation: "Remove the self-redirect.",
|
|
630
|
-
path: "brandlab.config.ts",
|
|
631
|
-
}));
|
|
632
|
-
}
|
|
633
|
-
if (from)
|
|
634
|
-
fromPaths.push(from);
|
|
635
|
-
if (to)
|
|
636
|
-
toPaths.push(to);
|
|
637
|
-
});
|
|
638
|
-
addDuplicateIssue(fromPaths, "brandlab-config-v3-route-alias-duplicate", "routes.aliases.from", issues);
|
|
639
|
-
const chain = fromPaths.find((from) => toPaths.includes(from));
|
|
640
|
-
if (chain) {
|
|
641
|
-
issues.push(configIssue({
|
|
642
|
-
ruleId: "brandlab-config-v3-route-alias-chain",
|
|
643
|
-
expected: "Direct aliases without redirect chains or cycles.",
|
|
644
|
-
actual: `Alias source ${chain} is also an alias destination.`,
|
|
645
|
-
remediation: "Point every historical alias directly to its final canonical route.",
|
|
646
|
-
path: "brandlab.config.ts",
|
|
647
|
-
}));
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
function validateCapabilities(capabilities, issues) {
|
|
651
|
-
addUnknownKeyIssues(capabilities, new Set(["blog", "contact", "locations", "reviews"]), new Set(), "capabilities", issues);
|
|
652
|
-
if (capabilities.blog !== undefined) {
|
|
653
|
-
if (!isRecord(capabilities.blog)) {
|
|
654
|
-
addShapeIssue("blog", issues);
|
|
655
|
-
}
|
|
656
|
-
else {
|
|
657
|
-
addUnknownKeyIssues(capabilities.blog, new Set(["adapter"]), new Set(), "capabilities.blog", issues);
|
|
658
|
-
if (typeof capabilities.blog.adapter !== "string" || !BLOG_ADAPTERS.has(capabilities.blog.adapter)) {
|
|
659
|
-
issues.push(configIssue({
|
|
660
|
-
ruleId: "brandlab-config-v3-blog-adapter",
|
|
661
|
-
expected: `One blog adapter: ${[...BLOG_ADAPTERS].join(", ")}.`,
|
|
662
|
-
actual: `blog.adapter is ${JSON.stringify(capabilities.blog.adapter)}.`,
|
|
663
|
-
remediation: "Select the adapter matching the project-owned content source.",
|
|
664
|
-
path: "brandlab.config.ts",
|
|
665
|
-
}));
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
if (capabilities.contact !== undefined) {
|
|
670
|
-
if (!isRecord(capabilities.contact)) {
|
|
671
|
-
addShapeIssue("contact", issues);
|
|
672
|
-
}
|
|
673
|
-
else {
|
|
674
|
-
addUnknownKeyIssues(capabilities.contact, new Set(["fieldProfile", "attachments"]), new Set(), "capabilities.contact", issues);
|
|
675
|
-
if (typeof capabilities.contact.fieldProfile !== "string" ||
|
|
676
|
-
!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(capabilities.contact.fieldProfile)) {
|
|
677
|
-
issues.push(configIssue({
|
|
678
|
-
ruleId: "brandlab-config-v3-contact-field-profile",
|
|
679
|
-
expected: "A non-empty lowercase kebab-case contact fieldProfile.",
|
|
680
|
-
actual: `contact.fieldProfile is ${JSON.stringify(capabilities.contact.fieldProfile)}.`,
|
|
681
|
-
remediation: "Use the approved declarative field-profile identifier.",
|
|
682
|
-
path: "brandlab.config.ts",
|
|
683
|
-
}));
|
|
684
|
-
}
|
|
685
|
-
if (typeof capabilities.contact.attachments !== "string" ||
|
|
686
|
-
!ATTACHMENT_MODES.has(capabilities.contact.attachments)) {
|
|
687
|
-
issues.push(configIssue({
|
|
688
|
-
ruleId: "brandlab-config-v3-contact-attachments",
|
|
689
|
-
expected: `One attachment mode: ${[...ATTACHMENT_MODES].join(", ")}.`,
|
|
690
|
-
actual: `contact.attachments is ${JSON.stringify(capabilities.contact.attachments)}.`,
|
|
691
|
-
remediation: "Use an approved shared attachment policy.",
|
|
692
|
-
path: "brandlab.config.ts",
|
|
693
|
-
}));
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
|
-
if (capabilities.locations !== undefined) {
|
|
698
|
-
if (!isRecord(capabilities.locations)) {
|
|
699
|
-
addShapeIssue("locations", issues);
|
|
700
|
-
}
|
|
701
|
-
else {
|
|
702
|
-
addUnknownKeyIssues(capabilities.locations, new Set(["strategy"]), new Set(), "capabilities.locations", issues);
|
|
703
|
-
if (typeof capabilities.locations.strategy !== "string" ||
|
|
704
|
-
!LOCATION_STRATEGIES.has(capabilities.locations.strategy)) {
|
|
705
|
-
issues.push(configIssue({
|
|
706
|
-
ruleId: "brandlab-config-v3-location-strategy",
|
|
707
|
-
expected: `One location strategy: ${[...LOCATION_STRATEGIES].join(", ")}.`,
|
|
708
|
-
actual: `locations.strategy is ${JSON.stringify(capabilities.locations.strategy)}.`,
|
|
709
|
-
remediation: "Use the verified business-location model.",
|
|
710
|
-
path: "brandlab.config.ts",
|
|
711
|
-
}));
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
if (capabilities.reviews !== undefined) {
|
|
716
|
-
if (!isRecord(capabilities.reviews)) {
|
|
717
|
-
addShapeIssue("reviews", issues);
|
|
718
|
-
}
|
|
719
|
-
else {
|
|
720
|
-
addUnknownKeyIssues(capabilities.reviews, new Set(["provider"]), new Set(), "capabilities.reviews", issues);
|
|
721
|
-
if (typeof capabilities.reviews.provider !== "string" ||
|
|
722
|
-
!REVIEWS_PROVIDERS.has(capabilities.reviews.provider)) {
|
|
723
|
-
issues.push(configIssue({
|
|
724
|
-
ruleId: "brandlab-config-v3-reviews-provider",
|
|
725
|
-
expected: `One reviews provider: ${[...REVIEWS_PROVIDERS].join(", ")}.`,
|
|
726
|
-
actual: `reviews.provider is ${JSON.stringify(capabilities.reviews.provider)}.`,
|
|
727
|
-
remediation: "Select the shared Google Places provider or verified project-owned records.",
|
|
728
|
-
path: "brandlab.config.ts",
|
|
729
|
-
}));
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
function addShapeIssue(surface, issues) {
|
|
735
|
-
issues.push(configIssue({
|
|
736
|
-
ruleId: `brandlab-config-v3-${surface}-shape`,
|
|
737
|
-
expected: `capabilities.${surface} as a supported literal object.`,
|
|
738
|
-
actual: `capabilities.${surface} is not an object.`,
|
|
739
|
-
remediation: "Use the documented variation-only capability shape.",
|
|
740
|
-
path: "brandlab.config.ts",
|
|
741
|
-
}));
|
|
742
|
-
}
|
|
743
|
-
function validateSitemap(value, issues) {
|
|
744
|
-
if (value === undefined)
|
|
745
|
-
return;
|
|
746
|
-
if (!isRecord(value)) {
|
|
747
|
-
issues.push(configIssue({
|
|
748
|
-
ruleId: "brandlab-config-v3-sitemap",
|
|
749
|
-
expected: 'sitemap as { mode: "single" } or a documented split configuration.',
|
|
750
|
-
actual: "sitemap is not an object.",
|
|
751
|
-
remediation: "Use the shared sitemap variation contract.",
|
|
752
|
-
path: "brandlab.config.ts",
|
|
753
|
-
}));
|
|
754
|
-
return;
|
|
755
|
-
}
|
|
756
|
-
if (value.mode === "single") {
|
|
757
|
-
addUnknownKeyIssues(value, new Set(["mode"]), new Set(), "sitemap", issues);
|
|
758
|
-
return;
|
|
759
|
-
}
|
|
760
|
-
if (value.mode !== "split") {
|
|
761
|
-
issues.push(configIssue({
|
|
762
|
-
ruleId: "brandlab-config-v3-sitemap-mode",
|
|
763
|
-
expected: 'sitemap.mode as "single" or "split".',
|
|
764
|
-
actual: `sitemap.mode is ${JSON.stringify(value.mode)}.`,
|
|
765
|
-
remediation: "Select a supported sitemap mode.",
|
|
766
|
-
path: "brandlab.config.ts",
|
|
767
|
-
}));
|
|
768
|
-
return;
|
|
769
|
-
}
|
|
770
|
-
addUnknownKeyIssues(value, new Set(["mode", "families", "reason"]), new Set(), "sitemap", issues);
|
|
771
|
-
const families = Array.isArray(value.families)
|
|
772
|
-
? value.families.filter((entry) => typeof entry === "string")
|
|
773
|
-
: [];
|
|
774
|
-
if (!Array.isArray(value.families) || families.length === 0 || families.length !== value.families.length) {
|
|
775
|
-
issues.push(configIssue({
|
|
776
|
-
ruleId: "brandlab-config-v3-sitemap-families",
|
|
777
|
-
expected: "At least one literal split-sitemap root-relative family path.",
|
|
778
|
-
actual: "sitemap.families is missing, empty or invalid.",
|
|
779
|
-
remediation: "Declare the approved split boundaries explicitly.",
|
|
780
|
-
path: "brandlab.config.ts",
|
|
781
|
-
}));
|
|
782
|
-
}
|
|
783
|
-
else {
|
|
784
|
-
addDuplicateIssue(families, "brandlab-config-v3-sitemap-family-duplicate", "sitemap.families", issues);
|
|
785
|
-
if (families.some((path) => !isRootRelativePath(path))) {
|
|
786
|
-
issues.push(configIssue({
|
|
787
|
-
ruleId: "brandlab-config-v3-sitemap-family-path",
|
|
788
|
-
expected: "Strict root-relative sitemap family paths.",
|
|
789
|
-
actual: "A split-sitemap family path is invalid.",
|
|
790
|
-
remediation: "Use canonical route-family boundaries without query, hash or encoded separators.",
|
|
791
|
-
path: "brandlab.config.ts",
|
|
792
|
-
}));
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
if (typeof value.reason !== "string" || !value.reason.trim()) {
|
|
796
|
-
issues.push(configIssue({
|
|
797
|
-
ruleId: "brandlab-config-v3-sitemap-reason",
|
|
798
|
-
expected: "A non-empty reason for split sitemap variation.",
|
|
799
|
-
actual: "sitemap.reason is missing or empty.",
|
|
800
|
-
remediation: "Explain the evidence-backed split requirement.",
|
|
801
|
-
path: "brandlab.config.ts",
|
|
802
|
-
}));
|
|
803
|
-
}
|
|
804
|
-
}
|
|
805
|
-
function validateExceptions(value, issues) {
|
|
806
|
-
if (value === undefined)
|
|
807
|
-
return;
|
|
808
|
-
if (!Array.isArray(value)) {
|
|
809
|
-
issues.push(configIssue({
|
|
810
|
-
ruleId: "brandlab-config-v3-exceptions",
|
|
811
|
-
expected: "exceptions as an optional literal array.",
|
|
812
|
-
actual: "exceptions is not an array.",
|
|
813
|
-
remediation: "Use machine-readable exception records only.",
|
|
814
|
-
path: "brandlab.config.ts",
|
|
815
|
-
}));
|
|
816
|
-
return;
|
|
817
|
-
}
|
|
818
|
-
const ruleIds = [];
|
|
819
|
-
value.forEach((exception, index) => {
|
|
820
|
-
if (!isRecord(exception)) {
|
|
821
|
-
issues.push(configIssue({
|
|
822
|
-
ruleId: "brandlab-config-v3-exception-shape",
|
|
823
|
-
expected: `exceptions[${index}] as a complete record.`,
|
|
824
|
-
actual: "The exception is not an object.",
|
|
825
|
-
remediation: "Declare ruleId, rationale, owner, evidence, reviewTrigger and optional expiresAt.",
|
|
826
|
-
path: "brandlab.config.ts",
|
|
827
|
-
}));
|
|
828
|
-
return;
|
|
829
|
-
}
|
|
830
|
-
addUnknownKeyIssues(exception, new Set(["ruleId", "rationale", "owner", "evidence", "expiresAt", "reviewTrigger"]), new Set(), `exceptions[${index}]`, issues);
|
|
831
|
-
for (const field of ["ruleId", "rationale", "evidence", "reviewTrigger"]) {
|
|
832
|
-
if (typeof exception[field] !== "string" || !exception[field].trim()) {
|
|
833
|
-
issues.push(configIssue({
|
|
834
|
-
ruleId: "brandlab-config-v3-exception-required",
|
|
835
|
-
expected: `exceptions[${index}].${field} as a non-empty string.`,
|
|
836
|
-
actual: `${field} is missing or empty.`,
|
|
837
|
-
remediation: "Complete the owned, evidenced, reviewable exception record.",
|
|
838
|
-
path: "brandlab.config.ts",
|
|
839
|
-
}));
|
|
840
|
-
}
|
|
841
|
-
}
|
|
842
|
-
if (exception.owner !== "Romie") {
|
|
843
|
-
issues.push(configIssue({
|
|
844
|
-
ruleId: "brandlab-config-v3-exception-owner",
|
|
845
|
-
expected: 'Exception owner "Romie".',
|
|
846
|
-
actual: `owner is ${JSON.stringify(exception.owner)}.`,
|
|
847
|
-
remediation: "Assign the owner explicitly; exceptions never approve themselves.",
|
|
848
|
-
path: "brandlab.config.ts",
|
|
849
|
-
}));
|
|
850
|
-
}
|
|
851
|
-
if (exception.expiresAt !== undefined &&
|
|
852
|
-
(typeof exception.expiresAt !== "string" || !isIsoDate(exception.expiresAt))) {
|
|
853
|
-
issues.push(configIssue({
|
|
854
|
-
ruleId: "brandlab-config-v3-exception-expiry",
|
|
855
|
-
expected: "expiresAt as an ISO date or timestamp when present.",
|
|
856
|
-
actual: `expiresAt is ${JSON.stringify(exception.expiresAt)}.`,
|
|
857
|
-
remediation: "Use a reviewable ISO expiry or omit it and retain the explicit review trigger.",
|
|
858
|
-
path: "brandlab.config.ts",
|
|
859
|
-
}));
|
|
860
|
-
}
|
|
861
|
-
if (typeof exception.ruleId === "string" && exception.ruleId)
|
|
862
|
-
ruleIds.push(exception.ruleId);
|
|
863
|
-
});
|
|
864
|
-
addDuplicateIssue(ruleIds, "brandlab-config-v3-exception-duplicate", "exceptions.ruleId", issues);
|
|
865
|
-
}
|
|
866
|
-
function addUnknownKeyIssues(value, allowed, separatelyReported, field, issues) {
|
|
867
|
-
for (const key of Object.keys(value)) {
|
|
868
|
-
if (allowed.has(key) || separatelyReported.has(key))
|
|
869
|
-
continue;
|
|
870
|
-
issues.push(configIssue({
|
|
871
|
-
ruleId: "brandlab-config-v3-unknown-key",
|
|
872
|
-
expected: `Only documented variation keys under ${field}.`,
|
|
873
|
-
actual: `Unknown key "${field}.${key}".`,
|
|
874
|
-
remediation: "Remove the unversioned key or add it through a reviewed contract release.",
|
|
875
|
-
path: "brandlab.config.ts",
|
|
876
|
-
}));
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
function addDuplicateIssue(values, ruleId, field, issues) {
|
|
880
|
-
const duplicate = values.find((value, index) => values.indexOf(value) !== index);
|
|
881
|
-
if (!duplicate)
|
|
882
|
-
return;
|
|
883
|
-
issues.push(configIssue({
|
|
884
|
-
ruleId,
|
|
885
|
-
expected: `Unique ${field} values.`,
|
|
886
|
-
actual: `${field} contains duplicate "${duplicate}".`,
|
|
887
|
-
remediation: "Remove the duplicate variation.",
|
|
888
|
-
path: "brandlab.config.ts",
|
|
889
|
-
}));
|
|
890
|
-
}
|
|
891
|
-
function isRootRelativePath(value) {
|
|
892
|
-
const trimmed = value.trim();
|
|
893
|
-
return Boolean(trimmed) &&
|
|
894
|
-
trimmed.startsWith("/") &&
|
|
895
|
-
!trimmed.startsWith("//") &&
|
|
896
|
-
!trimmed.includes("\\") &&
|
|
897
|
-
!/%(?:2f|5c)/i.test(trimmed) &&
|
|
898
|
-
!/[\u0000-\u001f\u007f]/.test(trimmed) &&
|
|
899
|
-
!trimmed.includes("?") &&
|
|
900
|
-
!trimmed.includes("#") &&
|
|
901
|
-
!trimmed.split("/").some((segment) => segment === "." || segment === "..");
|
|
902
|
-
}
|
|
903
|
-
function isIsoDate(value) {
|
|
904
|
-
if (!/^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z)?$/.test(value))
|
|
905
|
-
return false;
|
|
906
|
-
return !Number.isNaN(Date.parse(value));
|
|
907
|
-
}
|
|
908
|
-
function addImportContractIssues(root, config, usage, issues) {
|
|
909
|
-
if (config.profile === "legacy-php") {
|
|
910
|
-
const nextEvidence = detectNextPackageBackedEvidence(root, usage);
|
|
911
|
-
if (nextEvidence.length === 0)
|
|
912
|
-
return;
|
|
913
|
-
issues.push(configIssue({
|
|
914
|
-
ruleId: "brandlab-config-v3-profile-stack-mismatch",
|
|
915
|
-
surface: "architecture",
|
|
916
|
-
expected: "A next-* profile for a package-backed Next.js project.",
|
|
917
|
-
actual: `profile=\"legacy-php\" conflicts with ${nextEvidence.slice(0, 3).join(", ")}.`,
|
|
918
|
-
remediation: "Select the matching Next.js architecture profile; legacy-php cannot suppress shared-kernel enforcement.",
|
|
919
|
-
path: "brandlab.config.ts",
|
|
920
|
-
}));
|
|
921
|
-
}
|
|
922
|
-
for (const [moduleName, surface] of REQUIRED_KERNEL_IMPORTS) {
|
|
923
|
-
requireModuleExecution(moduleName, surface, usage, issues);
|
|
924
|
-
}
|
|
925
|
-
for (const requirement of STRICT_KERNEL_USAGES) {
|
|
926
|
-
requireStrictUsage(requirement, usage, issues);
|
|
927
|
-
}
|
|
928
|
-
requireStrictUsage({
|
|
929
|
-
moduleName: "@brandlab-digital/site-next/headers",
|
|
930
|
-
surface: "headers",
|
|
931
|
-
exports: ["buildBrandlabHeaderRules"],
|
|
932
|
-
}, usage, issues);
|
|
933
|
-
if (config.routes.families.includes("locations") ||
|
|
934
|
-
config.routes.families.includes("service-locations")) {
|
|
935
|
-
requireStrictUsage({
|
|
936
|
-
moduleName: "@brandlab-digital/site-core/locations",
|
|
937
|
-
surface: "location-proof",
|
|
938
|
-
exports: ["defineLocationRouteQuality"],
|
|
939
|
-
}, usage, issues);
|
|
940
|
-
}
|
|
941
|
-
const contactEvidence = detectContactSurfaceEvidence(root, usage);
|
|
942
|
-
const contactRequired = config.capabilities.contactEnabled || contactEvidence.length > 0;
|
|
943
|
-
if (!config.capabilities.contactEnabled && contactEvidence.length > 0) {
|
|
944
|
-
issues.push(configIssue({
|
|
945
|
-
ruleId: "brandlab-config-v3-contact-capability-required",
|
|
946
|
-
surface: "contact",
|
|
947
|
-
expected: "capabilities.contact whenever a contact route or contact implementation exists.",
|
|
948
|
-
actual: `Contact capability is omitted despite ${contactEvidence.slice(0, 3).join(", ")}.`,
|
|
949
|
-
remediation: "Declare the contact field profile and attachment policy, then bind the shared production and client runtimes.",
|
|
950
|
-
path: "brandlab.config.ts",
|
|
951
|
-
}));
|
|
952
|
-
}
|
|
953
|
-
if (contactEvidence.length > 0 &&
|
|
954
|
-
typeof config.searchCertification.representativeRoutes.contact !== "string") {
|
|
955
|
-
issues.push(configIssue({
|
|
956
|
-
ruleId: "brandlab-config-v3-search-contact-route",
|
|
957
|
-
surface: "search-certification",
|
|
958
|
-
expected: "searchCertification.representativeRoutes.contact for a detected contact route or runtime.",
|
|
959
|
-
actual: `Contact implementation exists without a representative contact route (${contactEvidence[0]}).`,
|
|
960
|
-
remediation: "Bind the canonical contact page into Search Certification.",
|
|
961
|
-
path: "brandlab.config.ts",
|
|
962
|
-
}));
|
|
963
|
-
}
|
|
964
|
-
if (contactRequired) {
|
|
965
|
-
requireStrictUsage({
|
|
966
|
-
moduleName: "@brandlab-digital/site-next/contact/production",
|
|
967
|
-
surface: "contact-production",
|
|
968
|
-
exports: ["createBrandlabProductionContactRuntime"],
|
|
969
|
-
}, usage, issues);
|
|
970
|
-
requireStrictUsage({
|
|
971
|
-
moduleName: "@brandlab-digital/site-next/contact/client",
|
|
972
|
-
surface: "contact-client",
|
|
973
|
-
exports: ["createContactHttpSubmissionController"],
|
|
974
|
-
}, usage, issues);
|
|
975
|
-
}
|
|
976
|
-
if (config.capabilities.blogAdapter && config.capabilities.blogAdapter !== "none") {
|
|
977
|
-
const adapterExport = config.capabilities.blogAdapter === "mdx"
|
|
978
|
-
? "createRouteAwareMdxBlogAdapter"
|
|
979
|
-
: "createRouteAwareStructuredBlogAdapter";
|
|
980
|
-
requireStrictUsage({
|
|
981
|
-
moduleName: "@brandlab-digital/site-next/blog",
|
|
982
|
-
surface: "blog",
|
|
983
|
-
exports: [adapterExport],
|
|
984
|
-
}, usage, issues);
|
|
985
|
-
}
|
|
986
|
-
if (config.capabilities.reviewsProvider === "google-places") {
|
|
987
|
-
requireStrictUsage({
|
|
988
|
-
moduleName: "@brandlab-digital/site-next/google-reviews",
|
|
989
|
-
surface: "google-reviews-route",
|
|
990
|
-
exports: ["createGooglePlaceReviewsRouteResponse"],
|
|
991
|
-
}, usage, issues);
|
|
992
|
-
requireStrictUsage({
|
|
993
|
-
moduleName: "@brandlab-digital/site-next/google-reviews",
|
|
994
|
-
surface: "google-reviews-display",
|
|
995
|
-
exports: ["GooglePlaceReviews"],
|
|
996
|
-
}, usage, issues);
|
|
997
|
-
}
|
|
998
|
-
else if (config.capabilities.reviewsProvider === "project-owned") {
|
|
999
|
-
requireStrictUsage({
|
|
1000
|
-
moduleName: "@brandlab-digital/site-next/reviews",
|
|
1001
|
-
surface: "project-reviews",
|
|
1002
|
-
exports: ["preparePublicReviews"],
|
|
1003
|
-
}, usage, issues);
|
|
1004
|
-
}
|
|
1005
|
-
else if (usage.imports.has("@brandlab-digital/site-next/google-reviews")) {
|
|
1006
|
-
issues.push(configIssue({
|
|
1007
|
-
ruleId: "brandlab-config-v3-google-reviews-capability",
|
|
1008
|
-
surface: "google-reviews",
|
|
1009
|
-
expected: 'capabilities.reviews.provider="google-places".',
|
|
1010
|
-
actual: "Google Places runtime is imported without a declared reviews capability.",
|
|
1011
|
-
remediation: "Declare the provider and legal surfaces or remove the Google Places runtime.",
|
|
1012
|
-
}));
|
|
1013
|
-
}
|
|
1014
|
-
for (const forbidden of FORBIDDEN_CANDIDATE_EXPORTS) {
|
|
1015
|
-
const called = usage.executableExports.get(forbidden.moduleName) ?? new Set();
|
|
1016
|
-
for (const exportName of forbidden.exports) {
|
|
1017
|
-
if (!called.has(exportName))
|
|
1018
|
-
continue;
|
|
1019
|
-
issues.push(configIssue({
|
|
1020
|
-
ruleId: `brandlab-config-v3-compat-${forbidden.surface}`,
|
|
1021
|
-
surface: forbidden.surface,
|
|
1022
|
-
expected: "Only route-aware, proof-safe candidate APIs.",
|
|
1023
|
-
actual: `${forbidden.moduleName}.${exportName} executes a compatibility or retired API.`,
|
|
1024
|
-
remediation: "Replace the compatibility call with the strict candidate contract.",
|
|
1025
|
-
}));
|
|
1026
|
-
}
|
|
1027
|
-
}
|
|
1028
|
-
}
|
|
1029
|
-
function detectNextPackageBackedEvidence(root, usage) {
|
|
1030
|
-
const evidence = new Set();
|
|
1031
|
-
const packagePath = join(root, "package.json");
|
|
1032
|
-
if (existsSync(packagePath)) {
|
|
1033
|
-
try {
|
|
1034
|
-
const packageJson = JSON.parse(readFileSync(packagePath, "utf8"));
|
|
1035
|
-
const dependencyNames = [
|
|
1036
|
-
packageJson.dependencies,
|
|
1037
|
-
packageJson.devDependencies,
|
|
1038
|
-
packageJson.optionalDependencies,
|
|
1039
|
-
packageJson.peerDependencies,
|
|
1040
|
-
].flatMap((value) => isRecord(value) ? Object.keys(value) : []);
|
|
1041
|
-
if (dependencyNames.includes("next"))
|
|
1042
|
-
evidence.add("package.json dependency next");
|
|
1043
|
-
if (dependencyNames.some((name) => name.startsWith("@brandlab-digital/site-next"))) {
|
|
1044
|
-
evidence.add("package.json Brandlab Next package");
|
|
1045
|
-
}
|
|
1046
|
-
}
|
|
1047
|
-
catch {
|
|
1048
|
-
// Package-shape diagnostics belong to the project evaluator. Other static
|
|
1049
|
-
// Next evidence below still prevents profile-based suppression.
|
|
1050
|
-
}
|
|
1051
|
-
}
|
|
1052
|
-
if (usage.imports.has("next") || [...usage.imports].some((name) => name.startsWith("next/"))) {
|
|
1053
|
-
evidence.add("Next.js runtime import");
|
|
1054
|
-
}
|
|
1055
|
-
if ([...usage.imports].some((name) => name.startsWith("@brandlab-digital/site-next"))) {
|
|
1056
|
-
evidence.add("Brandlab Next runtime import");
|
|
1057
|
-
}
|
|
1058
|
-
for (const name of ["next.config.ts", "next.config.mjs", "next.config.js", "next.config.cjs"]) {
|
|
1059
|
-
if (existsSync(join(root, name)))
|
|
1060
|
-
evidence.add(name);
|
|
1061
|
-
}
|
|
1062
|
-
const nextRoute = walkProjectRuntimeFiles(root)
|
|
1063
|
-
.map((file) => normalizePath(relative(root, file)))
|
|
1064
|
-
.find((path) => /^(?:src\/)?(?:app|pages)\//.test(path));
|
|
1065
|
-
if (nextRoute)
|
|
1066
|
-
evidence.add(`Next.js route ${nextRoute}`);
|
|
1067
|
-
return [...evidence].sort();
|
|
1068
|
-
}
|
|
1069
|
-
function detectContactSurfaceEvidence(root, usage) {
|
|
1070
|
-
const evidence = new Set();
|
|
1071
|
-
for (const moduleName of usage.imports) {
|
|
1072
|
-
if (moduleName.startsWith("@brandlab-digital/site-next/contact/")) {
|
|
1073
|
-
evidence.add(`shared contact import ${moduleName}`);
|
|
1074
|
-
}
|
|
1075
|
-
}
|
|
1076
|
-
for (const file of walkProjectRuntimeFiles(root)) {
|
|
1077
|
-
const relativePath = normalizePath(relative(root, file));
|
|
1078
|
-
if (/^(?:src\/)?(?:app|pages)\/(?:api\/)?contact(?:\/|\.|$)/i.test(relativePath) ||
|
|
1079
|
-
/(?:^|\/)(?:components?|config|hooks?|lib)\/[^/]*contact[^/]*\.[cm]?[jt]sx?$/i.test(relativePath)) {
|
|
1080
|
-
evidence.add(`contact runtime path ${relativePath}`);
|
|
1081
|
-
continue;
|
|
1082
|
-
}
|
|
1083
|
-
const source = readFileSync(file, "utf8");
|
|
1084
|
-
if (/\b(?:ContactForm|submitContact(?:Form|Enquiry|Request)?|handleContact(?:Form|Submission|Request)|createContact(?:Form|Runtime|Route|Submission)|contactRouteHandler|sendContact(?:Email|Enquiry|Request))\b/.test(source)) {
|
|
1085
|
-
evidence.add(`contact implementation ${relativePath}`);
|
|
1086
|
-
}
|
|
1087
|
-
}
|
|
1088
|
-
return [...evidence].sort();
|
|
1089
|
-
}
|
|
1090
|
-
function requireModuleExecution(moduleName, surface, usage, issues) {
|
|
1091
|
-
if (!usage.imports.has(moduleName)) {
|
|
1092
|
-
issues.push(configIssue({
|
|
1093
|
-
ruleId: `brandlab-config-v3-shared-${surface}`,
|
|
1094
|
-
surface,
|
|
1095
|
-
expected: `An import from ${moduleName}.`,
|
|
1096
|
-
actual: "No project source file consumes the shared executable contract.",
|
|
1097
|
-
remediation: `Replace the local ${surface} implementation with ${moduleName}.`,
|
|
1098
|
-
}));
|
|
1099
|
-
}
|
|
1100
|
-
else if (!usage.executableImports.has(moduleName)) {
|
|
1101
|
-
issues.push(configIssue({
|
|
1102
|
-
ruleId: `brandlab-config-v3-shared-${surface}-unused`,
|
|
1103
|
-
surface,
|
|
1104
|
-
expected: `An executable call or rendered component from ${moduleName}.`,
|
|
1105
|
-
actual: `${moduleName} is only imported or re-exported.`,
|
|
1106
|
-
remediation: `Wire the shared ${surface} contract into the runtime path.`,
|
|
1107
|
-
}));
|
|
1108
|
-
}
|
|
1109
|
-
}
|
|
1110
|
-
function requireStrictUsage(requirement, usage, issues) {
|
|
1111
|
-
if (!usage.imports.has(requirement.moduleName)) {
|
|
1112
|
-
issues.push(configIssue({
|
|
1113
|
-
ruleId: `brandlab-config-v3-shared-${requirement.surface}`,
|
|
1114
|
-
surface: requirement.surface,
|
|
1115
|
-
expected: `${requirement.moduleName} using ${requirement.exports.join(" or ")}.`,
|
|
1116
|
-
actual: "The required candidate contract is not imported.",
|
|
1117
|
-
remediation: "Consume the strict shared candidate contract in the owning runtime path.",
|
|
1118
|
-
}));
|
|
1119
|
-
return;
|
|
1120
|
-
}
|
|
1121
|
-
const executable = usage.executableExports.get(requirement.moduleName) ?? new Set();
|
|
1122
|
-
const outputOwned = usage.outputOwnedExports.get(requirement.moduleName) ?? new Set();
|
|
1123
|
-
const frameworkOwned = usage.frameworkOwnedExports.get(requirement.moduleName) ?? new Set();
|
|
1124
|
-
const calledExport = requirement.exports.find((exportName) => executable.has(exportName));
|
|
1125
|
-
if (!calledExport) {
|
|
1126
|
-
issues.push(configIssue({
|
|
1127
|
-
ruleId: `brandlab-config-v3-shared-${requirement.surface}-strict-api`,
|
|
1128
|
-
surface: requirement.surface,
|
|
1129
|
-
expected: `An executable ${requirement.exports.join(" or ")} call.`,
|
|
1130
|
-
actual: `${requirement.moduleName} is called only through an unsupported or generic export.`,
|
|
1131
|
-
remediation: "Use the exact route-aware or production candidate API.",
|
|
1132
|
-
}));
|
|
1133
|
-
return;
|
|
1134
|
-
}
|
|
1135
|
-
if (!requirement.exports.some((exportName) => outputOwned.has(exportName))) {
|
|
1136
|
-
issues.push(configIssue({
|
|
1137
|
-
ruleId: `brandlab-config-v3-shared-${requirement.surface}-unowned-output`,
|
|
1138
|
-
surface: requirement.surface,
|
|
1139
|
-
expected: "The strict shared result reaches an exported binding, rendered or returned route output, or another owned contract.",
|
|
1140
|
-
actual: `${calledExport} executes without a statically reachable runtime output.`,
|
|
1141
|
-
remediation: "Wire the shared result into an exported runtime path instead of a dead assignment or disconnected compliance call.",
|
|
1142
|
-
}));
|
|
1143
|
-
return;
|
|
1144
|
-
}
|
|
1145
|
-
if (requirement.frameworkOutput &&
|
|
1146
|
-
!requirement.exports.some((exportName) => frameworkOwned.has(exportName))) {
|
|
1147
|
-
issues.push(configIssue({
|
|
1148
|
-
ruleId: requirement.frameworkOutput.ruleId,
|
|
1149
|
-
surface: requirement.surface,
|
|
1150
|
-
expected: frameworkOwnerExpectation(requirement.frameworkOutput.owner),
|
|
1151
|
-
actual: `${calledExport} reaches an exported value, but not an owning Next.js framework entrypoint.`,
|
|
1152
|
-
remediation: frameworkOwnerRemediation(requirement.frameworkOutput.owner),
|
|
1153
|
-
}));
|
|
1154
|
-
}
|
|
1155
|
-
}
|
|
1156
|
-
function frameworkOwnerExpectation(owner) {
|
|
1157
|
-
if (owner === "metadata") {
|
|
1158
|
-
return "The strict metadata builder reaches metadata or generateMetadata in an App Router page or layout entrypoint.";
|
|
1159
|
-
}
|
|
1160
|
-
if (owner === "render") {
|
|
1161
|
-
return "The strict schema or breadcrumb output reaches the default rendered output of an App Router page, layout or template.";
|
|
1162
|
-
}
|
|
1163
|
-
if (owner === "sitemap") {
|
|
1164
|
-
return "The strict sitemap output reaches the default App Router sitemap entrypoint or GET response of a sitemap XML route.";
|
|
1165
|
-
}
|
|
1166
|
-
return "The strict robots output reaches the default App Router robots entrypoint.";
|
|
1167
|
-
}
|
|
1168
|
-
function frameworkOwnerRemediation(owner) {
|
|
1169
|
-
if (owner === "metadata") {
|
|
1170
|
-
return "Return the shared metadata result from generateMetadata or export it as metadata in the owning page or layout.";
|
|
1171
|
-
}
|
|
1172
|
-
if (owner === "render") {
|
|
1173
|
-
return "Render the shared schema or breadcrumb result from the owning page, layout or template; arbitrary exported decoys do not count.";
|
|
1174
|
-
}
|
|
1175
|
-
if (owner === "sitemap") {
|
|
1176
|
-
return "Return the shared sitemap result from app/sitemap or a canonical sitemap XML GET route.";
|
|
1177
|
-
}
|
|
1178
|
-
return "Return the shared robots result from app/robots.";
|
|
1179
|
-
}
|
|
1180
|
-
function nextFrameworkOwnerForImport(moduleName, exportName) {
|
|
1181
|
-
if (!exportName)
|
|
1182
|
-
return null;
|
|
1183
|
-
if (moduleName === "@brandlab-digital/site-next/metadata" &&
|
|
1184
|
-
exportName === "createRouteMetadataBuilder")
|
|
1185
|
-
return "metadata";
|
|
1186
|
-
if ((moduleName === "@brandlab-digital/site-core/schema" &&
|
|
1187
|
-
exportName === "buildSchemaGraph") ||
|
|
1188
|
-
(moduleName === "@brandlab-digital/site-next/schema" &&
|
|
1189
|
-
exportName === "SchemaScript") ||
|
|
1190
|
-
(moduleName === "@brandlab-digital/site-next/breadcrumbs" &&
|
|
1191
|
-
exportName === "createRouteBreadcrumbOutput"))
|
|
1192
|
-
return "render";
|
|
1193
|
-
if ((moduleName === "@brandlab-digital/site-core/sitemaps" &&
|
|
1194
|
-
exportName === "buildSitemapEntries") ||
|
|
1195
|
-
(moduleName === "@brandlab-digital/site-next/sitemaps" &&
|
|
1196
|
-
["sitemapXmlResponse", "sitemapIndexXmlResponse"].includes(exportName)))
|
|
1197
|
-
return "sitemap";
|
|
1198
|
-
if (moduleName === "@brandlab-digital/site-next/robots" &&
|
|
1199
|
-
exportName === "buildRobotsMetadata")
|
|
1200
|
-
return "robots";
|
|
1201
|
-
return null;
|
|
1202
|
-
}
|
|
1203
|
-
function createNextFrameworkOwnershipInspectors(source, relativePath, getProvenanceKey) {
|
|
1204
|
-
const isAppSource = /^(?:src\/)?app\//.test(relativePath);
|
|
1205
|
-
const isMetadataEntry = isAppSource && /\/(?:page|layout)\.[cm]?[jt]sx?$/.test(relativePath);
|
|
1206
|
-
const isRenderedEntry = isAppSource && /\/(?:page|layout|template)\.[cm]?[jt]sx?$/.test(relativePath);
|
|
1207
|
-
const isSitemapEntry = isAppSource && /\/sitemap\.[cm]?[jt]s$/.test(relativePath);
|
|
1208
|
-
const isSitemapRoute = isAppSource &&
|
|
1209
|
-
/\/(?:sitemap\.xml|sitemaps\/[^/]+\.xml)\/route\.[cm]?[jt]s$/.test(relativePath);
|
|
1210
|
-
const isRobotsEntry = isAppSource && /\/robots\.[cm]?[jt]s$/.test(relativePath);
|
|
1211
|
-
const neverOwned = () => false;
|
|
1212
|
-
return {
|
|
1213
|
-
metadata: isMetadataEntry
|
|
1214
|
-
? createOutputOwnershipInspector(source, ({ kind, name }) => kind === "named" && ["metadata", "generateMetadata"].includes(name), getProvenanceKey)
|
|
1215
|
-
: neverOwned,
|
|
1216
|
-
render: isRenderedEntry
|
|
1217
|
-
? createOutputOwnershipInspector(source, ({ kind }) => kind === "default", getProvenanceKey)
|
|
1218
|
-
: neverOwned,
|
|
1219
|
-
sitemap: isSitemapEntry
|
|
1220
|
-
? createOutputOwnershipInspector(source, ({ kind }) => kind === "default", getProvenanceKey)
|
|
1221
|
-
: isSitemapRoute
|
|
1222
|
-
? createOutputOwnershipInspector(source, ({ kind, name }) => kind === "named" && name === "GET", getProvenanceKey)
|
|
1223
|
-
: neverOwned,
|
|
1224
|
-
robots: isRobotsEntry
|
|
1225
|
-
? createOutputOwnershipInspector(source, ({ kind }) => kind === "default", getProvenanceKey)
|
|
1226
|
-
: neverOwned,
|
|
1227
|
-
};
|
|
1228
|
-
}
|
|
1229
|
-
function collectProjectImports(root, issues) {
|
|
1230
|
-
const imports = new Set();
|
|
1231
|
-
const executableImports = new Set();
|
|
1232
|
-
const outputOwnedImports = new Set();
|
|
1233
|
-
const executableExports = new Map();
|
|
1234
|
-
const outputOwnedExports = new Map();
|
|
1235
|
-
const frameworkOwnedExports = new Map();
|
|
1236
|
-
for (const file of walkProjectRuntimeFiles(root)) {
|
|
1237
|
-
const relativePath = normalizePath(relative(root, file));
|
|
1238
|
-
const sourceText = readFileSync(file, "utf8");
|
|
1239
|
-
const source = ts.createSourceFile(file, sourceText, ts.ScriptTarget.ES2022, true, file.endsWith("x") ? ts.ScriptKind.TSX : ts.ScriptKind.TS);
|
|
1240
|
-
const localImportBindings = new Map();
|
|
1241
|
-
for (const statement of source.statements) {
|
|
1242
|
-
if (ts.isImportDeclaration(statement) && ts.isStringLiteral(statement.moduleSpecifier)) {
|
|
1243
|
-
const moduleName = statement.moduleSpecifier.text;
|
|
1244
|
-
imports.add(moduleName);
|
|
1245
|
-
const clause = statement.importClause;
|
|
1246
|
-
if (clause && !clause.isTypeOnly) {
|
|
1247
|
-
if (clause.name) {
|
|
1248
|
-
localImportBindings.set(clause.name.text, {
|
|
1249
|
-
moduleName,
|
|
1250
|
-
exportName: "default",
|
|
1251
|
-
namespace: false,
|
|
1252
|
-
});
|
|
1253
|
-
}
|
|
1254
|
-
const bindings = clause.namedBindings;
|
|
1255
|
-
if (bindings && ts.isNamespaceImport(bindings)) {
|
|
1256
|
-
localImportBindings.set(bindings.name.text, {
|
|
1257
|
-
moduleName,
|
|
1258
|
-
exportName: null,
|
|
1259
|
-
namespace: true,
|
|
1260
|
-
});
|
|
1261
|
-
}
|
|
1262
|
-
else if (bindings && ts.isNamedImports(bindings)) {
|
|
1263
|
-
for (const element of bindings.elements) {
|
|
1264
|
-
if (!element.isTypeOnly) {
|
|
1265
|
-
localImportBindings.set(element.name.text, {
|
|
1266
|
-
moduleName,
|
|
1267
|
-
exportName: (element.propertyName ?? element.name).text,
|
|
1268
|
-
namespace: false,
|
|
1269
|
-
});
|
|
1270
|
-
}
|
|
1271
|
-
}
|
|
1272
|
-
}
|
|
1273
|
-
}
|
|
1274
|
-
}
|
|
1275
|
-
if (ts.isExportDeclaration(statement) &&
|
|
1276
|
-
statement.moduleSpecifier &&
|
|
1277
|
-
ts.isStringLiteral(statement.moduleSpecifier)) {
|
|
1278
|
-
imports.add(statement.moduleSpecifier.text);
|
|
1279
|
-
}
|
|
1280
|
-
}
|
|
1281
|
-
const isShadowedImportReference = createLexicalShadowInspector(source);
|
|
1282
|
-
const resolveImport = (expression) => {
|
|
1283
|
-
if (ts.isIdentifier(expression)) {
|
|
1284
|
-
const binding = localImportBindings.get(expression.text);
|
|
1285
|
-
return binding && !isShadowedImportReference(expression, source)
|
|
1286
|
-
? binding
|
|
1287
|
-
: undefined;
|
|
1288
|
-
}
|
|
1289
|
-
if ((ts.isPropertyAccessExpression(expression) || ts.isPropertyAccessChain(expression)) &&
|
|
1290
|
-
ts.isIdentifier(expression.expression)) {
|
|
1291
|
-
const binding = localImportBindings.get(expression.expression.text);
|
|
1292
|
-
if (binding?.namespace &&
|
|
1293
|
-
!isShadowedImportReference(expression.expression, source)) {
|
|
1294
|
-
return { ...binding, exportName: expression.name.text };
|
|
1295
|
-
}
|
|
1296
|
-
}
|
|
1297
|
-
if (ts.isElementAccessExpression(expression) &&
|
|
1298
|
-
ts.isIdentifier(expression.expression) &&
|
|
1299
|
-
expression.argumentExpression &&
|
|
1300
|
-
ts.isStringLiteral(expression.argumentExpression)) {
|
|
1301
|
-
const binding = localImportBindings.get(expression.expression.text);
|
|
1302
|
-
if (binding?.namespace &&
|
|
1303
|
-
!isShadowedImportReference(expression.expression, source)) {
|
|
1304
|
-
return { ...binding, exportName: expression.argumentExpression.text };
|
|
1305
|
-
}
|
|
1306
|
-
}
|
|
1307
|
-
return undefined;
|
|
1308
|
-
};
|
|
1309
|
-
const addExportUsage = (target, moduleName, exportName) => {
|
|
1310
|
-
if (!exportName)
|
|
1311
|
-
return;
|
|
1312
|
-
const names = target.get(moduleName) ?? new Set();
|
|
1313
|
-
names.add(exportName);
|
|
1314
|
-
target.set(moduleName, names);
|
|
1315
|
-
};
|
|
1316
|
-
const getProvenanceKey = (expression) => {
|
|
1317
|
-
const binding = resolveImport(expression);
|
|
1318
|
-
return binding?.exportName && binding.moduleName.startsWith("@brandlab-digital/")
|
|
1319
|
-
? `${binding.moduleName}#${binding.exportName}`
|
|
1320
|
-
: null;
|
|
1321
|
-
};
|
|
1322
|
-
const isOutputOwned = createOutputOwnershipInspector(source, () => true, getProvenanceKey);
|
|
1323
|
-
const isContactClientRuntimeOwned = createContactClientRuntimeOwnershipInspector(source, isOutputOwned);
|
|
1324
|
-
const frameworkOwnership = createNextFrameworkOwnershipInspectors(source, relativePath, getProvenanceKey);
|
|
1325
|
-
const markExecutableImport = (expression, node) => {
|
|
1326
|
-
const binding = resolveImport(expression);
|
|
1327
|
-
if (!binding)
|
|
1328
|
-
return;
|
|
1329
|
-
executableImports.add(binding.moduleName);
|
|
1330
|
-
addExportUsage(executableExports, binding.moduleName, binding.exportName);
|
|
1331
|
-
const isSharedContactClientController = binding.moduleName === "@brandlab-digital/site-next/contact/client" &&
|
|
1332
|
-
binding.exportName === "createContactHttpSubmissionController";
|
|
1333
|
-
if (isOutputOwned(node) ||
|
|
1334
|
-
(isSharedContactClientController && isContactClientRuntimeOwned(node))) {
|
|
1335
|
-
outputOwnedImports.add(binding.moduleName);
|
|
1336
|
-
addExportUsage(outputOwnedExports, binding.moduleName, binding.exportName);
|
|
1337
|
-
}
|
|
1338
|
-
const frameworkOwner = nextFrameworkOwnerForImport(binding.moduleName, binding.exportName);
|
|
1339
|
-
if (frameworkOwner && frameworkOwnership[frameworkOwner](node)) {
|
|
1340
|
-
addExportUsage(frameworkOwnedExports, binding.moduleName, binding.exportName);
|
|
1341
|
-
}
|
|
1342
|
-
};
|
|
1343
|
-
const visit = (node) => {
|
|
1344
|
-
if (ts.isCallExpression(node) || ts.isNewExpression(node)) {
|
|
1345
|
-
const binding = resolveImport(node.expression);
|
|
1346
|
-
if (ts.isCallExpression(node) &&
|
|
1347
|
-
binding?.moduleName === "@brandlab-digital/site-core/locations" &&
|
|
1348
|
-
binding.exportName === "defineLocationRouteQuality" &&
|
|
1349
|
-
node.arguments.length !== 2) {
|
|
1350
|
-
const location = source.getLineAndCharacterOfPosition(node.getStart(source));
|
|
1351
|
-
issues.push(configIssue({
|
|
1352
|
-
ruleId: "brandlab-config-v3-location-proof-binding",
|
|
1353
|
-
surface: "location-proof",
|
|
1354
|
-
expected: "defineLocationRouteQuality(input, business.proof) with a verified, explicit proof fact.",
|
|
1355
|
-
actual: `The location-quality call received ${node.arguments.length} argument(s).`,
|
|
1356
|
-
remediation: "Bind location proof IDs to the canonical business.proof fact; use unknown or not-applicable with an empty proofAssetIds list when proof is unavailable.",
|
|
1357
|
-
path: relativePath,
|
|
1358
|
-
line: location.line + 1,
|
|
1359
|
-
}));
|
|
1360
|
-
}
|
|
1361
|
-
markExecutableImport(node.expression, node);
|
|
1362
|
-
}
|
|
1363
|
-
else if (ts.isTaggedTemplateExpression(node)) {
|
|
1364
|
-
markExecutableImport(node.tag, node);
|
|
1365
|
-
}
|
|
1366
|
-
else if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
|
|
1367
|
-
markExecutableImport(node.tagName, node);
|
|
1368
|
-
}
|
|
1369
|
-
ts.forEachChild(node, visit);
|
|
1370
|
-
};
|
|
1371
|
-
ts.forEachChild(source, visit);
|
|
1372
|
-
if (getParseDiagnostics(source).length > 0) {
|
|
1373
|
-
issues.push(configIssue({
|
|
1374
|
-
ruleId: "brandlab-config-v3-source-syntax",
|
|
1375
|
-
surface: "architecture",
|
|
1376
|
-
expected: "Statically parseable project source.",
|
|
1377
|
-
actual: `${relativePath} contains a syntax error.`,
|
|
1378
|
-
remediation: "Correct source syntax before conformance evaluation.",
|
|
1379
|
-
path: relativePath,
|
|
1380
|
-
}));
|
|
1381
|
-
}
|
|
1382
|
-
}
|
|
1383
|
-
return {
|
|
1384
|
-
imports,
|
|
1385
|
-
executableImports,
|
|
1386
|
-
outputOwnedImports,
|
|
1387
|
-
executableExports,
|
|
1388
|
-
outputOwnedExports,
|
|
1389
|
-
frameworkOwnedExports,
|
|
1390
|
-
};
|
|
1391
|
-
}
|
|
1392
|
-
/**
|
|
1393
|
-
* Returns whether an identifier is shadowed by a runtime value declaration
|
|
1394
|
-
* between its use site and an owning boundary. Imports are intentionally not
|
|
1395
|
-
* indexed: the caller already owns the exact import binding being proved.
|
|
1396
|
-
*/
|
|
1397
|
-
function createLexicalShadowInspector(source) {
|
|
1398
|
-
const namesByScope = new Map();
|
|
1399
|
-
const addName = (scope, name) => {
|
|
1400
|
-
const names = namesByScope.get(scope) ?? new Set();
|
|
1401
|
-
names.add(name);
|
|
1402
|
-
namesByScope.set(scope, names);
|
|
1403
|
-
};
|
|
1404
|
-
const addBindingName = (name, scope) => {
|
|
1405
|
-
if (ts.isIdentifier(name)) {
|
|
1406
|
-
addName(scope, name.text);
|
|
1407
|
-
return;
|
|
1408
|
-
}
|
|
1409
|
-
for (const element of name.elements) {
|
|
1410
|
-
if (ts.isBindingElement(element))
|
|
1411
|
-
addBindingName(element.name, scope);
|
|
1412
|
-
}
|
|
1413
|
-
};
|
|
1414
|
-
const collect = (node) => {
|
|
1415
|
-
if (ts.isVariableDeclaration(node) && !ts.isCatchClause(node.parent)) {
|
|
1416
|
-
addBindingName(node.name, variableDeclarationScope(node, source));
|
|
1417
|
-
}
|
|
1418
|
-
else if ((ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node)) &&
|
|
1419
|
-
node.name) {
|
|
1420
|
-
addName(nearestLexicalScope(node, source), node.name.text);
|
|
1421
|
-
}
|
|
1422
|
-
else if ((ts.isFunctionExpression(node) || ts.isClassExpression(node)) &&
|
|
1423
|
-
node.name) {
|
|
1424
|
-
addName(node, node.name.text);
|
|
1425
|
-
}
|
|
1426
|
-
else if (ts.isParameter(node)) {
|
|
1427
|
-
const owner = nearestFunctionLike(node);
|
|
1428
|
-
const scope = owner?.body && ts.isBlock(owner.body) ? owner.body : owner ?? source;
|
|
1429
|
-
addBindingName(node.name, scope);
|
|
1430
|
-
}
|
|
1431
|
-
else if (ts.isCatchClause(node) && node.variableDeclaration) {
|
|
1432
|
-
addBindingName(node.variableDeclaration.name, node.block);
|
|
1433
|
-
}
|
|
1434
|
-
ts.forEachChild(node, collect);
|
|
1435
|
-
};
|
|
1436
|
-
collect(source);
|
|
1437
|
-
return (identifier, stopAt) => {
|
|
1438
|
-
let current = identifier.parent;
|
|
1439
|
-
while (current) {
|
|
1440
|
-
if (namesByScope.get(current)?.has(identifier.text))
|
|
1441
|
-
return true;
|
|
1442
|
-
if (current === stopAt)
|
|
1443
|
-
return false;
|
|
1444
|
-
current = current.parent;
|
|
1445
|
-
}
|
|
1446
|
-
return false;
|
|
1447
|
-
};
|
|
1448
|
-
}
|
|
1449
|
-
/**
|
|
1450
|
-
* Finds values that statically flow into an exported module result or a return
|
|
1451
|
-
* value of an exported runtime entry point. Merely assigning a candidate call
|
|
1452
|
-
* to a local variable (or writing `void candidate`) is deliberately not a
|
|
1453
|
-
* runtime-output proof.
|
|
1454
|
-
*/
|
|
1455
|
-
function createOutputOwnershipInspector(source, includeExport = () => true, getProvenanceKey = () => null) {
|
|
1456
|
-
const bindingsByScope = new Map();
|
|
1457
|
-
const outputRegions = new Set();
|
|
1458
|
-
const pendingBindings = [];
|
|
1459
|
-
const visitedBindingUses = new Map();
|
|
1460
|
-
const writesByBinding = new Map();
|
|
1461
|
-
const addBinding = (binding, name) => {
|
|
1462
|
-
const scopeBindings = bindingsByScope.get(binding.scope) ?? new Map();
|
|
1463
|
-
if (!scopeBindings.has(name))
|
|
1464
|
-
scopeBindings.set(name, binding);
|
|
1465
|
-
bindingsByScope.set(binding.scope, scopeBindings);
|
|
1466
|
-
};
|
|
1467
|
-
const collectBindings = (node) => {
|
|
1468
|
-
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name)) {
|
|
1469
|
-
addBinding({ declaration: node, scope: variableDeclarationScope(node, source) }, node.name.text);
|
|
1470
|
-
}
|
|
1471
|
-
else if (ts.isFunctionDeclaration(node) && node.name) {
|
|
1472
|
-
addBinding({ declaration: node, scope: nearestLexicalScope(node, source) }, node.name.text);
|
|
1473
|
-
}
|
|
1474
|
-
else if (ts.isParameter(node) && ts.isIdentifier(node.name)) {
|
|
1475
|
-
const owner = nearestFunctionLike(node);
|
|
1476
|
-
const scope = owner && owner.body && ts.isBlock(owner.body) ? owner.body : owner ?? source;
|
|
1477
|
-
addBinding({ declaration: node, scope }, node.name.text);
|
|
1478
|
-
}
|
|
1479
|
-
ts.forEachChild(node, collectBindings);
|
|
1480
|
-
};
|
|
1481
|
-
collectBindings(source);
|
|
1482
|
-
const resolveAt = (identifier) => {
|
|
1483
|
-
let current = identifier.parent;
|
|
1484
|
-
while (current) {
|
|
1485
|
-
const binding = bindingsByScope.get(current)?.get(identifier.text);
|
|
1486
|
-
if (binding)
|
|
1487
|
-
return binding;
|
|
1488
|
-
current = current.parent;
|
|
1489
|
-
}
|
|
1490
|
-
return undefined;
|
|
1491
|
-
};
|
|
1492
|
-
const resolveTopLevel = (name) => bindingsByScope.get(source)?.get(name);
|
|
1493
|
-
const addWrite = (binding, node, value) => {
|
|
1494
|
-
if (!binding)
|
|
1495
|
-
return;
|
|
1496
|
-
const writes = writesByBinding.get(binding) ?? [];
|
|
1497
|
-
writes.push({
|
|
1498
|
-
node,
|
|
1499
|
-
value,
|
|
1500
|
-
executionContext: nearestFunctionLike(node) ?? source,
|
|
1501
|
-
});
|
|
1502
|
-
writesByBinding.set(binding, writes);
|
|
1503
|
-
};
|
|
1504
|
-
const collectWrites = (node) => {
|
|
1505
|
-
if (ts.isBinaryExpression(node) &&
|
|
1506
|
-
node.operatorToken.kind >= ts.SyntaxKind.FirstAssignment &&
|
|
1507
|
-
node.operatorToken.kind <= ts.SyntaxKind.LastAssignment) {
|
|
1508
|
-
const target = unwrapExpression(node.left);
|
|
1509
|
-
if (ts.isIdentifier(target)) {
|
|
1510
|
-
addWrite(resolveAt(target), node, node.operatorToken.kind === ts.SyntaxKind.EqualsToken ? node.right : null);
|
|
1511
|
-
}
|
|
1512
|
-
}
|
|
1513
|
-
else if ((ts.isPrefixUnaryExpression(node) || ts.isPostfixUnaryExpression(node)) &&
|
|
1514
|
-
(node.operator === ts.SyntaxKind.PlusPlusToken ||
|
|
1515
|
-
node.operator === ts.SyntaxKind.MinusMinusToken)) {
|
|
1516
|
-
const target = unwrapExpression(node.operand);
|
|
1517
|
-
if (ts.isIdentifier(target))
|
|
1518
|
-
addWrite(resolveAt(target), node, null);
|
|
1519
|
-
}
|
|
1520
|
-
ts.forEachChild(node, collectWrites);
|
|
1521
|
-
};
|
|
1522
|
-
collectWrites(source);
|
|
1523
|
-
const enqueue = (binding, executeFunction = false, useNode = source, allowedProvenanceKeys = null) => {
|
|
1524
|
-
if (!binding)
|
|
1525
|
-
return;
|
|
1526
|
-
pendingBindings.push({ binding, executeFunction, useNode, allowedProvenanceKeys });
|
|
1527
|
-
};
|
|
1528
|
-
const provenanceKeyForNode = (node) => {
|
|
1529
|
-
if (ts.isCallExpression(node) || ts.isNewExpression(node)) {
|
|
1530
|
-
return getProvenanceKey(node.expression);
|
|
1531
|
-
}
|
|
1532
|
-
if (ts.isTaggedTemplateExpression(node))
|
|
1533
|
-
return getProvenanceKey(node.tag);
|
|
1534
|
-
if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
|
|
1535
|
-
return getProvenanceKey(node.tagName);
|
|
1536
|
-
}
|
|
1537
|
-
return null;
|
|
1538
|
-
};
|
|
1539
|
-
const intersectProvenanceKeys = (left, right, allowed = null) => {
|
|
1540
|
-
const intersection = new Set();
|
|
1541
|
-
for (const key of left) {
|
|
1542
|
-
if (right.has(key) && (!allowed || allowed.has(key)))
|
|
1543
|
-
intersection.add(key);
|
|
1544
|
-
}
|
|
1545
|
-
return intersection;
|
|
1546
|
-
};
|
|
1547
|
-
const collectProvenanceKeys = (node) => {
|
|
1548
|
-
const keys = new Set();
|
|
1549
|
-
const visit = (child) => {
|
|
1550
|
-
if (ts.isTypeNode(child))
|
|
1551
|
-
return;
|
|
1552
|
-
const value = ts.isExpression(child) ? unwrapExpression(child) : child;
|
|
1553
|
-
const key = provenanceKeyForNode(value);
|
|
1554
|
-
if (key)
|
|
1555
|
-
keys.add(key);
|
|
1556
|
-
if (ts.isVoidExpression(value))
|
|
1557
|
-
return;
|
|
1558
|
-
if (ts.isConditionalExpression(value)) {
|
|
1559
|
-
const condition = staticTruthiness(value.condition);
|
|
1560
|
-
if (condition === true)
|
|
1561
|
-
visit(value.whenTrue);
|
|
1562
|
-
else if (condition === false)
|
|
1563
|
-
visit(value.whenFalse);
|
|
1564
|
-
else {
|
|
1565
|
-
const common = intersectProvenanceKeys(collectProvenanceKeys(value.whenTrue), collectProvenanceKeys(value.whenFalse));
|
|
1566
|
-
for (const sharedKey of common)
|
|
1567
|
-
keys.add(sharedKey);
|
|
1568
|
-
}
|
|
1569
|
-
return;
|
|
1570
|
-
}
|
|
1571
|
-
if (ts.isBinaryExpression(value)) {
|
|
1572
|
-
if (value.operatorToken.kind === ts.SyntaxKind.CommaToken) {
|
|
1573
|
-
visit(value.right);
|
|
1574
|
-
return;
|
|
1575
|
-
}
|
|
1576
|
-
if (value.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) {
|
|
1577
|
-
const left = staticTruthiness(value.left);
|
|
1578
|
-
if (left === false)
|
|
1579
|
-
visit(value.left);
|
|
1580
|
-
else if (left === true)
|
|
1581
|
-
visit(value.right);
|
|
1582
|
-
else {
|
|
1583
|
-
const common = intersectProvenanceKeys(collectProvenanceKeys(value.left), collectProvenanceKeys(value.right));
|
|
1584
|
-
for (const sharedKey of common)
|
|
1585
|
-
keys.add(sharedKey);
|
|
1586
|
-
}
|
|
1587
|
-
return;
|
|
1588
|
-
}
|
|
1589
|
-
if (value.operatorToken.kind === ts.SyntaxKind.BarBarToken) {
|
|
1590
|
-
const left = staticTruthiness(value.left);
|
|
1591
|
-
if (left === true)
|
|
1592
|
-
visit(value.left);
|
|
1593
|
-
else if (left === false)
|
|
1594
|
-
visit(value.right);
|
|
1595
|
-
else {
|
|
1596
|
-
const common = intersectProvenanceKeys(collectProvenanceKeys(value.left), collectProvenanceKeys(value.right));
|
|
1597
|
-
for (const sharedKey of common)
|
|
1598
|
-
keys.add(sharedKey);
|
|
1599
|
-
}
|
|
1600
|
-
return;
|
|
1601
|
-
}
|
|
1602
|
-
if (value.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken) {
|
|
1603
|
-
const left = staticNullishness(value.left);
|
|
1604
|
-
if (left === false)
|
|
1605
|
-
visit(value.left);
|
|
1606
|
-
else if (left === true)
|
|
1607
|
-
visit(value.right);
|
|
1608
|
-
else {
|
|
1609
|
-
const common = intersectProvenanceKeys(collectProvenanceKeys(value.left), collectProvenanceKeys(value.right));
|
|
1610
|
-
for (const sharedKey of common)
|
|
1611
|
-
keys.add(sharedKey);
|
|
1612
|
-
}
|
|
1613
|
-
return;
|
|
1614
|
-
}
|
|
1615
|
-
}
|
|
1616
|
-
if (ts.isArrowFunction(value) || ts.isFunctionExpression(value))
|
|
1617
|
-
return;
|
|
1618
|
-
ts.forEachChild(value, visit);
|
|
1619
|
-
};
|
|
1620
|
-
visit(node);
|
|
1621
|
-
return keys;
|
|
1622
|
-
};
|
|
1623
|
-
const addOutputRegion = (node, executeFunction = false, allowedProvenanceKeys = null) => {
|
|
1624
|
-
const visit = (child, executeNestedFunction = false) => {
|
|
1625
|
-
if (ts.isTypeNode(child))
|
|
1626
|
-
return;
|
|
1627
|
-
const value = ts.isExpression(child) ? unwrapExpression(child) : child;
|
|
1628
|
-
const provenanceKey = provenanceKeyForNode(value);
|
|
1629
|
-
if (!provenanceKey ||
|
|
1630
|
-
!allowedProvenanceKeys ||
|
|
1631
|
-
allowedProvenanceKeys.has(provenanceKey))
|
|
1632
|
-
outputRegions.add(value);
|
|
1633
|
-
if (ts.isIdentifier(value) && isRuntimeIdentifierReference(value)) {
|
|
1634
|
-
enqueue(resolveAt(value), executeNestedFunction, value, allowedProvenanceKeys);
|
|
1635
|
-
}
|
|
1636
|
-
if (ts.isVoidExpression(value))
|
|
1637
|
-
return;
|
|
1638
|
-
if (ts.isConditionalExpression(value)) {
|
|
1639
|
-
const condition = staticTruthiness(value.condition);
|
|
1640
|
-
if (condition === true)
|
|
1641
|
-
visit(value.whenTrue);
|
|
1642
|
-
else if (condition === false)
|
|
1643
|
-
visit(value.whenFalse);
|
|
1644
|
-
else {
|
|
1645
|
-
const common = intersectProvenanceKeys(collectProvenanceKeys(value.whenTrue), collectProvenanceKeys(value.whenFalse), allowedProvenanceKeys);
|
|
1646
|
-
addOutputRegion(value.whenTrue, executeNestedFunction, common);
|
|
1647
|
-
addOutputRegion(value.whenFalse, executeNestedFunction, common);
|
|
1648
|
-
}
|
|
1649
|
-
return;
|
|
1650
|
-
}
|
|
1651
|
-
if (ts.isBinaryExpression(value)) {
|
|
1652
|
-
if (value.operatorToken.kind === ts.SyntaxKind.CommaToken) {
|
|
1653
|
-
visit(value.right);
|
|
1654
|
-
return;
|
|
1655
|
-
}
|
|
1656
|
-
if (value.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) {
|
|
1657
|
-
const left = staticTruthiness(value.left);
|
|
1658
|
-
if (left === false)
|
|
1659
|
-
visit(value.left);
|
|
1660
|
-
else if (left === true)
|
|
1661
|
-
visit(value.right);
|
|
1662
|
-
else {
|
|
1663
|
-
const common = intersectProvenanceKeys(collectProvenanceKeys(value.left), collectProvenanceKeys(value.right), allowedProvenanceKeys);
|
|
1664
|
-
addOutputRegion(value.left, executeNestedFunction, common);
|
|
1665
|
-
addOutputRegion(value.right, executeNestedFunction, common);
|
|
1666
|
-
}
|
|
1667
|
-
return;
|
|
1668
|
-
}
|
|
1669
|
-
if (value.operatorToken.kind === ts.SyntaxKind.BarBarToken) {
|
|
1670
|
-
const left = staticTruthiness(value.left);
|
|
1671
|
-
if (left === true)
|
|
1672
|
-
visit(value.left);
|
|
1673
|
-
else if (left === false)
|
|
1674
|
-
visit(value.right);
|
|
1675
|
-
else {
|
|
1676
|
-
const common = intersectProvenanceKeys(collectProvenanceKeys(value.left), collectProvenanceKeys(value.right), allowedProvenanceKeys);
|
|
1677
|
-
addOutputRegion(value.left, executeNestedFunction, common);
|
|
1678
|
-
addOutputRegion(value.right, executeNestedFunction, common);
|
|
1679
|
-
}
|
|
1680
|
-
return;
|
|
1681
|
-
}
|
|
1682
|
-
if (value.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken) {
|
|
1683
|
-
const left = staticNullishness(value.left);
|
|
1684
|
-
if (left === false)
|
|
1685
|
-
visit(value.left);
|
|
1686
|
-
else if (left === true)
|
|
1687
|
-
visit(value.right);
|
|
1688
|
-
else {
|
|
1689
|
-
const common = intersectProvenanceKeys(collectProvenanceKeys(value.left), collectProvenanceKeys(value.right), allowedProvenanceKeys);
|
|
1690
|
-
addOutputRegion(value.left, executeNestedFunction, common);
|
|
1691
|
-
addOutputRegion(value.right, executeNestedFunction, common);
|
|
1692
|
-
}
|
|
1693
|
-
return;
|
|
1694
|
-
}
|
|
1695
|
-
}
|
|
1696
|
-
if (ts.isArrowFunction(value) || ts.isFunctionExpression(value)) {
|
|
1697
|
-
if (executeNestedFunction) {
|
|
1698
|
-
for (const output of collectFunctionOutputValues(value))
|
|
1699
|
-
visit(output);
|
|
1700
|
-
}
|
|
1701
|
-
return;
|
|
1702
|
-
}
|
|
1703
|
-
if (ts.isObjectLiteralExpression(value)) {
|
|
1704
|
-
for (const property of value.properties) {
|
|
1705
|
-
if (ts.isPropertyAssignment(property)) {
|
|
1706
|
-
visit(property.initializer, executeNestedFunction);
|
|
1707
|
-
}
|
|
1708
|
-
else if (ts.isShorthandPropertyAssignment(property))
|
|
1709
|
-
visit(property.name);
|
|
1710
|
-
else if (ts.isSpreadAssignment(property))
|
|
1711
|
-
visit(property.expression);
|
|
1712
|
-
else if (ts.isMethodDeclaration(property) ||
|
|
1713
|
-
ts.isGetAccessorDeclaration(property) ||
|
|
1714
|
-
ts.isSetAccessorDeclaration(property)) {
|
|
1715
|
-
if (executeNestedFunction) {
|
|
1716
|
-
for (const output of collectFunctionOutputValues(property))
|
|
1717
|
-
visit(output);
|
|
1718
|
-
}
|
|
1719
|
-
}
|
|
1720
|
-
}
|
|
1721
|
-
return;
|
|
1722
|
-
}
|
|
1723
|
-
if (ts.isFunctionDeclaration(value)) {
|
|
1724
|
-
if (executeNestedFunction) {
|
|
1725
|
-
for (const output of collectFunctionOutputValues(value))
|
|
1726
|
-
visit(output);
|
|
1727
|
-
}
|
|
1728
|
-
return;
|
|
1729
|
-
}
|
|
1730
|
-
if (ts.isCallExpression(value) || ts.isNewExpression(value)) {
|
|
1731
|
-
visit(value.expression, true);
|
|
1732
|
-
for (const argument of value.arguments ?? [])
|
|
1733
|
-
visit(argument);
|
|
1734
|
-
return;
|
|
1735
|
-
}
|
|
1736
|
-
if (ts.isTaggedTemplateExpression(value)) {
|
|
1737
|
-
visit(value.tag, true);
|
|
1738
|
-
visit(value.template);
|
|
1739
|
-
return;
|
|
1740
|
-
}
|
|
1741
|
-
if (ts.isJsxOpeningElement(value) || ts.isJsxSelfClosingElement(value)) {
|
|
1742
|
-
visit(value.tagName, true);
|
|
1743
|
-
visit(value.attributes);
|
|
1744
|
-
return;
|
|
1745
|
-
}
|
|
1746
|
-
ts.forEachChild(value, visit);
|
|
1747
|
-
};
|
|
1748
|
-
visit(node, executeFunction);
|
|
1749
|
-
};
|
|
1750
|
-
const addOutputValue = (node) => addOutputRegion(node, true);
|
|
1751
|
-
for (const statement of source.statements) {
|
|
1752
|
-
if (ts.isVariableStatement(statement) && hasModifier(statement, ts.SyntaxKind.ExportKeyword)) {
|
|
1753
|
-
for (const declaration of statement.declarationList.declarations) {
|
|
1754
|
-
if (ts.isIdentifier(declaration.name) &&
|
|
1755
|
-
includeExport({ kind: "named", name: declaration.name.text })) {
|
|
1756
|
-
enqueue(resolveTopLevel(declaration.name.text), true, source);
|
|
1757
|
-
}
|
|
1758
|
-
}
|
|
1759
|
-
}
|
|
1760
|
-
else if (ts.isFunctionDeclaration(statement) &&
|
|
1761
|
-
hasModifier(statement, ts.SyntaxKind.ExportKeyword)) {
|
|
1762
|
-
const isDefault = hasModifier(statement, ts.SyntaxKind.DefaultKeyword);
|
|
1763
|
-
const descriptor = {
|
|
1764
|
-
kind: isDefault ? "default" : "named",
|
|
1765
|
-
name: isDefault ? "default" : statement.name?.text ?? "anonymous",
|
|
1766
|
-
};
|
|
1767
|
-
if (includeExport(descriptor)) {
|
|
1768
|
-
if (statement.name)
|
|
1769
|
-
enqueue(resolveTopLevel(statement.name.text), true, source);
|
|
1770
|
-
else
|
|
1771
|
-
for (const output of collectFunctionOutputValues(statement))
|
|
1772
|
-
addOutputRegion(output);
|
|
1773
|
-
}
|
|
1774
|
-
}
|
|
1775
|
-
else if (ts.isExportAssignment(statement)) {
|
|
1776
|
-
if (includeExport({ kind: "default", name: "default" })) {
|
|
1777
|
-
const expression = unwrapExpression(statement.expression);
|
|
1778
|
-
if (ts.isIdentifier(expression))
|
|
1779
|
-
enqueue(resolveTopLevel(expression.text), true, source);
|
|
1780
|
-
else
|
|
1781
|
-
addOutputValue(expression);
|
|
1782
|
-
}
|
|
1783
|
-
}
|
|
1784
|
-
else if (ts.isExportDeclaration(statement) &&
|
|
1785
|
-
!statement.moduleSpecifier &&
|
|
1786
|
-
statement.exportClause &&
|
|
1787
|
-
ts.isNamedExports(statement.exportClause)) {
|
|
1788
|
-
for (const element of statement.exportClause.elements) {
|
|
1789
|
-
const exportedName = element.name.text;
|
|
1790
|
-
if (includeExport({
|
|
1791
|
-
kind: exportedName === "default" ? "default" : "named",
|
|
1792
|
-
name: exportedName,
|
|
1793
|
-
})) {
|
|
1794
|
-
enqueue(resolveTopLevel((element.propertyName ?? element.name).text), true, source);
|
|
1795
|
-
}
|
|
1796
|
-
}
|
|
1797
|
-
}
|
|
1798
|
-
}
|
|
1799
|
-
while (pendingBindings.length > 0) {
|
|
1800
|
-
const { binding, executeFunction, useNode, allowedProvenanceKeys, } = pendingBindings.shift();
|
|
1801
|
-
const usePosition = useNode === source ? source.end : useNode.getStart(source);
|
|
1802
|
-
const useContext = nearestFunctionLike(useNode) ?? source;
|
|
1803
|
-
const allowedKey = allowedProvenanceKeys
|
|
1804
|
-
? [...allowedProvenanceKeys].sort().join(",")
|
|
1805
|
-
: "*";
|
|
1806
|
-
const visitKey = `${executeFunction ? 1 : 0}:${usePosition}:${useContext.pos}:${allowedKey}`;
|
|
1807
|
-
const visitedUses = visitedBindingUses.get(binding) ?? new Set();
|
|
1808
|
-
if (visitedUses.has(visitKey))
|
|
1809
|
-
continue;
|
|
1810
|
-
visitedUses.add(visitKey);
|
|
1811
|
-
visitedBindingUses.set(binding, visitedUses);
|
|
1812
|
-
const declarationContext = nearestFunctionLike(binding.declaration) ?? source;
|
|
1813
|
-
const relevantWrites = (writesByBinding.get(binding) ?? []).filter((write) => write.node.getStart(source) < usePosition &&
|
|
1814
|
-
isStaticallyReachableExecutionNode(write.node, write.executionContext) &&
|
|
1815
|
-
(write.executionContext === useContext || write.executionContext === declarationContext));
|
|
1816
|
-
const declarationAfterUse = ts.isVariableDeclaration(binding.declaration) &&
|
|
1817
|
-
binding.declaration.getStart(source) > usePosition;
|
|
1818
|
-
const initialDefinitions = declarationAfterUse
|
|
1819
|
-
? []
|
|
1820
|
-
: bindingRuntimeOutputValues(binding.declaration);
|
|
1821
|
-
const declarationContextForReachability = nearestFunctionLike(binding.declaration) ?? source;
|
|
1822
|
-
const conditionallyInitialized = ts.isVariableDeclaration(binding.declaration) &&
|
|
1823
|
-
initialDefinitions.length > 0 &&
|
|
1824
|
-
isConditionallyExecutedNode(binding.declaration, declarationContextForReachability);
|
|
1825
|
-
const definitions = conditionallyInitialized
|
|
1826
|
-
? [null, ...initialDefinitions]
|
|
1827
|
-
: initialDefinitions.length > 0
|
|
1828
|
-
? [...initialDefinitions]
|
|
1829
|
-
: [null];
|
|
1830
|
-
for (const write of relevantWrites.sort((left, right) => left.node.getStart(source) - right.node.getStart(source))) {
|
|
1831
|
-
if (isConditionallyExecutedNode(write.node, write.executionContext)) {
|
|
1832
|
-
definitions.push(write.value);
|
|
1833
|
-
}
|
|
1834
|
-
else {
|
|
1835
|
-
definitions.splice(0, definitions.length, write.value);
|
|
1836
|
-
}
|
|
1837
|
-
}
|
|
1838
|
-
let definitionKeys = allowedProvenanceKeys;
|
|
1839
|
-
if (definitions.length > 1) {
|
|
1840
|
-
const keySets = definitions.map((definition) => definition ? collectProvenanceKeys(definition) : new Set());
|
|
1841
|
-
const intersection = new Set(keySets[0]);
|
|
1842
|
-
for (const key of intersection) {
|
|
1843
|
-
if (!keySets.slice(1).every((keys) => keys.has(key)))
|
|
1844
|
-
intersection.delete(key);
|
|
1845
|
-
}
|
|
1846
|
-
if (allowedProvenanceKeys) {
|
|
1847
|
-
for (const key of intersection) {
|
|
1848
|
-
if (!allowedProvenanceKeys.has(key))
|
|
1849
|
-
intersection.delete(key);
|
|
1850
|
-
}
|
|
1851
|
-
}
|
|
1852
|
-
if (intersection.size === 0)
|
|
1853
|
-
continue;
|
|
1854
|
-
definitionKeys = intersection;
|
|
1855
|
-
}
|
|
1856
|
-
for (const output of definitions) {
|
|
1857
|
-
if (output)
|
|
1858
|
-
addOutputRegion(output, executeFunction, definitionKeys);
|
|
1859
|
-
}
|
|
1860
|
-
}
|
|
1861
|
-
return (node) => outputRegions.has(node);
|
|
1862
|
-
}
|
|
1863
|
-
function createContactClientRuntimeOwnershipInspector(source, isOutputOwned) {
|
|
1864
|
-
const reactUseMemo = collectReactUseMemoImports(source);
|
|
1865
|
-
return (candidate) => {
|
|
1866
|
-
if (!ts.isCallExpression(candidate))
|
|
1867
|
-
return false;
|
|
1868
|
-
const memoCallback = nearestFunctionLike(candidate);
|
|
1869
|
-
if (!memoCallback ||
|
|
1870
|
-
!collectFunctionOutputValues(memoCallback).some((output) => ts.isExpression(output) && unwrapExpression(output) === candidate))
|
|
1871
|
-
return false;
|
|
1872
|
-
const callbackValue = outerExpressionWrapper(memoCallback);
|
|
1873
|
-
const memoCall = callbackValue.parent;
|
|
1874
|
-
if (!ts.isCallExpression(memoCall) ||
|
|
1875
|
-
memoCall.arguments.length < 1 ||
|
|
1876
|
-
unwrapExpression(memoCall.arguments[0]) !== memoCallback ||
|
|
1877
|
-
!isReactUseMemoCallee(memoCall.expression, reactUseMemo))
|
|
1878
|
-
return false;
|
|
1879
|
-
const memoValue = outerExpressionWrapper(memoCall);
|
|
1880
|
-
const controllerDeclaration = memoValue.parent;
|
|
1881
|
-
if (!ts.isVariableDeclaration(controllerDeclaration) ||
|
|
1882
|
-
!ts.isIdentifier(controllerDeclaration.name) ||
|
|
1883
|
-
!controllerDeclaration.initializer ||
|
|
1884
|
-
unwrapExpression(controllerDeclaration.initializer) !== memoCall)
|
|
1885
|
-
return false;
|
|
1886
|
-
const component = nearestFunctionLike(controllerDeclaration);
|
|
1887
|
-
if (!component?.body)
|
|
1888
|
-
return false;
|
|
1889
|
-
return returnedFormHandlerUsesController(component, controllerDeclaration, controllerDeclaration.name.text, isOutputOwned);
|
|
1890
|
-
};
|
|
1891
|
-
}
|
|
1892
|
-
function collectReactUseMemoImports(source) {
|
|
1893
|
-
const named = new Set();
|
|
1894
|
-
const namespaces = new Set();
|
|
1895
|
-
for (const statement of source.statements) {
|
|
1896
|
-
if (!ts.isImportDeclaration(statement) ||
|
|
1897
|
-
!ts.isStringLiteral(statement.moduleSpecifier) ||
|
|
1898
|
-
statement.moduleSpecifier.text !== "react" ||
|
|
1899
|
-
!statement.importClause ||
|
|
1900
|
-
statement.importClause.isTypeOnly)
|
|
1901
|
-
continue;
|
|
1902
|
-
if (statement.importClause.name)
|
|
1903
|
-
namespaces.add(statement.importClause.name.text);
|
|
1904
|
-
const bindings = statement.importClause.namedBindings;
|
|
1905
|
-
if (bindings && ts.isNamespaceImport(bindings)) {
|
|
1906
|
-
namespaces.add(bindings.name.text);
|
|
1907
|
-
}
|
|
1908
|
-
else if (bindings && ts.isNamedImports(bindings)) {
|
|
1909
|
-
for (const element of bindings.elements) {
|
|
1910
|
-
if (!element.isTypeOnly &&
|
|
1911
|
-
(element.propertyName ?? element.name).text === "useMemo")
|
|
1912
|
-
named.add(element.name.text);
|
|
1913
|
-
}
|
|
1914
|
-
}
|
|
1915
|
-
}
|
|
1916
|
-
return {
|
|
1917
|
-
named,
|
|
1918
|
-
namespaces,
|
|
1919
|
-
isShadowed: createLexicalShadowInspector(source),
|
|
1920
|
-
};
|
|
1921
|
-
}
|
|
1922
|
-
function isReactUseMemoCallee(expression, imports) {
|
|
1923
|
-
const callee = unwrapExpression(expression);
|
|
1924
|
-
if (ts.isIdentifier(callee)) {
|
|
1925
|
-
return imports.named.has(callee.text) && !imports.isShadowed(callee, callee.getSourceFile());
|
|
1926
|
-
}
|
|
1927
|
-
if (ts.isPropertyAccessExpression(callee) &&
|
|
1928
|
-
ts.isIdentifier(callee.expression) &&
|
|
1929
|
-
imports.namespaces.has(callee.expression.text) &&
|
|
1930
|
-
!imports.isShadowed(callee.expression, callee.getSourceFile()))
|
|
1931
|
-
return callee.name.text === "useMemo";
|
|
1932
|
-
return Boolean(ts.isElementAccessExpression(callee) &&
|
|
1933
|
-
ts.isIdentifier(callee.expression) &&
|
|
1934
|
-
imports.namespaces.has(callee.expression.text) &&
|
|
1935
|
-
!imports.isShadowed(callee.expression, callee.getSourceFile()) &&
|
|
1936
|
-
callee.argumentExpression &&
|
|
1937
|
-
ts.isStringLiteral(callee.argumentExpression) &&
|
|
1938
|
-
callee.argumentExpression.text === "useMemo");
|
|
1939
|
-
}
|
|
1940
|
-
function outerExpressionWrapper(node) {
|
|
1941
|
-
let current = node;
|
|
1942
|
-
while (current.parent &&
|
|
1943
|
-
(ts.isParenthesizedExpression(current.parent) ||
|
|
1944
|
-
ts.isAsExpression(current.parent) ||
|
|
1945
|
-
ts.isSatisfiesExpression(current.parent) ||
|
|
1946
|
-
ts.isTypeAssertionExpression(current.parent)) &&
|
|
1947
|
-
current.parent.expression === current)
|
|
1948
|
-
current = current.parent;
|
|
1949
|
-
return current;
|
|
1950
|
-
}
|
|
1951
|
-
function isStaticallyReachableExecutionNode(node, boundary) {
|
|
1952
|
-
let current = node;
|
|
1953
|
-
while (current !== boundary && current.parent) {
|
|
1954
|
-
const parent = current.parent;
|
|
1955
|
-
if (ts.isIfStatement(parent)) {
|
|
1956
|
-
const condition = staticTruthiness(parent.expression);
|
|
1957
|
-
if (current === parent.thenStatement && condition === false)
|
|
1958
|
-
return false;
|
|
1959
|
-
if (current === parent.elseStatement && condition === true)
|
|
1960
|
-
return false;
|
|
1961
|
-
}
|
|
1962
|
-
else if (ts.isConditionalExpression(parent)) {
|
|
1963
|
-
const condition = staticTruthiness(parent.condition);
|
|
1964
|
-
if (current === parent.whenTrue && condition === false)
|
|
1965
|
-
return false;
|
|
1966
|
-
if (current === parent.whenFalse && condition === true)
|
|
1967
|
-
return false;
|
|
1968
|
-
}
|
|
1969
|
-
else if (ts.isBinaryExpression(parent) && current === parent.right) {
|
|
1970
|
-
if (parent.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken &&
|
|
1971
|
-
staticTruthiness(parent.left) === false)
|
|
1972
|
-
return false;
|
|
1973
|
-
if (parent.operatorToken.kind === ts.SyntaxKind.BarBarToken &&
|
|
1974
|
-
staticTruthiness(parent.left) === true)
|
|
1975
|
-
return false;
|
|
1976
|
-
if (parent.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken &&
|
|
1977
|
-
staticNullishness(parent.left) === false)
|
|
1978
|
-
return false;
|
|
1979
|
-
}
|
|
1980
|
-
else if ((ts.isWhileStatement(parent) || ts.isForStatement(parent)) &&
|
|
1981
|
-
current === parent.statement) {
|
|
1982
|
-
const condition = ts.isWhileStatement(parent) ? parent.expression : parent.condition;
|
|
1983
|
-
if (condition && staticTruthiness(condition) === false)
|
|
1984
|
-
return false;
|
|
1985
|
-
}
|
|
1986
|
-
else if (ts.isSwitchStatement(parent) && current === parent.caseBlock) {
|
|
1987
|
-
const entryAnalysis = analyzeStaticSwitchEntries(parent);
|
|
1988
|
-
if (entryAnalysis) {
|
|
1989
|
-
const clauseIndex = parent.caseBlock.clauses.findIndex((clause) => node.pos >= clause.pos && node.end <= clause.end);
|
|
1990
|
-
if (!switchClauseIsReachable(parent, clauseIndex, entryAnalysis.entryIndices))
|
|
1991
|
-
return false;
|
|
1992
|
-
}
|
|
1993
|
-
}
|
|
1994
|
-
else if (ts.isBlock(parent) || ts.isSourceFile(parent)) {
|
|
1995
|
-
const statements = parent.statements;
|
|
1996
|
-
const index = statements.findIndex((statement) => statement === current);
|
|
1997
|
-
if (index >= 0 &&
|
|
1998
|
-
!collectReachableStatementListOutputs(statements.slice(0, index)).canContinue)
|
|
1999
|
-
return false;
|
|
2000
|
-
}
|
|
2001
|
-
current = parent;
|
|
2002
|
-
}
|
|
2003
|
-
return current === boundary;
|
|
2004
|
-
}
|
|
2005
|
-
function analyzeStaticSwitchEntries(statement) {
|
|
2006
|
-
const discriminant = staticPrimitiveValue(statement.expression);
|
|
2007
|
-
if (!discriminant.known)
|
|
2008
|
-
return undefined;
|
|
2009
|
-
const entryIndices = [];
|
|
2010
|
-
let defaultIndex = -1;
|
|
2011
|
-
let exactMatch = false;
|
|
2012
|
-
for (let index = 0; index < statement.caseBlock.clauses.length; index += 1) {
|
|
2013
|
-
const clause = statement.caseBlock.clauses[index];
|
|
2014
|
-
if (ts.isDefaultClause(clause)) {
|
|
2015
|
-
defaultIndex = index;
|
|
2016
|
-
continue;
|
|
2017
|
-
}
|
|
2018
|
-
const candidate = staticPrimitiveValue(clause.expression);
|
|
2019
|
-
if (!candidate.known) {
|
|
2020
|
-
entryIndices.push(index);
|
|
2021
|
-
continue;
|
|
2022
|
-
}
|
|
2023
|
-
if (candidate.value === discriminant.value) {
|
|
2024
|
-
entryIndices.push(index);
|
|
2025
|
-
exactMatch = true;
|
|
2026
|
-
break;
|
|
2027
|
-
}
|
|
2028
|
-
}
|
|
2029
|
-
if (!exactMatch && defaultIndex >= 0)
|
|
2030
|
-
entryIndices.push(defaultIndex);
|
|
2031
|
-
return {
|
|
2032
|
-
entryIndices: [...new Set(entryIndices)].sort((left, right) => left - right),
|
|
2033
|
-
noMatchPossible: !exactMatch && defaultIndex < 0,
|
|
2034
|
-
};
|
|
2035
|
-
}
|
|
2036
|
-
function switchClauseIsReachable(statement, clauseIndex, entryIndices) {
|
|
2037
|
-
if (clauseIndex < 0)
|
|
2038
|
-
return false;
|
|
2039
|
-
return entryIndices.some((entryIndex) => {
|
|
2040
|
-
if (entryIndex > clauseIndex)
|
|
2041
|
-
return false;
|
|
2042
|
-
for (let index = entryIndex; index < clauseIndex; index += 1) {
|
|
2043
|
-
if (!switchClauseCanFallThrough(statement.caseBlock.clauses[index], statement)) {
|
|
2044
|
-
return false;
|
|
2045
|
-
}
|
|
2046
|
-
}
|
|
2047
|
-
return true;
|
|
2048
|
-
});
|
|
2049
|
-
}
|
|
2050
|
-
function switchClauseCanFallThrough(clause, targetSwitch) {
|
|
2051
|
-
for (const statement of clause.statements) {
|
|
2052
|
-
if (statementGuaranteesSwitchExit(statement, targetSwitch))
|
|
2053
|
-
return false;
|
|
2054
|
-
}
|
|
2055
|
-
return true;
|
|
2056
|
-
}
|
|
2057
|
-
function statementGuaranteesSwitchExit(statement, targetSwitch) {
|
|
2058
|
-
if (ts.isReturnStatement(statement) || ts.isThrowStatement(statement))
|
|
2059
|
-
return true;
|
|
2060
|
-
if (ts.isBreakStatement(statement))
|
|
2061
|
-
return breakExitsSwitch(statement, targetSwitch);
|
|
2062
|
-
if (ts.isBlock(statement)) {
|
|
2063
|
-
return statement.statements.some((nested) => statementGuaranteesSwitchExit(nested, targetSwitch));
|
|
2064
|
-
}
|
|
2065
|
-
if (ts.isLabeledStatement(statement)) {
|
|
2066
|
-
return statementGuaranteesSwitchExit(statement.statement, targetSwitch);
|
|
2067
|
-
}
|
|
2068
|
-
if (ts.isIfStatement(statement)) {
|
|
2069
|
-
const condition = staticTruthiness(statement.expression);
|
|
2070
|
-
if (condition === true) {
|
|
2071
|
-
return statementGuaranteesSwitchExit(statement.thenStatement, targetSwitch);
|
|
2072
|
-
}
|
|
2073
|
-
if (condition === false) {
|
|
2074
|
-
return Boolean(statement.elseStatement &&
|
|
2075
|
-
statementGuaranteesSwitchExit(statement.elseStatement, targetSwitch));
|
|
2076
|
-
}
|
|
2077
|
-
return Boolean(statement.elseStatement &&
|
|
2078
|
-
statementGuaranteesSwitchExit(statement.thenStatement, targetSwitch) &&
|
|
2079
|
-
statementGuaranteesSwitchExit(statement.elseStatement, targetSwitch));
|
|
2080
|
-
}
|
|
2081
|
-
if (ts.isTryStatement(statement)) {
|
|
2082
|
-
if (statement.finallyBlock &&
|
|
2083
|
-
statementGuaranteesSwitchExit(statement.finallyBlock, targetSwitch))
|
|
2084
|
-
return true;
|
|
2085
|
-
return statementGuaranteesSwitchExit(statement.tryBlock, targetSwitch) &&
|
|
2086
|
-
Boolean(statement.catchClause &&
|
|
2087
|
-
statementGuaranteesSwitchExit(statement.catchClause.block, targetSwitch));
|
|
2088
|
-
}
|
|
2089
|
-
if (ts.isWithStatement(statement)) {
|
|
2090
|
-
return statementGuaranteesSwitchExit(statement.statement, targetSwitch);
|
|
2091
|
-
}
|
|
2092
|
-
return false;
|
|
2093
|
-
}
|
|
2094
|
-
function statementGuaranteesFunctionExit(statement) {
|
|
2095
|
-
if (ts.isReturnStatement(statement) || ts.isThrowStatement(statement))
|
|
2096
|
-
return true;
|
|
2097
|
-
if (ts.isBlock(statement)) {
|
|
2098
|
-
return statement.statements.some(statementGuaranteesFunctionExit);
|
|
2099
|
-
}
|
|
2100
|
-
if (ts.isLabeledStatement(statement) || ts.isWithStatement(statement)) {
|
|
2101
|
-
return statementGuaranteesFunctionExit(statement.statement);
|
|
2102
|
-
}
|
|
2103
|
-
if (ts.isIfStatement(statement)) {
|
|
2104
|
-
const condition = staticTruthiness(statement.expression);
|
|
2105
|
-
if (condition === true)
|
|
2106
|
-
return statementGuaranteesFunctionExit(statement.thenStatement);
|
|
2107
|
-
if (condition === false) {
|
|
2108
|
-
return Boolean(statement.elseStatement &&
|
|
2109
|
-
statementGuaranteesFunctionExit(statement.elseStatement));
|
|
2110
|
-
}
|
|
2111
|
-
return Boolean(statement.elseStatement &&
|
|
2112
|
-
statementGuaranteesFunctionExit(statement.thenStatement) &&
|
|
2113
|
-
statementGuaranteesFunctionExit(statement.elseStatement));
|
|
2114
|
-
}
|
|
2115
|
-
if (ts.isTryStatement(statement)) {
|
|
2116
|
-
if (statement.finallyBlock &&
|
|
2117
|
-
statementGuaranteesFunctionExit(statement.finallyBlock))
|
|
2118
|
-
return true;
|
|
2119
|
-
return statementGuaranteesFunctionExit(statement.tryBlock) &&
|
|
2120
|
-
Boolean(statement.catchClause &&
|
|
2121
|
-
statementGuaranteesFunctionExit(statement.catchClause.block));
|
|
2122
|
-
}
|
|
2123
|
-
return false;
|
|
2124
|
-
}
|
|
2125
|
-
function breakExitsSwitch(statement, targetSwitch) {
|
|
2126
|
-
if (!statement.label) {
|
|
2127
|
-
let current = statement.parent;
|
|
2128
|
-
while (current) {
|
|
2129
|
-
if (ts.isSwitchStatement(current) ||
|
|
2130
|
-
ts.isForStatement(current) ||
|
|
2131
|
-
ts.isForInStatement(current) ||
|
|
2132
|
-
ts.isForOfStatement(current) ||
|
|
2133
|
-
ts.isWhileStatement(current) ||
|
|
2134
|
-
ts.isDoStatement(current))
|
|
2135
|
-
return current === targetSwitch;
|
|
2136
|
-
current = current.parent;
|
|
2137
|
-
}
|
|
2138
|
-
return false;
|
|
2139
|
-
}
|
|
2140
|
-
let current = statement.parent;
|
|
2141
|
-
while (current) {
|
|
2142
|
-
if (ts.isLabeledStatement(current) &&
|
|
2143
|
-
current.label.text === statement.label.text) {
|
|
2144
|
-
return targetSwitch.pos >= current.statement.pos &&
|
|
2145
|
-
targetSwitch.end <= current.statement.end;
|
|
2146
|
-
}
|
|
2147
|
-
current = current.parent;
|
|
2148
|
-
}
|
|
2149
|
-
return false;
|
|
2150
|
-
}
|
|
2151
|
-
function staticPrimitiveValue(node) {
|
|
2152
|
-
const value = unwrapExpression(node);
|
|
2153
|
-
if (value.kind === ts.SyntaxKind.TrueKeyword)
|
|
2154
|
-
return { known: true, value: true };
|
|
2155
|
-
if (value.kind === ts.SyntaxKind.FalseKeyword)
|
|
2156
|
-
return { known: true, value: false };
|
|
2157
|
-
if (value.kind === ts.SyntaxKind.NullKeyword)
|
|
2158
|
-
return { known: true, value: null };
|
|
2159
|
-
if (ts.isStringLiteral(value) || ts.isNoSubstitutionTemplateLiteral(value)) {
|
|
2160
|
-
return { known: true, value: value.text };
|
|
2161
|
-
}
|
|
2162
|
-
if (ts.isNumericLiteral(value))
|
|
2163
|
-
return { known: true, value: Number(value.text) };
|
|
2164
|
-
if (ts.isTemplateExpression(value)) {
|
|
2165
|
-
let text = value.head.text;
|
|
2166
|
-
for (const span of value.templateSpans) {
|
|
2167
|
-
const expression = staticPrimitiveValue(span.expression);
|
|
2168
|
-
if (!expression.known)
|
|
2169
|
-
return { known: false };
|
|
2170
|
-
text += String(expression.value) + span.literal.text;
|
|
2171
|
-
}
|
|
2172
|
-
return { known: true, value: text };
|
|
2173
|
-
}
|
|
2174
|
-
if (ts.isVoidExpression(value)) {
|
|
2175
|
-
return staticPrimitiveValue(value.expression).known
|
|
2176
|
-
? { known: true, value: undefined }
|
|
2177
|
-
: { known: false };
|
|
2178
|
-
}
|
|
2179
|
-
if (ts.isPrefixUnaryExpression(value)) {
|
|
2180
|
-
const operand = staticPrimitiveValue(value.operand);
|
|
2181
|
-
if (!operand.known)
|
|
2182
|
-
return { known: false };
|
|
2183
|
-
if (value.operator === ts.SyntaxKind.PlusToken) {
|
|
2184
|
-
return { known: true, value: Number(operand.value) };
|
|
2185
|
-
}
|
|
2186
|
-
if (value.operator === ts.SyntaxKind.MinusToken) {
|
|
2187
|
-
return { known: true, value: -Number(operand.value) };
|
|
2188
|
-
}
|
|
2189
|
-
if (value.operator === ts.SyntaxKind.ExclamationToken) {
|
|
2190
|
-
return { known: true, value: !primitiveTruthiness(operand.value) };
|
|
2191
|
-
}
|
|
2192
|
-
if (value.operator === ts.SyntaxKind.TildeToken) {
|
|
2193
|
-
return { known: true, value: ~Number(operand.value) };
|
|
2194
|
-
}
|
|
2195
|
-
return { known: false };
|
|
2196
|
-
}
|
|
2197
|
-
if (ts.isConditionalExpression(value)) {
|
|
2198
|
-
const condition = staticPrimitiveValue(value.condition);
|
|
2199
|
-
if (!condition.known)
|
|
2200
|
-
return { known: false };
|
|
2201
|
-
return staticPrimitiveValue(primitiveTruthiness(condition.value) ? value.whenTrue : value.whenFalse);
|
|
2202
|
-
}
|
|
2203
|
-
if (ts.isBinaryExpression(value)) {
|
|
2204
|
-
const left = staticPrimitiveValue(value.left);
|
|
2205
|
-
if (!left.known)
|
|
2206
|
-
return { known: false };
|
|
2207
|
-
if (value.operatorToken.kind === ts.SyntaxKind.CommaToken) {
|
|
2208
|
-
return staticPrimitiveValue(value.right);
|
|
2209
|
-
}
|
|
2210
|
-
if (value.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) {
|
|
2211
|
-
return primitiveTruthiness(left.value)
|
|
2212
|
-
? staticPrimitiveValue(value.right)
|
|
2213
|
-
: left;
|
|
2214
|
-
}
|
|
2215
|
-
if (value.operatorToken.kind === ts.SyntaxKind.BarBarToken) {
|
|
2216
|
-
return primitiveTruthiness(left.value)
|
|
2217
|
-
? left
|
|
2218
|
-
: staticPrimitiveValue(value.right);
|
|
2219
|
-
}
|
|
2220
|
-
if (value.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken) {
|
|
2221
|
-
return left.value === null || left.value === undefined
|
|
2222
|
-
? staticPrimitiveValue(value.right)
|
|
2223
|
-
: left;
|
|
2224
|
-
}
|
|
2225
|
-
const right = staticPrimitiveValue(value.right);
|
|
2226
|
-
if (!right.known)
|
|
2227
|
-
return { known: false };
|
|
2228
|
-
const leftValue = left.value;
|
|
2229
|
-
const rightValue = right.value;
|
|
2230
|
-
switch (value.operatorToken.kind) {
|
|
2231
|
-
case ts.SyntaxKind.PlusToken:
|
|
2232
|
-
return {
|
|
2233
|
-
known: true,
|
|
2234
|
-
value: typeof leftValue === "string" || typeof rightValue === "string"
|
|
2235
|
-
? String(leftValue) + String(rightValue)
|
|
2236
|
-
: Number(leftValue) + Number(rightValue),
|
|
2237
|
-
};
|
|
2238
|
-
case ts.SyntaxKind.MinusToken:
|
|
2239
|
-
return { known: true, value: Number(leftValue) - Number(rightValue) };
|
|
2240
|
-
case ts.SyntaxKind.AsteriskToken:
|
|
2241
|
-
return { known: true, value: Number(leftValue) * Number(rightValue) };
|
|
2242
|
-
case ts.SyntaxKind.SlashToken:
|
|
2243
|
-
return { known: true, value: Number(leftValue) / Number(rightValue) };
|
|
2244
|
-
case ts.SyntaxKind.PercentToken:
|
|
2245
|
-
return { known: true, value: Number(leftValue) % Number(rightValue) };
|
|
2246
|
-
case ts.SyntaxKind.AsteriskAsteriskToken:
|
|
2247
|
-
return { known: true, value: Number(leftValue) ** Number(rightValue) };
|
|
2248
|
-
case ts.SyntaxKind.AmpersandToken:
|
|
2249
|
-
return { known: true, value: Number(leftValue) & Number(rightValue) };
|
|
2250
|
-
case ts.SyntaxKind.BarToken:
|
|
2251
|
-
return { known: true, value: Number(leftValue) | Number(rightValue) };
|
|
2252
|
-
case ts.SyntaxKind.CaretToken:
|
|
2253
|
-
return { known: true, value: Number(leftValue) ^ Number(rightValue) };
|
|
2254
|
-
case ts.SyntaxKind.LessThanLessThanToken:
|
|
2255
|
-
return { known: true, value: Number(leftValue) << Number(rightValue) };
|
|
2256
|
-
case ts.SyntaxKind.GreaterThanGreaterThanToken:
|
|
2257
|
-
return { known: true, value: Number(leftValue) >> Number(rightValue) };
|
|
2258
|
-
case ts.SyntaxKind.GreaterThanGreaterThanGreaterThanToken:
|
|
2259
|
-
return { known: true, value: Number(leftValue) >>> Number(rightValue) };
|
|
2260
|
-
case ts.SyntaxKind.EqualsEqualsEqualsToken:
|
|
2261
|
-
return { known: true, value: leftValue === rightValue };
|
|
2262
|
-
case ts.SyntaxKind.ExclamationEqualsEqualsToken:
|
|
2263
|
-
return { known: true, value: leftValue !== rightValue };
|
|
2264
|
-
case ts.SyntaxKind.EqualsEqualsToken:
|
|
2265
|
-
return { known: true, value: looselyEqualStaticPrimitives(leftValue, rightValue) };
|
|
2266
|
-
case ts.SyntaxKind.ExclamationEqualsToken:
|
|
2267
|
-
return { known: true, value: !looselyEqualStaticPrimitives(leftValue, rightValue) };
|
|
2268
|
-
case ts.SyntaxKind.LessThanToken:
|
|
2269
|
-
return { known: true, value: compareStaticPrimitives(leftValue, rightValue) < 0 };
|
|
2270
|
-
case ts.SyntaxKind.LessThanEqualsToken:
|
|
2271
|
-
return { known: true, value: compareStaticPrimitives(leftValue, rightValue) <= 0 };
|
|
2272
|
-
case ts.SyntaxKind.GreaterThanToken:
|
|
2273
|
-
return { known: true, value: compareStaticPrimitives(leftValue, rightValue) > 0 };
|
|
2274
|
-
case ts.SyntaxKind.GreaterThanEqualsToken:
|
|
2275
|
-
return { known: true, value: compareStaticPrimitives(leftValue, rightValue) >= 0 };
|
|
2276
|
-
default:
|
|
2277
|
-
return { known: false };
|
|
2278
|
-
}
|
|
2279
|
-
}
|
|
2280
|
-
return { known: false };
|
|
2281
|
-
}
|
|
2282
|
-
function primitiveTruthiness(value) {
|
|
2283
|
-
return Boolean(value);
|
|
2284
|
-
}
|
|
2285
|
-
function looselyEqualStaticPrimitives(left, right) {
|
|
2286
|
-
if (left === right)
|
|
2287
|
-
return true;
|
|
2288
|
-
if ((left === null && right === undefined) ||
|
|
2289
|
-
(left === undefined && right === null))
|
|
2290
|
-
return true;
|
|
2291
|
-
if (typeof left === "boolean") {
|
|
2292
|
-
return looselyEqualStaticPrimitives(Number(left), right);
|
|
2293
|
-
}
|
|
2294
|
-
if (typeof right === "boolean") {
|
|
2295
|
-
return looselyEqualStaticPrimitives(left, Number(right));
|
|
2296
|
-
}
|
|
2297
|
-
if (typeof left === "string" && typeof right === "number") {
|
|
2298
|
-
return Number(left) === right;
|
|
2299
|
-
}
|
|
2300
|
-
if (typeof left === "number" && typeof right === "string") {
|
|
2301
|
-
return left === Number(right);
|
|
2302
|
-
}
|
|
2303
|
-
return false;
|
|
2304
|
-
}
|
|
2305
|
-
function compareStaticPrimitives(left, right) {
|
|
2306
|
-
if (typeof left === "string" && typeof right === "string") {
|
|
2307
|
-
return left < right ? -1 : left > right ? 1 : 0;
|
|
2308
|
-
}
|
|
2309
|
-
const leftNumber = Number(left);
|
|
2310
|
-
const rightNumber = Number(right);
|
|
2311
|
-
if (Number.isNaN(leftNumber) || Number.isNaN(rightNumber))
|
|
2312
|
-
return Number.NaN;
|
|
2313
|
-
return leftNumber < rightNumber ? -1 : leftNumber > rightNumber ? 1 : 0;
|
|
2314
|
-
}
|
|
2315
|
-
function isConditionallyExecutedNode(node, boundary) {
|
|
2316
|
-
let current = node;
|
|
2317
|
-
while (current !== boundary && current.parent) {
|
|
2318
|
-
const parent = current.parent;
|
|
2319
|
-
if (ts.isIfStatement(parent)) {
|
|
2320
|
-
const condition = staticTruthiness(parent.expression);
|
|
2321
|
-
if ((current === parent.thenStatement && condition === undefined) ||
|
|
2322
|
-
(current === parent.elseStatement && condition === undefined))
|
|
2323
|
-
return true;
|
|
2324
|
-
}
|
|
2325
|
-
else if (ts.isConditionalExpression(parent)) {
|
|
2326
|
-
if ((current === parent.whenTrue || current === parent.whenFalse) &&
|
|
2327
|
-
staticTruthiness(parent.condition) === undefined)
|
|
2328
|
-
return true;
|
|
2329
|
-
}
|
|
2330
|
-
else if (ts.isBinaryExpression(parent) && current === parent.right) {
|
|
2331
|
-
if ((parent.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken ||
|
|
2332
|
-
parent.operatorToken.kind === ts.SyntaxKind.BarBarToken) &&
|
|
2333
|
-
staticTruthiness(parent.left) === undefined)
|
|
2334
|
-
return true;
|
|
2335
|
-
if (parent.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken &&
|
|
2336
|
-
staticNullishness(parent.left) === undefined)
|
|
2337
|
-
return true;
|
|
2338
|
-
}
|
|
2339
|
-
else if (ts.isWhileStatement(parent) ||
|
|
2340
|
-
ts.isForStatement(parent) ||
|
|
2341
|
-
ts.isForInStatement(parent) ||
|
|
2342
|
-
ts.isForOfStatement(parent) ||
|
|
2343
|
-
ts.isSwitchStatement(parent) ||
|
|
2344
|
-
ts.isTryStatement(parent) ||
|
|
2345
|
-
ts.isCatchClause(parent)) {
|
|
2346
|
-
return true;
|
|
2347
|
-
}
|
|
2348
|
-
current = parent;
|
|
2349
|
-
}
|
|
2350
|
-
return current !== boundary;
|
|
2351
|
-
}
|
|
2352
|
-
function returnedFormHandlerUsesController(component, controllerDeclaration, controllerName, isOutputOwned) {
|
|
2353
|
-
let ownsHandler = false;
|
|
2354
|
-
const visit = (node) => {
|
|
2355
|
-
if (ownsHandler || (node !== component && ts.isFunctionLike(node)))
|
|
2356
|
-
return;
|
|
2357
|
-
if ((ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) &&
|
|
2358
|
-
ts.isIdentifier(node.tagName) &&
|
|
2359
|
-
node.tagName.text === "form" &&
|
|
2360
|
-
isOutputOwned(node)) {
|
|
2361
|
-
const onSubmit = node.attributes.properties.find((property) => ts.isJsxAttribute(property) &&
|
|
2362
|
-
ts.isIdentifier(property.name) &&
|
|
2363
|
-
property.name.text === "onSubmit");
|
|
2364
|
-
if (onSubmit && ts.isJsxAttribute(onSubmit) && onSubmit.initializer &&
|
|
2365
|
-
ts.isJsxExpression(onSubmit.initializer) && onSubmit.initializer.expression) {
|
|
2366
|
-
const handler = resolveComponentHandler(component, onSubmit.initializer.expression);
|
|
2367
|
-
ownsHandler = Boolean(handler &&
|
|
2368
|
-
!hasReachableControllerWriteBefore(component, controllerName, node, controllerDeclaration) &&
|
|
2369
|
-
handlerCallsControllerSubmit(handler, controllerName));
|
|
2370
|
-
}
|
|
2371
|
-
}
|
|
2372
|
-
ts.forEachChild(node, visit);
|
|
2373
|
-
};
|
|
2374
|
-
visit(component);
|
|
2375
|
-
return ownsHandler;
|
|
2376
|
-
}
|
|
2377
|
-
function resolveComponentHandler(component, expression) {
|
|
2378
|
-
const handler = unwrapExpression(expression);
|
|
2379
|
-
if (ts.isArrowFunction(handler) || ts.isFunctionExpression(handler))
|
|
2380
|
-
return handler;
|
|
2381
|
-
if (!ts.isIdentifier(handler) || !component.body || !ts.isBlock(component.body))
|
|
2382
|
-
return null;
|
|
2383
|
-
for (const statement of component.body.statements) {
|
|
2384
|
-
if (ts.isFunctionDeclaration(statement) && statement.name?.text === handler.text) {
|
|
2385
|
-
return statement;
|
|
2386
|
-
}
|
|
2387
|
-
if (!ts.isVariableStatement(statement))
|
|
2388
|
-
continue;
|
|
2389
|
-
for (const declaration of statement.declarationList.declarations) {
|
|
2390
|
-
if (ts.isIdentifier(declaration.name) &&
|
|
2391
|
-
declaration.name.text === handler.text &&
|
|
2392
|
-
declaration.initializer &&
|
|
2393
|
-
(ts.isArrowFunction(unwrapExpression(declaration.initializer)) ||
|
|
2394
|
-
ts.isFunctionExpression(unwrapExpression(declaration.initializer))))
|
|
2395
|
-
return unwrapExpression(declaration.initializer);
|
|
2396
|
-
}
|
|
2397
|
-
}
|
|
2398
|
-
return null;
|
|
2399
|
-
}
|
|
2400
|
-
function handlerCallsControllerSubmit(handler, controllerName) {
|
|
2401
|
-
if (handler.parameters.some((parameter) => ts.isIdentifier(parameter.name) && parameter.name.text === controllerName))
|
|
2402
|
-
return false;
|
|
2403
|
-
let callsSubmit = false;
|
|
2404
|
-
let shadowsController = false;
|
|
2405
|
-
const visit = (node) => {
|
|
2406
|
-
if (callsSubmit || (node !== handler && ts.isFunctionLike(node)))
|
|
2407
|
-
return;
|
|
2408
|
-
if (ts.isVariableDeclaration(node) &&
|
|
2409
|
-
ts.isIdentifier(node.name) &&
|
|
2410
|
-
node.name.text === controllerName) {
|
|
2411
|
-
shadowsController = true;
|
|
2412
|
-
return;
|
|
2413
|
-
}
|
|
2414
|
-
if (ts.isCallExpression(node)) {
|
|
2415
|
-
const callee = unwrapExpression(node.expression);
|
|
2416
|
-
if (ts.isPropertyAccessExpression(callee) &&
|
|
2417
|
-
ts.isIdentifier(callee.expression) &&
|
|
2418
|
-
callee.expression.text === controllerName &&
|
|
2419
|
-
callee.name.text === "submit" &&
|
|
2420
|
-
isStaticallyReachableExecutionNode(node, handler) &&
|
|
2421
|
-
!hasReachableControllerWriteBefore(handler, controllerName, node))
|
|
2422
|
-
callsSubmit = true;
|
|
2423
|
-
}
|
|
2424
|
-
ts.forEachChild(node, visit);
|
|
2425
|
-
};
|
|
2426
|
-
visit(handler);
|
|
2427
|
-
return callsSubmit && !shadowsController;
|
|
2428
|
-
}
|
|
2429
|
-
function hasReachableControllerWriteBefore(owner, controllerName, reference, declarationToIgnore) {
|
|
2430
|
-
const source = owner.getSourceFile();
|
|
2431
|
-
const referencePosition = reference.getStart(source);
|
|
2432
|
-
let found = false;
|
|
2433
|
-
const visit = (node) => {
|
|
2434
|
-
if (found ||
|
|
2435
|
-
node.getStart(source) >= referencePosition ||
|
|
2436
|
-
(node !== owner && ts.isFunctionLike(node)))
|
|
2437
|
-
return;
|
|
2438
|
-
if (ts.isBinaryExpression(node) &&
|
|
2439
|
-
node.operatorToken.kind >= ts.SyntaxKind.FirstAssignment &&
|
|
2440
|
-
node.operatorToken.kind <= ts.SyntaxKind.LastAssignment) {
|
|
2441
|
-
const target = unwrapExpression(node.left);
|
|
2442
|
-
if (ts.isIdentifier(target) &&
|
|
2443
|
-
target.text === controllerName &&
|
|
2444
|
-
isStaticallyReachableExecutionNode(node, owner))
|
|
2445
|
-
found = true;
|
|
2446
|
-
}
|
|
2447
|
-
else if ((ts.isPrefixUnaryExpression(node) || ts.isPostfixUnaryExpression(node)) &&
|
|
2448
|
-
(node.operator === ts.SyntaxKind.PlusPlusToken ||
|
|
2449
|
-
node.operator === ts.SyntaxKind.MinusMinusToken)) {
|
|
2450
|
-
const target = unwrapExpression(node.operand);
|
|
2451
|
-
if (ts.isIdentifier(target) &&
|
|
2452
|
-
target.text === controllerName &&
|
|
2453
|
-
isStaticallyReachableExecutionNode(node, owner))
|
|
2454
|
-
found = true;
|
|
2455
|
-
}
|
|
2456
|
-
else if (ts.isVariableDeclaration(node) &&
|
|
2457
|
-
node !== declarationToIgnore &&
|
|
2458
|
-
ts.isIdentifier(node.name) &&
|
|
2459
|
-
node.name.text === controllerName &&
|
|
2460
|
-
isStaticallyReachableExecutionNode(node, owner)) {
|
|
2461
|
-
found = true;
|
|
2462
|
-
}
|
|
2463
|
-
ts.forEachChild(node, visit);
|
|
2464
|
-
};
|
|
2465
|
-
visit(owner);
|
|
2466
|
-
return found;
|
|
2467
|
-
}
|
|
2468
|
-
function nearestLexicalScope(node, source) {
|
|
2469
|
-
let current = node.parent;
|
|
2470
|
-
while (current && current !== source) {
|
|
2471
|
-
if (ts.isBlock(current) || ts.isCaseBlock(current))
|
|
2472
|
-
return current;
|
|
2473
|
-
current = current.parent;
|
|
2474
|
-
}
|
|
2475
|
-
return source;
|
|
2476
|
-
}
|
|
2477
|
-
function variableDeclarationScope(declaration, source) {
|
|
2478
|
-
const declarationList = ts.isVariableDeclarationList(declaration.parent)
|
|
2479
|
-
? declaration.parent
|
|
2480
|
-
: null;
|
|
2481
|
-
if (declarationList && (declarationList.flags & ts.NodeFlags.BlockScoped) !== 0) {
|
|
2482
|
-
return nearestLexicalScope(declaration, source);
|
|
2483
|
-
}
|
|
2484
|
-
const owner = nearestFunctionLike(declaration);
|
|
2485
|
-
if (owner)
|
|
2486
|
-
return owner.body && ts.isBlock(owner.body) ? owner.body : owner;
|
|
2487
|
-
return source;
|
|
2488
|
-
}
|
|
2489
|
-
function nearestFunctionLike(node) {
|
|
2490
|
-
let current = node.parent;
|
|
2491
|
-
while (current) {
|
|
2492
|
-
if (ts.isFunctionDeclaration(current) ||
|
|
2493
|
-
ts.isMethodDeclaration(current) ||
|
|
2494
|
-
ts.isGetAccessorDeclaration(current) ||
|
|
2495
|
-
ts.isSetAccessorDeclaration(current) ||
|
|
2496
|
-
ts.isConstructorDeclaration(current) ||
|
|
2497
|
-
ts.isFunctionExpression(current) ||
|
|
2498
|
-
ts.isArrowFunction(current)) {
|
|
2499
|
-
return current;
|
|
2500
|
-
}
|
|
2501
|
-
current = current.parent;
|
|
2502
|
-
}
|
|
2503
|
-
return undefined;
|
|
2504
|
-
}
|
|
2505
|
-
function hasModifier(node, kind) {
|
|
2506
|
-
return ts.canHaveModifiers(node) && Boolean(ts.getModifiers(node)?.some((modifier) => modifier.kind === kind));
|
|
2507
|
-
}
|
|
2508
|
-
function bindingRuntimeOutputValues(declaration) {
|
|
2509
|
-
if (ts.isVariableDeclaration(declaration)) {
|
|
2510
|
-
return declaration.initializer ? [declaration.initializer] : [];
|
|
2511
|
-
}
|
|
2512
|
-
if (ts.isFunctionDeclaration(declaration))
|
|
2513
|
-
return [declaration];
|
|
2514
|
-
return [];
|
|
2515
|
-
}
|
|
2516
|
-
function collectFunctionOutputValues(declaration) {
|
|
2517
|
-
if (!declaration.body)
|
|
2518
|
-
return [];
|
|
2519
|
-
if (!ts.isBlock(declaration.body))
|
|
2520
|
-
return [declaration.body];
|
|
2521
|
-
return collectReachableStatementListOutputs(declaration.body.statements).outputs;
|
|
2522
|
-
}
|
|
2523
|
-
function collectReachableStatementListOutputs(statements) {
|
|
2524
|
-
const outputs = [];
|
|
2525
|
-
let canContinue = true;
|
|
2526
|
-
for (const statement of statements) {
|
|
2527
|
-
if (!canContinue)
|
|
2528
|
-
break;
|
|
2529
|
-
const result = collectReachableStatementOutputs(statement);
|
|
2530
|
-
outputs.push(...result.outputs);
|
|
2531
|
-
canContinue = result.canContinue;
|
|
2532
|
-
}
|
|
2533
|
-
return { outputs, canContinue };
|
|
2534
|
-
}
|
|
2535
|
-
function collectReachableStatementOutputs(statement) {
|
|
2536
|
-
if (ts.isReturnStatement(statement)) {
|
|
2537
|
-
return {
|
|
2538
|
-
outputs: statement.expression ? [statement.expression] : [],
|
|
2539
|
-
canContinue: false,
|
|
2540
|
-
};
|
|
2541
|
-
}
|
|
2542
|
-
if (ts.isThrowStatement(statement))
|
|
2543
|
-
return { outputs: [], canContinue: false };
|
|
2544
|
-
if (ts.isBlock(statement))
|
|
2545
|
-
return collectReachableStatementListOutputs(statement.statements);
|
|
2546
|
-
if (ts.isIfStatement(statement)) {
|
|
2547
|
-
const condition = staticTruthiness(statement.expression);
|
|
2548
|
-
const whenTrue = condition === false
|
|
2549
|
-
? { outputs: [], canContinue: false }
|
|
2550
|
-
: collectReachableStatementOutputs(statement.thenStatement);
|
|
2551
|
-
const whenFalse = condition === true
|
|
2552
|
-
? { outputs: [], canContinue: false }
|
|
2553
|
-
: statement.elseStatement
|
|
2554
|
-
? collectReachableStatementOutputs(statement.elseStatement)
|
|
2555
|
-
: { outputs: [], canContinue: true };
|
|
2556
|
-
if (condition === true)
|
|
2557
|
-
return whenTrue;
|
|
2558
|
-
if (condition === false)
|
|
2559
|
-
return whenFalse;
|
|
2560
|
-
return {
|
|
2561
|
-
outputs: [...whenTrue.outputs, ...whenFalse.outputs],
|
|
2562
|
-
canContinue: whenTrue.canContinue || whenFalse.canContinue,
|
|
2563
|
-
};
|
|
2564
|
-
}
|
|
2565
|
-
if (ts.isWhileStatement(statement)) {
|
|
2566
|
-
if (staticTruthiness(statement.expression) === false) {
|
|
2567
|
-
return { outputs: [], canContinue: true };
|
|
2568
|
-
}
|
|
2569
|
-
return {
|
|
2570
|
-
outputs: collectReachableStatementOutputs(statement.statement).outputs,
|
|
2571
|
-
canContinue: true,
|
|
2572
|
-
};
|
|
2573
|
-
}
|
|
2574
|
-
if (ts.isForStatement(statement)) {
|
|
2575
|
-
if (statement.condition && staticTruthiness(statement.condition) === false) {
|
|
2576
|
-
return { outputs: [], canContinue: true };
|
|
2577
|
-
}
|
|
2578
|
-
return {
|
|
2579
|
-
outputs: collectReachableStatementOutputs(statement.statement).outputs,
|
|
2580
|
-
canContinue: true,
|
|
2581
|
-
};
|
|
2582
|
-
}
|
|
2583
|
-
if (ts.isDoStatement(statement)) {
|
|
2584
|
-
return {
|
|
2585
|
-
outputs: collectReachableStatementOutputs(statement.statement).outputs,
|
|
2586
|
-
canContinue: true,
|
|
2587
|
-
};
|
|
2588
|
-
}
|
|
2589
|
-
if (ts.isLabeledStatement(statement)) {
|
|
2590
|
-
return collectReachableStatementOutputs(statement.statement);
|
|
2591
|
-
}
|
|
2592
|
-
if (ts.isTryStatement(statement)) {
|
|
2593
|
-
const attempted = collectReachableStatementListOutputs(statement.tryBlock.statements);
|
|
2594
|
-
const recovered = statement.catchClause
|
|
2595
|
-
? collectReachableStatementListOutputs(statement.catchClause.block.statements)
|
|
2596
|
-
: { outputs: [], canContinue: false };
|
|
2597
|
-
const finalized = statement.finallyBlock
|
|
2598
|
-
? collectReachableStatementListOutputs(statement.finallyBlock.statements)
|
|
2599
|
-
: { outputs: [], canContinue: true };
|
|
2600
|
-
if (statement.finallyBlock && !finalized.canContinue)
|
|
2601
|
-
return finalized;
|
|
2602
|
-
return {
|
|
2603
|
-
outputs: [...attempted.outputs, ...recovered.outputs, ...finalized.outputs],
|
|
2604
|
-
canContinue: finalized.canContinue &&
|
|
2605
|
-
(attempted.canContinue || (statement.catchClause ? recovered.canContinue : false)),
|
|
2606
|
-
};
|
|
2607
|
-
}
|
|
2608
|
-
if (ts.isSwitchStatement(statement)) {
|
|
2609
|
-
const entryAnalysis = analyzeStaticSwitchEntries(statement);
|
|
2610
|
-
if (entryAnalysis) {
|
|
2611
|
-
const outputs = statement.caseBlock.clauses.flatMap((clause, index) => switchClauseIsReachable(statement, index, entryAnalysis.entryIndices)
|
|
2612
|
-
? collectReachableStatementListOutputs(clause.statements).outputs
|
|
2613
|
-
: []);
|
|
2614
|
-
const entryCanContinue = entryAnalysis.entryIndices.some((entryIndex) => {
|
|
2615
|
-
for (let index = entryIndex; index < statement.caseBlock.clauses.length; index += 1) {
|
|
2616
|
-
const clause = statement.caseBlock.clauses[index];
|
|
2617
|
-
if (!switchClauseCanFallThrough(clause, statement)) {
|
|
2618
|
-
return !clause.statements.some(statementGuaranteesFunctionExit);
|
|
2619
|
-
}
|
|
2620
|
-
}
|
|
2621
|
-
return true;
|
|
2622
|
-
});
|
|
2623
|
-
return {
|
|
2624
|
-
outputs,
|
|
2625
|
-
canContinue: entryAnalysis.noMatchPossible || entryCanContinue,
|
|
2626
|
-
};
|
|
2627
|
-
}
|
|
2628
|
-
const clauseResults = statement.caseBlock.clauses.map((clause) => collectReachableStatementListOutputs(clause.statements));
|
|
2629
|
-
return {
|
|
2630
|
-
outputs: clauseResults.flatMap((result) => result.outputs),
|
|
2631
|
-
canContinue: clauseResults.length === 0 ||
|
|
2632
|
-
!statement.caseBlock.clauses.some(ts.isDefaultClause) ||
|
|
2633
|
-
clauseResults.some((result) => result.canContinue),
|
|
2634
|
-
};
|
|
2635
|
-
}
|
|
2636
|
-
if (ts.isWithStatement(statement)) {
|
|
2637
|
-
return collectReachableStatementOutputs(statement.statement);
|
|
2638
|
-
}
|
|
2639
|
-
return { outputs: [], canContinue: true };
|
|
2640
|
-
}
|
|
2641
|
-
function staticTruthiness(node) {
|
|
2642
|
-
const value = unwrapExpression(node);
|
|
2643
|
-
if (value.kind === ts.SyntaxKind.TrueKeyword)
|
|
2644
|
-
return true;
|
|
2645
|
-
if (value.kind === ts.SyntaxKind.FalseKeyword ||
|
|
2646
|
-
value.kind === ts.SyntaxKind.NullKeyword ||
|
|
2647
|
-
ts.isVoidExpression(value))
|
|
2648
|
-
return false;
|
|
2649
|
-
if (ts.isStringLiteral(value) || ts.isNoSubstitutionTemplateLiteral(value)) {
|
|
2650
|
-
return value.text.length > 0;
|
|
2651
|
-
}
|
|
2652
|
-
if (ts.isNumericLiteral(value))
|
|
2653
|
-
return Number(value.text) !== 0;
|
|
2654
|
-
if (ts.isPrefixUnaryExpression(value)) {
|
|
2655
|
-
if (value.operator === ts.SyntaxKind.ExclamationToken) {
|
|
2656
|
-
const operand = staticTruthiness(value.operand);
|
|
2657
|
-
return operand === undefined ? undefined : !operand;
|
|
2658
|
-
}
|
|
2659
|
-
if (value.operator === ts.SyntaxKind.PlusToken ||
|
|
2660
|
-
value.operator === ts.SyntaxKind.MinusToken) {
|
|
2661
|
-
const operand = unwrapExpression(value.operand);
|
|
2662
|
-
if (ts.isNumericLiteral(operand))
|
|
2663
|
-
return Number(operand.text) !== 0;
|
|
2664
|
-
}
|
|
2665
|
-
}
|
|
2666
|
-
if (ts.isConditionalExpression(value)) {
|
|
2667
|
-
const condition = staticTruthiness(value.condition);
|
|
2668
|
-
if (condition === true)
|
|
2669
|
-
return staticTruthiness(value.whenTrue);
|
|
2670
|
-
if (condition === false)
|
|
2671
|
-
return staticTruthiness(value.whenFalse);
|
|
2672
|
-
}
|
|
2673
|
-
if (ts.isBinaryExpression(value)) {
|
|
2674
|
-
if (value.operatorToken.kind === ts.SyntaxKind.CommaToken) {
|
|
2675
|
-
return staticTruthiness(value.right);
|
|
2676
|
-
}
|
|
2677
|
-
if (value.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) {
|
|
2678
|
-
const left = staticTruthiness(value.left);
|
|
2679
|
-
if (left === false)
|
|
2680
|
-
return false;
|
|
2681
|
-
if (left === true)
|
|
2682
|
-
return staticTruthiness(value.right);
|
|
2683
|
-
}
|
|
2684
|
-
if (value.operatorToken.kind === ts.SyntaxKind.BarBarToken) {
|
|
2685
|
-
const left = staticTruthiness(value.left);
|
|
2686
|
-
if (left === true)
|
|
2687
|
-
return true;
|
|
2688
|
-
if (left === false)
|
|
2689
|
-
return staticTruthiness(value.right);
|
|
2690
|
-
}
|
|
2691
|
-
if (value.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken) {
|
|
2692
|
-
const left = staticNullishness(value.left);
|
|
2693
|
-
if (left === true)
|
|
2694
|
-
return staticTruthiness(value.right);
|
|
2695
|
-
if (left === false)
|
|
2696
|
-
return staticTruthiness(value.left);
|
|
2697
|
-
}
|
|
2698
|
-
}
|
|
2699
|
-
return undefined;
|
|
2700
|
-
}
|
|
2701
|
-
function staticNullishness(node) {
|
|
2702
|
-
const value = unwrapExpression(node);
|
|
2703
|
-
if (value.kind === ts.SyntaxKind.NullKeyword || ts.isVoidExpression(value))
|
|
2704
|
-
return true;
|
|
2705
|
-
if (value.kind === ts.SyntaxKind.TrueKeyword ||
|
|
2706
|
-
value.kind === ts.SyntaxKind.FalseKeyword ||
|
|
2707
|
-
ts.isStringLiteral(value) ||
|
|
2708
|
-
ts.isNoSubstitutionTemplateLiteral(value) ||
|
|
2709
|
-
ts.isNumericLiteral(value) ||
|
|
2710
|
-
ts.isObjectLiteralExpression(value) ||
|
|
2711
|
-
ts.isArrayLiteralExpression(value) ||
|
|
2712
|
-
ts.isFunctionExpression(value) ||
|
|
2713
|
-
ts.isArrowFunction(value) ||
|
|
2714
|
-
ts.isClassExpression(value))
|
|
2715
|
-
return false;
|
|
2716
|
-
if (ts.isConditionalExpression(value)) {
|
|
2717
|
-
const condition = staticTruthiness(value.condition);
|
|
2718
|
-
if (condition === true)
|
|
2719
|
-
return staticNullishness(value.whenTrue);
|
|
2720
|
-
if (condition === false)
|
|
2721
|
-
return staticNullishness(value.whenFalse);
|
|
2722
|
-
}
|
|
2723
|
-
if (ts.isBinaryExpression(value) && value.operatorToken.kind === ts.SyntaxKind.CommaToken) {
|
|
2724
|
-
return staticNullishness(value.right);
|
|
2725
|
-
}
|
|
2726
|
-
return undefined;
|
|
2727
|
-
}
|
|
2728
|
-
function isRuntimeIdentifierReference(identifier) {
|
|
2729
|
-
const parent = identifier.parent;
|
|
2730
|
-
if (ts.isVoidExpression(parent)) {
|
|
2731
|
-
return false;
|
|
2732
|
-
}
|
|
2733
|
-
if (((ts.isVariableDeclaration(parent) ||
|
|
2734
|
-
ts.isFunctionDeclaration(parent) ||
|
|
2735
|
-
ts.isFunctionExpression(parent) ||
|
|
2736
|
-
ts.isParameter(parent) ||
|
|
2737
|
-
ts.isClassDeclaration(parent) ||
|
|
2738
|
-
ts.isClassExpression(parent) ||
|
|
2739
|
-
ts.isBindingElement(parent)) && parent.name === identifier) ||
|
|
2740
|
-
((ts.isPropertyAccessExpression(parent) || ts.isPropertyAccessChain(parent)) && parent.name === identifier) ||
|
|
2741
|
-
((ts.isPropertyAssignment(parent) ||
|
|
2742
|
-
ts.isMethodDeclaration(parent) ||
|
|
2743
|
-
ts.isGetAccessorDeclaration(parent) ||
|
|
2744
|
-
ts.isSetAccessorDeclaration(parent)) && parent.name === identifier) ||
|
|
2745
|
-
ts.isImportSpecifier(parent) ||
|
|
2746
|
-
ts.isImportClause(parent) ||
|
|
2747
|
-
ts.isNamespaceImport(parent) ||
|
|
2748
|
-
ts.isExportSpecifier(parent) ||
|
|
2749
|
-
ts.isLabeledStatement(parent) ||
|
|
2750
|
-
ts.isBreakOrContinueStatement(parent)) {
|
|
2751
|
-
return false;
|
|
2752
|
-
}
|
|
2753
|
-
return true;
|
|
2754
|
-
}
|
|
2755
|
-
function addProtectedImplementationIssues(root, issues) {
|
|
2756
|
-
for (const file of walkProjectRuntimeFiles(root)) {
|
|
2757
|
-
const relativePath = normalizePath(relative(root, file));
|
|
2758
|
-
const source = readFileSync(file, "utf8");
|
|
2759
|
-
for (const rule of PROTECTED_LOCAL_PATTERNS) {
|
|
2760
|
-
const match = rule.pattern.exec(source);
|
|
2761
|
-
if (!match || match.index === undefined)
|
|
2762
|
-
continue;
|
|
2763
|
-
const line = source.slice(0, match.index).split("\n").length;
|
|
2764
|
-
issues.push(configIssue({
|
|
2765
|
-
ruleId: rule.ruleId,
|
|
2766
|
-
surface: rule.surface,
|
|
2767
|
-
expected: rule.expected,
|
|
2768
|
-
actual: `A protected local implementation was found in ${relativePath}.`,
|
|
2769
|
-
remediation: rule.ruleId === "brandlab-config-v3-url-derived-breadcrumbs"
|
|
2770
|
-
? "Use BrandlabRouteBreadcrumbs with the typed route catalog; keep path labels only as a development fallback."
|
|
2771
|
-
: "Keep only the thin project binding and consume the shared runtime.",
|
|
2772
|
-
severity: "severity" in rule ? rule.severity : "blocker",
|
|
2773
|
-
path: relativePath,
|
|
2774
|
-
line,
|
|
2775
|
-
}));
|
|
2776
|
-
}
|
|
2777
|
-
}
|
|
2778
|
-
}
|
|
2779
|
-
function walkSourceFiles(root) {
|
|
2780
|
-
if (!existsSync(root))
|
|
2781
|
-
return [];
|
|
2782
|
-
const files = [];
|
|
2783
|
-
const visit = (directory) => {
|
|
2784
|
-
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
2785
|
-
if (entry.isDirectory()) {
|
|
2786
|
-
if (!SKIPPED_DIRECTORIES.has(entry.name))
|
|
2787
|
-
visit(join(directory, entry.name));
|
|
2788
|
-
}
|
|
2789
|
-
else if (entry.isFile() && SOURCE_EXTENSIONS.has(extname(entry.name))) {
|
|
2790
|
-
files.push(join(directory, entry.name));
|
|
2791
|
-
}
|
|
2792
|
-
}
|
|
2793
|
-
};
|
|
2794
|
-
visit(root);
|
|
2795
|
-
return files.sort();
|
|
2796
|
-
}
|
|
2797
|
-
const RUNTIME_SOURCE_ROOTS = ["src", "app", "pages", "components", "lib"];
|
|
2798
|
-
const NON_RUNTIME_DIRECTORIES = new Set([
|
|
2799
|
-
"__tests__",
|
|
2800
|
-
"__mocks__",
|
|
2801
|
-
"fixtures",
|
|
2802
|
-
"stories",
|
|
2803
|
-
"test",
|
|
2804
|
-
"tests",
|
|
2805
|
-
]);
|
|
2806
|
-
const NON_RUNTIME_FILE_PATTERN = /(?:^|\.)(?:test|spec|stories?|fixtures?)\.[^.]+$/i;
|
|
2807
|
-
function walkProjectRuntimeFiles(root) {
|
|
2808
|
-
const files = new Set();
|
|
2809
|
-
for (const directory of RUNTIME_SOURCE_ROOTS) {
|
|
2810
|
-
for (const file of walkSourceFiles(join(root, directory))) {
|
|
2811
|
-
const relativePath = normalizePath(relative(root, file));
|
|
2812
|
-
if (relativePath.split("/").some((segment) => NON_RUNTIME_DIRECTORIES.has(segment)) ||
|
|
2813
|
-
NON_RUNTIME_FILE_PATTERN.test(relativePath)) {
|
|
2814
|
-
continue;
|
|
2815
|
-
}
|
|
2816
|
-
files.add(file);
|
|
2817
|
-
}
|
|
2818
|
-
}
|
|
2819
|
-
for (const name of ["next.config.ts", "next.config.mjs", "next.config.js", "next.config.cjs"]) {
|
|
2820
|
-
const file = join(root, name);
|
|
2821
|
-
if (existsSync(file))
|
|
2822
|
-
files.add(file);
|
|
2823
|
-
}
|
|
2824
|
-
return [...files].sort();
|
|
2825
|
-
}
|
|
2826
|
-
function configIssue(options) {
|
|
2827
|
-
return {
|
|
2828
|
-
ruleId: options.ruleId,
|
|
2829
|
-
surface: options.surface ?? "configuration",
|
|
2830
|
-
severity: options.severity ?? "blocker",
|
|
2831
|
-
evidenceKind: "static",
|
|
2832
|
-
expected: options.expected,
|
|
2833
|
-
actual: options.actual,
|
|
2834
|
-
remediation: options.remediation,
|
|
2835
|
-
...(options.path ? { path: options.path } : {}),
|
|
2836
|
-
...(options.line ? { line: options.line } : {}),
|
|
2837
|
-
};
|
|
2838
|
-
}
|
|
2839
|
-
function isRecord(value) {
|
|
2840
|
-
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
2841
|
-
}
|
|
2842
|
-
function normalizePath(value) {
|
|
2843
|
-
return value.split(/[/\\]/).join("/");
|
|
2844
|
-
}
|
|
2845
|
-
function getParseDiagnostics(source) {
|
|
2846
|
-
return source.parseDiagnostics ?? [];
|
|
2847
|
-
}
|
|
2848
|
-
/** SHA-256 evidence for the exact canonical serializer output produced at runtime. */
|
|
2849
|
-
export function digestBrandlabPlatformContractSerialization(serialized) {
|
|
2850
|
-
if (!serialized)
|
|
2851
|
-
throw new Error("Serialized platform contract cannot be empty.");
|
|
2852
|
-
let parsed;
|
|
2853
|
-
try {
|
|
2854
|
-
parsed = JSON.parse(serialized);
|
|
2855
|
-
}
|
|
2856
|
-
catch {
|
|
2857
|
-
throw new Error("Serialized platform contract must be valid JSON.");
|
|
2858
|
-
}
|
|
2859
|
-
if (!isRecord(parsed) || parsed.schemaVersion !== "brandlab.platform-contract.v1") {
|
|
2860
|
-
throw new Error("Serialized platform contract has an unsupported schemaVersion.");
|
|
2861
|
-
}
|
|
2862
|
-
if (JSON.stringify(canonicalizeJsonValue(parsed)) !== serialized) {
|
|
2863
|
-
throw new Error("Platform contract evidence must use the canonical serializer output.");
|
|
2864
|
-
}
|
|
2865
|
-
return createHash("sha256").update(serialized).digest("hex");
|
|
2866
|
-
}
|
|
2867
|
-
function canonicalizeJsonValue(value) {
|
|
2868
|
-
if (Array.isArray(value))
|
|
2869
|
-
return value.map(canonicalizeJsonValue);
|
|
2870
|
-
if (!isRecord(value))
|
|
2871
|
-
return value;
|
|
2872
|
-
return Object.fromEntries(Object.entries(value)
|
|
2873
|
-
.sort(([left], [right]) => left.localeCompare(right))
|
|
2874
|
-
.map(([key, child]) => [key, canonicalizeJsonValue(child)]));
|
|
2875
|
-
}
|
|
2876
|
-
//# sourceMappingURL=project-config-v3.js.map
|