@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
|
@@ -6,14 +6,31 @@
|
|
|
6
6
|
* or a tracked Result-typed variable.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { dirname, isAbsolute, resolve } from 'node:path';
|
|
10
9
|
import { existsSync, readFileSync } from 'node:fs';
|
|
10
|
+
import { dirname, isAbsolute, resolve } from 'node:path';
|
|
11
11
|
import type { AstNode } from './ast.js';
|
|
12
12
|
import {
|
|
13
13
|
collectScopeFrameBindings,
|
|
14
14
|
findBlazeBodies,
|
|
15
15
|
findTrailDefinitions,
|
|
16
16
|
getMemberExpression,
|
|
17
|
+
getNodeAlternate,
|
|
18
|
+
getNodeArgument,
|
|
19
|
+
getNodeBodyNode,
|
|
20
|
+
getNodeBodyStatements,
|
|
21
|
+
getNodeConsequent,
|
|
22
|
+
getNodeDeclaration,
|
|
23
|
+
getNodeExportKind,
|
|
24
|
+
getNodeExpression,
|
|
25
|
+
getNodeId,
|
|
26
|
+
getNodeImported,
|
|
27
|
+
getNodeInit,
|
|
28
|
+
getNodeLocal,
|
|
29
|
+
getNodeName,
|
|
30
|
+
getNodeReturnType,
|
|
31
|
+
getNodeSource,
|
|
32
|
+
getNodeTypeAnnotation,
|
|
33
|
+
getNodeValue,
|
|
17
34
|
identifierName,
|
|
18
35
|
offsetToLine,
|
|
19
36
|
parse,
|
|
@@ -61,7 +78,7 @@ export const isResultExpression = (node: AstNode): boolean => {
|
|
|
61
78
|
}
|
|
62
79
|
|
|
63
80
|
if (node.type === 'AwaitExpression') {
|
|
64
|
-
const arg = (node
|
|
81
|
+
const arg = getNodeArgument(node);
|
|
65
82
|
return arg ? isResultExpression(arg) : false;
|
|
66
83
|
}
|
|
67
84
|
|
|
@@ -79,6 +96,13 @@ export type ScopedHelperMap = ReadonlyMap<
|
|
|
79
96
|
|
|
80
97
|
export type MutableScopedHelperMap = Map<ReadonlySet<string>, Set<string>>;
|
|
81
98
|
|
|
99
|
+
type ScopedResultVariableMap = ReadonlyMap<
|
|
100
|
+
ReadonlySet<string>,
|
|
101
|
+
ReadonlySet<string>
|
|
102
|
+
>;
|
|
103
|
+
|
|
104
|
+
type MutableScopedResultVariableMap = Map<ReadonlySet<string>, Set<string>>;
|
|
105
|
+
|
|
82
106
|
export const findNearestBindingScope = (
|
|
83
107
|
name: string,
|
|
84
108
|
scopes: readonly ReadonlySet<string>[]
|
|
@@ -91,6 +115,15 @@ const isScopedHelperBinding = (
|
|
|
91
115
|
scopedHelpers: ScopedHelperMap
|
|
92
116
|
): boolean => scopedHelpers.get(scope)?.has(name) ?? false;
|
|
93
117
|
|
|
118
|
+
const isScopedResultVariableBinding = (
|
|
119
|
+
name: string,
|
|
120
|
+
scopes: readonly ReadonlySet<string>[],
|
|
121
|
+
resultVars: ScopedResultVariableMap
|
|
122
|
+
): boolean => {
|
|
123
|
+
const bindingScope = findNearestBindingScope(name, scopes);
|
|
124
|
+
return Boolean(bindingScope && resultVars.get(bindingScope)?.has(name));
|
|
125
|
+
};
|
|
126
|
+
|
|
94
127
|
/**
|
|
95
128
|
* Check whether a namespace-member call like `ns.helper(...)` resolves to a
|
|
96
129
|
* known Result helper.
|
|
@@ -131,9 +164,7 @@ export const isHelperCall = (
|
|
|
131
164
|
scopedHelpers: ScopedHelperMap = new Map()
|
|
132
165
|
): boolean => {
|
|
133
166
|
const target =
|
|
134
|
-
node.type === 'AwaitExpression'
|
|
135
|
-
? ((node as unknown as { argument?: AstNode }).argument ?? null)
|
|
136
|
-
: node;
|
|
167
|
+
node.type === 'AwaitExpression' ? (getNodeArgument(node) ?? null) : node;
|
|
137
168
|
|
|
138
169
|
if (!target || target.type !== 'CallExpression') {
|
|
139
170
|
return false;
|
|
@@ -141,7 +172,10 @@ export const isHelperCall = (
|
|
|
141
172
|
|
|
142
173
|
const callee = target['callee'] as AstNode | undefined;
|
|
143
174
|
if (callee?.type === 'Identifier') {
|
|
144
|
-
const
|
|
175
|
+
const name = getNodeName(callee);
|
|
176
|
+
if (!name) {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
145
179
|
const bindingScope = findNearestBindingScope(name, scopes);
|
|
146
180
|
if (
|
|
147
181
|
bindingScope &&
|
|
@@ -160,37 +194,83 @@ export const isHelperCall = (
|
|
|
160
194
|
/** Unwrap an optional AwaitExpression to get the inner identifier name. */
|
|
161
195
|
const resolveIdentifierName = (node: AstNode): string | null => {
|
|
162
196
|
if (node.type === 'Identifier') {
|
|
163
|
-
return (node
|
|
197
|
+
return getNodeName(node) ?? null;
|
|
164
198
|
}
|
|
165
199
|
if (node.type === 'AwaitExpression') {
|
|
166
|
-
const inner = (node
|
|
200
|
+
const inner = getNodeArgument(node);
|
|
167
201
|
if (inner?.type === 'Identifier') {
|
|
168
|
-
return (inner
|
|
202
|
+
return getNodeName(inner) ?? null;
|
|
169
203
|
}
|
|
170
204
|
}
|
|
171
205
|
return null;
|
|
172
206
|
};
|
|
173
207
|
|
|
208
|
+
const unwrapReturnExpression = (node: AstNode): AstNode => {
|
|
209
|
+
let current = node;
|
|
210
|
+
while (
|
|
211
|
+
current.type === 'AwaitExpression' ||
|
|
212
|
+
current.type === 'ParenthesizedExpression'
|
|
213
|
+
) {
|
|
214
|
+
const next =
|
|
215
|
+
current.type === 'AwaitExpression'
|
|
216
|
+
? getNodeArgument(current)
|
|
217
|
+
: getNodeExpression(current);
|
|
218
|
+
if (!next) {
|
|
219
|
+
return current;
|
|
220
|
+
}
|
|
221
|
+
current = next;
|
|
222
|
+
}
|
|
223
|
+
return current;
|
|
224
|
+
};
|
|
225
|
+
|
|
174
226
|
/** Check if a return argument is an allowed Result value. */
|
|
175
227
|
const isAllowedReturnArgument = (
|
|
176
228
|
argument: AstNode,
|
|
177
229
|
helperNames: ReadonlySet<string>,
|
|
178
|
-
resultVars:
|
|
230
|
+
resultVars: ScopedResultVariableMap,
|
|
179
231
|
namespaceHelpers: NamespaceHelperMap,
|
|
180
232
|
scopes: readonly ReadonlySet<string>[] = [],
|
|
181
233
|
scopedHelpers: ScopedHelperMap = new Map()
|
|
182
234
|
): boolean => {
|
|
183
|
-
|
|
235
|
+
const target = unwrapReturnExpression(argument);
|
|
236
|
+
if (target.type === 'ConditionalExpression') {
|
|
237
|
+
const alternate = getNodeAlternate(target);
|
|
238
|
+
const consequent = getNodeConsequent(target);
|
|
239
|
+
return (
|
|
240
|
+
consequent !== undefined &&
|
|
241
|
+
alternate !== undefined &&
|
|
242
|
+
isAllowedReturnArgument(
|
|
243
|
+
consequent,
|
|
244
|
+
helperNames,
|
|
245
|
+
resultVars,
|
|
246
|
+
namespaceHelpers,
|
|
247
|
+
scopes,
|
|
248
|
+
scopedHelpers
|
|
249
|
+
) &&
|
|
250
|
+
isAllowedReturnArgument(
|
|
251
|
+
alternate,
|
|
252
|
+
helperNames,
|
|
253
|
+
resultVars,
|
|
254
|
+
namespaceHelpers,
|
|
255
|
+
scopes,
|
|
256
|
+
scopedHelpers
|
|
257
|
+
)
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
if (isResultExpression(target)) {
|
|
184
261
|
return true;
|
|
185
262
|
}
|
|
186
263
|
if (
|
|
187
|
-
isHelperCall(
|
|
264
|
+
isHelperCall(target, helperNames, namespaceHelpers, scopes, scopedHelpers)
|
|
188
265
|
) {
|
|
189
266
|
return true;
|
|
190
267
|
}
|
|
191
268
|
|
|
192
|
-
const varName = resolveIdentifierName(
|
|
193
|
-
return
|
|
269
|
+
const varName = resolveIdentifierName(target);
|
|
270
|
+
return (
|
|
271
|
+
varName !== null &&
|
|
272
|
+
isScopedResultVariableBinding(varName, scopes, resultVars)
|
|
273
|
+
);
|
|
194
274
|
};
|
|
195
275
|
|
|
196
276
|
// ---------------------------------------------------------------------------
|
|
@@ -198,17 +278,13 @@ const isAllowedReturnArgument = (
|
|
|
198
278
|
// ---------------------------------------------------------------------------
|
|
199
279
|
|
|
200
280
|
const getImportSourceValue = (node: AstNode): string | null => {
|
|
201
|
-
const sourceNode = (node
|
|
202
|
-
const sourceValue = sourceNode
|
|
203
|
-
? (sourceNode as unknown as { value?: unknown }).value
|
|
204
|
-
: undefined;
|
|
281
|
+
const sourceNode = getNodeSource(node);
|
|
282
|
+
const sourceValue = sourceNode ? getNodeValue(sourceNode) : undefined;
|
|
205
283
|
return typeof sourceValue === 'string' ? sourceValue : null;
|
|
206
284
|
};
|
|
207
285
|
|
|
208
286
|
const extractIdentifierName = (node: AstNode | undefined): string | null =>
|
|
209
|
-
node?.type === 'Identifier'
|
|
210
|
-
? ((node as unknown as { name: string }).name ?? null)
|
|
211
|
-
: null;
|
|
287
|
+
node?.type === 'Identifier' ? (getNodeName(node) ?? null) : null;
|
|
212
288
|
|
|
213
289
|
const DEFAULT_RESULT_TYPE_NAMES = new Set(['Result']);
|
|
214
290
|
|
|
@@ -236,10 +312,8 @@ export const collectResultTypeNames = (ast: AstNode): ReadonlySet<string> => {
|
|
|
236
312
|
if (specifier.type !== 'ImportSpecifier') {
|
|
237
313
|
continue;
|
|
238
314
|
}
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
local?: AstNode;
|
|
242
|
-
};
|
|
315
|
+
const imported = getNodeImported(specifier);
|
|
316
|
+
const local = getNodeLocal(specifier);
|
|
243
317
|
if (extractIdentifierName(imported) !== 'Result') {
|
|
244
318
|
continue;
|
|
245
319
|
}
|
|
@@ -255,7 +329,7 @@ const hasResultReturnType = (
|
|
|
255
329
|
sourceCode: string,
|
|
256
330
|
resultTypeNames: ReadonlySet<string> = DEFAULT_RESULT_TYPE_NAMES
|
|
257
331
|
): boolean => {
|
|
258
|
-
const
|
|
332
|
+
const returnType = getNodeReturnType(node);
|
|
259
333
|
if (!returnType) {
|
|
260
334
|
return false;
|
|
261
335
|
}
|
|
@@ -284,6 +358,19 @@ const addScopedHelper = (
|
|
|
284
358
|
scopedHelpers.set(scope, new Set([name]));
|
|
285
359
|
};
|
|
286
360
|
|
|
361
|
+
const addScopedResultVariable = (
|
|
362
|
+
resultVars: MutableScopedResultVariableMap,
|
|
363
|
+
scope: ReadonlySet<string>,
|
|
364
|
+
name: string
|
|
365
|
+
): void => {
|
|
366
|
+
const existing = resultVars.get(scope);
|
|
367
|
+
if (existing) {
|
|
368
|
+
existing.add(name);
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
resultVars.set(scope, new Set([name]));
|
|
372
|
+
};
|
|
373
|
+
|
|
287
374
|
/** Record `const helper = (): Result<...> => ...` declarations for the current lexical scope. */
|
|
288
375
|
export const trackScopedResultHelperDeclaration = (
|
|
289
376
|
node: AstNode,
|
|
@@ -295,7 +382,8 @@ export const trackScopedResultHelperDeclaration = (
|
|
|
295
382
|
if (node.type !== 'VariableDeclarator') {
|
|
296
383
|
return;
|
|
297
384
|
}
|
|
298
|
-
const
|
|
385
|
+
const id = getNodeId(node);
|
|
386
|
+
const init = getNodeInit(node);
|
|
299
387
|
const name = extractIdentifierName(id);
|
|
300
388
|
if (!(name && init && isFunctionLikeExpression(init))) {
|
|
301
389
|
return;
|
|
@@ -313,24 +401,54 @@ export const trackScopedResultHelperDeclaration = (
|
|
|
313
401
|
// Variable tracking
|
|
314
402
|
// ---------------------------------------------------------------------------
|
|
315
403
|
|
|
404
|
+
const hasResultVariableAnnotation = (
|
|
405
|
+
node: AstNode,
|
|
406
|
+
sourceCode: string,
|
|
407
|
+
resultTypeNames: ReadonlySet<string>
|
|
408
|
+
): boolean => {
|
|
409
|
+
const typeAnnotation = getNodeTypeAnnotation(node);
|
|
410
|
+
if (!typeAnnotation) {
|
|
411
|
+
return false;
|
|
412
|
+
}
|
|
413
|
+
const annotationText = sourceCode.slice(
|
|
414
|
+
typeAnnotation.start,
|
|
415
|
+
typeAnnotation.end
|
|
416
|
+
);
|
|
417
|
+
for (const name of resultTypeNames) {
|
|
418
|
+
if (hasGenericTypeReference(annotationText, name)) {
|
|
419
|
+
return true;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
return false;
|
|
423
|
+
};
|
|
424
|
+
|
|
316
425
|
/** Track a VariableDeclarator, adding to resultVars if it produces a Result. */
|
|
317
426
|
const trackResultVariable = (
|
|
318
427
|
node: AstNode,
|
|
319
|
-
resultVars:
|
|
428
|
+
resultVars: MutableScopedResultVariableMap,
|
|
320
429
|
helperNames: ReadonlySet<string>,
|
|
321
430
|
namespaceHelpers: NamespaceHelperMap,
|
|
322
431
|
scopes: readonly ReadonlySet<string>[],
|
|
323
|
-
scopedHelpers: ScopedHelperMap
|
|
432
|
+
scopedHelpers: ScopedHelperMap,
|
|
433
|
+
sourceCode: string,
|
|
434
|
+
resultTypeNames: ReadonlySet<string>
|
|
324
435
|
): void => {
|
|
325
|
-
const
|
|
326
|
-
const
|
|
436
|
+
const init = getNodeInit(node);
|
|
437
|
+
const id = getNodeId(node);
|
|
327
438
|
if (init && id?.type === 'Identifier') {
|
|
328
|
-
const
|
|
439
|
+
const name = getNodeName(id);
|
|
440
|
+
if (!name) {
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
329
443
|
if (
|
|
444
|
+
hasResultVariableAnnotation(id, sourceCode, resultTypeNames) ||
|
|
330
445
|
isResultExpression(init) ||
|
|
331
446
|
isHelperCall(init, helperNames, namespaceHelpers, scopes, scopedHelpers)
|
|
332
447
|
) {
|
|
333
|
-
|
|
448
|
+
const bindingScope = findNearestBindingScope(name, scopes);
|
|
449
|
+
if (bindingScope) {
|
|
450
|
+
addScopedResultVariable(resultVars, bindingScope, name);
|
|
451
|
+
}
|
|
334
452
|
}
|
|
335
453
|
}
|
|
336
454
|
};
|
|
@@ -351,7 +469,7 @@ const checkReturnStatements = (
|
|
|
351
469
|
diagnostics: WardenDiagnostic[],
|
|
352
470
|
implScope: ReadonlySet<string> = new Set<string>()
|
|
353
471
|
): void => {
|
|
354
|
-
const resultVars = new
|
|
472
|
+
const resultVars: MutableScopedResultVariableMap = new Map();
|
|
355
473
|
const scopedHelpers: MutableScopedHelperMap = new Map();
|
|
356
474
|
const initialScopes = implScope.size > 0 ? [implScope] : [];
|
|
357
475
|
|
|
@@ -372,7 +490,9 @@ const checkReturnStatements = (
|
|
|
372
490
|
helperNames,
|
|
373
491
|
namespaceHelpers,
|
|
374
492
|
currentScopes,
|
|
375
|
-
scopedHelpers
|
|
493
|
+
scopedHelpers,
|
|
494
|
+
sourceCode,
|
|
495
|
+
resultTypeNames
|
|
376
496
|
);
|
|
377
497
|
}
|
|
378
498
|
|
|
@@ -380,7 +500,7 @@ const checkReturnStatements = (
|
|
|
380
500
|
return;
|
|
381
501
|
}
|
|
382
502
|
|
|
383
|
-
const
|
|
503
|
+
const argument = getNodeArgument(node);
|
|
384
504
|
// Bare return is not a value return.
|
|
385
505
|
if (!argument) {
|
|
386
506
|
return;
|
|
@@ -421,25 +541,31 @@ const collectResultHelperNames = (
|
|
|
421
541
|
|
|
422
542
|
walk(ast, (node) => {
|
|
423
543
|
if (node.type === 'VariableDeclarator') {
|
|
424
|
-
const
|
|
425
|
-
const
|
|
544
|
+
const id = getNodeId(node);
|
|
545
|
+
const init = getNodeInit(node);
|
|
426
546
|
if (
|
|
427
547
|
id?.type === 'Identifier' &&
|
|
428
548
|
init &&
|
|
429
549
|
isFunctionLikeExpression(init) &&
|
|
430
550
|
hasResultReturnType(init, sourceCode, resultTypeNames)
|
|
431
551
|
) {
|
|
432
|
-
|
|
552
|
+
const name = getNodeName(id);
|
|
553
|
+
if (name) {
|
|
554
|
+
names.add(name);
|
|
555
|
+
}
|
|
433
556
|
}
|
|
434
557
|
}
|
|
435
558
|
|
|
436
559
|
if (node.type === 'FunctionDeclaration') {
|
|
437
|
-
const
|
|
560
|
+
const id = getNodeId(node);
|
|
438
561
|
if (
|
|
439
562
|
id?.type === 'Identifier' &&
|
|
440
563
|
hasResultReturnType(node, sourceCode, resultTypeNames)
|
|
441
564
|
) {
|
|
442
|
-
|
|
565
|
+
const name = getNodeName(id);
|
|
566
|
+
if (name) {
|
|
567
|
+
names.add(name);
|
|
568
|
+
}
|
|
443
569
|
}
|
|
444
570
|
}
|
|
445
571
|
});
|
|
@@ -490,7 +616,7 @@ const buildDefaultImportBinding = (
|
|
|
490
616
|
specifier: AstNode,
|
|
491
617
|
source: string
|
|
492
618
|
): ImportBinding | null => {
|
|
493
|
-
const
|
|
619
|
+
const local = getNodeLocal(specifier);
|
|
494
620
|
const localName = extractIdentifierName(local);
|
|
495
621
|
if (!localName) {
|
|
496
622
|
return null;
|
|
@@ -502,10 +628,8 @@ const buildNamedImportBinding = (
|
|
|
502
628
|
specifier: AstNode,
|
|
503
629
|
source: string
|
|
504
630
|
): ImportBinding | null => {
|
|
505
|
-
const
|
|
506
|
-
|
|
507
|
-
imported?: AstNode;
|
|
508
|
-
};
|
|
631
|
+
const local = getNodeLocal(specifier);
|
|
632
|
+
const imported = getNodeImported(specifier);
|
|
509
633
|
const localName = extractIdentifierName(local);
|
|
510
634
|
const importedName = extractIdentifierName(imported) ?? localName;
|
|
511
635
|
if (!(localName && importedName)) {
|
|
@@ -551,7 +675,9 @@ const collectBindingsFromImportDeclaration = (
|
|
|
551
675
|
});
|
|
552
676
|
};
|
|
553
677
|
|
|
554
|
-
/** Collect `import {
|
|
678
|
+
/** Collect `import {
|
|
679
|
+
foo as bar
|
|
680
|
+
} from './...';` bindings keyed by local name. */
|
|
555
681
|
const collectResolvableImports = (ast: AstNode): readonly ImportBinding[] => {
|
|
556
682
|
const imports: ImportBinding[] = [];
|
|
557
683
|
walk(ast, (node) => {
|
|
@@ -609,7 +735,7 @@ const getExportedDeclaration = (node: AstNode): AstNode | null => {
|
|
|
609
735
|
if (node.type !== 'ExportNamedDeclaration') {
|
|
610
736
|
return null;
|
|
611
737
|
}
|
|
612
|
-
const decl = (node
|
|
738
|
+
const decl = getNodeDeclaration(node);
|
|
613
739
|
return decl ?? null;
|
|
614
740
|
};
|
|
615
741
|
|
|
@@ -622,10 +748,8 @@ const addExportedVariableResultHelper = (
|
|
|
622
748
|
const declarations =
|
|
623
749
|
(decl['declarations'] as readonly AstNode[] | undefined) ?? [];
|
|
624
750
|
for (const declarator of declarations) {
|
|
625
|
-
const
|
|
626
|
-
|
|
627
|
-
init?: AstNode;
|
|
628
|
-
};
|
|
751
|
+
const id = getNodeId(declarator);
|
|
752
|
+
const init = getNodeInit(declarator);
|
|
629
753
|
const name = extractIdentifierName(id);
|
|
630
754
|
if (
|
|
631
755
|
name &&
|
|
@@ -644,7 +768,7 @@ const addExportedFunctionResultHelper = (
|
|
|
644
768
|
collected: Set<string>,
|
|
645
769
|
resultTypeNames: ReadonlySet<string>
|
|
646
770
|
): void => {
|
|
647
|
-
const name = extractIdentifierName((decl
|
|
771
|
+
const name = extractIdentifierName(getNodeId(decl));
|
|
648
772
|
if (name && hasResultReturnType(decl, source, resultTypeNames)) {
|
|
649
773
|
collected.add(name);
|
|
650
774
|
}
|
|
@@ -671,10 +795,8 @@ const indexVariableDeclarationInto = (
|
|
|
671
795
|
const declarators =
|
|
672
796
|
(decl['declarations'] as readonly AstNode[] | undefined) ?? [];
|
|
673
797
|
for (const declarator of declarators) {
|
|
674
|
-
const
|
|
675
|
-
|
|
676
|
-
init?: AstNode;
|
|
677
|
-
};
|
|
798
|
+
const id = getNodeId(declarator);
|
|
799
|
+
const init = getNodeInit(declarator);
|
|
678
800
|
const name = extractIdentifierName(id);
|
|
679
801
|
if (name && init && isFunctionLikeExpression(init)) {
|
|
680
802
|
index.set(name, init);
|
|
@@ -686,7 +808,7 @@ const indexFunctionDeclarationInto = (
|
|
|
686
808
|
decl: AstNode,
|
|
687
809
|
index: Map<string, AstNode>
|
|
688
810
|
): void => {
|
|
689
|
-
const name = extractIdentifierName((decl
|
|
811
|
+
const name = extractIdentifierName(getNodeId(decl));
|
|
690
812
|
if (name) {
|
|
691
813
|
index.set(name, decl);
|
|
692
814
|
}
|
|
@@ -719,8 +841,7 @@ const indexBodyNodeInto = (
|
|
|
719
841
|
|
|
720
842
|
const indexLocalDeclarations = (ast: AstNode): DeclarationIndex => {
|
|
721
843
|
const index = new Map<string, AstNode>();
|
|
722
|
-
const
|
|
723
|
-
const bodyNodes = program.body ?? [];
|
|
844
|
+
const bodyNodes = getNodeBodyStatements(ast);
|
|
724
845
|
for (const node of bodyNodes) {
|
|
725
846
|
indexBodyNodeInto(node, index);
|
|
726
847
|
}
|
|
@@ -749,10 +870,10 @@ const getSpecifierNameNode = (
|
|
|
749
870
|
return null;
|
|
750
871
|
}
|
|
751
872
|
if (node.type === 'Identifier') {
|
|
752
|
-
return (node
|
|
873
|
+
return getNodeName(node) ?? null;
|
|
753
874
|
}
|
|
754
875
|
// Support string-literal specifiers (`export { "default" as X }`, etc).
|
|
755
|
-
const
|
|
876
|
+
const value = getNodeValue(node);
|
|
756
877
|
return typeof value === 'string' ? value : null;
|
|
757
878
|
};
|
|
758
879
|
|
|
@@ -775,11 +896,10 @@ const buildExportSpecifierInfo = (
|
|
|
775
896
|
};
|
|
776
897
|
|
|
777
898
|
const getExportDefaultDeclaration = (ast: AstNode): AstNode | null => {
|
|
778
|
-
const
|
|
779
|
-
const bodyNodes = program.body ?? [];
|
|
899
|
+
const bodyNodes = getNodeBodyStatements(ast);
|
|
780
900
|
for (const node of bodyNodes) {
|
|
781
901
|
if (node.type === 'ExportDefaultDeclaration') {
|
|
782
|
-
const decl = (node
|
|
902
|
+
const decl = getNodeDeclaration(node);
|
|
783
903
|
return decl ?? null;
|
|
784
904
|
}
|
|
785
905
|
}
|
|
@@ -1082,8 +1202,7 @@ const processExportDefaultDeclaration = (
|
|
|
1082
1202
|
collected: Set<string>,
|
|
1083
1203
|
resultTypeNames: ReadonlySet<string>
|
|
1084
1204
|
): void => {
|
|
1085
|
-
const defaultDecl = (node
|
|
1086
|
-
.declaration;
|
|
1205
|
+
const defaultDecl = getNodeDeclaration(node);
|
|
1087
1206
|
if (!defaultDecl) {
|
|
1088
1207
|
return;
|
|
1089
1208
|
}
|
|
@@ -1115,8 +1234,7 @@ const collectExportedResultHelpersFromAst = (
|
|
|
1115
1234
|
preloadedLocalDeclarations ?? indexLocalDeclarations(ast);
|
|
1116
1235
|
const resultTypeNames =
|
|
1117
1236
|
preloadedResultTypeNames ?? collectResultTypeNames(ast);
|
|
1118
|
-
const
|
|
1119
|
-
const bodyNodes = program.body ?? [];
|
|
1237
|
+
const bodyNodes = getNodeBodyStatements(ast);
|
|
1120
1238
|
|
|
1121
1239
|
for (const node of bodyNodes) {
|
|
1122
1240
|
if (node.type === 'ExportNamedDeclaration') {
|
|
@@ -1161,7 +1279,7 @@ const processExportAllDeclaration = (
|
|
|
1161
1279
|
depth: number,
|
|
1162
1280
|
collected: Set<string>
|
|
1163
1281
|
): void => {
|
|
1164
|
-
const
|
|
1282
|
+
const exportKind = getNodeExportKind(node);
|
|
1165
1283
|
if (exportKind === 'type') {
|
|
1166
1284
|
return;
|
|
1167
1285
|
}
|
|
@@ -1309,7 +1427,7 @@ const getNamespaceLocalName = (spec: AstNode): string | null => {
|
|
|
1309
1427
|
if (spec.type !== 'ImportNamespaceSpecifier') {
|
|
1310
1428
|
return null;
|
|
1311
1429
|
}
|
|
1312
|
-
const
|
|
1430
|
+
const local = getNodeLocal(spec);
|
|
1313
1431
|
return extractIdentifierName(local);
|
|
1314
1432
|
};
|
|
1315
1433
|
|
|
@@ -1417,7 +1535,7 @@ const checkImplementation = (
|
|
|
1417
1535
|
resultTypeNames: ReadonlySet<string>,
|
|
1418
1536
|
diagnostics: WardenDiagnostic[]
|
|
1419
1537
|
): void => {
|
|
1420
|
-
const fnBody = (implValue
|
|
1538
|
+
const fnBody = getNodeBodyNode(implValue);
|
|
1421
1539
|
if (!fnBody) {
|
|
1422
1540
|
return;
|
|
1423
1541
|
}
|
|
@@ -1443,10 +1561,24 @@ const checkImplementation = (
|
|
|
1443
1561
|
|
|
1444
1562
|
const conciseScopes: readonly ReadonlySet<string>[] =
|
|
1445
1563
|
implScope.size > 0 ? [implScope] : [];
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1564
|
+
const isConciseResultBody = (node: AstNode): boolean => {
|
|
1565
|
+
const target = unwrapReturnExpression(node);
|
|
1566
|
+
if (target.type === 'ConditionalExpression') {
|
|
1567
|
+
const alternate = getNodeAlternate(target);
|
|
1568
|
+
const consequent = getNodeConsequent(target);
|
|
1569
|
+
return (
|
|
1570
|
+
consequent !== undefined &&
|
|
1571
|
+
alternate !== undefined &&
|
|
1572
|
+
isConciseResultBody(consequent) &&
|
|
1573
|
+
isConciseResultBody(alternate)
|
|
1574
|
+
);
|
|
1575
|
+
}
|
|
1576
|
+
return (
|
|
1577
|
+
isResultExpression(target) ||
|
|
1578
|
+
isHelperCall(target, helperNames, namespaceHelpers, conciseScopes)
|
|
1579
|
+
);
|
|
1580
|
+
};
|
|
1581
|
+
if (!isConciseResultBody(fnBody)) {
|
|
1450
1582
|
diagnostics.push({
|
|
1451
1583
|
filePath,
|
|
1452
1584
|
line: offsetToLine(sourceCode, implValue.start),
|
|
@@ -4,6 +4,14 @@ import {
|
|
|
4
4
|
collectNamedContourIds,
|
|
5
5
|
collectNamedStoreTableIds,
|
|
6
6
|
deriveStoreTableId,
|
|
7
|
+
getNodeArguments,
|
|
8
|
+
getNodeElements,
|
|
9
|
+
getNodeId,
|
|
10
|
+
getNodeImported,
|
|
11
|
+
getNodeInit,
|
|
12
|
+
getNodeLocal,
|
|
13
|
+
getNodeSource,
|
|
14
|
+
getNodeSpecifiers,
|
|
7
15
|
getStringValue,
|
|
8
16
|
identifierName,
|
|
9
17
|
isNamedCall,
|
|
@@ -39,7 +47,7 @@ interface ImportAliasResolution {
|
|
|
39
47
|
}
|
|
40
48
|
|
|
41
49
|
const getImportSource = (node: AstNode): string | null => {
|
|
42
|
-
const sourceNode = (node
|
|
50
|
+
const sourceNode = getNodeSource(node);
|
|
43
51
|
return sourceNode && isStringLiteral(sourceNode)
|
|
44
52
|
? getStringValue(sourceNode)
|
|
45
53
|
: null;
|
|
@@ -56,8 +64,8 @@ const extractImportAliasResolution = (
|
|
|
56
64
|
return null;
|
|
57
65
|
}
|
|
58
66
|
|
|
59
|
-
const
|
|
60
|
-
const
|
|
67
|
+
const imported = getNodeImported(specifier);
|
|
68
|
+
const local = getNodeLocal(specifier);
|
|
61
69
|
const localName = identifierName(local);
|
|
62
70
|
if (!localName) {
|
|
63
71
|
return null;
|
|
@@ -85,8 +93,7 @@ const collectImportDeclarationAliases = (
|
|
|
85
93
|
return;
|
|
86
94
|
}
|
|
87
95
|
|
|
88
|
-
const specifiers = (
|
|
89
|
-
.specifiers ?? []) as readonly AstNode[];
|
|
96
|
+
const specifiers = getNodeSpecifiers(node) as readonly AstNode[];
|
|
90
97
|
for (const specifier of specifiers) {
|
|
91
98
|
const alias = extractImportAliasResolution(specifier, source);
|
|
92
99
|
if (!alias) {
|
|
@@ -101,8 +108,7 @@ const extractInlineContourId = (node: AstNode | undefined): string | null => {
|
|
|
101
108
|
return null;
|
|
102
109
|
}
|
|
103
110
|
|
|
104
|
-
const [nameArg] = (
|
|
105
|
-
.arguments ?? []) as readonly AstNode[];
|
|
111
|
+
const [nameArg] = getNodeArguments(node) as readonly AstNode[];
|
|
106
112
|
return nameArg && isStringLiteral(nameArg) ? getStringValue(nameArg) : null;
|
|
107
113
|
};
|
|
108
114
|
|
|
@@ -278,11 +284,7 @@ const extractDerivedCrudEntry = (
|
|
|
278
284
|
return null;
|
|
279
285
|
}
|
|
280
286
|
|
|
281
|
-
const [contourArg, operationArg] = (
|
|
282
|
-
node as unknown as {
|
|
283
|
-
arguments?: readonly AstNode[];
|
|
284
|
-
}
|
|
285
|
-
).arguments ?? []) as readonly AstNode[];
|
|
287
|
+
const [contourArg, operationArg] = getNodeArguments(node);
|
|
286
288
|
const operation = extractCrudOperation(operationArg);
|
|
287
289
|
const entityId = resolveContourId(contourArg, namedContourIds, importAliases);
|
|
288
290
|
return operation && entityId ? { entityId, operation } : null;
|
|
@@ -334,18 +336,17 @@ const extractCrudTuplePattern = (
|
|
|
334
336
|
return null;
|
|
335
337
|
}
|
|
336
338
|
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
readonly init?: AstNode;
|
|
340
|
-
};
|
|
339
|
+
const id = getNodeId(node);
|
|
340
|
+
const init = getNodeInit(node);
|
|
341
341
|
if (!id || id.type !== 'ArrayPattern' || !isNamedCall(init, 'crud')) {
|
|
342
342
|
return null;
|
|
343
343
|
}
|
|
344
344
|
|
|
345
|
-
const [tableArg] = (
|
|
346
|
-
.arguments ?? []) as readonly AstNode[];
|
|
345
|
+
const [tableArg] = getNodeArguments(init) as readonly AstNode[];
|
|
347
346
|
const entityId = deriveStoreTableId(tableArg, namedStoreTableIds);
|
|
348
|
-
const
|
|
347
|
+
const elements = getNodeElements(id).filter(
|
|
348
|
+
(element): element is AstNode => element !== null
|
|
349
|
+
);
|
|
349
350
|
return entityId && elements ? { elements, entityId } : null;
|
|
350
351
|
};
|
|
351
352
|
|