@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.
Files changed (61) hide show
  1. package/CHANGELOG.md +113 -0
  2. package/README.md +35 -1
  3. package/package.json +10 -9
  4. package/src/ast.ts +109 -0
  5. package/src/cli.ts +44 -4
  6. package/src/command.ts +24 -34
  7. package/src/drift.ts +24 -8
  8. package/src/index.ts +9 -0
  9. package/src/project-rules.ts +290 -0
  10. package/src/resolve.ts +8 -7
  11. package/src/rules/ast.ts +837 -6
  12. package/src/rules/cli-command-route-coherence.ts +177 -0
  13. package/src/rules/composes-declarations.ts +210 -77
  14. package/src/rules/context-no-surface-types.ts +15 -12
  15. package/src/rules/contour-exists.ts +7 -7
  16. package/src/rules/dead-internal-trail.ts +11 -4
  17. package/src/rules/dead-public-trail.ts +258 -0
  18. package/src/rules/duplicate-public-contract.ts +91 -0
  19. package/src/rules/error-mapping-completeness.ts +5 -3
  20. package/src/rules/example-valid.ts +6 -12
  21. package/src/rules/fires-declarations.ts +41 -64
  22. package/src/rules/implementation-returns-result.ts +208 -76
  23. package/src/rules/incomplete-crud.ts +20 -19
  24. package/src/rules/index.ts +15 -0
  25. package/src/rules/layer-field-name-drift.ts +12 -6
  26. package/src/rules/library-projection-coherence.ts +100 -0
  27. package/src/rules/metadata.ts +102 -1
  28. package/src/rules/no-destructured-compose.ts +16 -14
  29. package/src/rules/no-native-error-result.ts +15 -8
  30. package/src/rules/no-redundant-result-error-wrap.ts +82 -29
  31. package/src/rules/no-sync-result-assumption.ts +70 -68
  32. package/src/rules/no-top-level-surface.ts +46 -64
  33. package/src/rules/on-references-exist.ts +1 -1
  34. package/src/rules/owner-projection-parity.ts +10 -13
  35. package/src/rules/public-export-example-coverage.ts +29 -20
  36. package/src/rules/public-internal-deep-imports.ts +2 -2
  37. package/src/rules/read-intent-fires.ts +8 -8
  38. package/src/rules/registry-names.ts +10 -0
  39. package/src/rules/resource-declarations.ts +20 -31
  40. package/src/rules/resource-exists.ts +2 -2
  41. package/src/rules/resource-id-grammar.ts +2 -2
  42. package/src/rules/resource-mock-coverage.ts +2 -2
  43. package/src/rules/static-resource-accessor-preference.ts +26 -34
  44. package/src/rules/surface-facet-coherence.ts +21 -29
  45. package/src/rules/trail-fork-coaching.ts +616 -0
  46. package/src/rules/trail-versioning-source.ts +56 -78
  47. package/src/rules/types.ts +2 -0
  48. package/src/rules/unreachable-detour-shadowing.ts +16 -21
  49. package/src/rules/valid-describe-refs.ts +14 -12
  50. package/src/rules/warden-export-symmetry.ts +42 -35
  51. package/src/rules/warden-rules-use-ast.ts +168 -50
  52. package/src/trails/cli-command-route-coherence.trail.ts +47 -0
  53. package/src/trails/dead-public-trail.trail.ts +31 -0
  54. package/src/trails/duplicate-public-contract.trail.ts +47 -0
  55. package/src/trails/error-mapping-completeness.trail.ts +1 -0
  56. package/src/trails/index.ts +5 -0
  57. package/src/trails/library-projection-coherence.trail.ts +43 -0
  58. package/src/trails/schema.ts +4 -0
  59. package/src/trails/trail-fork-coaching.trail.ts +42 -0
  60. package/src/trails/warden-rules-use-ast.trail.ts +19 -0
  61. 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 as unknown as { argument?: AstNode }).argument;
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 { name } = callee as unknown as { name: string };
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 as unknown as { name: string }).name;
197
+ return getNodeName(node) ?? null;
164
198
  }
165
199
  if (node.type === 'AwaitExpression') {
166
- const inner = (node as unknown as { argument?: AstNode }).argument;
200
+ const inner = getNodeArgument(node);
167
201
  if (inner?.type === 'Identifier') {
168
- return (inner as unknown as { name: string }).name;
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: ReadonlySet<string>,
230
+ resultVars: ScopedResultVariableMap,
179
231
  namespaceHelpers: NamespaceHelperMap,
180
232
  scopes: readonly ReadonlySet<string>[] = [],
181
233
  scopedHelpers: ScopedHelperMap = new Map()
182
234
  ): boolean => {
183
- if (isResultExpression(argument)) {
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(argument, helperNames, namespaceHelpers, scopes, scopedHelpers)
264
+ isHelperCall(target, helperNames, namespaceHelpers, scopes, scopedHelpers)
188
265
  ) {
189
266
  return true;
190
267
  }
191
268
 
192
- const varName = resolveIdentifierName(argument);
193
- return varName !== null && resultVars.has(varName);
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 as unknown as { source?: AstNode }).source;
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 { imported, local } = specifier as unknown as {
240
- imported?: AstNode;
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 { returnType } = node as unknown as { returnType?: AstNode };
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 { id, init } = node as unknown as { id?: AstNode; init?: AstNode };
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: Set<string>,
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 { init } = node as unknown as { init?: AstNode };
326
- const { id } = node as unknown as { id?: AstNode };
436
+ const init = getNodeInit(node);
437
+ const id = getNodeId(node);
327
438
  if (init && id?.type === 'Identifier') {
328
- const { name } = id as unknown as { name: string };
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
- resultVars.add(name);
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 Set<string>();
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 { argument } = node as unknown as { argument?: AstNode };
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 { id } = node as unknown as { id?: AstNode };
425
- const { init } = node as unknown as { init?: AstNode };
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
- names.add((id as unknown as { name: string }).name);
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 { id } = node as unknown as { id?: AstNode };
560
+ const id = getNodeId(node);
438
561
  if (
439
562
  id?.type === 'Identifier' &&
440
563
  hasResultReturnType(node, sourceCode, resultTypeNames)
441
564
  ) {
442
- names.add((id as unknown as { name: string }).name);
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 { local } = specifier as unknown as { local?: AstNode };
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 { local, imported } = specifier as unknown as {
506
- local?: AstNode;
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 { foo as bar } from './...'` bindings keyed by local name. */
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 as unknown as { declaration?: AstNode }).declaration;
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 { id, init } = declarator as unknown as {
626
- id?: AstNode;
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 as unknown as { id?: AstNode }).id);
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 { id, init } = declarator as unknown as {
675
- id?: AstNode;
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 as unknown as { id?: AstNode }).id);
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 program = ast as unknown as { body?: readonly AstNode[] };
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 as unknown as { name?: string }).name ?? null;
873
+ return getNodeName(node) ?? null;
753
874
  }
754
875
  // Support string-literal specifiers (`export { "default" as X }`, etc).
755
- const { value } = node as unknown as { value?: unknown };
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 program = ast as unknown as { body?: readonly AstNode[] };
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 as unknown as { declaration?: AstNode }).declaration;
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 as unknown as { declaration?: AstNode })
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 program = ast as unknown as { body?: readonly AstNode[] };
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 { exportKind } = node as unknown as { exportKind?: string };
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 { local } = spec as unknown as { local?: AstNode };
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 as unknown as { body?: AstNode }).body;
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
- if (
1447
- !isResultExpression(fnBody) &&
1448
- !isHelperCall(fnBody, helperNames, namespaceHelpers, conciseScopes)
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 as unknown as { source?: AstNode }).source;
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 { imported } = specifier as unknown as { imported?: AstNode };
60
- const { local } = specifier as unknown as { local?: AstNode };
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 = ((node as unknown as { specifiers?: readonly AstNode[] })
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] = ((node as unknown as { arguments?: readonly AstNode[] })
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 { id, init } = node as unknown as {
338
- readonly id?: AstNode;
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] = ((init as unknown as { arguments?: readonly AstNode[] })
346
- .arguments ?? []) as readonly AstNode[];
345
+ const [tableArg] = getNodeArguments(init) as readonly AstNode[];
347
346
  const entityId = deriveStoreTableId(tableArg, namedStoreTableIds);
348
- const { elements } = id as unknown as { elements?: readonly AstNode[] };
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