@ontrails/warden 1.0.0-beta.2 → 1.0.0-beta.21
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 +497 -6
- package/README.md +77 -26
- package/bin/warden.ts +50 -0
- package/package.json +27 -5
- package/src/adapter-check.ts +136 -0
- package/src/ast.ts +28 -0
- package/src/cli.ts +1374 -103
- package/src/command.ts +953 -0
- package/src/config.ts +184 -0
- package/src/draft.ts +22 -0
- package/src/drift.ts +106 -22
- package/src/fix.ts +120 -0
- package/src/formatters.ts +79 -9
- package/src/guide.ts +245 -0
- package/src/index.ts +206 -14
- package/src/project-context.ts +163 -0
- package/src/resolve.ts +530 -0
- package/src/rules/activation-orphan.ts +97 -0
- package/src/rules/ast.ts +3176 -85
- package/src/rules/circular-refs.ts +154 -0
- package/src/rules/composes-declarations.ts +704 -0
- package/src/rules/context-no-surface-types.ts +68 -8
- package/src/rules/contour-exists.ts +251 -0
- package/src/rules/contour-ids.ts +15 -0
- package/src/rules/dead-internal-trail.ts +154 -0
- package/src/rules/draft-file-marking.ts +160 -0
- package/src/rules/draft-visible-debt.ts +87 -0
- package/src/rules/error-mapping-completeness.ts +288 -0
- package/src/rules/example-valid.ts +401 -0
- package/src/rules/fires-declarations.ts +758 -0
- package/src/rules/implementation-returns-result.ts +1265 -95
- package/src/rules/incomplete-accessor-for-standard-op.ts +272 -0
- package/src/rules/incomplete-crud.ts +580 -0
- package/src/rules/index.ts +219 -18
- package/src/rules/intent-propagation.ts +127 -0
- package/src/rules/layer-field-name-drift.ts +96 -0
- package/src/rules/metadata.ts +654 -0
- package/src/rules/missing-reconcile.ts +98 -0
- package/src/rules/missing-visibility.ts +110 -0
- package/src/rules/no-destructured-compose.ts +192 -0
- package/src/rules/no-dev-permit-in-source.ts +99 -0
- package/src/rules/no-direct-implementation-call.ts +7 -7
- package/src/rules/no-legacy-layer-imports.ts +211 -0
- package/src/rules/no-native-error-result.ts +111 -0
- package/src/rules/no-redundant-result-error-wrap.ts +331 -0
- package/src/rules/no-retired-cross-vocabulary.ts +194 -0
- package/src/rules/no-sync-result-assumption.ts +1134 -99
- package/src/rules/no-throw-in-detour-recover.ts +225 -0
- package/src/rules/no-throw-in-implementation.ts +10 -9
- package/src/rules/no-top-level-surface.ts +389 -0
- package/src/rules/on-references-exist.ts +194 -0
- package/src/rules/orphaned-signal.ts +150 -0
- package/src/rules/owner-projection-parity.ts +146 -0
- package/src/rules/permit-governance.ts +25 -0
- package/src/rules/public-export-example-coverage.ts +553 -0
- package/src/rules/public-internal-deep-imports.ts +517 -0
- package/src/rules/public-output-schema.ts +29 -0
- package/src/rules/public-union-output-discriminants.ts +150 -0
- package/src/rules/read-intent-fires.ts +187 -0
- package/src/rules/reference-exists.ts +98 -0
- package/src/rules/registry-names.ts +145 -0
- package/src/rules/resolved-import-boundary.ts +146 -0
- package/src/rules/resource-declarations.ts +704 -0
- package/src/rules/resource-exists.ts +179 -0
- package/src/rules/resource-id-grammar.ts +65 -0
- package/src/rules/resource-mock-coverage.ts +115 -0
- package/src/rules/scan.ts +38 -25
- package/src/rules/scheduled-destroy-intent.ts +44 -0
- package/src/rules/signal-graph-coaching.ts +191 -0
- package/src/rules/specs.ts +9 -5
- package/src/rules/static-resource-accessor-preference.ts +657 -0
- package/src/rules/surface-facet-coherence.ts +370 -0
- package/src/rules/trail-versioning-source.ts +1094 -0
- package/src/rules/trail-versioning-topo.ts +172 -0
- package/src/rules/types.ts +270 -6
- package/src/rules/unmaterialized-activation-source.ts +84 -0
- package/src/rules/unreachable-detour-shadowing.ts +344 -0
- package/src/rules/valid-describe-refs.ts +160 -32
- package/src/rules/valid-detour-contract.ts +78 -0
- package/src/rules/warden-export-symmetry.ts +533 -0
- package/src/rules/warden-rules-use-ast.ts +996 -0
- package/src/rules/webhook-route-collision.ts +243 -0
- package/src/trails/activation-orphan.trail.ts +84 -0
- package/src/trails/circular-refs.trail.ts +29 -0
- package/src/trails/composes-declarations.trail.ts +22 -0
- package/src/trails/context-no-surface-types.trail.ts +21 -0
- package/src/trails/contour-exists.trail.ts +21 -0
- package/src/trails/dead-internal-trail.trail.ts +26 -0
- package/src/trails/deprecation-without-guidance.trail.ts +21 -0
- package/src/trails/draft-file-marking.trail.ts +16 -0
- package/src/trails/draft-visible-debt.trail.ts +16 -0
- package/src/trails/error-mapping-completeness.trail.ts +29 -0
- package/src/trails/example-valid.trail.ts +25 -0
- package/src/trails/fires-declarations.trail.ts +23 -0
- package/src/trails/fork-without-preserved-blaze.trail.ts +31 -0
- package/src/trails/implementation-returns-result.trail.ts +20 -0
- package/src/trails/incomplete-accessor-for-standard-op.trail.ts +76 -0
- package/src/trails/incomplete-crud.trail.ts +39 -0
- package/src/trails/index.ts +78 -0
- package/src/trails/intent-propagation.trail.ts +30 -0
- package/src/trails/layer-field-name-drift.trail.ts +39 -0
- package/src/trails/marker-schema-unsupported.trail.ts +23 -0
- package/src/trails/missing-reconcile.trail.ts +33 -0
- package/src/trails/missing-visibility.trail.ts +22 -0
- package/src/trails/no-destructured-compose.trail.ts +44 -0
- package/src/trails/no-dev-permit-in-source.trail.ts +16 -0
- package/src/trails/no-direct-implementation-call.trail.ts +16 -0
- package/src/trails/no-legacy-layer-imports.trail.ts +41 -0
- package/src/trails/no-native-error-result.trail.ts +18 -0
- package/src/trails/no-redundant-result-error-wrap.trail.ts +55 -0
- package/src/trails/no-retired-cross-vocabulary.trail.ts +42 -0
- package/src/trails/no-sync-result-assumption.trail.ts +19 -0
- package/src/trails/no-throw-in-detour-recover.trail.ts +24 -0
- package/src/trails/no-throw-in-implementation.trail.ts +20 -0
- package/src/trails/no-top-level-surface.trail.ts +43 -0
- package/src/trails/on-references-exist.trail.ts +21 -0
- package/src/trails/orphaned-signal.trail.ts +36 -0
- package/src/trails/owner-projection-parity.trail.ts +26 -0
- package/src/trails/pending-force.trail.ts +21 -0
- package/src/trails/permit-governance.trail.ts +51 -0
- package/src/trails/prefer-schema-inference.trail.ts +21 -0
- package/src/trails/public-export-example-coverage.trail.ts +16 -0
- package/src/trails/public-internal-deep-imports.trail.ts +94 -0
- package/src/trails/public-output-schema.trail.ts +55 -0
- package/src/trails/public-union-output-discriminants.trail.ts +33 -0
- package/src/trails/read-intent-fires.trail.ts +20 -0
- package/src/trails/reference-exists.trail.ts +25 -0
- package/src/trails/resolved-import-boundary.trail.ts +109 -0
- package/src/trails/resource-declarations.trail.ts +25 -0
- package/src/trails/resource-exists.trail.ts +27 -0
- package/src/trails/resource-id-grammar.trail.ts +39 -0
- package/src/trails/resource-mock-coverage.trail.ts +40 -0
- package/src/trails/run.ts +162 -0
- package/src/trails/scheduled-destroy-intent.trail.ts +56 -0
- package/src/trails/schema.ts +194 -0
- package/src/trails/signal-graph-coaching.trail.ts +77 -0
- package/src/trails/static-resource-accessor-preference.trail.ts +25 -0
- package/src/trails/surface-facet-coherence.trail.ts +25 -0
- package/src/trails/topo.ts +6 -0
- package/src/trails/unmaterialized-activation-source.trail.ts +72 -0
- package/src/trails/unreachable-detour-shadowing.trail.ts +45 -0
- package/src/trails/valid-describe-refs.trail.ts +18 -0
- package/src/trails/valid-detour-contract.trail.ts +71 -0
- package/src/trails/version-gap.trail.ts +35 -0
- package/src/trails/version-pinned-compose.trail.ts +23 -0
- package/src/trails/version-without-examples.trail.ts +38 -0
- package/src/trails/warden-export-symmetry.trail.ts +16 -0
- package/src/trails/warden-rules-use-ast.trail.ts +45 -0
- package/src/trails/webhook-route-collision.trail.ts +50 -0
- package/src/trails/wrap-rule.ts +213 -0
- package/src/workspaces.ts +238 -0
- package/.turbo/turbo-build.log +0 -1
- package/.turbo/turbo-lint.log +0 -3
- package/.turbo/turbo-typecheck.log +0 -1
- package/dist/cli.d.ts +0 -46
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -221
- package/dist/cli.js.map +0 -1
- package/dist/drift.d.ts +0 -26
- package/dist/drift.d.ts.map +0 -1
- package/dist/drift.js +0 -27
- package/dist/drift.js.map +0 -1
- package/dist/formatters.d.ts +0 -29
- package/dist/formatters.d.ts.map +0 -1
- package/dist/formatters.js +0 -87
- package/dist/formatters.js.map +0 -1
- package/dist/index.d.ts +0 -26
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -26
- package/dist/index.js.map +0 -1
- package/dist/rules/ast.d.ts +0 -41
- package/dist/rules/ast.d.ts.map +0 -1
- package/dist/rules/ast.js +0 -163
- package/dist/rules/ast.js.map +0 -1
- package/dist/rules/context-no-surface-types.d.ts +0 -12
- package/dist/rules/context-no-surface-types.d.ts.map +0 -1
- package/dist/rules/context-no-surface-types.js +0 -96
- package/dist/rules/context-no-surface-types.js.map +0 -1
- package/dist/rules/implementation-returns-result.d.ts +0 -13
- package/dist/rules/implementation-returns-result.d.ts.map +0 -1
- package/dist/rules/implementation-returns-result.js +0 -231
- package/dist/rules/implementation-returns-result.js.map +0 -1
- package/dist/rules/index.d.ts +0 -22
- package/dist/rules/index.d.ts.map +0 -1
- package/dist/rules/index.js +0 -41
- package/dist/rules/index.js.map +0 -1
- package/dist/rules/no-direct-impl-in-route.d.ts +0 -12
- package/dist/rules/no-direct-impl-in-route.d.ts.map +0 -1
- package/dist/rules/no-direct-impl-in-route.js +0 -46
- package/dist/rules/no-direct-impl-in-route.js.map +0 -1
- package/dist/rules/no-direct-implementation-call.d.ts +0 -12
- package/dist/rules/no-direct-implementation-call.d.ts.map +0 -1
- package/dist/rules/no-direct-implementation-call.js +0 -39
- package/dist/rules/no-direct-implementation-call.js.map +0 -1
- package/dist/rules/no-sync-result-assumption.d.ts +0 -6
- package/dist/rules/no-sync-result-assumption.d.ts.map +0 -1
- package/dist/rules/no-sync-result-assumption.js +0 -98
- package/dist/rules/no-sync-result-assumption.js.map +0 -1
- package/dist/rules/no-throw-in-detour-target.d.ts +0 -12
- package/dist/rules/no-throw-in-detour-target.d.ts.map +0 -1
- package/dist/rules/no-throw-in-detour-target.js +0 -87
- package/dist/rules/no-throw-in-detour-target.js.map +0 -1
- package/dist/rules/no-throw-in-implementation.d.ts +0 -9
- package/dist/rules/no-throw-in-implementation.d.ts.map +0 -1
- package/dist/rules/no-throw-in-implementation.js +0 -34
- package/dist/rules/no-throw-in-implementation.js.map +0 -1
- package/dist/rules/prefer-schema-inference.d.ts +0 -7
- package/dist/rules/prefer-schema-inference.d.ts.map +0 -1
- package/dist/rules/prefer-schema-inference.js +0 -86
- package/dist/rules/prefer-schema-inference.js.map +0 -1
- package/dist/rules/scan.d.ts +0 -8
- package/dist/rules/scan.d.ts.map +0 -1
- package/dist/rules/scan.js +0 -32
- package/dist/rules/scan.js.map +0 -1
- package/dist/rules/specs.d.ts +0 -29
- package/dist/rules/specs.d.ts.map +0 -1
- package/dist/rules/specs.js +0 -192
- package/dist/rules/specs.js.map +0 -1
- package/dist/rules/structure.d.ts +0 -13
- package/dist/rules/structure.d.ts.map +0 -1
- package/dist/rules/structure.js +0 -142
- package/dist/rules/structure.js.map +0 -1
- package/dist/rules/types.d.ts +0 -52
- package/dist/rules/types.d.ts.map +0 -1
- package/dist/rules/types.js +0 -2
- package/dist/rules/types.js.map +0 -1
- package/dist/rules/valid-describe-refs.d.ts +0 -7
- package/dist/rules/valid-describe-refs.d.ts.map +0 -1
- package/dist/rules/valid-describe-refs.js +0 -51
- package/dist/rules/valid-describe-refs.js.map +0 -1
- package/dist/rules/valid-detour-refs.d.ts +0 -6
- package/dist/rules/valid-detour-refs.d.ts.map +0 -1
- package/dist/rules/valid-detour-refs.js +0 -116
- package/dist/rules/valid-detour-refs.js.map +0 -1
- package/src/__tests__/cli.test.ts +0 -198
- package/src/__tests__/drift.test.ts +0 -74
- package/src/__tests__/formatters.test.ts +0 -157
- package/src/__tests__/implementation-returns-result.test.ts +0 -75
- package/src/__tests__/no-direct-implementation-call.test.ts +0 -83
- package/src/__tests__/no-sync-result-assumption.test.ts +0 -85
- package/src/__tests__/no-throw-in-detour-target.test.ts +0 -78
- package/src/__tests__/prefer-schema-inference.test.ts +0 -84
- package/src/__tests__/rules.test.ts +0 -188
- package/src/__tests__/valid-describe-refs.test.ts +0 -60
- package/src/rules/no-direct-impl-in-route.ts +0 -77
- package/src/rules/no-throw-in-detour-target.ts +0 -150
- package/src/rules/valid-detour-refs.ts +0 -187
- package/tsconfig.json +0 -9
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -0,0 +1,704 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validates that resource access matches the declared `resources` array.
|
|
3
|
+
*
|
|
4
|
+
* Statically analyzes trail `blaze` functions to find `db.from(ctx)` and
|
|
5
|
+
* `ctx.resource('db.main')` calls and compares them against the declared
|
|
6
|
+
* `resources: [...]` array in the trail config. Reports errors for undeclared
|
|
7
|
+
* access and warnings for unused declarations.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
collectNamedResourceIds,
|
|
12
|
+
extractFirstStringArg,
|
|
13
|
+
findConfigProperty,
|
|
14
|
+
findBlazeBodies,
|
|
15
|
+
findTrailDefinitions,
|
|
16
|
+
getStringValue,
|
|
17
|
+
identifierName,
|
|
18
|
+
isStringLiteral,
|
|
19
|
+
offsetToLine,
|
|
20
|
+
parse,
|
|
21
|
+
walkScope,
|
|
22
|
+
} from './ast.js';
|
|
23
|
+
import type { AstNode } from './ast.js';
|
|
24
|
+
import { isTestFile } from './scan.js';
|
|
25
|
+
import type { WardenDiagnostic, WardenRule } from './types.js';
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Resource declaration extraction
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
interface DeclaredResource {
|
|
32
|
+
readonly id: string | null;
|
|
33
|
+
readonly name: string | null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface CalledResources {
|
|
37
|
+
readonly fromNames: ReadonlySet<string>;
|
|
38
|
+
readonly lookupIds: ReadonlySet<string>;
|
|
39
|
+
readonly lookupNames: ReadonlySet<string>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const MEMBER_TYPES = new Set(['StaticMemberExpression', 'MemberExpression']);
|
|
43
|
+
|
|
44
|
+
/** Extract object and property Identifier names from a MemberExpression. */
|
|
45
|
+
const extractMemberPair = (
|
|
46
|
+
callee: AstNode
|
|
47
|
+
): { objName: string; propName: string } | null => {
|
|
48
|
+
if (!MEMBER_TYPES.has(callee.type)) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const objName = identifierName(
|
|
53
|
+
(callee as unknown as { object?: AstNode }).object
|
|
54
|
+
);
|
|
55
|
+
const propName = identifierName(
|
|
56
|
+
(callee as unknown as { property?: AstNode }).property
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
return objName && propName ? { objName, propName } : null;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/** Check if a node is an inline `resource('id', ...)` call. */
|
|
63
|
+
const isInlineResourceCall = (node: AstNode): boolean => {
|
|
64
|
+
if (node.type !== 'CallExpression') {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
return (
|
|
68
|
+
identifierName((node as unknown as { callee?: AstNode }).callee) ===
|
|
69
|
+
'resource'
|
|
70
|
+
);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/** Get `resources` array elements from a trail config. */
|
|
74
|
+
const getResourceElements = (config: AstNode): readonly AstNode[] => {
|
|
75
|
+
const resourcesProp = findConfigProperty(config, 'resources');
|
|
76
|
+
if (!resourcesProp) {
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const arrayNode = resourcesProp.value;
|
|
81
|
+
if (!arrayNode || (arrayNode as AstNode).type !== 'ArrayExpression') {
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const elements = (arrayNode as AstNode)['elements'] as
|
|
86
|
+
| readonly AstNode[]
|
|
87
|
+
| undefined;
|
|
88
|
+
return elements ?? [];
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/** Extract one declared resource from a `resources` array element. */
|
|
92
|
+
const extractDeclaredResource = (
|
|
93
|
+
element: AstNode,
|
|
94
|
+
resourceIdsByName: ReadonlyMap<string, string>
|
|
95
|
+
): DeclaredResource | null => {
|
|
96
|
+
if (element.type === 'Identifier') {
|
|
97
|
+
const name = identifierName(element);
|
|
98
|
+
return {
|
|
99
|
+
id: name ? (resourceIdsByName.get(name) ?? null) : null,
|
|
100
|
+
name,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (isStringLiteral(element)) {
|
|
105
|
+
return { id: getStringValue(element), name: null };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (isInlineResourceCall(element)) {
|
|
109
|
+
return { id: extractFirstStringArg(element), name: null };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return null;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/** Extract declared resources from a trail config's `resources` array. */
|
|
116
|
+
const extractDeclaredResources = (
|
|
117
|
+
config: AstNode,
|
|
118
|
+
resourceIdsByName: ReadonlyMap<string, string>
|
|
119
|
+
): readonly DeclaredResource[] =>
|
|
120
|
+
getResourceElements(config).flatMap((element) => {
|
|
121
|
+
const resource = extractDeclaredResource(element, resourceIdsByName);
|
|
122
|
+
return resource ? [resource] : [];
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
// Called resource extraction
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
/** Extract the raw second parameter node from a blaze function. */
|
|
130
|
+
const extractContextParamNode = (blazeBody: AstNode): AstNode | null => {
|
|
131
|
+
const params = blazeBody['params'] as readonly AstNode[] | undefined;
|
|
132
|
+
if (!params || params.length < 2) {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
return params[1] ?? null;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Extract the second parameter name from a blaze function node.
|
|
140
|
+
*
|
|
141
|
+
* Returns null when the parameter is not a plain Identifier (e.g. when the
|
|
142
|
+
* author destructures `{ resource }` in the parameter list). Parameter-level
|
|
143
|
+
* destructuring is handled separately by `collectParamResourceAliases`.
|
|
144
|
+
*
|
|
145
|
+
* Also handles defaulted parameters like `(input, ctx = fallback) => ...`
|
|
146
|
+
* (AssignmentPattern whose `.left` is the Identifier). Without this, valid
|
|
147
|
+
* signatures would silently drop out of ctx-access analysis.
|
|
148
|
+
*/
|
|
149
|
+
const extractContextParamName = (blazeBody: AstNode): string | null => {
|
|
150
|
+
const param = extractContextParamNode(blazeBody);
|
|
151
|
+
if (!param) {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
if (param.type === 'AssignmentPattern') {
|
|
155
|
+
const { left } = param as unknown as { left?: AstNode };
|
|
156
|
+
return identifierName(left);
|
|
157
|
+
}
|
|
158
|
+
return identifierName(param);
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
/** Extract the alias name from a Property node whose key is `resource`. */
|
|
162
|
+
const extractResourceAlias = (property: AstNode): string | null => {
|
|
163
|
+
if (property.type !== 'Property') {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
const keyName = identifierName(
|
|
167
|
+
(property as unknown as { key?: AstNode }).key
|
|
168
|
+
);
|
|
169
|
+
if (keyName !== 'resource') {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
return (
|
|
173
|
+
identifierName((property as unknown as { value?: AstNode }).value) ??
|
|
174
|
+
keyName
|
|
175
|
+
);
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Collect `resource` aliases bound via parameter-level destructuring.
|
|
180
|
+
*
|
|
181
|
+
* Recognizes `(input, { resource }) => ...` and `(input, { resource: r }) => ...`.
|
|
182
|
+
* When the blaze author destructures in the parameter list, there is no
|
|
183
|
+
* enclosing `ctx` identifier to track — we seed the resource alias set directly
|
|
184
|
+
* from the ObjectPattern in `params[1]`.
|
|
185
|
+
*/
|
|
186
|
+
const collectParamResourceAliases = (body: AstNode): ReadonlySet<string> => {
|
|
187
|
+
const param = extractContextParamNode(body);
|
|
188
|
+
if (!param || param.type !== 'ObjectPattern') {
|
|
189
|
+
return new Set();
|
|
190
|
+
}
|
|
191
|
+
const aliases = new Set<string>();
|
|
192
|
+
const properties = param['properties'] as readonly AstNode[] | undefined;
|
|
193
|
+
for (const property of properties ?? []) {
|
|
194
|
+
const alias = extractResourceAlias(property);
|
|
195
|
+
if (alias) {
|
|
196
|
+
aliases.add(alias);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return aliases;
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Build the set of context parameter names to match against.
|
|
204
|
+
*
|
|
205
|
+
* Returns ONLY the actual second-parameter name from the blaze signature.
|
|
206
|
+
* No seeded defaults: if the blaze has no second parameter, the returned set
|
|
207
|
+
* is empty and no `ctx.resource(...)` / `context.resource(...)` calls are
|
|
208
|
+
* tracked for that blaze. An unrelated closure-scoped `ctx` identifier is not
|
|
209
|
+
* the trail context and must not be treated as one.
|
|
210
|
+
*
|
|
211
|
+
* Mirrors `fires-declarations.ts` `buildCtxNames` for the same reason.
|
|
212
|
+
*/
|
|
213
|
+
const buildCtxNames = (body: AstNode): ReadonlySet<string> => {
|
|
214
|
+
const ctxNames = new Set<string>();
|
|
215
|
+
const paramName = extractContextParamName(body);
|
|
216
|
+
if (paramName) {
|
|
217
|
+
ctxNames.add(paramName);
|
|
218
|
+
}
|
|
219
|
+
return ctxNames;
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
/** Extract a CallExpression callee, or null. */
|
|
223
|
+
const extractCallCallee = (node: AstNode): AstNode | null => {
|
|
224
|
+
if (node.type !== 'CallExpression') {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
return ((node as unknown as { callee?: AstNode }).callee ??
|
|
228
|
+
null) as AstNode | null;
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
/** Extract the first identifier argument from a CallExpression. */
|
|
232
|
+
const extractFirstIdentifierArg = (node: AstNode): string | null => {
|
|
233
|
+
const args = node['arguments'] as readonly AstNode[] | undefined;
|
|
234
|
+
const [firstArg] = args ?? [];
|
|
235
|
+
return identifierName(firstArg);
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const extractCallInfo = (
|
|
239
|
+
node: AstNode
|
|
240
|
+
): { callee: AstNode; firstArgName: string | null } | null => {
|
|
241
|
+
const callee = extractCallCallee(node);
|
|
242
|
+
return callee
|
|
243
|
+
? {
|
|
244
|
+
callee,
|
|
245
|
+
firstArgName: extractFirstIdentifierArg(node),
|
|
246
|
+
}
|
|
247
|
+
: null;
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
/** Extract `db.from(ctx)` object names. */
|
|
251
|
+
const extractFromCallName = (
|
|
252
|
+
node: AstNode,
|
|
253
|
+
ctxNames: ReadonlySet<string>
|
|
254
|
+
): string | null => {
|
|
255
|
+
const call = extractCallInfo(node);
|
|
256
|
+
const pair = call ? extractMemberPair(call.callee) : null;
|
|
257
|
+
|
|
258
|
+
return pair &&
|
|
259
|
+
pair.propName === 'from' &&
|
|
260
|
+
call?.firstArgName &&
|
|
261
|
+
ctxNames.has(call.firstArgName)
|
|
262
|
+
? pair.objName
|
|
263
|
+
: null;
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
/** Check if a callee is a member-style `ctx.resource(...)` call. */
|
|
267
|
+
const isMemberResourceCall = (
|
|
268
|
+
callee: AstNode,
|
|
269
|
+
ctxNames: ReadonlySet<string>
|
|
270
|
+
): boolean => {
|
|
271
|
+
const pair = extractMemberPair(callee);
|
|
272
|
+
return !!pair && ctxNames.has(pair.objName) && pair.propName === 'resource';
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
/** Extract `ctx.resource(db)` and destructured `resource(db)` lookup names. */
|
|
276
|
+
const extractLookupResourceName = (
|
|
277
|
+
node: AstNode,
|
|
278
|
+
ctxNames: ReadonlySet<string>,
|
|
279
|
+
resourceAliases: ReadonlySet<string>
|
|
280
|
+
): string | null => {
|
|
281
|
+
const callee = extractCallCallee(node);
|
|
282
|
+
if (!callee) {
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (isMemberResourceCall(callee, ctxNames)) {
|
|
287
|
+
return extractFirstIdentifierArg(node);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (resourceAliases.has(identifierName(callee) ?? '')) {
|
|
291
|
+
return extractFirstIdentifierArg(node);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return null;
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
/** Extract `ctx.resource('id')` and destructured `resource('id')` lookup IDs. */
|
|
298
|
+
const extractLookupResourceId = (
|
|
299
|
+
node: AstNode,
|
|
300
|
+
ctxNames: ReadonlySet<string>,
|
|
301
|
+
resourceAliases: ReadonlySet<string>
|
|
302
|
+
): string | null => {
|
|
303
|
+
const callee = extractCallCallee(node);
|
|
304
|
+
if (!callee) {
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (isMemberResourceCall(callee, ctxNames)) {
|
|
309
|
+
return extractFirstStringArg(node);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const calleeName = identifierName(callee);
|
|
313
|
+
const args = node['arguments'] as readonly AstNode[] | undefined;
|
|
314
|
+
if (calleeName && resourceAliases.has(calleeName) && args?.length === 1) {
|
|
315
|
+
return extractFirstStringArg(node);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return null;
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
/** Collect local aliases for the resource accessor (e.g. `const { resource } = ctx`). */
|
|
322
|
+
const collectResourceAliases = (
|
|
323
|
+
body: AstNode,
|
|
324
|
+
ctxNames: ReadonlySet<string>
|
|
325
|
+
): ReadonlySet<string> => {
|
|
326
|
+
const aliases = new Set<string>();
|
|
327
|
+
|
|
328
|
+
const extractAliasNames = (
|
|
329
|
+
pattern: AstNode | undefined
|
|
330
|
+
): readonly string[] => {
|
|
331
|
+
if (pattern?.type !== 'ObjectPattern') {
|
|
332
|
+
return [];
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const properties = pattern['properties'] as readonly AstNode[] | undefined;
|
|
336
|
+
return (properties ?? []).flatMap((property) => {
|
|
337
|
+
if (property.type !== 'Property') {
|
|
338
|
+
return [];
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const keyName = identifierName(
|
|
342
|
+
(property as unknown as { key?: AstNode }).key
|
|
343
|
+
);
|
|
344
|
+
if (keyName !== 'resource') {
|
|
345
|
+
return [];
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const alias =
|
|
349
|
+
identifierName((property as unknown as { value?: AstNode }).value) ??
|
|
350
|
+
keyName;
|
|
351
|
+
return [alias];
|
|
352
|
+
});
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
walkScope(body, (node) => {
|
|
356
|
+
if (node.type !== 'VariableDeclarator') {
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const { id, init } = node as unknown as {
|
|
361
|
+
readonly id?: AstNode;
|
|
362
|
+
readonly init?: AstNode;
|
|
363
|
+
};
|
|
364
|
+
const initName = identifierName(init);
|
|
365
|
+
if (!initName || !ctxNames.has(initName)) {
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
for (const alias of extractAliasNames(id)) {
|
|
370
|
+
aliases.add(alias);
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
return aliases;
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
/** Walk blaze bodies and collect resource access that can be resolved statically. */
|
|
378
|
+
const extractCalledResources = (config: AstNode): CalledResources => {
|
|
379
|
+
const fromNames = new Set<string>();
|
|
380
|
+
const lookupIds = new Set<string>();
|
|
381
|
+
const lookupNames = new Set<string>();
|
|
382
|
+
|
|
383
|
+
for (const body of findBlazeBodies(config)) {
|
|
384
|
+
const ctxNames = buildCtxNames(body);
|
|
385
|
+
const paramAliases = collectParamResourceAliases(body);
|
|
386
|
+
const bodyAliases = collectResourceAliases(body, ctxNames);
|
|
387
|
+
const resourceAliases = new Set([...paramAliases, ...bodyAliases]);
|
|
388
|
+
|
|
389
|
+
walkScope(body, (node) => {
|
|
390
|
+
const fromName = extractFromCallName(node, ctxNames);
|
|
391
|
+
if (fromName) {
|
|
392
|
+
fromNames.add(fromName);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const lookupId = extractLookupResourceId(node, ctxNames, resourceAliases);
|
|
396
|
+
if (lookupId) {
|
|
397
|
+
lookupIds.add(lookupId);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const lookupName = extractLookupResourceName(
|
|
401
|
+
node,
|
|
402
|
+
ctxNames,
|
|
403
|
+
resourceAliases
|
|
404
|
+
);
|
|
405
|
+
if (lookupName) {
|
|
406
|
+
lookupNames.add(lookupName);
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return { fromNames, lookupIds, lookupNames };
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
// ---------------------------------------------------------------------------
|
|
415
|
+
// Diagnostics
|
|
416
|
+
// ---------------------------------------------------------------------------
|
|
417
|
+
|
|
418
|
+
const renderDeclaredResource = (resource: DeclaredResource): string =>
|
|
419
|
+
resource.name ?? resource.id ?? '<unknown>';
|
|
420
|
+
|
|
421
|
+
const buildUndeclaredFromDiagnostic = (
|
|
422
|
+
trailId: string,
|
|
423
|
+
resourceName: string,
|
|
424
|
+
filePath: string,
|
|
425
|
+
line: number
|
|
426
|
+
): WardenDiagnostic => ({
|
|
427
|
+
filePath,
|
|
428
|
+
line,
|
|
429
|
+
message: `Trail "${trailId}": ${resourceName}.from(ctx) called but '${resourceName}' is not declared in resources. Add it to the trail resources array: resources: [${resourceName}].`,
|
|
430
|
+
rule: 'resource-declarations',
|
|
431
|
+
severity: 'error',
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
const buildUndeclaredLookupDiagnostic = (
|
|
435
|
+
trailId: string,
|
|
436
|
+
resourceId: string,
|
|
437
|
+
filePath: string,
|
|
438
|
+
line: number
|
|
439
|
+
): WardenDiagnostic => ({
|
|
440
|
+
filePath,
|
|
441
|
+
line,
|
|
442
|
+
message: `Trail "${trailId}": ctx.resource('${resourceId}') called but '${resourceId}' is not declared in resources. Add it to the trail resources array: resources: ['${resourceId}'], or prefer the resource definition's .from(ctx) helper when it is statically in scope.`,
|
|
443
|
+
rule: 'resource-declarations',
|
|
444
|
+
severity: 'error',
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
const buildUndeclaredLookupNameDiagnostic = (
|
|
448
|
+
trailId: string,
|
|
449
|
+
resourceName: string,
|
|
450
|
+
filePath: string,
|
|
451
|
+
line: number
|
|
452
|
+
): WardenDiagnostic => ({
|
|
453
|
+
filePath,
|
|
454
|
+
line,
|
|
455
|
+
message: `Trail "${trailId}": ctx.resource(${resourceName}) called but '${resourceName}' is not declared in resources. Add it to the trail resources array: resources: [${resourceName}].`,
|
|
456
|
+
rule: 'resource-declarations',
|
|
457
|
+
severity: 'error',
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
const buildUnusedDiagnostic = (
|
|
461
|
+
trailId: string,
|
|
462
|
+
declaredResource: DeclaredResource,
|
|
463
|
+
filePath: string,
|
|
464
|
+
line: number
|
|
465
|
+
): WardenDiagnostic => ({
|
|
466
|
+
filePath,
|
|
467
|
+
line,
|
|
468
|
+
message: `Trail "${trailId}": '${renderDeclaredResource(declaredResource)}' declared in resources but never used. Remove it from resources, or access it through the resource's static .from(ctx) helper if the trail really depends on it.`,
|
|
469
|
+
rule: 'resource-declarations',
|
|
470
|
+
severity: 'warn',
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
// ---------------------------------------------------------------------------
|
|
474
|
+
// Comparison
|
|
475
|
+
// ---------------------------------------------------------------------------
|
|
476
|
+
|
|
477
|
+
const resourceWasUsed = (
|
|
478
|
+
declaredResource: DeclaredResource,
|
|
479
|
+
calledResources: CalledResources
|
|
480
|
+
): boolean => {
|
|
481
|
+
if (
|
|
482
|
+
declaredResource.name &&
|
|
483
|
+
(calledResources.fromNames.has(declaredResource.name) ||
|
|
484
|
+
calledResources.lookupNames.has(declaredResource.name))
|
|
485
|
+
) {
|
|
486
|
+
return true;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
if (
|
|
490
|
+
declaredResource.id &&
|
|
491
|
+
calledResources.lookupIds.has(declaredResource.id)
|
|
492
|
+
) {
|
|
493
|
+
return true;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
return false;
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
const buildDeclaredNames = (
|
|
500
|
+
declaredResources: readonly DeclaredResource[]
|
|
501
|
+
): ReadonlySet<string> =>
|
|
502
|
+
new Set(
|
|
503
|
+
declaredResources.flatMap((resource) =>
|
|
504
|
+
resource.name ? [resource.name] : []
|
|
505
|
+
)
|
|
506
|
+
);
|
|
507
|
+
|
|
508
|
+
const buildDeclaredIds = (
|
|
509
|
+
declaredResources: readonly DeclaredResource[]
|
|
510
|
+
): ReadonlySet<string> =>
|
|
511
|
+
new Set(
|
|
512
|
+
declaredResources.flatMap((resource) => (resource.id ? [resource.id] : []))
|
|
513
|
+
);
|
|
514
|
+
|
|
515
|
+
const reportUndeclaredFromCalls = (
|
|
516
|
+
trailId: string,
|
|
517
|
+
filePath: string,
|
|
518
|
+
line: number,
|
|
519
|
+
calledResources: CalledResources,
|
|
520
|
+
declaredNames: ReadonlySet<string>,
|
|
521
|
+
diagnostics: WardenDiagnostic[]
|
|
522
|
+
): void => {
|
|
523
|
+
for (const resourceName of calledResources.fromNames) {
|
|
524
|
+
if (!declaredNames.has(resourceName)) {
|
|
525
|
+
diagnostics.push(
|
|
526
|
+
buildUndeclaredFromDiagnostic(trailId, resourceName, filePath, line)
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
const reportUndeclaredLookupCalls = (
|
|
533
|
+
trailId: string,
|
|
534
|
+
filePath: string,
|
|
535
|
+
line: number,
|
|
536
|
+
calledResources: CalledResources,
|
|
537
|
+
declaredIds: ReadonlySet<string>,
|
|
538
|
+
declaredNames: ReadonlySet<string>,
|
|
539
|
+
diagnostics: WardenDiagnostic[]
|
|
540
|
+
): void => {
|
|
541
|
+
for (const resourceName of calledResources.lookupNames) {
|
|
542
|
+
// Name-based lookup checks remain reliable even when an imported resource ID
|
|
543
|
+
// cannot be resolved locally.
|
|
544
|
+
if (!declaredNames.has(resourceName)) {
|
|
545
|
+
diagnostics.push(
|
|
546
|
+
buildUndeclaredLookupNameDiagnostic(
|
|
547
|
+
trailId,
|
|
548
|
+
resourceName,
|
|
549
|
+
filePath,
|
|
550
|
+
line
|
|
551
|
+
)
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
for (const resourceId of calledResources.lookupIds) {
|
|
557
|
+
if (!declaredIds.has(resourceId)) {
|
|
558
|
+
diagnostics.push(
|
|
559
|
+
buildUndeclaredLookupDiagnostic(trailId, resourceId, filePath, line)
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
};
|
|
564
|
+
|
|
565
|
+
const reportUnusedDeclarations = (
|
|
566
|
+
trailId: string,
|
|
567
|
+
filePath: string,
|
|
568
|
+
line: number,
|
|
569
|
+
declaredResources: readonly DeclaredResource[],
|
|
570
|
+
calledResources: CalledResources,
|
|
571
|
+
diagnostics: WardenDiagnostic[]
|
|
572
|
+
): void => {
|
|
573
|
+
for (const declaredResource of declaredResources) {
|
|
574
|
+
if (resourceWasUsed(declaredResource, calledResources)) {
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
if (declaredResource.name && declaredResource.id === null) {
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
diagnostics.push(
|
|
583
|
+
buildUnusedDiagnostic(trailId, declaredResource, filePath, line)
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
const hasNoResourceActivity = (
|
|
589
|
+
declaredResources: readonly DeclaredResource[],
|
|
590
|
+
calledResources: CalledResources
|
|
591
|
+
): boolean =>
|
|
592
|
+
declaredResources.length === 0 &&
|
|
593
|
+
calledResources.fromNames.size === 0 &&
|
|
594
|
+
calledResources.lookupIds.size === 0 &&
|
|
595
|
+
calledResources.lookupNames.size === 0;
|
|
596
|
+
|
|
597
|
+
const analyzeTrailServices = (
|
|
598
|
+
def: { config: AstNode; start: number },
|
|
599
|
+
sourceCode: string,
|
|
600
|
+
resourceIdsByName: ReadonlyMap<string, string>
|
|
601
|
+
): {
|
|
602
|
+
readonly calledResources: CalledResources;
|
|
603
|
+
readonly declaredIds: ReadonlySet<string>;
|
|
604
|
+
readonly declaredNames: ReadonlySet<string>;
|
|
605
|
+
readonly declaredResources: readonly DeclaredResource[];
|
|
606
|
+
readonly line: number;
|
|
607
|
+
} => {
|
|
608
|
+
const declaredResources = extractDeclaredResources(
|
|
609
|
+
def.config,
|
|
610
|
+
resourceIdsByName
|
|
611
|
+
);
|
|
612
|
+
return {
|
|
613
|
+
calledResources: extractCalledResources(def.config),
|
|
614
|
+
declaredIds: buildDeclaredIds(declaredResources),
|
|
615
|
+
declaredNames: buildDeclaredNames(declaredResources),
|
|
616
|
+
declaredResources,
|
|
617
|
+
line: offsetToLine(sourceCode, def.start),
|
|
618
|
+
};
|
|
619
|
+
};
|
|
620
|
+
|
|
621
|
+
const checkTrailDefinition = (
|
|
622
|
+
def: { id: string; config: AstNode; start: number },
|
|
623
|
+
filePath: string,
|
|
624
|
+
sourceCode: string,
|
|
625
|
+
resourceIdsByName: ReadonlyMap<string, string>,
|
|
626
|
+
diagnostics: WardenDiagnostic[]
|
|
627
|
+
): void => {
|
|
628
|
+
const {
|
|
629
|
+
calledResources,
|
|
630
|
+
declaredIds,
|
|
631
|
+
declaredNames,
|
|
632
|
+
declaredResources,
|
|
633
|
+
line,
|
|
634
|
+
} = analyzeTrailServices(def, sourceCode, resourceIdsByName);
|
|
635
|
+
|
|
636
|
+
if (hasNoResourceActivity(declaredResources, calledResources)) {
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
reportUndeclaredFromCalls(
|
|
641
|
+
def.id,
|
|
642
|
+
filePath,
|
|
643
|
+
line,
|
|
644
|
+
calledResources,
|
|
645
|
+
declaredNames,
|
|
646
|
+
diagnostics
|
|
647
|
+
);
|
|
648
|
+
reportUndeclaredLookupCalls(
|
|
649
|
+
def.id,
|
|
650
|
+
filePath,
|
|
651
|
+
line,
|
|
652
|
+
calledResources,
|
|
653
|
+
declaredIds,
|
|
654
|
+
declaredNames,
|
|
655
|
+
diagnostics
|
|
656
|
+
);
|
|
657
|
+
reportUnusedDeclarations(
|
|
658
|
+
def.id,
|
|
659
|
+
filePath,
|
|
660
|
+
line,
|
|
661
|
+
declaredResources,
|
|
662
|
+
calledResources,
|
|
663
|
+
diagnostics
|
|
664
|
+
);
|
|
665
|
+
};
|
|
666
|
+
|
|
667
|
+
// ---------------------------------------------------------------------------
|
|
668
|
+
// Rule
|
|
669
|
+
// ---------------------------------------------------------------------------
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* Validates that resource access aligns with declared `resources` arrays.
|
|
673
|
+
*/
|
|
674
|
+
export const resourceDeclarations: WardenRule = {
|
|
675
|
+
check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
|
|
676
|
+
if (isTestFile(filePath)) {
|
|
677
|
+
return [];
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
const ast = parse(filePath, sourceCode);
|
|
681
|
+
if (!ast) {
|
|
682
|
+
return [];
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
const diagnostics: WardenDiagnostic[] = [];
|
|
686
|
+
const resourceIdsByName = collectNamedResourceIds(ast);
|
|
687
|
+
|
|
688
|
+
for (const def of findTrailDefinitions(ast)) {
|
|
689
|
+
checkTrailDefinition(
|
|
690
|
+
def,
|
|
691
|
+
filePath,
|
|
692
|
+
sourceCode,
|
|
693
|
+
resourceIdsByName,
|
|
694
|
+
diagnostics
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
return diagnostics;
|
|
699
|
+
},
|
|
700
|
+
description:
|
|
701
|
+
'Ensure resource.from(ctx) and ctx.resource() calls match the declared resources array in trail definitions.',
|
|
702
|
+
name: 'resource-declarations',
|
|
703
|
+
severity: 'error',
|
|
704
|
+
};
|