@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
|
@@ -2,6 +2,22 @@ import {
|
|
|
2
2
|
findBlazeBodies,
|
|
3
3
|
findConfigProperty,
|
|
4
4
|
findTrailDefinitions,
|
|
5
|
+
getNodeArgument,
|
|
6
|
+
getNodeArguments,
|
|
7
|
+
getNodeCallee,
|
|
8
|
+
getNodeElements,
|
|
9
|
+
getNodeExpression,
|
|
10
|
+
getNodeId,
|
|
11
|
+
getNodeInit,
|
|
12
|
+
getNodeKey,
|
|
13
|
+
getNodeLeft,
|
|
14
|
+
getNodeName,
|
|
15
|
+
getNodeObject,
|
|
16
|
+
getNodeParams,
|
|
17
|
+
getNodeProperties,
|
|
18
|
+
getNodeProperty,
|
|
19
|
+
getNodeValue,
|
|
20
|
+
getNodeValueNode,
|
|
5
21
|
isMemberAccessNonComputed,
|
|
6
22
|
offsetToLine,
|
|
7
23
|
parse,
|
|
@@ -148,14 +164,14 @@ const diagnostic = (
|
|
|
148
164
|
|
|
149
165
|
const staticPropertyKeyName = (node: AstNode | undefined): string | null => {
|
|
150
166
|
if (node?.type === 'Identifier') {
|
|
151
|
-
return (node
|
|
167
|
+
return getNodeName(node) ?? null;
|
|
152
168
|
}
|
|
153
169
|
if (
|
|
154
170
|
node?.type === 'Literal' ||
|
|
155
171
|
node?.type === 'StringLiteral' ||
|
|
156
172
|
node?.type === 'NumericLiteral'
|
|
157
173
|
) {
|
|
158
|
-
const
|
|
174
|
+
const value = getNodeValue(node);
|
|
159
175
|
return typeof value === 'string' || typeof value === 'number'
|
|
160
176
|
? String(value)
|
|
161
177
|
: null;
|
|
@@ -164,22 +180,17 @@ const staticPropertyKeyName = (node: AstNode | undefined): string | null => {
|
|
|
164
180
|
};
|
|
165
181
|
|
|
166
182
|
const objectProperties = (node: AstNode | undefined): readonly AstNode[] =>
|
|
167
|
-
node?.type === 'ObjectExpression'
|
|
168
|
-
? ((node as unknown as { properties?: readonly AstNode[] }).properties ??
|
|
169
|
-
[])
|
|
170
|
-
: [];
|
|
183
|
+
node?.type === 'ObjectExpression' ? (getNodeProperties(node) ?? []) : [];
|
|
171
184
|
|
|
172
185
|
const propertyName = (node: AstNode): string | null =>
|
|
173
|
-
node.type === 'Property'
|
|
174
|
-
? staticPropertyKeyName((node as unknown as { key?: AstNode }).key)
|
|
175
|
-
: null;
|
|
186
|
+
node.type === 'Property' ? staticPropertyKeyName(getNodeKey(node)) : null;
|
|
176
187
|
|
|
177
188
|
const hasProperty = (node: AstNode, name: string): boolean =>
|
|
178
189
|
objectProperties(node).some((property) => propertyName(property) === name);
|
|
179
190
|
|
|
180
191
|
const propertyValue = (property: AstNode | null): AstNode | undefined =>
|
|
181
192
|
property?.type === 'Property'
|
|
182
|
-
? ((property
|
|
193
|
+
? (getNodeValueNode(property) ?? undefined)
|
|
183
194
|
: undefined;
|
|
184
195
|
|
|
185
196
|
const trailIsVersioned = (config: AstNode): boolean =>
|
|
@@ -194,9 +205,7 @@ const versionEntries = (config: AstNode): readonly AstNode[] => {
|
|
|
194
205
|
};
|
|
195
206
|
|
|
196
207
|
const identifierName = (node: AstNode | undefined): string | undefined =>
|
|
197
|
-
node?.type === 'Identifier'
|
|
198
|
-
? (node as unknown as { name?: string }).name
|
|
199
|
-
: undefined;
|
|
208
|
+
node?.type === 'Identifier' ? getNodeName(node) : undefined;
|
|
200
209
|
|
|
201
210
|
const schemaBindingInitializer = (
|
|
202
211
|
schemaBindings: SchemaBindings,
|
|
@@ -221,18 +230,16 @@ const schemaBindingInitializer = (
|
|
|
221
230
|
|
|
222
231
|
const memberObject = (node: AstNode | undefined): AstNode | undefined =>
|
|
223
232
|
node !== undefined && isMemberAccessNonComputed(node)
|
|
224
|
-
? (node
|
|
233
|
+
? getNodeObject(node)
|
|
225
234
|
: undefined;
|
|
226
235
|
|
|
227
236
|
const memberPropertyName = (node: AstNode | undefined): string | undefined =>
|
|
228
237
|
node !== undefined && isMemberAccessNonComputed(node)
|
|
229
|
-
? identifierName((node
|
|
238
|
+
? identifierName(getNodeProperty(node))
|
|
230
239
|
: undefined;
|
|
231
240
|
|
|
232
241
|
const callCallee = (node: AstNode): AstNode | undefined =>
|
|
233
|
-
node.type === 'CallExpression'
|
|
234
|
-
? (node as unknown as { callee?: AstNode }).callee
|
|
235
|
-
: undefined;
|
|
242
|
+
node.type === 'CallExpression' ? getNodeCallee(node) : undefined;
|
|
236
243
|
|
|
237
244
|
const isZodSchemaReceiver = (
|
|
238
245
|
node: AstNode | undefined,
|
|
@@ -272,9 +279,7 @@ const isZodSchemaCallee = (
|
|
|
272
279
|
node !== undefined && isZodSchemaReceiver(memberObject(node), schemaBindings);
|
|
273
280
|
|
|
274
281
|
const callArguments = (node: AstNode): readonly AstNode[] =>
|
|
275
|
-
node.type === 'CallExpression'
|
|
276
|
-
? ((node as unknown as { arguments?: readonly AstNode[] }).arguments ?? [])
|
|
277
|
-
: [];
|
|
282
|
+
node.type === 'CallExpression' ? (getNodeArguments(node) ?? []) : [];
|
|
278
283
|
|
|
279
284
|
const unwrapExpression = (node: AstNode | undefined): AstNode | undefined => {
|
|
280
285
|
let current = node;
|
|
@@ -283,7 +288,7 @@ const unwrapExpression = (node: AstNode | undefined): AstNode | undefined => {
|
|
|
283
288
|
current?.type === 'TSSatisfiesExpression' ||
|
|
284
289
|
current?.type === 'TSNonNullExpression'
|
|
285
290
|
) {
|
|
286
|
-
current = (current
|
|
291
|
+
current = getNodeExpression(current);
|
|
287
292
|
}
|
|
288
293
|
return current;
|
|
289
294
|
};
|
|
@@ -291,10 +296,7 @@ const unwrapExpression = (node: AstNode | undefined): AstNode | undefined => {
|
|
|
291
296
|
const arrayExpressionLength = (node: AstNode | undefined): number => {
|
|
292
297
|
const expression = unwrapExpression(node);
|
|
293
298
|
return expression?.type === 'ArrayExpression'
|
|
294
|
-
? (
|
|
295
|
-
(expression as unknown as { elements?: readonly unknown[] }).elements ??
|
|
296
|
-
[]
|
|
297
|
-
).length
|
|
299
|
+
? (getNodeElements(expression) ?? []).length
|
|
298
300
|
: 0;
|
|
299
301
|
};
|
|
300
302
|
|
|
@@ -320,18 +322,14 @@ const literalExpressionIsJsonLossy = (expression: AstNode): boolean => {
|
|
|
320
322
|
) {
|
|
321
323
|
return true;
|
|
322
324
|
}
|
|
323
|
-
|
|
324
|
-
readonly bigint?: unknown;
|
|
325
|
-
readonly regex?: unknown;
|
|
326
|
-
readonly value?: unknown;
|
|
327
|
-
};
|
|
328
|
-
if (literal.bigint !== undefined || literal.regex !== undefined) {
|
|
325
|
+
if (expression['bigint'] !== undefined || expression['regex'] !== undefined) {
|
|
329
326
|
return true;
|
|
330
327
|
}
|
|
331
|
-
|
|
328
|
+
const value = getNodeValue(expression);
|
|
329
|
+
if (value instanceof RegExp) {
|
|
332
330
|
return true;
|
|
333
331
|
}
|
|
334
|
-
return typeof
|
|
332
|
+
return typeof value === 'number' && !Number.isFinite(value);
|
|
335
333
|
};
|
|
336
334
|
|
|
337
335
|
const expressionIsJsonLossy = (node: AstNode | undefined): boolean => {
|
|
@@ -350,9 +348,7 @@ const expressionIsJsonLossy = (node: AstNode | undefined): boolean => {
|
|
|
350
348
|
}
|
|
351
349
|
|
|
352
350
|
if (expression.type === 'UnaryExpression') {
|
|
353
|
-
return expressionIsJsonLossy(
|
|
354
|
-
(expression as unknown as { argument?: AstNode }).argument
|
|
355
|
-
);
|
|
351
|
+
return expressionIsJsonLossy(getNodeArgument(expression));
|
|
356
352
|
}
|
|
357
353
|
|
|
358
354
|
if (expression.type === 'Literal' || expression.type === 'NumericLiteral') {
|
|
@@ -360,9 +356,7 @@ const expressionIsJsonLossy = (node: AstNode | undefined): boolean => {
|
|
|
360
356
|
}
|
|
361
357
|
|
|
362
358
|
if (expression.type === 'ArrayExpression') {
|
|
363
|
-
const elements =
|
|
364
|
-
(expression as unknown as { elements?: readonly (AstNode | null)[] })
|
|
365
|
-
.elements ?? [];
|
|
359
|
+
const elements = getNodeElements(expression);
|
|
366
360
|
return elements.some((element) =>
|
|
367
361
|
expressionIsJsonLossy(element ?? undefined)
|
|
368
362
|
);
|
|
@@ -435,10 +429,9 @@ const isReferenceValuedLiteralCall = (
|
|
|
435
429
|
if (value?.type !== 'ArrayExpression') {
|
|
436
430
|
return false;
|
|
437
431
|
}
|
|
438
|
-
return (
|
|
439
|
-
(
|
|
440
|
-
|
|
441
|
-
).some((element) => expressionIsReferenceValued(element ?? undefined));
|
|
432
|
+
return (getNodeElements(value) ?? []).some((element) =>
|
|
433
|
+
expressionIsReferenceValued(element ?? undefined)
|
|
434
|
+
);
|
|
442
435
|
};
|
|
443
436
|
|
|
444
437
|
const isJsonLossyEnumCall = (
|
|
@@ -486,10 +479,9 @@ const isReferenceValuedEnumCall = (
|
|
|
486
479
|
const [rawOptions] = callArguments(node);
|
|
487
480
|
const options = unwrapExpression(rawOptions);
|
|
488
481
|
if (options?.type === 'ArrayExpression') {
|
|
489
|
-
return (
|
|
490
|
-
(
|
|
491
|
-
|
|
492
|
-
).some((element) => expressionIsReferenceValued(element ?? undefined));
|
|
482
|
+
return getNodeElements(options).some((element) =>
|
|
483
|
+
expressionIsReferenceValued(element ?? undefined)
|
|
484
|
+
);
|
|
493
485
|
}
|
|
494
486
|
if (options?.type !== 'ObjectExpression') {
|
|
495
487
|
return false;
|
|
@@ -583,10 +575,9 @@ const nestedSchemaArguments = (node: AstNode): readonly AstNode[] => {
|
|
|
583
575
|
const [rawOptions] = callArguments(node);
|
|
584
576
|
const options = unwrapExpression(rawOptions);
|
|
585
577
|
return options?.type === 'ArrayExpression'
|
|
586
|
-
? (
|
|
587
|
-
(
|
|
588
|
-
|
|
589
|
-
).filter((element): element is AstNode => element !== null)
|
|
578
|
+
? getNodeElements(options).filter(
|
|
579
|
+
(element): element is AstNode => element !== null
|
|
580
|
+
)
|
|
590
581
|
: [];
|
|
591
582
|
};
|
|
592
583
|
|
|
@@ -689,7 +680,7 @@ const isMemberCallNamed = (
|
|
|
689
680
|
|
|
690
681
|
const bindingName = (node: AstNode): string | undefined =>
|
|
691
682
|
node.type === 'VariableDeclarator'
|
|
692
|
-
? identifierName((node
|
|
683
|
+
? identifierName(getNodeId(node))
|
|
693
684
|
: undefined;
|
|
694
685
|
|
|
695
686
|
const lexicalScopeTypes = new Set([
|
|
@@ -716,20 +707,15 @@ const addPatternBindingNames = (
|
|
|
716
707
|
return;
|
|
717
708
|
}
|
|
718
709
|
if (node.type === 'AssignmentPattern') {
|
|
719
|
-
addPatternBindingNames((node
|
|
710
|
+
addPatternBindingNames(getNodeLeft(node), into);
|
|
720
711
|
return;
|
|
721
712
|
}
|
|
722
713
|
if (node.type === 'RestElement') {
|
|
723
|
-
addPatternBindingNames(
|
|
724
|
-
(node as unknown as { argument?: AstNode }).argument,
|
|
725
|
-
into
|
|
726
|
-
);
|
|
714
|
+
addPatternBindingNames(getNodeArgument(node), into);
|
|
727
715
|
return;
|
|
728
716
|
}
|
|
729
717
|
if (node.type === 'ArrayPattern') {
|
|
730
|
-
const elements =
|
|
731
|
-
(node as unknown as { elements?: readonly (AstNode | null)[] })
|
|
732
|
-
.elements ?? [];
|
|
718
|
+
const elements = getNodeElements(node);
|
|
733
719
|
for (const element of elements) {
|
|
734
720
|
addPatternBindingNames(element ?? undefined, into);
|
|
735
721
|
}
|
|
@@ -738,17 +724,13 @@ const addPatternBindingNames = (
|
|
|
738
724
|
if (node.type !== 'ObjectPattern') {
|
|
739
725
|
return;
|
|
740
726
|
}
|
|
741
|
-
const properties =
|
|
742
|
-
(node as unknown as { properties?: readonly AstNode[] }).properties ?? [];
|
|
727
|
+
const properties = getNodeProperties(node) ?? [];
|
|
743
728
|
for (const property of properties) {
|
|
744
729
|
if (property.type === 'RestElement') {
|
|
745
730
|
addPatternBindingNames(property, into);
|
|
746
731
|
continue;
|
|
747
732
|
}
|
|
748
|
-
addPatternBindingNames(
|
|
749
|
-
(property as unknown as { value?: AstNode }).value,
|
|
750
|
-
into
|
|
751
|
-
);
|
|
733
|
+
addPatternBindingNames(getNodeValueNode(property), into);
|
|
752
734
|
}
|
|
753
735
|
};
|
|
754
736
|
|
|
@@ -757,8 +739,7 @@ const parameterBindingNames = (node: AstNode): readonly string[] => {
|
|
|
757
739
|
return [];
|
|
758
740
|
}
|
|
759
741
|
const names = new Set<string>();
|
|
760
|
-
const params =
|
|
761
|
-
(node as unknown as { params?: readonly AstNode[] }).params ?? [];
|
|
742
|
+
const params = getNodeParams(node) ?? [];
|
|
762
743
|
for (const param of params) {
|
|
763
744
|
addPatternBindingNames(param, names);
|
|
764
745
|
}
|
|
@@ -767,7 +748,7 @@ const parameterBindingNames = (node: AstNode): readonly string[] => {
|
|
|
767
748
|
|
|
768
749
|
const variableInitializer = (node: AstNode): AstNode | undefined =>
|
|
769
750
|
node.type === 'VariableDeclarator'
|
|
770
|
-
? ((node
|
|
751
|
+
? (getNodeInit(node) ?? undefined)
|
|
771
752
|
: undefined;
|
|
772
753
|
|
|
773
754
|
const isZodSchemaExpression = (
|
|
@@ -887,22 +868,19 @@ const composeCallHasVersionPin = (node: AstNode): boolean => {
|
|
|
887
868
|
if (node.type !== 'CallExpression') {
|
|
888
869
|
return false;
|
|
889
870
|
}
|
|
890
|
-
const
|
|
891
|
-
|
|
892
|
-
callee?: AstNode;
|
|
893
|
-
};
|
|
871
|
+
const args = getNodeArguments(node);
|
|
872
|
+
const callee = getNodeCallee(node);
|
|
894
873
|
if (!callee || !args) {
|
|
895
874
|
return false;
|
|
896
875
|
}
|
|
897
876
|
|
|
898
877
|
const isComposeIdentifier =
|
|
899
|
-
callee.type === 'Identifier' &&
|
|
900
|
-
|
|
901
|
-
const { property } = callee as unknown as { property?: AstNode };
|
|
878
|
+
callee.type === 'Identifier' && getNodeName(callee) === 'compose';
|
|
879
|
+
const property = getNodeProperty(callee);
|
|
902
880
|
const isComposeMember =
|
|
903
881
|
isMemberAccessNonComputed(callee) &&
|
|
904
882
|
property?.type === 'Identifier' &&
|
|
905
|
-
(property
|
|
883
|
+
getNodeName(property) === 'compose';
|
|
906
884
|
|
|
907
885
|
return (isComposeIdentifier || isComposeMember) && hasVersionOption(args[2]);
|
|
908
886
|
};
|
package/src/rules/types.ts
CHANGED
|
@@ -245,6 +245,8 @@ export interface ProjectContext {
|
|
|
245
245
|
readonly crudTableIds?: ReadonlySet<string>;
|
|
246
246
|
/** All known trail IDs in the project */
|
|
247
247
|
readonly knownTrailIds: ReadonlySet<string>;
|
|
248
|
+
/** Trail IDs registered in configured app topo targets. */
|
|
249
|
+
readonly topoTrailIds?: ReadonlySet<string>;
|
|
248
250
|
/** Declared contour references keyed by source contour name. */
|
|
249
251
|
readonly contourReferencesByName?: ReadonlyMap<string, readonly string[]>;
|
|
250
252
|
/** All known resource IDs in the project */
|
|
@@ -4,6 +4,13 @@ import {
|
|
|
4
4
|
extractStringLiteral,
|
|
5
5
|
findConfigProperty,
|
|
6
6
|
findTrailDefinitions,
|
|
7
|
+
getNodeId,
|
|
8
|
+
getNodeImported,
|
|
9
|
+
getNodeInit,
|
|
10
|
+
getNodeLocal,
|
|
11
|
+
getNodeSource,
|
|
12
|
+
getNodeSpecifiers,
|
|
13
|
+
getNodeSuperClass,
|
|
7
14
|
identifierName,
|
|
8
15
|
offsetToLine,
|
|
9
16
|
parse,
|
|
@@ -46,7 +53,7 @@ const resolveKnownErrorName = (
|
|
|
46
53
|
): string => aliases.get(name) ?? name;
|
|
47
54
|
|
|
48
55
|
const coreImportSource = (node: AstNode): string | null =>
|
|
49
|
-
extractStringLiteral((node
|
|
56
|
+
extractStringLiteral(getNodeSource(node));
|
|
50
57
|
|
|
51
58
|
const collectImportSpecifierAliases = (
|
|
52
59
|
specifiers: readonly AstNode[] | undefined,
|
|
@@ -57,13 +64,9 @@ const collectImportSpecifierAliases = (
|
|
|
57
64
|
continue;
|
|
58
65
|
}
|
|
59
66
|
|
|
60
|
-
const localName = identifierName(
|
|
61
|
-
(specifier as unknown as { local?: AstNode }).local
|
|
62
|
-
);
|
|
67
|
+
const localName = identifierName(getNodeLocal(specifier));
|
|
63
68
|
const importedName =
|
|
64
|
-
identifierName(
|
|
65
|
-
(specifier as unknown as { imported?: AstNode }).imported
|
|
66
|
-
) ?? localName;
|
|
69
|
+
identifierName(getNodeImported(specifier)) ?? localName;
|
|
67
70
|
|
|
68
71
|
if (localName && importedName && knownErrorConstructors.has(importedName)) {
|
|
69
72
|
aliases.set(localName, importedName);
|
|
@@ -85,9 +88,7 @@ const collectKnownErrorAliases = (
|
|
|
85
88
|
return;
|
|
86
89
|
}
|
|
87
90
|
|
|
88
|
-
const
|
|
89
|
-
specifiers?: readonly AstNode[];
|
|
90
|
-
};
|
|
91
|
+
const specifiers = getNodeSpecifiers(node);
|
|
91
92
|
collectImportSpecifierAliases(specifiers, aliases);
|
|
92
93
|
});
|
|
93
94
|
|
|
@@ -116,15 +117,13 @@ const collectClassExpressionParent = (
|
|
|
116
117
|
return;
|
|
117
118
|
}
|
|
118
119
|
|
|
119
|
-
const
|
|
120
|
+
const init = getNodeInit(node);
|
|
120
121
|
if (!init || init.type !== 'ClassExpression') {
|
|
121
122
|
return;
|
|
122
123
|
}
|
|
123
124
|
|
|
124
|
-
const className = identifierName((node
|
|
125
|
-
const parentName = identifierName(
|
|
126
|
-
(init as unknown as { superClass?: AstNode }).superClass
|
|
127
|
-
);
|
|
125
|
+
const className = identifierName(getNodeId(node));
|
|
126
|
+
const parentName = identifierName(getNodeSuperClass(init));
|
|
128
127
|
recordLocalErrorParent(parents, aliases, className, parentName);
|
|
129
128
|
};
|
|
130
129
|
|
|
@@ -136,12 +135,8 @@ const collectLocalErrorParents = (
|
|
|
136
135
|
|
|
137
136
|
walk(ast, (node) => {
|
|
138
137
|
if (node.type === 'ClassDeclaration') {
|
|
139
|
-
const className = identifierName(
|
|
140
|
-
|
|
141
|
-
);
|
|
142
|
-
const parentName = identifierName(
|
|
143
|
-
(node as unknown as { superClass?: AstNode }).superClass
|
|
144
|
-
);
|
|
138
|
+
const className = identifierName(getNodeId(node));
|
|
139
|
+
const parentName = identifierName(getNodeSuperClass(node));
|
|
145
140
|
recordLocalErrorParent(parents, aliases, className, parentName);
|
|
146
141
|
return;
|
|
147
142
|
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
extractStringOrTemplateLiteral,
|
|
3
|
+
getNodeName,
|
|
4
|
+
getNodeProperty,
|
|
5
|
+
getNodeValue,
|
|
3
6
|
offsetToLine,
|
|
4
7
|
parse,
|
|
5
8
|
walk,
|
|
@@ -35,11 +38,8 @@ const isDescribeMemberCallee = (callee: AstNode | undefined): boolean => {
|
|
|
35
38
|
if (!callee || !MEMBER_CALLEE_TYPES.has(callee.type)) {
|
|
36
39
|
return false;
|
|
37
40
|
}
|
|
38
|
-
const prop = (callee
|
|
39
|
-
return (
|
|
40
|
-
prop?.type === 'Identifier' &&
|
|
41
|
-
(prop as unknown as { name?: string }).name === 'describe'
|
|
42
|
-
);
|
|
41
|
+
const prop = getNodeProperty(callee);
|
|
42
|
+
return prop?.type === 'Identifier' && getNodeName(prop) === 'describe';
|
|
43
43
|
};
|
|
44
44
|
|
|
45
45
|
const hasStringLiteralFirstArg = (node: AstNode): boolean => {
|
|
@@ -87,14 +87,16 @@ const isDescribeCall = (node: AstNode): boolean => {
|
|
|
87
87
|
* missing an `@see` token.
|
|
88
88
|
*/
|
|
89
89
|
const extractQuasiText = (quasi: AstNode): string | null => {
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
90
|
+
const value = getNodeValue(quasi);
|
|
91
|
+
if (!value || typeof value !== 'object') {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
const record = value as Record<string, unknown>;
|
|
95
|
+
if (typeof record['cooked'] === 'string') {
|
|
96
|
+
return record['cooked'];
|
|
95
97
|
}
|
|
96
|
-
if (typeof
|
|
97
|
-
return
|
|
98
|
+
if (typeof record['raw'] === 'string') {
|
|
99
|
+
return record['raw'];
|
|
98
100
|
}
|
|
99
101
|
return null;
|
|
100
102
|
};
|
|
@@ -16,7 +16,26 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { resolve } from 'node:path';
|
|
18
18
|
import { fileURLToPath } from 'node:url';
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
getNodeArgument,
|
|
21
|
+
getNodeDeclaration,
|
|
22
|
+
getNodeDeclarations,
|
|
23
|
+
getNodeElements,
|
|
24
|
+
getNodeExportKind,
|
|
25
|
+
getNodeExported,
|
|
26
|
+
getNodeId,
|
|
27
|
+
getNodeKey,
|
|
28
|
+
getNodeLeft,
|
|
29
|
+
getNodeLocal,
|
|
30
|
+
getNodeName,
|
|
31
|
+
getNodeProperties,
|
|
32
|
+
getNodeSource,
|
|
33
|
+
getNodeValue,
|
|
34
|
+
getNodeValueNode,
|
|
35
|
+
offsetToLine,
|
|
36
|
+
parse,
|
|
37
|
+
walk,
|
|
38
|
+
} from './ast.js';
|
|
20
39
|
import type { AstNode } from './ast.js';
|
|
21
40
|
import { registeredRuleNames } from './registry-names.js';
|
|
22
41
|
import type { WardenDiagnostic, WardenRule } from './types.js';
|
|
@@ -61,10 +80,10 @@ const readIdentifierOrStringName = (
|
|
|
61
80
|
return null;
|
|
62
81
|
}
|
|
63
82
|
if (node.type === 'Identifier') {
|
|
64
|
-
return (node
|
|
83
|
+
return getNodeName(node) ?? null;
|
|
65
84
|
}
|
|
66
85
|
if (node.type === 'Literal' || node.type === 'StringLiteral') {
|
|
67
|
-
const
|
|
86
|
+
const value = getNodeValue(node);
|
|
68
87
|
return typeof value === 'string' ? value : null;
|
|
69
88
|
}
|
|
70
89
|
return null;
|
|
@@ -73,10 +92,8 @@ const readIdentifierOrStringName = (
|
|
|
73
92
|
const extractSpecifierNames = (
|
|
74
93
|
specifier: AstNode
|
|
75
94
|
): { readonly name: string; readonly localName: string } | null => {
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
local?: AstNode;
|
|
79
|
-
};
|
|
95
|
+
const exported = getNodeExported(specifier);
|
|
96
|
+
const local = getNodeLocal(specifier);
|
|
80
97
|
const name = readIdentifierOrStringName(exported);
|
|
81
98
|
if (!name) {
|
|
82
99
|
return null;
|
|
@@ -86,7 +103,7 @@ const extractSpecifierNames = (
|
|
|
86
103
|
};
|
|
87
104
|
|
|
88
105
|
const isTypeExportSpecifier = (specifier: AstNode): boolean =>
|
|
89
|
-
(specifier
|
|
106
|
+
getNodeExportKind(specifier) === 'type';
|
|
90
107
|
|
|
91
108
|
const specifierSite = (specifier: AstNode): ExportSite | null => {
|
|
92
109
|
if (
|
|
@@ -129,7 +146,7 @@ const siteFromSimpleBinding = (
|
|
|
129
146
|
return name ? { localName: name, name, start } : null;
|
|
130
147
|
}
|
|
131
148
|
if (node.type === 'AssignmentPattern') {
|
|
132
|
-
const
|
|
149
|
+
const left = getNodeLeft(node);
|
|
133
150
|
return left ? siteFromSimpleBinding(left, start) : null;
|
|
134
151
|
}
|
|
135
152
|
return null;
|
|
@@ -172,16 +189,14 @@ const sitesFromObjectProperty = (
|
|
|
172
189
|
recurse: PatternSitesFn
|
|
173
190
|
): readonly ExportSite[] => {
|
|
174
191
|
if (prop.type === 'RestElement') {
|
|
175
|
-
const
|
|
192
|
+
const argument = getNodeArgument(prop);
|
|
176
193
|
return recurse(argument, start);
|
|
177
194
|
}
|
|
178
195
|
if (prop.type !== 'Property') {
|
|
179
196
|
return [];
|
|
180
197
|
}
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
value?: AstNode;
|
|
184
|
-
};
|
|
198
|
+
const key = getNodeKey(prop);
|
|
199
|
+
const value = getNodeValueNode(prop);
|
|
185
200
|
if (isNestedPatternValue(value)) {
|
|
186
201
|
return recurse(value, start);
|
|
187
202
|
}
|
|
@@ -198,7 +213,7 @@ const sitesFromArrayElement = (
|
|
|
198
213
|
return [];
|
|
199
214
|
}
|
|
200
215
|
if (element.type === 'RestElement') {
|
|
201
|
-
const
|
|
216
|
+
const argument = getNodeArgument(element);
|
|
202
217
|
return recurse(argument, start);
|
|
203
218
|
}
|
|
204
219
|
return recurse(element, start);
|
|
@@ -209,9 +224,7 @@ const sitesFromObjectPattern = (
|
|
|
209
224
|
start: number,
|
|
210
225
|
recurse: PatternSitesFn
|
|
211
226
|
): readonly ExportSite[] => {
|
|
212
|
-
const properties =
|
|
213
|
-
(pattern as unknown as { properties?: readonly AstNode[] }).properties ??
|
|
214
|
-
[];
|
|
227
|
+
const properties = getNodeProperties(pattern) ?? [];
|
|
215
228
|
return properties.flatMap((prop) =>
|
|
216
229
|
sitesFromObjectProperty(prop, start, recurse)
|
|
217
230
|
);
|
|
@@ -222,9 +235,7 @@ const sitesFromArrayPattern = (
|
|
|
222
235
|
start: number,
|
|
223
236
|
recurse: PatternSitesFn
|
|
224
237
|
): readonly ExportSite[] => {
|
|
225
|
-
const elements =
|
|
226
|
-
(pattern as unknown as { elements?: readonly (AstNode | null)[] })
|
|
227
|
-
.elements ?? [];
|
|
238
|
+
const elements = getNodeElements(pattern);
|
|
228
239
|
return elements.flatMap((element) =>
|
|
229
240
|
sitesFromArrayElement(element, start, recurse)
|
|
230
241
|
);
|
|
@@ -262,16 +273,14 @@ const sitesForDeclaration = (declaration: AstNode): readonly ExportSite[] => {
|
|
|
262
273
|
declaration.type === 'FunctionDeclaration' ||
|
|
263
274
|
declaration.type === 'ClassDeclaration'
|
|
264
275
|
) {
|
|
265
|
-
const
|
|
276
|
+
const id = getNodeId(declaration);
|
|
266
277
|
const site = namedSiteFromDeclId(id, declaration.start);
|
|
267
278
|
return site ? [site] : [];
|
|
268
279
|
}
|
|
269
280
|
if (declaration.type === 'VariableDeclaration') {
|
|
270
|
-
const declarations =
|
|
271
|
-
(declaration as unknown as { declarations?: readonly AstNode[] })
|
|
272
|
-
.declarations ?? [];
|
|
281
|
+
const declarations = getNodeDeclarations(declaration);
|
|
273
282
|
return declarations.flatMap((declarator) => {
|
|
274
|
-
const
|
|
283
|
+
const id = getNodeId(declarator);
|
|
275
284
|
return sitesFromPattern(id, declarator.start);
|
|
276
285
|
});
|
|
277
286
|
}
|
|
@@ -282,10 +291,10 @@ const sitesForExportNode = (node: AstNode): readonly ExportSite[] => {
|
|
|
282
291
|
if (node.type !== 'ExportNamedDeclaration') {
|
|
283
292
|
return [];
|
|
284
293
|
}
|
|
285
|
-
if ((node
|
|
294
|
+
if (getNodeExportKind(node) === 'type') {
|
|
286
295
|
return [];
|
|
287
296
|
}
|
|
288
|
-
const
|
|
297
|
+
const declaration = getNodeDeclaration(node);
|
|
289
298
|
if (declaration) {
|
|
290
299
|
return sitesForDeclaration(declaration);
|
|
291
300
|
}
|
|
@@ -324,15 +333,13 @@ const collectNamespaceReexports = (
|
|
|
324
333
|
// Mirror the `ExportNamedDeclaration` guard: `export type * from ...` and
|
|
325
334
|
// `export type * as ns from ...` propagate types only, never runtime
|
|
326
335
|
// identifiers, so they cannot leak raw rule objects and must be allowed.
|
|
327
|
-
if ((node
|
|
336
|
+
if (getNodeExportKind(node) === 'type') {
|
|
328
337
|
return;
|
|
329
338
|
}
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
const target =
|
|
335
|
-
typeof source?.value === 'string' ? source.value : '<unknown>';
|
|
339
|
+
const source = getNodeSource(node);
|
|
340
|
+
const exported = getNodeExported(node);
|
|
341
|
+
const sourceValue = getNodeValue(source);
|
|
342
|
+
const target = typeof sourceValue === 'string' ? sourceValue : '<unknown>';
|
|
336
343
|
// `export * as <alias> from '...'` exposes the alias as an
|
|
337
344
|
// `IdentifierName` / string-literal node on `exported`. Bare `export *`
|
|
338
345
|
// has `exported === null`.
|