@ontrails/warden 1.0.0-beta.12 → 1.0.0-beta.13
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 +51 -31
- package/README.md +17 -17
- package/dist/cli.d.ts +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +14 -10
- package/dist/cli.js.map +1 -1
- package/dist/drift.d.ts +6 -6
- package/dist/drift.d.ts.map +1 -1
- package/dist/drift.js +8 -8
- package/dist/drift.js.map +1 -1
- package/dist/formatters.js +2 -2
- package/dist/formatters.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -5
- package/dist/index.js.map +1 -1
- package/dist/rules/ast.d.ts +15 -11
- package/dist/rules/ast.d.ts.map +1 -1
- package/dist/rules/ast.js +34 -30
- package/dist/rules/ast.js.map +1 -1
- package/dist/rules/context-no-trailhead-types.d.ts +12 -0
- package/dist/rules/context-no-trailhead-types.d.ts.map +1 -0
- package/dist/rules/context-no-trailhead-types.js +96 -0
- package/dist/rules/context-no-trailhead-types.js.map +1 -0
- package/dist/rules/cross-declarations.d.ts +13 -0
- package/dist/rules/cross-declarations.d.ts.map +1 -0
- package/dist/rules/cross-declarations.js +264 -0
- package/dist/rules/cross-declarations.js.map +1 -0
- package/dist/rules/follow-declarations.d.ts +1 -1
- package/dist/rules/follow-declarations.js +5 -5
- package/dist/rules/follow-declarations.js.map +1 -1
- package/dist/rules/implementation-returns-result.d.ts +2 -2
- package/dist/rules/implementation-returns-result.js +6 -6
- package/dist/rules/implementation-returns-result.js.map +1 -1
- package/dist/rules/index.d.ts +4 -4
- package/dist/rules/index.d.ts.map +1 -1
- package/dist/rules/index.js +12 -12
- package/dist/rules/index.js.map +1 -1
- package/dist/rules/no-direct-impl-in-route.d.ts +4 -4
- package/dist/rules/no-direct-impl-in-route.js +14 -14
- package/dist/rules/no-direct-impl-in-route.js.map +1 -1
- package/dist/rules/no-direct-implementation-call.d.ts +3 -3
- package/dist/rules/no-direct-implementation-call.js +7 -7
- package/dist/rules/no-direct-implementation-call.js.map +1 -1
- package/dist/rules/no-sync-result-assumption.d.ts +1 -1
- package/dist/rules/no-sync-result-assumption.js +5 -5
- package/dist/rules/no-sync-result-assumption.js.map +1 -1
- package/dist/rules/no-throw-in-detour-target.js +2 -2
- package/dist/rules/no-throw-in-detour-target.js.map +1 -1
- package/dist/rules/no-throw-in-implementation.d.ts +1 -1
- package/dist/rules/no-throw-in-implementation.js +3 -3
- package/dist/rules/no-throw-in-implementation.js.map +1 -1
- package/dist/rules/provision-declarations.d.ts +14 -0
- package/dist/rules/provision-declarations.d.ts.map +1 -0
- package/dist/rules/provision-declarations.js +344 -0
- package/dist/rules/provision-declarations.js.map +1 -0
- package/dist/rules/provision-exists.d.ts +6 -0
- package/dist/rules/provision-exists.d.ts.map +1 -0
- package/dist/rules/provision-exists.js +89 -0
- package/dist/rules/provision-exists.js.map +1 -0
- package/dist/rules/service-declarations.d.ts +7 -5
- package/dist/rules/service-declarations.d.ts.map +1 -1
- package/dist/rules/service-declarations.js +106 -103
- package/dist/rules/service-declarations.js.map +1 -1
- package/dist/rules/service-exists.d.ts +3 -1
- package/dist/rules/service-exists.d.ts.map +1 -1
- package/dist/rules/service-exists.js +35 -33
- package/dist/rules/service-exists.js.map +1 -1
- package/dist/rules/specs.d.ts +1 -1
- package/dist/rules/specs.d.ts.map +1 -1
- package/dist/rules/specs.js +1 -1
- package/dist/rules/specs.js.map +1 -1
- package/dist/rules/types.d.ts +2 -2
- package/dist/rules/types.d.ts.map +1 -1
- package/dist/trails/context-no-surface-types.trail.js +1 -1
- package/dist/trails/context-no-trailhead-types.trail.d.ts +13 -0
- package/dist/trails/context-no-trailhead-types.trail.d.ts.map +1 -0
- package/dist/trails/context-no-trailhead-types.trail.js +21 -0
- package/dist/trails/context-no-trailhead-types.trail.js.map +1 -0
- package/dist/trails/cross-declarations.trail.d.ts +13 -0
- package/dist/trails/cross-declarations.trail.d.ts.map +1 -0
- package/dist/trails/cross-declarations.trail.js +22 -0
- package/dist/trails/cross-declarations.trail.js.map +1 -0
- package/dist/trails/follow-declarations.trail.js +1 -1
- package/dist/trails/implementation-returns-result.trail.js +1 -1
- package/dist/trails/index.d.ts +4 -4
- package/dist/trails/index.d.ts.map +1 -1
- package/dist/trails/index.js +4 -4
- package/dist/trails/index.js.map +1 -1
- package/dist/trails/no-direct-impl-in-route.trail.js +4 -4
- package/dist/trails/no-direct-impl-in-route.trail.js.map +1 -1
- package/dist/trails/no-direct-implementation-call.trail.js +2 -2
- package/dist/trails/no-direct-implementation-call.trail.js.map +1 -1
- package/dist/trails/no-sync-result-assumption.trail.js +2 -2
- package/dist/trails/no-sync-result-assumption.trail.js.map +1 -1
- package/dist/trails/no-throw-in-detour-target.trail.d.ts +1 -1
- package/dist/trails/no-throw-in-detour-target.trail.js +1 -1
- package/dist/trails/no-throw-in-implementation.trail.js +1 -1
- package/dist/trails/prefer-schema-inference.trail.js +1 -1
- package/dist/trails/provision-declarations.trail.d.ts +13 -0
- package/dist/trails/provision-declarations.trail.d.ts.map +1 -0
- package/dist/trails/provision-declarations.trail.js +25 -0
- package/dist/trails/provision-declarations.trail.js.map +1 -0
- package/dist/trails/provision-exists.trail.d.ts +15 -0
- package/dist/trails/provision-exists.trail.d.ts.map +1 -0
- package/dist/trails/provision-exists.trail.js +27 -0
- package/dist/trails/provision-exists.trail.js.map +1 -0
- package/dist/trails/run.d.ts +2 -2
- package/dist/trails/run.d.ts.map +1 -1
- package/dist/trails/run.js +6 -6
- package/dist/trails/run.js.map +1 -1
- package/dist/trails/schema.d.ts +1 -1
- package/dist/trails/schema.js +2 -2
- package/dist/trails/schema.js.map +1 -1
- package/dist/trails/service-declarations.trail.d.ts +13 -0
- package/dist/trails/service-declarations.trail.d.ts.map +1 -1
- package/dist/trails/service-declarations.trail.js +9 -7
- package/dist/trails/service-declarations.trail.js.map +1 -1
- package/dist/trails/service-exists.trail.d.ts +17 -0
- package/dist/trails/service-exists.trail.d.ts.map +1 -1
- package/dist/trails/service-exists.trail.js +10 -8
- package/dist/trails/service-exists.trail.js.map +1 -1
- package/dist/trails/valid-describe-refs.trail.d.ts +1 -1
- package/dist/trails/valid-detour-refs.trail.d.ts +1 -1
- package/dist/trails/valid-detour-refs.trail.js +2 -2
- package/dist/trails/wrap-rule.js +14 -14
- package/dist/trails/wrap-rule.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/cli.test.ts +8 -8
- package/src/__tests__/{follow-declarations.test.ts → cross-declarations.test.ts} +78 -78
- package/src/__tests__/drift.test.ts +5 -5
- package/src/__tests__/formatters.test.ts +2 -2
- package/src/__tests__/implementation-returns-result.test.ts +11 -11
- package/src/__tests__/no-direct-implementation-call.test.ts +10 -10
- package/src/__tests__/no-sync-result-assumption.test.ts +6 -6
- package/src/__tests__/no-throw-in-detour-target.test.ts +6 -6
- package/src/__tests__/prefer-schema-inference.test.ts +4 -4
- package/src/__tests__/provision-declarations.test.ts +318 -0
- package/src/__tests__/provision-exists.test.ts +122 -0
- package/src/__tests__/rules.test.ts +38 -38
- package/src/__tests__/valid-describe-refs.test.ts +4 -4
- package/src/__tests__/wrap-rule.test.ts +4 -4
- package/src/cli.ts +17 -13
- package/src/drift.ts +12 -12
- package/src/formatters.ts +2 -2
- package/src/index.ts +8 -8
- package/src/rules/ast.ts +36 -31
- package/src/rules/{context-no-surface-types.ts → context-no-trailhead-types.ts} +8 -8
- package/src/rules/{follow-declarations.ts → cross-declarations.ts} +63 -56
- package/src/rules/implementation-returns-result.ts +6 -6
- package/src/rules/index.ts +12 -12
- package/src/rules/no-direct-impl-in-route.ts +17 -17
- package/src/rules/no-direct-implementation-call.ts +7 -7
- package/src/rules/no-sync-result-assumption.ts +5 -5
- package/src/rules/no-throw-in-detour-target.ts +2 -2
- package/src/rules/no-throw-in-implementation.ts +3 -3
- package/src/rules/{service-declarations.ts → provision-declarations.ts} +145 -129
- package/src/rules/{service-exists.ts → provision-exists.ts} +51 -46
- package/src/rules/specs.ts +4 -4
- package/src/rules/types.ts +2 -2
- package/src/trails/{context-no-surface-types.trail.ts → context-no-trailhead-types.trail.ts} +5 -5
- package/src/trails/cross-declarations.trail.ts +22 -0
- package/src/trails/implementation-returns-result.trail.ts +1 -1
- package/src/trails/index.ts +4 -4
- package/src/trails/no-direct-impl-in-route.trail.ts +4 -4
- package/src/trails/no-direct-implementation-call.trail.ts +2 -2
- package/src/trails/no-sync-result-assumption.trail.ts +2 -2
- package/src/trails/no-throw-in-detour-target.trail.ts +1 -1
- package/src/trails/no-throw-in-implementation.trail.ts +1 -1
- package/src/trails/prefer-schema-inference.trail.ts +1 -1
- package/src/trails/provision-declarations.trail.ts +25 -0
- package/src/trails/provision-exists.trail.ts +27 -0
- package/src/trails/run.ts +7 -7
- package/src/trails/schema.ts +2 -2
- package/src/trails/valid-detour-refs.trail.ts +2 -2
- package/src/trails/wrap-rule.ts +17 -17
- package/tsconfig.tsbuildinfo +1 -1
- package/src/__tests__/service-declarations.test.ts +0 -318
- package/src/__tests__/service-exists.test.ts +0 -122
- package/src/trails/follow-declarations.trail.ts +0 -22
- package/src/trails/service-declarations.trail.ts +0 -25
- package/src/trails/service-exists.trail.ts +0 -27
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Validates that
|
|
2
|
+
* Validates that provision access matches the declared `provisions` array.
|
|
3
3
|
*
|
|
4
|
-
* Statically analyzes trail
|
|
5
|
-
* `ctx.
|
|
6
|
-
* `
|
|
4
|
+
* Statically analyzes trail `blaze` functions to find `db.from(ctx)` and
|
|
5
|
+
* `ctx.provision('db.main')` calls and compares them against the declared
|
|
6
|
+
* `provisions: [...]` array in the trail config. Reports errors for undeclared
|
|
7
7
|
* access and warnings for unused declarations.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import {
|
|
11
|
-
|
|
11
|
+
collectNamedProvisionIds,
|
|
12
12
|
extractFirstStringArg,
|
|
13
13
|
findConfigProperty,
|
|
14
|
-
|
|
14
|
+
findBlazeBodies,
|
|
15
15
|
findTrailDefinitions,
|
|
16
16
|
getStringValue,
|
|
17
17
|
identifierName,
|
|
@@ -25,15 +25,15 @@ import { isTestFile } from './scan.js';
|
|
|
25
25
|
import type { WardenDiagnostic, WardenRule } from './types.js';
|
|
26
26
|
|
|
27
27
|
// ---------------------------------------------------------------------------
|
|
28
|
-
//
|
|
28
|
+
// Provision declaration extraction
|
|
29
29
|
// ---------------------------------------------------------------------------
|
|
30
30
|
|
|
31
|
-
interface
|
|
31
|
+
interface DeclaredProvision {
|
|
32
32
|
readonly id: string | null;
|
|
33
33
|
readonly name: string | null;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
interface
|
|
36
|
+
interface CalledProvisions {
|
|
37
37
|
readonly fromNames: ReadonlySet<string>;
|
|
38
38
|
readonly lookupIds: ReadonlySet<string>;
|
|
39
39
|
readonly lookupNames: ReadonlySet<string>;
|
|
@@ -59,25 +59,25 @@ const extractMemberPair = (
|
|
|
59
59
|
return objName && propName ? { objName, propName } : null;
|
|
60
60
|
};
|
|
61
61
|
|
|
62
|
-
/** Check if a node is an inline `
|
|
63
|
-
const
|
|
62
|
+
/** Check if a node is an inline `provision('id', ...)` call. */
|
|
63
|
+
const isInlineProvisionCall = (node: AstNode): boolean => {
|
|
64
64
|
if (node.type !== 'CallExpression') {
|
|
65
65
|
return false;
|
|
66
66
|
}
|
|
67
67
|
return (
|
|
68
68
|
identifierName((node as unknown as { callee?: AstNode }).callee) ===
|
|
69
|
-
'
|
|
69
|
+
'provision'
|
|
70
70
|
);
|
|
71
71
|
};
|
|
72
72
|
|
|
73
|
-
/** Get `
|
|
74
|
-
const
|
|
75
|
-
const
|
|
76
|
-
if (!
|
|
73
|
+
/** Get `provisions` array elements from a trail config. */
|
|
74
|
+
const getProvisionElements = (config: AstNode): readonly AstNode[] => {
|
|
75
|
+
const provisionsProp = findConfigProperty(config, 'provisions');
|
|
76
|
+
if (!provisionsProp) {
|
|
77
77
|
return [];
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
const arrayNode =
|
|
80
|
+
const arrayNode = provisionsProp.value;
|
|
81
81
|
if (!arrayNode || (arrayNode as AstNode).type !== 'ArrayExpression') {
|
|
82
82
|
return [];
|
|
83
83
|
}
|
|
@@ -88,15 +88,15 @@ const getServiceElements = (config: AstNode): readonly AstNode[] => {
|
|
|
88
88
|
return elements ?? [];
|
|
89
89
|
};
|
|
90
90
|
|
|
91
|
-
/** Extract one declared
|
|
92
|
-
const
|
|
91
|
+
/** Extract one declared provision from a `provisions` array element. */
|
|
92
|
+
const extractDeclaredProvision = (
|
|
93
93
|
element: AstNode,
|
|
94
|
-
|
|
95
|
-
):
|
|
94
|
+
provisionIdsByName: ReadonlyMap<string, string>
|
|
95
|
+
): DeclaredProvision | null => {
|
|
96
96
|
if (element.type === 'Identifier') {
|
|
97
97
|
const name = identifierName(element);
|
|
98
98
|
return {
|
|
99
|
-
id: name ? (
|
|
99
|
+
id: name ? (provisionIdsByName.get(name) ?? null) : null,
|
|
100
100
|
name,
|
|
101
101
|
};
|
|
102
102
|
}
|
|
@@ -105,30 +105,30 @@ const extractDeclaredService = (
|
|
|
105
105
|
return { id: getStringValue(element), name: null };
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
if (
|
|
108
|
+
if (isInlineProvisionCall(element)) {
|
|
109
109
|
return { id: extractFirstStringArg(element), name: null };
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
return null;
|
|
113
113
|
};
|
|
114
114
|
|
|
115
|
-
/** Extract declared
|
|
116
|
-
const
|
|
115
|
+
/** Extract declared provisions from a trail config's `provisions` array. */
|
|
116
|
+
const extractDeclaredProvisions = (
|
|
117
117
|
config: AstNode,
|
|
118
|
-
|
|
119
|
-
): readonly
|
|
120
|
-
|
|
121
|
-
const
|
|
122
|
-
return
|
|
118
|
+
provisionIdsByName: ReadonlyMap<string, string>
|
|
119
|
+
): readonly DeclaredProvision[] =>
|
|
120
|
+
getProvisionElements(config).flatMap((element) => {
|
|
121
|
+
const provision = extractDeclaredProvision(element, provisionIdsByName);
|
|
122
|
+
return provision ? [provision] : [];
|
|
123
123
|
});
|
|
124
124
|
|
|
125
125
|
// ---------------------------------------------------------------------------
|
|
126
126
|
// Called service extraction
|
|
127
127
|
// ---------------------------------------------------------------------------
|
|
128
128
|
|
|
129
|
-
/** Extract the second parameter name from a
|
|
130
|
-
const extractContextParamName = (
|
|
131
|
-
const params =
|
|
129
|
+
/** Extract the second parameter name from a blaze function node. */
|
|
130
|
+
const extractContextParamName = (blazeBody: AstNode): string | null => {
|
|
131
|
+
const params = blazeBody['params'] as readonly AstNode[] | undefined;
|
|
132
132
|
if (!params || params.length < 2) {
|
|
133
133
|
return null;
|
|
134
134
|
}
|
|
@@ -189,63 +189,63 @@ const extractFromCallName = (
|
|
|
189
189
|
: null;
|
|
190
190
|
};
|
|
191
191
|
|
|
192
|
-
/** Check if a callee is a member-style `ctx.
|
|
193
|
-
const
|
|
192
|
+
/** Check if a callee is a member-style `ctx.provision(...)` call. */
|
|
193
|
+
const isMemberProvisionCall = (
|
|
194
194
|
callee: AstNode,
|
|
195
195
|
ctxNames: ReadonlySet<string>
|
|
196
196
|
): boolean => {
|
|
197
197
|
const pair = extractMemberPair(callee);
|
|
198
|
-
return !!pair && ctxNames.has(pair.objName) && pair.propName === '
|
|
198
|
+
return !!pair && ctxNames.has(pair.objName) && pair.propName === 'provision';
|
|
199
199
|
};
|
|
200
200
|
|
|
201
|
-
/** Extract `ctx.
|
|
202
|
-
const
|
|
201
|
+
/** Extract `ctx.provision(db)` and destructured `provision(db)` lookup names. */
|
|
202
|
+
const extractLookupProvisionName = (
|
|
203
203
|
node: AstNode,
|
|
204
204
|
ctxNames: ReadonlySet<string>,
|
|
205
|
-
|
|
205
|
+
provisionAliases: ReadonlySet<string>
|
|
206
206
|
): string | null => {
|
|
207
207
|
const callee = extractCallCallee(node);
|
|
208
208
|
if (!callee) {
|
|
209
209
|
return null;
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
-
if (
|
|
212
|
+
if (isMemberProvisionCall(callee, ctxNames)) {
|
|
213
213
|
return extractFirstIdentifierArg(node);
|
|
214
214
|
}
|
|
215
215
|
|
|
216
|
-
if (
|
|
216
|
+
if (provisionAliases.has(identifierName(callee) ?? '')) {
|
|
217
217
|
return extractFirstIdentifierArg(node);
|
|
218
218
|
}
|
|
219
219
|
|
|
220
220
|
return null;
|
|
221
221
|
};
|
|
222
222
|
|
|
223
|
-
/** Extract `ctx.
|
|
224
|
-
const
|
|
223
|
+
/** Extract `ctx.provision('id')` and destructured `provision('id')` lookup IDs. */
|
|
224
|
+
const extractLookupProvisionId = (
|
|
225
225
|
node: AstNode,
|
|
226
226
|
ctxNames: ReadonlySet<string>,
|
|
227
|
-
|
|
227
|
+
provisionAliases: ReadonlySet<string>
|
|
228
228
|
): string | null => {
|
|
229
229
|
const callee = extractCallCallee(node);
|
|
230
230
|
if (!callee) {
|
|
231
231
|
return null;
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
-
if (
|
|
234
|
+
if (isMemberProvisionCall(callee, ctxNames)) {
|
|
235
235
|
return extractFirstStringArg(node);
|
|
236
236
|
}
|
|
237
237
|
|
|
238
238
|
const calleeName = identifierName(callee);
|
|
239
239
|
const args = node['arguments'] as readonly AstNode[] | undefined;
|
|
240
|
-
if (calleeName &&
|
|
240
|
+
if (calleeName && provisionAliases.has(calleeName) && args?.length === 1) {
|
|
241
241
|
return extractFirstStringArg(node);
|
|
242
242
|
}
|
|
243
243
|
|
|
244
244
|
return null;
|
|
245
245
|
};
|
|
246
246
|
|
|
247
|
-
/** Collect local aliases for the
|
|
248
|
-
const
|
|
247
|
+
/** Collect local aliases for the provision accessor (e.g. `const { provision } = ctx`). */
|
|
248
|
+
const collectProvisionAliases = (
|
|
249
249
|
body: AstNode,
|
|
250
250
|
ctxNames: ReadonlySet<string>
|
|
251
251
|
): ReadonlySet<string> => {
|
|
@@ -267,7 +267,7 @@ const collectServiceAliases = (
|
|
|
267
267
|
const keyName = identifierName(
|
|
268
268
|
(property as unknown as { key?: AstNode }).key
|
|
269
269
|
);
|
|
270
|
-
if (keyName !== '
|
|
270
|
+
if (keyName !== 'provision') {
|
|
271
271
|
return [];
|
|
272
272
|
}
|
|
273
273
|
|
|
@@ -300,15 +300,15 @@ const collectServiceAliases = (
|
|
|
300
300
|
return aliases;
|
|
301
301
|
};
|
|
302
302
|
|
|
303
|
-
/** Walk
|
|
304
|
-
const
|
|
303
|
+
/** Walk blaze bodies and collect provision access that can be resolved statically. */
|
|
304
|
+
const extractCalledProvisions = (config: AstNode): CalledProvisions => {
|
|
305
305
|
const fromNames = new Set<string>();
|
|
306
306
|
const lookupIds = new Set<string>();
|
|
307
307
|
const lookupNames = new Set<string>();
|
|
308
308
|
|
|
309
|
-
for (const body of
|
|
309
|
+
for (const body of findBlazeBodies(config)) {
|
|
310
310
|
const ctxNames = buildCtxNames(body);
|
|
311
|
-
const
|
|
311
|
+
const provisionAliases = collectProvisionAliases(body, ctxNames);
|
|
312
312
|
|
|
313
313
|
walkScope(body, (node) => {
|
|
314
314
|
const fromName = extractFromCallName(node, ctxNames);
|
|
@@ -316,15 +316,19 @@ const extractCalledServices = (config: AstNode): CalledServices => {
|
|
|
316
316
|
fromNames.add(fromName);
|
|
317
317
|
}
|
|
318
318
|
|
|
319
|
-
const lookupId =
|
|
319
|
+
const lookupId = extractLookupProvisionId(
|
|
320
|
+
node,
|
|
321
|
+
ctxNames,
|
|
322
|
+
provisionAliases
|
|
323
|
+
);
|
|
320
324
|
if (lookupId) {
|
|
321
325
|
lookupIds.add(lookupId);
|
|
322
326
|
}
|
|
323
327
|
|
|
324
|
-
const lookupName =
|
|
328
|
+
const lookupName = extractLookupProvisionName(
|
|
325
329
|
node,
|
|
326
330
|
ctxNames,
|
|
327
|
-
|
|
331
|
+
provisionAliases
|
|
328
332
|
);
|
|
329
333
|
if (lookupName) {
|
|
330
334
|
lookupNames.add(lookupName);
|
|
@@ -339,58 +343,58 @@ const extractCalledServices = (config: AstNode): CalledServices => {
|
|
|
339
343
|
// Diagnostics
|
|
340
344
|
// ---------------------------------------------------------------------------
|
|
341
345
|
|
|
342
|
-
const
|
|
343
|
-
|
|
346
|
+
const renderDeclaredProvision = (provision: DeclaredProvision): string =>
|
|
347
|
+
provision.name ?? provision.id ?? '<unknown>';
|
|
344
348
|
|
|
345
349
|
const buildUndeclaredFromDiagnostic = (
|
|
346
350
|
trailId: string,
|
|
347
|
-
|
|
351
|
+
provisionName: string,
|
|
348
352
|
filePath: string,
|
|
349
353
|
line: number
|
|
350
354
|
): WardenDiagnostic => ({
|
|
351
355
|
filePath,
|
|
352
356
|
line,
|
|
353
|
-
message: `Trail "${trailId}": ${
|
|
354
|
-
rule: '
|
|
357
|
+
message: `Trail "${trailId}": ${provisionName}.from(ctx) called but '${provisionName}' is not declared in provisions`,
|
|
358
|
+
rule: 'provision-declarations',
|
|
355
359
|
severity: 'error',
|
|
356
360
|
});
|
|
357
361
|
|
|
358
362
|
const buildUndeclaredLookupDiagnostic = (
|
|
359
363
|
trailId: string,
|
|
360
|
-
|
|
364
|
+
provisionId: string,
|
|
361
365
|
filePath: string,
|
|
362
366
|
line: number
|
|
363
367
|
): WardenDiagnostic => ({
|
|
364
368
|
filePath,
|
|
365
369
|
line,
|
|
366
|
-
message: `Trail "${trailId}": ctx.
|
|
367
|
-
rule: '
|
|
370
|
+
message: `Trail "${trailId}": ctx.provision('${provisionId}') called but '${provisionId}' is not declared in provisions`,
|
|
371
|
+
rule: 'provision-declarations',
|
|
368
372
|
severity: 'error',
|
|
369
373
|
});
|
|
370
374
|
|
|
371
375
|
const buildUndeclaredLookupNameDiagnostic = (
|
|
372
376
|
trailId: string,
|
|
373
|
-
|
|
377
|
+
provisionName: string,
|
|
374
378
|
filePath: string,
|
|
375
379
|
line: number
|
|
376
380
|
): WardenDiagnostic => ({
|
|
377
381
|
filePath,
|
|
378
382
|
line,
|
|
379
|
-
message: `Trail "${trailId}": ctx.
|
|
380
|
-
rule: '
|
|
383
|
+
message: `Trail "${trailId}": ctx.provision(${provisionName}) called but '${provisionName}' is not declared in provisions`,
|
|
384
|
+
rule: 'provision-declarations',
|
|
381
385
|
severity: 'error',
|
|
382
386
|
});
|
|
383
387
|
|
|
384
388
|
const buildUnusedDiagnostic = (
|
|
385
389
|
trailId: string,
|
|
386
|
-
|
|
390
|
+
declaredProvision: DeclaredProvision,
|
|
387
391
|
filePath: string,
|
|
388
392
|
line: number
|
|
389
393
|
): WardenDiagnostic => ({
|
|
390
394
|
filePath,
|
|
391
395
|
line,
|
|
392
|
-
message: `Trail "${trailId}": '${
|
|
393
|
-
rule: '
|
|
396
|
+
message: `Trail "${trailId}": '${renderDeclaredProvision(declaredProvision)}' declared in provisions but never used`,
|
|
397
|
+
rule: 'provision-declarations',
|
|
394
398
|
severity: 'warn',
|
|
395
399
|
});
|
|
396
400
|
|
|
@@ -398,19 +402,22 @@ const buildUnusedDiagnostic = (
|
|
|
398
402
|
// Comparison
|
|
399
403
|
// ---------------------------------------------------------------------------
|
|
400
404
|
|
|
401
|
-
const
|
|
402
|
-
|
|
403
|
-
|
|
405
|
+
const provisionWasUsed = (
|
|
406
|
+
declaredProvision: DeclaredProvision,
|
|
407
|
+
calledProvisions: CalledProvisions
|
|
404
408
|
): boolean => {
|
|
405
409
|
if (
|
|
406
|
-
|
|
407
|
-
(
|
|
408
|
-
|
|
410
|
+
declaredProvision.name &&
|
|
411
|
+
(calledProvisions.fromNames.has(declaredProvision.name) ||
|
|
412
|
+
calledProvisions.lookupNames.has(declaredProvision.name))
|
|
409
413
|
) {
|
|
410
414
|
return true;
|
|
411
415
|
}
|
|
412
416
|
|
|
413
|
-
if (
|
|
417
|
+
if (
|
|
418
|
+
declaredProvision.id &&
|
|
419
|
+
calledProvisions.lookupIds.has(declaredProvision.id)
|
|
420
|
+
) {
|
|
414
421
|
return true;
|
|
415
422
|
}
|
|
416
423
|
|
|
@@ -418,31 +425,35 @@ const serviceWasUsed = (
|
|
|
418
425
|
};
|
|
419
426
|
|
|
420
427
|
const buildDeclaredNames = (
|
|
421
|
-
|
|
428
|
+
declaredProvisions: readonly DeclaredProvision[]
|
|
422
429
|
): ReadonlySet<string> =>
|
|
423
430
|
new Set(
|
|
424
|
-
|
|
431
|
+
declaredProvisions.flatMap((provision) =>
|
|
432
|
+
provision.name ? [provision.name] : []
|
|
433
|
+
)
|
|
425
434
|
);
|
|
426
435
|
|
|
427
436
|
const buildDeclaredIds = (
|
|
428
|
-
|
|
437
|
+
declaredProvisions: readonly DeclaredProvision[]
|
|
429
438
|
): ReadonlySet<string> =>
|
|
430
439
|
new Set(
|
|
431
|
-
|
|
440
|
+
declaredProvisions.flatMap((provision) =>
|
|
441
|
+
provision.id ? [provision.id] : []
|
|
442
|
+
)
|
|
432
443
|
);
|
|
433
444
|
|
|
434
445
|
const reportUndeclaredFromCalls = (
|
|
435
446
|
trailId: string,
|
|
436
447
|
filePath: string,
|
|
437
448
|
line: number,
|
|
438
|
-
|
|
449
|
+
calledProvisions: CalledProvisions,
|
|
439
450
|
declaredNames: ReadonlySet<string>,
|
|
440
451
|
diagnostics: WardenDiagnostic[]
|
|
441
452
|
): void => {
|
|
442
|
-
for (const
|
|
443
|
-
if (!declaredNames.has(
|
|
453
|
+
for (const provisionName of calledProvisions.fromNames) {
|
|
454
|
+
if (!declaredNames.has(provisionName)) {
|
|
444
455
|
diagnostics.push(
|
|
445
|
-
buildUndeclaredFromDiagnostic(trailId,
|
|
456
|
+
buildUndeclaredFromDiagnostic(trailId, provisionName, filePath, line)
|
|
446
457
|
);
|
|
447
458
|
}
|
|
448
459
|
}
|
|
@@ -452,19 +463,19 @@ const reportUndeclaredLookupCalls = (
|
|
|
452
463
|
trailId: string,
|
|
453
464
|
filePath: string,
|
|
454
465
|
line: number,
|
|
455
|
-
|
|
466
|
+
calledProvisions: CalledProvisions,
|
|
456
467
|
declaredIds: ReadonlySet<string>,
|
|
457
468
|
declaredNames: ReadonlySet<string>,
|
|
458
469
|
diagnostics: WardenDiagnostic[]
|
|
459
470
|
): void => {
|
|
460
|
-
for (const
|
|
461
|
-
// Name-based lookup checks remain reliable even when an imported
|
|
471
|
+
for (const provisionName of calledProvisions.lookupNames) {
|
|
472
|
+
// Name-based lookup checks remain reliable even when an imported provision ID
|
|
462
473
|
// cannot be resolved locally.
|
|
463
|
-
if (!declaredNames.has(
|
|
474
|
+
if (!declaredNames.has(provisionName)) {
|
|
464
475
|
diagnostics.push(
|
|
465
476
|
buildUndeclaredLookupNameDiagnostic(
|
|
466
477
|
trailId,
|
|
467
|
-
|
|
478
|
+
provisionName,
|
|
468
479
|
filePath,
|
|
469
480
|
line
|
|
470
481
|
)
|
|
@@ -472,10 +483,10 @@ const reportUndeclaredLookupCalls = (
|
|
|
472
483
|
}
|
|
473
484
|
}
|
|
474
485
|
|
|
475
|
-
for (const
|
|
476
|
-
if (!declaredIds.has(
|
|
486
|
+
for (const provisionId of calledProvisions.lookupIds) {
|
|
487
|
+
if (!declaredIds.has(provisionId)) {
|
|
477
488
|
diagnostics.push(
|
|
478
|
-
buildUndeclaredLookupDiagnostic(trailId,
|
|
489
|
+
buildUndeclaredLookupDiagnostic(trailId, provisionId, filePath, line)
|
|
479
490
|
);
|
|
480
491
|
}
|
|
481
492
|
}
|
|
@@ -485,54 +496,54 @@ const reportUnusedDeclarations = (
|
|
|
485
496
|
trailId: string,
|
|
486
497
|
filePath: string,
|
|
487
498
|
line: number,
|
|
488
|
-
|
|
489
|
-
|
|
499
|
+
declaredProvisions: readonly DeclaredProvision[],
|
|
500
|
+
calledProvisions: CalledProvisions,
|
|
490
501
|
diagnostics: WardenDiagnostic[]
|
|
491
502
|
): void => {
|
|
492
|
-
for (const
|
|
493
|
-
if (
|
|
503
|
+
for (const declaredProvision of declaredProvisions) {
|
|
504
|
+
if (provisionWasUsed(declaredProvision, calledProvisions)) {
|
|
494
505
|
continue;
|
|
495
506
|
}
|
|
496
507
|
|
|
497
|
-
if (
|
|
508
|
+
if (declaredProvision.name && declaredProvision.id === null) {
|
|
498
509
|
continue;
|
|
499
510
|
}
|
|
500
511
|
|
|
501
512
|
diagnostics.push(
|
|
502
|
-
buildUnusedDiagnostic(trailId,
|
|
513
|
+
buildUnusedDiagnostic(trailId, declaredProvision, filePath, line)
|
|
503
514
|
);
|
|
504
515
|
}
|
|
505
516
|
};
|
|
506
517
|
|
|
507
|
-
const
|
|
508
|
-
|
|
509
|
-
|
|
518
|
+
const hasNoProvisionActivity = (
|
|
519
|
+
declaredProvisions: readonly DeclaredProvision[],
|
|
520
|
+
calledProvisions: CalledProvisions
|
|
510
521
|
): boolean =>
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
522
|
+
declaredProvisions.length === 0 &&
|
|
523
|
+
calledProvisions.fromNames.size === 0 &&
|
|
524
|
+
calledProvisions.lookupIds.size === 0 &&
|
|
525
|
+
calledProvisions.lookupNames.size === 0;
|
|
515
526
|
|
|
516
527
|
const analyzeTrailServices = (
|
|
517
528
|
def: { config: AstNode; start: number },
|
|
518
529
|
sourceCode: string,
|
|
519
|
-
|
|
530
|
+
provisionIdsByName: ReadonlyMap<string, string>
|
|
520
531
|
): {
|
|
521
|
-
readonly
|
|
532
|
+
readonly calledProvisions: CalledProvisions;
|
|
522
533
|
readonly declaredIds: ReadonlySet<string>;
|
|
523
534
|
readonly declaredNames: ReadonlySet<string>;
|
|
524
|
-
readonly
|
|
535
|
+
readonly declaredProvisions: readonly DeclaredProvision[];
|
|
525
536
|
readonly line: number;
|
|
526
537
|
} => {
|
|
527
|
-
const
|
|
538
|
+
const declaredProvisions = extractDeclaredProvisions(
|
|
528
539
|
def.config,
|
|
529
|
-
|
|
540
|
+
provisionIdsByName
|
|
530
541
|
);
|
|
531
542
|
return {
|
|
532
|
-
|
|
533
|
-
declaredIds: buildDeclaredIds(
|
|
534
|
-
declaredNames: buildDeclaredNames(
|
|
535
|
-
|
|
543
|
+
calledProvisions: extractCalledProvisions(def.config),
|
|
544
|
+
declaredIds: buildDeclaredIds(declaredProvisions),
|
|
545
|
+
declaredNames: buildDeclaredNames(declaredProvisions),
|
|
546
|
+
declaredProvisions,
|
|
536
547
|
line: offsetToLine(sourceCode, def.start),
|
|
537
548
|
};
|
|
538
549
|
};
|
|
@@ -541,13 +552,18 @@ const checkTrailDefinition = (
|
|
|
541
552
|
def: { id: string; config: AstNode; start: number },
|
|
542
553
|
filePath: string,
|
|
543
554
|
sourceCode: string,
|
|
544
|
-
|
|
555
|
+
provisionIdsByName: ReadonlyMap<string, string>,
|
|
545
556
|
diagnostics: WardenDiagnostic[]
|
|
546
557
|
): void => {
|
|
547
|
-
const {
|
|
548
|
-
|
|
558
|
+
const {
|
|
559
|
+
calledProvisions,
|
|
560
|
+
declaredIds,
|
|
561
|
+
declaredNames,
|
|
562
|
+
declaredProvisions,
|
|
563
|
+
line,
|
|
564
|
+
} = analyzeTrailServices(def, sourceCode, provisionIdsByName);
|
|
549
565
|
|
|
550
|
-
if (
|
|
566
|
+
if (hasNoProvisionActivity(declaredProvisions, calledProvisions)) {
|
|
551
567
|
return;
|
|
552
568
|
}
|
|
553
569
|
|
|
@@ -555,7 +571,7 @@ const checkTrailDefinition = (
|
|
|
555
571
|
def.id,
|
|
556
572
|
filePath,
|
|
557
573
|
line,
|
|
558
|
-
|
|
574
|
+
calledProvisions,
|
|
559
575
|
declaredNames,
|
|
560
576
|
diagnostics
|
|
561
577
|
);
|
|
@@ -563,7 +579,7 @@ const checkTrailDefinition = (
|
|
|
563
579
|
def.id,
|
|
564
580
|
filePath,
|
|
565
581
|
line,
|
|
566
|
-
|
|
582
|
+
calledProvisions,
|
|
567
583
|
declaredIds,
|
|
568
584
|
declaredNames,
|
|
569
585
|
diagnostics
|
|
@@ -572,8 +588,8 @@ const checkTrailDefinition = (
|
|
|
572
588
|
def.id,
|
|
573
589
|
filePath,
|
|
574
590
|
line,
|
|
575
|
-
|
|
576
|
-
|
|
591
|
+
declaredProvisions,
|
|
592
|
+
calledProvisions,
|
|
577
593
|
diagnostics
|
|
578
594
|
);
|
|
579
595
|
};
|
|
@@ -583,9 +599,9 @@ const checkTrailDefinition = (
|
|
|
583
599
|
// ---------------------------------------------------------------------------
|
|
584
600
|
|
|
585
601
|
/**
|
|
586
|
-
* Validates that
|
|
602
|
+
* Validates that provision access aligns with declared `provisions` arrays.
|
|
587
603
|
*/
|
|
588
|
-
export const
|
|
604
|
+
export const provisionDeclarations: WardenRule = {
|
|
589
605
|
check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
|
|
590
606
|
if (isTestFile(filePath)) {
|
|
591
607
|
return [];
|
|
@@ -597,14 +613,14 @@ export const serviceDeclarations: WardenRule = {
|
|
|
597
613
|
}
|
|
598
614
|
|
|
599
615
|
const diagnostics: WardenDiagnostic[] = [];
|
|
600
|
-
const
|
|
616
|
+
const provisionIdsByName = collectNamedProvisionIds(ast);
|
|
601
617
|
|
|
602
618
|
for (const def of findTrailDefinitions(ast)) {
|
|
603
619
|
checkTrailDefinition(
|
|
604
620
|
def,
|
|
605
621
|
filePath,
|
|
606
622
|
sourceCode,
|
|
607
|
-
|
|
623
|
+
provisionIdsByName,
|
|
608
624
|
diagnostics
|
|
609
625
|
);
|
|
610
626
|
}
|
|
@@ -612,7 +628,7 @@ export const serviceDeclarations: WardenRule = {
|
|
|
612
628
|
return diagnostics;
|
|
613
629
|
},
|
|
614
630
|
description:
|
|
615
|
-
'Ensure
|
|
616
|
-
name: '
|
|
631
|
+
'Ensure provision.from(ctx) and ctx.provision() calls match the declared provisions array in trail definitions.',
|
|
632
|
+
name: 'provision-declarations',
|
|
617
633
|
severity: 'error',
|
|
618
634
|
};
|