@ontrails/warden 1.0.0-beta.23 → 1.0.0-beta.29
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/CHANGELOG.md +113 -0
- package/README.md +35 -1
- package/package.json +10 -9
- package/src/ast.ts +109 -0
- package/src/cli.ts +44 -4
- package/src/command.ts +24 -34
- package/src/drift.ts +24 -8
- package/src/index.ts +9 -0
- package/src/project-rules.ts +290 -0
- package/src/resolve.ts +8 -7
- package/src/rules/ast.ts +837 -6
- package/src/rules/cli-command-route-coherence.ts +177 -0
- package/src/rules/composes-declarations.ts +210 -77
- package/src/rules/context-no-surface-types.ts +15 -12
- package/src/rules/contour-exists.ts +7 -7
- package/src/rules/dead-internal-trail.ts +11 -4
- package/src/rules/dead-public-trail.ts +258 -0
- package/src/rules/duplicate-public-contract.ts +91 -0
- package/src/rules/error-mapping-completeness.ts +5 -3
- package/src/rules/example-valid.ts +6 -12
- package/src/rules/fires-declarations.ts +41 -64
- package/src/rules/implementation-returns-result.ts +208 -76
- package/src/rules/incomplete-crud.ts +20 -19
- package/src/rules/index.ts +15 -0
- package/src/rules/layer-field-name-drift.ts +12 -6
- package/src/rules/library-projection-coherence.ts +100 -0
- package/src/rules/metadata.ts +102 -1
- package/src/rules/no-destructured-compose.ts +16 -14
- package/src/rules/no-native-error-result.ts +15 -8
- package/src/rules/no-redundant-result-error-wrap.ts +82 -29
- package/src/rules/no-sync-result-assumption.ts +70 -68
- package/src/rules/no-top-level-surface.ts +46 -64
- package/src/rules/on-references-exist.ts +1 -1
- package/src/rules/owner-projection-parity.ts +10 -13
- package/src/rules/public-export-example-coverage.ts +29 -20
- package/src/rules/public-internal-deep-imports.ts +2 -2
- package/src/rules/read-intent-fires.ts +8 -8
- package/src/rules/registry-names.ts +10 -0
- package/src/rules/resource-declarations.ts +20 -31
- package/src/rules/resource-exists.ts +2 -2
- package/src/rules/resource-id-grammar.ts +2 -2
- package/src/rules/resource-mock-coverage.ts +2 -2
- package/src/rules/static-resource-accessor-preference.ts +26 -34
- package/src/rules/surface-facet-coherence.ts +21 -29
- package/src/rules/trail-fork-coaching.ts +616 -0
- package/src/rules/trail-versioning-source.ts +56 -78
- package/src/rules/types.ts +2 -0
- package/src/rules/unreachable-detour-shadowing.ts +16 -21
- package/src/rules/valid-describe-refs.ts +14 -12
- package/src/rules/warden-export-symmetry.ts +42 -35
- package/src/rules/warden-rules-use-ast.ts +168 -50
- package/src/trails/cli-command-route-coherence.trail.ts +47 -0
- package/src/trails/dead-public-trail.trail.ts +31 -0
- package/src/trails/duplicate-public-contract.trail.ts +47 -0
- package/src/trails/error-mapping-completeness.trail.ts +1 -0
- package/src/trails/index.ts +5 -0
- package/src/trails/library-projection-coherence.trail.ts +43 -0
- package/src/trails/schema.ts +4 -0
- package/src/trails/trail-fork-coaching.trail.ts +42 -0
- package/src/trails/warden-rules-use-ast.trail.ts +19 -0
- package/src/trails/wrap-rule.ts +1 -0
|
@@ -1,4 +1,21 @@
|
|
|
1
1
|
import {
|
|
2
|
+
getNodeArgument,
|
|
3
|
+
getNodeBodyStatements,
|
|
4
|
+
getNodeCallee,
|
|
5
|
+
getNodeComputed,
|
|
6
|
+
getNodeDeclaration,
|
|
7
|
+
getNodeDeclarations,
|
|
8
|
+
getNodeExported,
|
|
9
|
+
getNodeExpression,
|
|
10
|
+
getNodeId,
|
|
11
|
+
getNodeImported,
|
|
12
|
+
getNodeInit,
|
|
13
|
+
getNodeLeft,
|
|
14
|
+
getNodeLocal,
|
|
15
|
+
getNodeObject,
|
|
16
|
+
getNodeProperty,
|
|
17
|
+
getNodeSource,
|
|
18
|
+
getNodeSpecifiers,
|
|
2
19
|
getStringValue,
|
|
3
20
|
identifierName,
|
|
4
21
|
isStringLiteral,
|
|
@@ -31,7 +48,7 @@ const diagnosticMessage =
|
|
|
31
48
|
const unwrapExportDeclaration = (node: AstNode): AstNode =>
|
|
32
49
|
node.type === 'ExportNamedDeclaration' ||
|
|
33
50
|
node.type === 'ExportDefaultDeclaration'
|
|
34
|
-
? ((node
|
|
51
|
+
? (getNodeDeclaration(node) ?? node)
|
|
35
52
|
: node;
|
|
36
53
|
|
|
37
54
|
interface ImportedBindings {
|
|
@@ -40,7 +57,7 @@ interface ImportedBindings {
|
|
|
40
57
|
}
|
|
41
58
|
|
|
42
59
|
const importSource = (node: AstNode): string | null => {
|
|
43
|
-
const
|
|
60
|
+
const source = getNodeSource(node);
|
|
44
61
|
return source && isStringLiteral(source) ? getStringValue(source) : null;
|
|
45
62
|
};
|
|
46
63
|
|
|
@@ -55,7 +72,7 @@ const addFrameworkImportBindings = (
|
|
|
55
72
|
): ImportedBindings => {
|
|
56
73
|
const named = new Map<string, string>();
|
|
57
74
|
const namespaces = new Set<string>();
|
|
58
|
-
const body = (ast
|
|
75
|
+
const body = getNodeBodyStatements(ast);
|
|
59
76
|
|
|
60
77
|
for (const statement of body) {
|
|
61
78
|
if (
|
|
@@ -65,14 +82,10 @@ const addFrameworkImportBindings = (
|
|
|
65
82
|
continue;
|
|
66
83
|
}
|
|
67
84
|
|
|
68
|
-
const specifiers =
|
|
69
|
-
(statement as unknown as { specifiers?: readonly AstNode[] })
|
|
70
|
-
.specifiers ?? [];
|
|
85
|
+
const specifiers = getNodeSpecifiers(statement);
|
|
71
86
|
for (const specifier of specifiers) {
|
|
72
87
|
if (specifier.type === 'ImportNamespaceSpecifier') {
|
|
73
|
-
const localName = identifierName(
|
|
74
|
-
(specifier as unknown as { local?: AstNode }).local
|
|
75
|
-
);
|
|
88
|
+
const localName = identifierName(getNodeLocal(specifier));
|
|
76
89
|
if (localName) {
|
|
77
90
|
namespaces.add(localName);
|
|
78
91
|
}
|
|
@@ -82,10 +95,8 @@ const addFrameworkImportBindings = (
|
|
|
82
95
|
continue;
|
|
83
96
|
}
|
|
84
97
|
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
readonly local?: AstNode;
|
|
88
|
-
};
|
|
98
|
+
const imported = getNodeImported(specifier);
|
|
99
|
+
const local = getNodeLocal(specifier);
|
|
89
100
|
const importedName = importedSpecifierName(imported);
|
|
90
101
|
const localName = identifierName(local);
|
|
91
102
|
if (importedName && allowedImports.has(importedName) && localName) {
|
|
@@ -105,11 +116,7 @@ const unwrapExpression = (node: AstNode | undefined): AstNode | undefined => {
|
|
|
105
116
|
current?.type === 'TSAsExpression' ||
|
|
106
117
|
current?.type === 'TSSatisfiesExpression'
|
|
107
118
|
) {
|
|
108
|
-
current =
|
|
109
|
-
(current as unknown as { argument?: AstNode; expression?: AstNode })
|
|
110
|
-
.expression ??
|
|
111
|
-
(current as unknown as { argument?: AstNode; expression?: AstNode })
|
|
112
|
-
.argument;
|
|
119
|
+
current = getNodeExpression(current) ?? getNodeArgument(current);
|
|
113
120
|
}
|
|
114
121
|
return current;
|
|
115
122
|
};
|
|
@@ -120,15 +127,10 @@ const memberExpressionParts = (
|
|
|
120
127
|
if (node?.type !== 'MemberExpression') {
|
|
121
128
|
return { objectName: null, propertyName: null };
|
|
122
129
|
}
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
readonly object?: AstNode;
|
|
126
|
-
readonly property?: AstNode;
|
|
127
|
-
};
|
|
130
|
+
const computed = getNodeComputed(node);
|
|
131
|
+
const property = getNodeProperty(node);
|
|
128
132
|
return {
|
|
129
|
-
objectName: identifierName(
|
|
130
|
-
(node as unknown as { object?: AstNode }).object
|
|
131
|
-
),
|
|
133
|
+
objectName: identifierName(getNodeObject(node)),
|
|
132
134
|
propertyName: computed ? null : identifierName(property),
|
|
133
135
|
};
|
|
134
136
|
};
|
|
@@ -163,10 +165,7 @@ const isTopoCall = (
|
|
|
163
165
|
node: AstNode | undefined,
|
|
164
166
|
bindings: ImportedBindings
|
|
165
167
|
): boolean =>
|
|
166
|
-
calleeName(
|
|
167
|
-
(unwrapExpression(node) as unknown as { callee?: AstNode })?.callee,
|
|
168
|
-
bindings
|
|
169
|
-
) === 'topo';
|
|
168
|
+
calleeName(getNodeCallee(unwrapExpression(node)), bindings) === 'topo';
|
|
170
169
|
|
|
171
170
|
const isSurfaceOpenCall = (
|
|
172
171
|
node: AstNode | undefined,
|
|
@@ -177,7 +176,7 @@ const isSurfaceOpenCall = (
|
|
|
177
176
|
return false;
|
|
178
177
|
}
|
|
179
178
|
|
|
180
|
-
const
|
|
179
|
+
const callee = getNodeCallee(expression);
|
|
181
180
|
const directName = calleeName(callee, bindings);
|
|
182
181
|
if (directName && SURFACE_OPEN_CALLEE_NAMES.has(directName)) {
|
|
183
182
|
return true;
|
|
@@ -195,27 +194,22 @@ const isSurfaceOpenCall = (
|
|
|
195
194
|
};
|
|
196
195
|
|
|
197
196
|
const declarationIdName = (node: AstNode | undefined): string | null =>
|
|
198
|
-
identifierName(node) ??
|
|
199
|
-
identifierName((node as unknown as { left?: AstNode } | undefined)?.left);
|
|
197
|
+
identifierName(node) ?? identifierName(node ? getNodeLeft(node) : undefined);
|
|
200
198
|
|
|
201
199
|
const collectTopLevelTopoBindings = (
|
|
202
200
|
ast: AstNode,
|
|
203
201
|
topoBindings: ImportedBindings
|
|
204
202
|
): ReadonlySet<string> => {
|
|
205
203
|
const bindings = new Set<string>();
|
|
206
|
-
const body = (ast
|
|
204
|
+
const body = getNodeBodyStatements(ast);
|
|
207
205
|
|
|
208
206
|
for (const statement of body) {
|
|
209
207
|
const declaration = unwrapExportDeclaration(statement);
|
|
210
208
|
if (declaration.type === 'VariableDeclaration') {
|
|
211
|
-
const declarations =
|
|
212
|
-
(declaration as unknown as { declarations?: readonly AstNode[] })
|
|
213
|
-
.declarations ?? [];
|
|
209
|
+
const declarations = getNodeDeclarations(declaration);
|
|
214
210
|
for (const item of declarations) {
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
readonly init?: AstNode;
|
|
218
|
-
};
|
|
211
|
+
const id = getNodeId(item);
|
|
212
|
+
const init = getNodeInit(item);
|
|
219
213
|
const name = declarationIdName(id);
|
|
220
214
|
if (name && isTopoCall(init, topoBindings)) {
|
|
221
215
|
bindings.add(name);
|
|
@@ -231,15 +225,11 @@ const namedExportCarriesTopo = (
|
|
|
231
225
|
statement: AstNode,
|
|
232
226
|
topLevelTopoBindings: ReadonlySet<string>
|
|
233
227
|
): boolean => {
|
|
234
|
-
const specifiers =
|
|
235
|
-
(statement as unknown as { specifiers?: readonly AstNode[] }).specifiers ??
|
|
236
|
-
[];
|
|
228
|
+
const specifiers = getNodeSpecifiers(statement) ?? [];
|
|
237
229
|
|
|
238
230
|
for (const specifier of specifiers) {
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
readonly local?: AstNode;
|
|
242
|
-
};
|
|
231
|
+
const exported = getNodeExported(specifier);
|
|
232
|
+
const local = getNodeLocal(specifier);
|
|
243
233
|
const exportedName = identifierName(exported);
|
|
244
234
|
const localName = identifierName(local);
|
|
245
235
|
if (
|
|
@@ -260,7 +250,7 @@ const moduleExportsTopo = (
|
|
|
260
250
|
topoBindings: ImportedBindings
|
|
261
251
|
): boolean => {
|
|
262
252
|
const topLevelTopoBindings = collectTopLevelTopoBindings(ast, topoBindings);
|
|
263
|
-
const body = (ast
|
|
253
|
+
const body = getNodeBodyStatements(ast);
|
|
264
254
|
|
|
265
255
|
for (const statement of body) {
|
|
266
256
|
if (statement.type === 'ExportDefaultDeclaration') {
|
|
@@ -282,14 +272,10 @@ const moduleExportsTopo = (
|
|
|
282
272
|
if (declaration.type !== 'VariableDeclaration') {
|
|
283
273
|
continue;
|
|
284
274
|
}
|
|
285
|
-
const declarations =
|
|
286
|
-
(declaration as unknown as { declarations?: readonly AstNode[] })
|
|
287
|
-
.declarations ?? [];
|
|
275
|
+
const declarations = getNodeDeclarations(declaration);
|
|
288
276
|
for (const item of declarations) {
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
readonly init?: AstNode;
|
|
292
|
-
};
|
|
277
|
+
const id = getNodeId(item);
|
|
278
|
+
const init = getNodeInit(item);
|
|
293
279
|
const name = declarationIdName(id);
|
|
294
280
|
if (
|
|
295
281
|
name &&
|
|
@@ -311,9 +297,7 @@ const topLevelSurfaceOpen = (
|
|
|
311
297
|
): AstNode | null => {
|
|
312
298
|
const declaration = unwrapExportDeclaration(statement);
|
|
313
299
|
if (declaration.type === 'ExpressionStatement') {
|
|
314
|
-
const
|
|
315
|
-
readonly expression?: AstNode;
|
|
316
|
-
};
|
|
300
|
+
const expression = getNodeExpression(declaration);
|
|
317
301
|
const unwrapped = unwrapExpression(expression);
|
|
318
302
|
return isSurfaceOpenCall(unwrapped, surfaceBindings)
|
|
319
303
|
? (unwrapped ?? null)
|
|
@@ -329,11 +313,9 @@ const topLevelSurfaceOpen = (
|
|
|
329
313
|
return null;
|
|
330
314
|
}
|
|
331
315
|
|
|
332
|
-
const declarations =
|
|
333
|
-
(declaration as unknown as { declarations?: readonly AstNode[] })
|
|
334
|
-
.declarations ?? [];
|
|
316
|
+
const declarations = getNodeDeclarations(declaration);
|
|
335
317
|
for (const item of declarations) {
|
|
336
|
-
const
|
|
318
|
+
const init = getNodeInit(item);
|
|
337
319
|
const unwrapped = unwrapExpression(init);
|
|
338
320
|
if (isSurfaceOpenCall(unwrapped, surfaceBindings)) {
|
|
339
321
|
return unwrapped ?? null;
|
|
@@ -365,7 +347,7 @@ export const noTopLevelSurface: WardenRule = {
|
|
|
365
347
|
);
|
|
366
348
|
|
|
367
349
|
const diagnostics: WardenDiagnostic[] = [];
|
|
368
|
-
const body = (ast
|
|
350
|
+
const body = getNodeBodyStatements(ast);
|
|
369
351
|
for (const statement of body) {
|
|
370
352
|
const surfaceOpen = topLevelSurfaceOpen(statement, surfaceBindings);
|
|
371
353
|
if (!surfaceOpen) {
|
|
@@ -11,6 +11,7 @@ import { isDraftId } from '@ontrails/core';
|
|
|
11
11
|
|
|
12
12
|
import {
|
|
13
13
|
collectSignalDefinitionIds,
|
|
14
|
+
deriveConstString,
|
|
14
15
|
findConfigProperty,
|
|
15
16
|
findTrailDefinitions,
|
|
16
17
|
getStringValue,
|
|
@@ -18,7 +19,6 @@ import {
|
|
|
18
19
|
isStringLiteral,
|
|
19
20
|
offsetToLine,
|
|
20
21
|
parse,
|
|
21
|
-
deriveConstString,
|
|
22
22
|
} from './ast.js';
|
|
23
23
|
import type { AstNode } from './ast.js';
|
|
24
24
|
import { isTestFile } from './scan.js';
|
|
@@ -4,6 +4,11 @@ import { fileURLToPath } from 'node:url';
|
|
|
4
4
|
import { intentValues } from '@ontrails/core';
|
|
5
5
|
|
|
6
6
|
import {
|
|
7
|
+
getNodeExpression,
|
|
8
|
+
getNodeId,
|
|
9
|
+
getNodeInit,
|
|
10
|
+
getNodeKey,
|
|
11
|
+
getNodeProperties,
|
|
7
12
|
getPropertyName,
|
|
8
13
|
identifierName,
|
|
9
14
|
offsetToLine,
|
|
@@ -34,7 +39,7 @@ const unwrapExpression = (node: AstNode | undefined): AstNode | undefined => {
|
|
|
34
39
|
'TSTypeAssertion',
|
|
35
40
|
].includes(current.type)
|
|
36
41
|
) {
|
|
37
|
-
current = (current
|
|
42
|
+
current = getNodeExpression(current);
|
|
38
43
|
}
|
|
39
44
|
return current;
|
|
40
45
|
};
|
|
@@ -52,10 +57,8 @@ const findHttpMethodByIntentMap = (ast: AstNode): ProjectionMap | null => {
|
|
|
52
57
|
return;
|
|
53
58
|
}
|
|
54
59
|
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
init?: AstNode;
|
|
58
|
-
};
|
|
60
|
+
const id = getNodeId(node);
|
|
61
|
+
const init = getNodeInit(node);
|
|
59
62
|
if (identifierName(id) !== 'httpMethodByIntent') {
|
|
60
63
|
return;
|
|
61
64
|
}
|
|
@@ -67,17 +70,11 @@ const findHttpMethodByIntentMap = (ast: AstNode): ProjectionMap | null => {
|
|
|
67
70
|
}
|
|
68
71
|
|
|
69
72
|
const keys = new Set<string>();
|
|
70
|
-
for (const property of (
|
|
71
|
-
objectExpression as unknown as {
|
|
72
|
-
properties?: readonly AstNode[];
|
|
73
|
-
}
|
|
74
|
-
).properties ?? []) {
|
|
73
|
+
for (const property of getNodeProperties(objectExpression) ?? []) {
|
|
75
74
|
if (property.type !== 'Property') {
|
|
76
75
|
continue;
|
|
77
76
|
}
|
|
78
|
-
const key = getPropertyName(
|
|
79
|
-
(property as unknown as { key?: AstNode }).key
|
|
80
|
-
);
|
|
77
|
+
const key = getPropertyName(getNodeKey(property));
|
|
81
78
|
if (key) {
|
|
82
79
|
keys.add(key);
|
|
83
80
|
}
|
|
@@ -30,7 +30,21 @@ import { readFileSync } from 'node:fs';
|
|
|
30
30
|
import { dirname, join, normalize, relative, resolve } from 'node:path';
|
|
31
31
|
import { fileURLToPath } from 'node:url';
|
|
32
32
|
|
|
33
|
-
import {
|
|
33
|
+
import {
|
|
34
|
+
getNodeBodyStatements,
|
|
35
|
+
getNodeDeclaration,
|
|
36
|
+
getNodeDeclarations,
|
|
37
|
+
getNodeExportKind,
|
|
38
|
+
getNodeExported,
|
|
39
|
+
getNodeId,
|
|
40
|
+
getNodeLocal,
|
|
41
|
+
getNodeName,
|
|
42
|
+
getNodeSource,
|
|
43
|
+
getNodeSpecifiers,
|
|
44
|
+
getNodeValue,
|
|
45
|
+
offsetToLine,
|
|
46
|
+
parse,
|
|
47
|
+
} from './ast.js';
|
|
34
48
|
import type { AstNode } from './ast.js';
|
|
35
49
|
import type { WardenDiagnostic, WardenRule } from './types.js';
|
|
36
50
|
|
|
@@ -158,33 +172,33 @@ interface BarrelInventory {
|
|
|
158
172
|
}
|
|
159
173
|
|
|
160
174
|
const isTypeKind = (node: AstNode): boolean =>
|
|
161
|
-
(node
|
|
175
|
+
getNodeExportKind(node) === 'type';
|
|
162
176
|
|
|
163
177
|
const readNameNode = (node: AstNode | undefined): string | null => {
|
|
164
178
|
if (!node) {
|
|
165
179
|
return null;
|
|
166
180
|
}
|
|
167
181
|
if (node.type === 'Identifier') {
|
|
168
|
-
return (node
|
|
182
|
+
return getNodeName(node) ?? null;
|
|
169
183
|
}
|
|
170
184
|
if (node.type === 'Literal' || node.type === 'StringLiteral') {
|
|
171
|
-
const
|
|
185
|
+
const value = getNodeValue(node);
|
|
172
186
|
return typeof value === 'string' ? value : null;
|
|
173
187
|
}
|
|
174
188
|
return null;
|
|
175
189
|
};
|
|
176
190
|
|
|
177
191
|
const moduleSpecifierValue = (node: AstNode): string | null => {
|
|
178
|
-
const
|
|
192
|
+
const source = getNodeSource(node);
|
|
179
193
|
if (!source) {
|
|
180
194
|
return null;
|
|
181
195
|
}
|
|
182
|
-
const
|
|
196
|
+
const value = getNodeValue(source);
|
|
183
197
|
return typeof value === 'string' ? value : null;
|
|
184
198
|
};
|
|
185
199
|
|
|
186
200
|
const programBody = (ast: AstNode): readonly AstNode[] =>
|
|
187
|
-
(ast
|
|
201
|
+
getNodeBodyStatements(ast);
|
|
188
202
|
|
|
189
203
|
const diagnostic = (
|
|
190
204
|
sourceCode: string,
|
|
@@ -204,16 +218,13 @@ const specifiersFromExportDeclaration = (
|
|
|
204
218
|
node: AstNode,
|
|
205
219
|
moduleSpecifier: string
|
|
206
220
|
): readonly PublicExportSpecifier[] => {
|
|
207
|
-
const specifiers =
|
|
208
|
-
(node as unknown as { specifiers?: readonly AstNode[] }).specifiers ?? [];
|
|
221
|
+
const specifiers = getNodeSpecifiers(node) ?? [];
|
|
209
222
|
return specifiers.flatMap((specifier) => {
|
|
210
223
|
if (specifier.type !== 'ExportSpecifier' || isTypeKind(specifier)) {
|
|
211
224
|
return [];
|
|
212
225
|
}
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
local?: AstNode;
|
|
216
|
-
};
|
|
226
|
+
const exported = getNodeExported(specifier);
|
|
227
|
+
const local = getNodeLocal(specifier);
|
|
217
228
|
const exportName = readNameNode(exported);
|
|
218
229
|
if (!exportName) {
|
|
219
230
|
return [];
|
|
@@ -241,7 +252,7 @@ const inventoryNamedExport = (node: AstNode, ctx: InventoryContext): void => {
|
|
|
241
252
|
if (isTypeKind(node)) {
|
|
242
253
|
return;
|
|
243
254
|
}
|
|
244
|
-
const
|
|
255
|
+
const declaration = getNodeDeclaration(node);
|
|
245
256
|
if (declaration) {
|
|
246
257
|
// Declaration-form exports (`export const foo = ...`) are not module
|
|
247
258
|
// re-exports; the script's inventory skipped them the same way.
|
|
@@ -344,15 +355,13 @@ const declarationNameMatches = (
|
|
|
344
355
|
declaration.type === 'TSInterfaceDeclaration' ||
|
|
345
356
|
declaration.type === 'TSTypeAliasDeclaration'
|
|
346
357
|
) {
|
|
347
|
-
const
|
|
358
|
+
const id = getNodeId(declaration);
|
|
348
359
|
return readNameNode(id) === exportName;
|
|
349
360
|
}
|
|
350
361
|
if (declaration.type === 'VariableDeclaration') {
|
|
351
|
-
const declarations =
|
|
352
|
-
(declaration as unknown as { declarations?: readonly AstNode[] })
|
|
353
|
-
.declarations ?? [];
|
|
362
|
+
const declarations = getNodeDeclarations(declaration);
|
|
354
363
|
return declarations.some((declarator) => {
|
|
355
|
-
const
|
|
364
|
+
const id = getNodeId(declarator);
|
|
356
365
|
return readNameNode(id) === exportName;
|
|
357
366
|
});
|
|
358
367
|
}
|
|
@@ -404,7 +413,7 @@ const hasLeadingExampleForExport = (
|
|
|
404
413
|
if (statement.type !== 'ExportNamedDeclaration') {
|
|
405
414
|
continue;
|
|
406
415
|
}
|
|
407
|
-
const
|
|
416
|
+
const declaration = getNodeDeclaration(statement);
|
|
408
417
|
if (!declaration || !declarationNameMatches(declaration, importedName)) {
|
|
409
418
|
continue;
|
|
410
419
|
}
|
|
@@ -3,13 +3,13 @@ import { resolve, sep } from 'node:path';
|
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
5
|
extractStringLiteral,
|
|
6
|
+
getNodeSource,
|
|
6
7
|
hasIgnoreCommentOnLine,
|
|
7
8
|
offsetToLine,
|
|
8
9
|
parse,
|
|
9
10
|
splitSourceLines,
|
|
10
11
|
walk,
|
|
11
12
|
} from './ast.js';
|
|
12
|
-
import type { AstNode } from './ast.js';
|
|
13
13
|
import type {
|
|
14
14
|
ProjectAwareWardenRule,
|
|
15
15
|
ProjectContext,
|
|
@@ -223,7 +223,7 @@ const collectReExportSites = (
|
|
|
223
223
|
return;
|
|
224
224
|
}
|
|
225
225
|
|
|
226
|
-
const
|
|
226
|
+
const source = getNodeSource(node);
|
|
227
227
|
const value = extractStringLiteral(source);
|
|
228
228
|
if (typeof value !== 'string') {
|
|
229
229
|
return;
|
|
@@ -4,6 +4,9 @@ import {
|
|
|
4
4
|
extractStringLiteral,
|
|
5
5
|
findConfigProperty,
|
|
6
6
|
findTrailDefinitions,
|
|
7
|
+
getNodeElements,
|
|
8
|
+
getNodeId,
|
|
9
|
+
getNodeInit,
|
|
7
10
|
getStringValue,
|
|
8
11
|
identifierName,
|
|
9
12
|
isStringLiteral,
|
|
@@ -34,10 +37,8 @@ const collectArrayBindings = (ast: AstNode): ReadonlyMap<string, AstNode> => {
|
|
|
34
37
|
return;
|
|
35
38
|
}
|
|
36
39
|
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
readonly init?: AstNode;
|
|
40
|
-
};
|
|
40
|
+
const id = getNodeId(node);
|
|
41
|
+
const init = getNodeInit(node);
|
|
41
42
|
const name = identifierName(id);
|
|
42
43
|
if (name && init?.type === 'ArrayExpression') {
|
|
43
44
|
bindings.set(name, init);
|
|
@@ -69,10 +70,9 @@ const getFiresElements = (
|
|
|
69
70
|
return [];
|
|
70
71
|
}
|
|
71
72
|
|
|
72
|
-
return (
|
|
73
|
-
(
|
|
74
|
-
|
|
75
|
-
).filter((element): element is AstNode => element !== null);
|
|
73
|
+
return getNodeElements(array).filter(
|
|
74
|
+
(element): element is AstNode => element !== null
|
|
75
|
+
);
|
|
76
76
|
};
|
|
77
77
|
|
|
78
78
|
const resolveFireElementId = (
|
|
@@ -7,13 +7,16 @@
|
|
|
7
7
|
* the `warden-export-symmetry` rule will fail the build if they drift.
|
|
8
8
|
*/
|
|
9
9
|
import { activationOrphan } from './activation-orphan.js';
|
|
10
|
+
import { cliCommandRouteCoherence } from './cli-command-route-coherence.js';
|
|
10
11
|
import { circularRefs } from './circular-refs.js';
|
|
11
12
|
import { contourExists } from './contour-exists.js';
|
|
12
13
|
import { contextNoSurfaceTypes } from './context-no-surface-types.js';
|
|
13
14
|
import { composesDeclarations } from './composes-declarations.js';
|
|
14
15
|
import { deadInternalTrail } from './dead-internal-trail.js';
|
|
16
|
+
import { deadPublicTrail } from './dead-public-trail.js';
|
|
15
17
|
import { draftFileMarking } from './draft-file-marking.js';
|
|
16
18
|
import { draftVisibleDebt } from './draft-visible-debt.js';
|
|
19
|
+
import { duplicatePublicContract } from './duplicate-public-contract.js';
|
|
17
20
|
import { errorMappingCompleteness } from './error-mapping-completeness.js';
|
|
18
21
|
import { exampleValid } from './example-valid.js';
|
|
19
22
|
import { firesDeclarations } from './fires-declarations.js';
|
|
@@ -22,6 +25,7 @@ import { incompleteAccessorForStandardOp } from './incomplete-accessor-for-stand
|
|
|
22
25
|
import { incompleteCrud } from './incomplete-crud.js';
|
|
23
26
|
import { intentPropagation } from './intent-propagation.js';
|
|
24
27
|
import { layerFieldNameDrift } from './layer-field-name-drift.js';
|
|
28
|
+
import { libraryProjectionCoherence } from './library-projection-coherence.js';
|
|
25
29
|
import { missingReconcile } from './missing-reconcile.js';
|
|
26
30
|
import { missingVisibility } from './missing-visibility.js';
|
|
27
31
|
import { noDevPermitInSource } from './no-dev-permit-in-source.js';
|
|
@@ -55,6 +59,7 @@ import { scheduledDestroyIntent } from './scheduled-destroy-intent.js';
|
|
|
55
59
|
import { signalGraphCoaching } from './signal-graph-coaching.js';
|
|
56
60
|
import { staticResourceAccessorPreference } from './static-resource-accessor-preference.js';
|
|
57
61
|
import { surfaceFacetCoherence } from './surface-facet-coherence.js';
|
|
62
|
+
import { trailForkCoaching } from './trail-fork-coaching.js';
|
|
58
63
|
import {
|
|
59
64
|
forkWithoutPreservedBlaze,
|
|
60
65
|
markerSchemaUnsupported,
|
|
@@ -81,12 +86,15 @@ import { webhookRouteCollision } from './webhook-route-collision.js';
|
|
|
81
86
|
*/
|
|
82
87
|
export const registeredRuleNames: readonly string[] = [
|
|
83
88
|
activationOrphan.name,
|
|
89
|
+
cliCommandRouteCoherence.name,
|
|
84
90
|
circularRefs.name,
|
|
85
91
|
contextNoSurfaceTypes.name,
|
|
86
92
|
contourExists.name,
|
|
87
93
|
composesDeclarations.name,
|
|
88
94
|
deadInternalTrail.name,
|
|
95
|
+
deadPublicTrail.name,
|
|
89
96
|
deprecationWithoutGuidance.name,
|
|
97
|
+
duplicatePublicContract.name,
|
|
90
98
|
draftFileMarking.name,
|
|
91
99
|
draftVisibleDebt.name,
|
|
92
100
|
errorMappingCompleteness.name,
|
|
@@ -98,6 +106,7 @@ export const registeredRuleNames: readonly string[] = [
|
|
|
98
106
|
incompleteCrud.name,
|
|
99
107
|
intentPropagation.name,
|
|
100
108
|
layerFieldNameDrift.name,
|
|
109
|
+
libraryProjectionCoherence.name,
|
|
101
110
|
markerSchemaUnsupported.name,
|
|
102
111
|
missingReconcile.name,
|
|
103
112
|
missingVisibility.name,
|
|
@@ -133,6 +142,7 @@ export const registeredRuleNames: readonly string[] = [
|
|
|
133
142
|
signalGraphCoaching.name,
|
|
134
143
|
staticResourceAccessorPreference.name,
|
|
135
144
|
surfaceFacetCoherence.name,
|
|
145
|
+
trailForkCoaching.name,
|
|
136
146
|
unmaterializedActivationSource.name,
|
|
137
147
|
unreachableDetourShadowing.name,
|
|
138
148
|
validDetourContract.name,
|
|
@@ -10,9 +10,17 @@
|
|
|
10
10
|
import {
|
|
11
11
|
collectNamedResourceIds,
|
|
12
12
|
extractFirstStringArg,
|
|
13
|
-
findConfigProperty,
|
|
14
13
|
findBlazeBodies,
|
|
14
|
+
findConfigProperty,
|
|
15
15
|
findTrailDefinitions,
|
|
16
|
+
getNodeCallee,
|
|
17
|
+
getNodeId,
|
|
18
|
+
getNodeInit,
|
|
19
|
+
getNodeKey,
|
|
20
|
+
getNodeLeft,
|
|
21
|
+
getNodeObject,
|
|
22
|
+
getNodeProperty,
|
|
23
|
+
getNodeValueNode,
|
|
16
24
|
getStringValue,
|
|
17
25
|
identifierName,
|
|
18
26
|
isStringLiteral,
|
|
@@ -49,12 +57,8 @@ const extractMemberPair = (
|
|
|
49
57
|
return null;
|
|
50
58
|
}
|
|
51
59
|
|
|
52
|
-
const objName = identifierName(
|
|
53
|
-
|
|
54
|
-
);
|
|
55
|
-
const propName = identifierName(
|
|
56
|
-
(callee as unknown as { property?: AstNode }).property
|
|
57
|
-
);
|
|
60
|
+
const objName = identifierName(getNodeObject(callee));
|
|
61
|
+
const propName = identifierName(getNodeProperty(callee));
|
|
58
62
|
|
|
59
63
|
return objName && propName ? { objName, propName } : null;
|
|
60
64
|
};
|
|
@@ -64,10 +68,7 @@ const isInlineResourceCall = (node: AstNode): boolean => {
|
|
|
64
68
|
if (node.type !== 'CallExpression') {
|
|
65
69
|
return false;
|
|
66
70
|
}
|
|
67
|
-
return (
|
|
68
|
-
identifierName((node as unknown as { callee?: AstNode }).callee) ===
|
|
69
|
-
'resource'
|
|
70
|
-
);
|
|
71
|
+
return identifierName(getNodeCallee(node)) === 'resource';
|
|
71
72
|
};
|
|
72
73
|
|
|
73
74
|
/** Get `resources` array elements from a trail config. */
|
|
@@ -152,7 +153,7 @@ const extractContextParamName = (blazeBody: AstNode): string | null => {
|
|
|
152
153
|
return null;
|
|
153
154
|
}
|
|
154
155
|
if (param.type === 'AssignmentPattern') {
|
|
155
|
-
const
|
|
156
|
+
const left = getNodeLeft(param);
|
|
156
157
|
return identifierName(left);
|
|
157
158
|
}
|
|
158
159
|
return identifierName(param);
|
|
@@ -163,16 +164,11 @@ const extractResourceAlias = (property: AstNode): string | null => {
|
|
|
163
164
|
if (property.type !== 'Property') {
|
|
164
165
|
return null;
|
|
165
166
|
}
|
|
166
|
-
const keyName = identifierName(
|
|
167
|
-
(property as unknown as { key?: AstNode }).key
|
|
168
|
-
);
|
|
167
|
+
const keyName = identifierName(getNodeKey(property));
|
|
169
168
|
if (keyName !== 'resource') {
|
|
170
169
|
return null;
|
|
171
170
|
}
|
|
172
|
-
return (
|
|
173
|
-
identifierName((property as unknown as { value?: AstNode }).value) ??
|
|
174
|
-
keyName
|
|
175
|
-
);
|
|
171
|
+
return identifierName(getNodeValueNode(property)) ?? keyName;
|
|
176
172
|
};
|
|
177
173
|
|
|
178
174
|
/**
|
|
@@ -224,8 +220,7 @@ const extractCallCallee = (node: AstNode): AstNode | null => {
|
|
|
224
220
|
if (node.type !== 'CallExpression') {
|
|
225
221
|
return null;
|
|
226
222
|
}
|
|
227
|
-
return ((node
|
|
228
|
-
null) as AstNode | null;
|
|
223
|
+
return (getNodeCallee(node) ?? null) as AstNode | null;
|
|
229
224
|
};
|
|
230
225
|
|
|
231
226
|
/** Extract the first identifier argument from a CallExpression. */
|
|
@@ -338,16 +333,12 @@ const collectResourceAliases = (
|
|
|
338
333
|
return [];
|
|
339
334
|
}
|
|
340
335
|
|
|
341
|
-
const keyName = identifierName(
|
|
342
|
-
(property as unknown as { key?: AstNode }).key
|
|
343
|
-
);
|
|
336
|
+
const keyName = identifierName(getNodeKey(property));
|
|
344
337
|
if (keyName !== 'resource') {
|
|
345
338
|
return [];
|
|
346
339
|
}
|
|
347
340
|
|
|
348
|
-
const alias =
|
|
349
|
-
identifierName((property as unknown as { value?: AstNode }).value) ??
|
|
350
|
-
keyName;
|
|
341
|
+
const alias = identifierName(getNodeValueNode(property)) ?? keyName;
|
|
351
342
|
return [alias];
|
|
352
343
|
});
|
|
353
344
|
};
|
|
@@ -357,10 +348,8 @@ const collectResourceAliases = (
|
|
|
357
348
|
return;
|
|
358
349
|
}
|
|
359
350
|
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
readonly init?: AstNode;
|
|
363
|
-
};
|
|
351
|
+
const id = getNodeId(node);
|
|
352
|
+
const init = getNodeInit(node);
|
|
364
353
|
const initName = identifierName(init);
|
|
365
354
|
if (!initName || !ctxNames.has(initName)) {
|
|
366
355
|
return;
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
extractFirstStringArg,
|
|
7
7
|
findConfigProperty,
|
|
8
8
|
findTrailDefinitions,
|
|
9
|
+
getNodeCallee,
|
|
9
10
|
getStringValue,
|
|
10
11
|
identifierName,
|
|
11
12
|
isStringLiteral,
|
|
@@ -22,8 +23,7 @@ import type {
|
|
|
22
23
|
|
|
23
24
|
const isResourceCall = (node: AstNode): boolean =>
|
|
24
25
|
node.type === 'CallExpression' &&
|
|
25
|
-
identifierName((node
|
|
26
|
-
'resource';
|
|
26
|
+
identifierName(getNodeCallee(node)) === 'resource';
|
|
27
27
|
|
|
28
28
|
const getResourceElements = (config: AstNode): readonly AstNode[] => {
|
|
29
29
|
const resourcesProp = findConfigProperty(config, 'resources');
|