@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
@@ -3,10 +3,21 @@ import {
3
3
  findBlazeBodies,
4
4
  findTrailDefinitions,
5
5
  getMemberExpression,
6
+ getNodeArguments,
7
+ getNodeArgument,
8
+ getNodeBodyNode,
9
+ getNodeCallee,
10
+ getNodeId,
11
+ getNodeInit,
12
+ getNodeLeft,
13
+ getNodeOperator,
14
+ getNodeProperty,
15
+ getNodeRight,
6
16
  identifierName,
7
17
  isMemberAccessNonComputed,
8
18
  offsetToLine,
9
19
  parse,
20
+ walkWithParents,
10
21
  walkWithScopes,
11
22
  } from './ast.js';
12
23
  import {
@@ -19,7 +30,7 @@ import {
19
30
  trackScopedResultHelperDeclaration,
20
31
  } from './implementation-returns-result.js';
21
32
  import { isTestFile } from './scan.js';
22
- import type { AstNode } from './ast.js';
33
+ import type { AstNode, AstParentContext } from './ast.js';
23
34
  import type {
24
35
  MutableScopedHelperMap,
25
36
  NamespaceHelperMap,
@@ -33,7 +44,7 @@ const getStaticMemberName = (node: AstNode | undefined): string | null => {
33
44
  if (!node || !isMemberAccessNonComputed(node)) {
34
45
  return null;
35
46
  }
36
- return identifierName((node as unknown as { property?: AstNode }).property);
47
+ return identifierName(getNodeProperty(node));
37
48
  };
38
49
 
39
50
  const isResultObject = (node: AstNode | undefined): boolean => {
@@ -50,7 +61,7 @@ const isResultErrCall = (node: AstNode): boolean => {
50
61
  if (node.type !== 'CallExpression') {
51
62
  return false;
52
63
  }
53
- const { callee } = node as unknown as { callee?: AstNode };
64
+ const callee = getNodeCallee(node);
54
65
  const member = getMemberExpression(callee);
55
66
  if (!member || getStaticMemberName(callee) !== 'err') {
56
67
  return false;
@@ -59,8 +70,7 @@ const isResultErrCall = (node: AstNode): boolean => {
59
70
  };
60
71
 
61
72
  const getSingleArgument = (node: AstNode): AstNode | null => {
62
- const args = (node as unknown as { arguments?: readonly AstNode[] })
63
- .arguments;
73
+ const args = getNodeArguments(node);
64
74
  return args?.length === 1 ? (args[0] ?? null) : null;
65
75
  };
66
76
 
@@ -115,12 +125,12 @@ const createDiagnostic = (
115
125
  filePath: string,
116
126
  sourceCode: string,
117
127
  node: AstNode,
118
- trailId: string,
128
+ ownerLabel: string,
119
129
  variableName: string
120
130
  ): WardenDiagnostic => ({
121
131
  filePath,
122
132
  line: offsetToLine(sourceCode, node.start),
123
- message: `Trail "${trailId}": Result.err(${variableName}.error) re-wraps a Result that already carries that error. Return ${variableName} directly to preserve the original Result boundary.`,
133
+ message: `${ownerLabel}: Result.err(${variableName}.error) re-wraps a Result that already carries that error. Return ${variableName} directly to preserve the original Result boundary.`,
124
134
  rule: RULE_NAME,
125
135
  severity: 'warn',
126
136
  });
@@ -133,7 +143,8 @@ const trackVariableDeclarator = (
133
143
  scopedHelpers: ScopedHelperMap,
134
144
  scopes: readonly ReadonlySet<string>[]
135
145
  ): void => {
136
- const { id, init } = node as unknown as { id?: AstNode; init?: AstNode };
146
+ const id = getNodeId(node);
147
+ const init = getNodeInit(node);
137
148
  const name = identifierName(id);
138
149
  if (!name) {
139
150
  return;
@@ -166,11 +177,9 @@ const trackAssignmentExpression = (
166
177
  scopedHelpers: ScopedHelperMap,
167
178
  scopes: readonly ReadonlySet<string>[]
168
179
  ): void => {
169
- const { left, operator, right } = node as unknown as {
170
- left?: AstNode;
171
- operator?: string;
172
- right?: AstNode;
173
- };
180
+ const left = getNodeLeft(node);
181
+ const operator = getNodeOperator(node);
182
+ const right = getNodeRight(node);
174
183
  const name = identifierName(left);
175
184
  if (!name) {
176
185
  return;
@@ -202,10 +211,10 @@ const checkReturnStatement = (
202
211
  scopes: readonly ReadonlySet<string>[],
203
212
  filePath: string,
204
213
  sourceCode: string,
205
- trailId: string,
214
+ ownerLabel: string,
206
215
  diagnostics: WardenDiagnostic[]
207
216
  ): void => {
208
- const { argument } = node as unknown as { argument?: AstNode };
217
+ const argument = getNodeArgument(node);
209
218
  if (!argument || !isResultErrCall(argument)) {
210
219
  return;
211
220
  }
@@ -218,13 +227,21 @@ const checkReturnStatement = (
218
227
  return;
219
228
  }
220
229
  diagnostics.push(
221
- createDiagnostic(filePath, sourceCode, argument, trailId, variableName)
230
+ createDiagnostic(filePath, sourceCode, argument, ownerLabel, variableName)
222
231
  );
223
232
  };
224
233
 
225
- const checkBlazeBody = (
226
- blaze: AstNode,
227
- trailId: string,
234
+ const functionBody = (node: AstNode): AstNode | null => {
235
+ const body = getNodeBodyNode(node);
236
+ return body &&
237
+ (body.type === 'BlockStatement' || body.type === 'FunctionBody')
238
+ ? body
239
+ : null;
240
+ };
241
+
242
+ const checkFunctionBody = (
243
+ owner: AstNode,
244
+ ownerLabel: string,
228
245
  filePath: string,
229
246
  sourceCode: string,
230
247
  helperNames: ReadonlySet<string>,
@@ -232,17 +249,14 @@ const checkBlazeBody = (
232
249
  resultTypeNames: ReadonlySet<string>,
233
250
  diagnostics: WardenDiagnostic[]
234
251
  ): void => {
235
- const { body } = blaze as unknown as { body?: AstNode };
236
- if (
237
- !body ||
238
- (body.type !== 'BlockStatement' && body.type !== 'FunctionBody')
239
- ) {
252
+ const body = functionBody(owner);
253
+ if (!body) {
240
254
  return;
241
255
  }
242
256
 
243
257
  const provenance: ResultProvenance = new Map();
244
258
  const scopedHelpers: MutableScopedHelperMap = new Map();
245
- const implScope = collectScopeFrameBindings(blaze);
259
+ const implScope = collectScopeFrameBindings(owner);
246
260
  const initialScopes = implScope.size > 0 ? [implScope] : [];
247
261
 
248
262
  walkWithScopes(
@@ -284,7 +298,7 @@ const checkBlazeBody = (
284
298
  scopes,
285
299
  filePath,
286
300
  sourceCode,
287
- trailId,
301
+ ownerLabel,
288
302
  diagnostics
289
303
  );
290
304
  }
@@ -293,6 +307,28 @@ const checkBlazeBody = (
293
307
  );
294
308
  };
295
309
 
310
+ const isFunctionLike = (node: AstNode): boolean =>
311
+ node.type === 'FunctionDeclaration' ||
312
+ node.type === 'FunctionExpression' ||
313
+ node.type === 'ArrowFunctionExpression';
314
+
315
+ const functionName = (node: AstNode, context: AstParentContext): string => {
316
+ const declaredName = identifierName(getNodeId(node));
317
+ if (declaredName) {
318
+ return declaredName;
319
+ }
320
+ if (context.parent?.type === 'VariableDeclarator') {
321
+ const assignedName = identifierName(getNodeId(context.parent));
322
+ if (assignedName) {
323
+ return assignedName;
324
+ }
325
+ }
326
+ return 'anonymous function';
327
+ };
328
+
329
+ const functionOwnerLabel = (node: AstNode, context: AstParentContext): string =>
330
+ `Function "${functionName(node, context)}"`;
331
+
296
332
  export const noRedundantResultErrorWrap: WardenRule = {
297
333
  check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
298
334
  if (isTestFile(filePath)) {
@@ -308,11 +344,13 @@ export const noRedundantResultErrorWrap: WardenRule = {
308
344
  const helperNames = collectAllResultHelperNames(ast, sourceCode, filePath);
309
345
  const namespaceHelpers = collectNamespaceHelperImports(ast, filePath);
310
346
  const resultTypeNames = collectResultTypeNames(ast);
347
+ const blazeStarts = new Set<number>();
311
348
  for (const def of findTrailDefinitions(ast)) {
312
349
  for (const blaze of findBlazeBodies(def.config)) {
313
- checkBlazeBody(
350
+ blazeStarts.add(blaze.start);
351
+ checkFunctionBody(
314
352
  blaze,
315
- def.id,
353
+ `Trail "${def.id}"`,
316
354
  filePath,
317
355
  sourceCode,
318
356
  helperNames,
@@ -322,10 +360,25 @@ export const noRedundantResultErrorWrap: WardenRule = {
322
360
  );
323
361
  }
324
362
  }
363
+ walkWithParents(ast, (node, context) => {
364
+ if (!isFunctionLike(node) || blazeStarts.has(node.start)) {
365
+ return;
366
+ }
367
+ checkFunctionBody(
368
+ node,
369
+ functionOwnerLabel(node, context),
370
+ filePath,
371
+ sourceCode,
372
+ helperNames,
373
+ namespaceHelpers,
374
+ resultTypeNames,
375
+ diagnostics
376
+ );
377
+ });
325
378
  return diagnostics;
326
379
  },
327
380
  description:
328
- 'Warn when blazes re-wrap an existing Result error with Result.err(x.error) instead of returning the Result directly.',
381
+ 'Warn when code re-wraps an existing Result error with Result.err(x.error) instead of returning the Result directly.',
329
382
  name: RULE_NAME,
330
383
  severity: 'warn',
331
384
  };
@@ -1,6 +1,33 @@
1
1
  import { resultAccessorNames } from '@ontrails/core';
2
2
 
3
- import { identifierName, isBlazeCall, offsetToLine, parse } from './ast.js';
3
+ import {
4
+ getNodeArgument,
5
+ getNodeAlternate,
6
+ getNodeBodyNode,
7
+ getNodeBodyStatements,
8
+ getNodeConsequent,
9
+ getNodeDeclarations,
10
+ getNodeElements,
11
+ getNodeExpression,
12
+ getNodeId,
13
+ getNodeInit,
14
+ getNodeKey,
15
+ getNodeKind,
16
+ getNodeLeft,
17
+ getNodeName,
18
+ getNodeObject,
19
+ getNodeOperator,
20
+ getNodeParam,
21
+ getNodeParams,
22
+ getNodeProperties,
23
+ getNodeProperty,
24
+ getNodeRight,
25
+ getNodeValueNode,
26
+ identifierName,
27
+ isBlazeCall,
28
+ offsetToLine,
29
+ parse,
30
+ } from './ast.js';
4
31
  import type { AstNode } from './ast.js';
5
32
  import { isFrameworkInternalFile, isTestFile } from './scan.js';
6
33
  import type { WardenDiagnostic, WardenRule } from './types.js';
@@ -104,11 +131,9 @@ const isBranchOfConditional = (outer: AstNode, parent: AstNode): boolean => {
104
131
  if (parent.type !== 'ConditionalExpression') {
105
132
  return false;
106
133
  }
107
- const cond = parent as unknown as {
108
- consequent?: AstNode;
109
- alternate?: AstNode;
110
- };
111
- return cond.consequent === outer || cond.alternate === outer;
134
+ return (
135
+ getNodeConsequent(parent) === outer || getNodeAlternate(parent) === outer
136
+ );
112
137
  };
113
138
 
114
139
  /**
@@ -121,8 +146,7 @@ const isOperandOfLogical = (outer: AstNode, parent: AstNode): boolean => {
121
146
  if (parent.type !== 'LogicalExpression') {
122
147
  return false;
123
148
  }
124
- const logical = parent as unknown as { left?: AstNode; right?: AstNode };
125
- return logical.left === outer || logical.right === outer;
149
+ return getNodeLeft(parent) === outer || getNodeRight(parent) === outer;
126
150
  };
127
151
 
128
152
  const skipParensAndBranchConditionals = (
@@ -165,11 +189,11 @@ const memberPropertyName = (node: AstNode): string | null => {
165
189
  ) {
166
190
  return null;
167
191
  }
168
- const prop = (node as unknown as { property?: AstNode }).property;
192
+ const prop = getNodeProperty(node);
169
193
  if (prop?.type !== 'Identifier') {
170
194
  return null;
171
195
  }
172
- return (prop as unknown as { name?: string }).name ?? null;
196
+ return getNodeName(prop) ?? null;
173
197
  };
174
198
 
175
199
  /**
@@ -208,7 +232,7 @@ const extractAssignedBinding = (
208
232
  if (!parent || parent.type !== 'VariableDeclarator') {
209
233
  return null;
210
234
  }
211
- const { id } = parent as unknown as { id?: AstNode };
235
+ const id = getNodeId(parent);
212
236
  return identifierName(id);
213
237
  };
214
238
 
@@ -231,7 +255,7 @@ const isResultAccessorMember = (node: AstNode): boolean => {
231
255
  };
232
256
 
233
257
  const getIdentifierObjectName = (node: AstNode): string | null => {
234
- const { object } = node as unknown as { object?: AstNode };
258
+ const object = getNodeObject(node);
235
259
  return object?.type === 'Identifier' ? identifierName(object) : null;
236
260
  };
237
261
 
@@ -250,7 +274,7 @@ const collectAssignmentPatternBindings = (
250
274
  pattern: AstNode,
251
275
  out: Set<string>
252
276
  ): void => {
253
- const { left } = pattern as unknown as { left?: AstNode };
277
+ const left = getNodeLeft(pattern);
254
278
  // eslint-disable-next-line no-use-before-define
255
279
  collectPatternBindings(left, out);
256
280
  };
@@ -259,7 +283,7 @@ const collectRestElementBindings = (
259
283
  pattern: AstNode,
260
284
  out: Set<string>
261
285
  ): void => {
262
- const { argument } = pattern as unknown as { argument?: AstNode };
286
+ const argument = getNodeArgument(pattern);
263
287
  // eslint-disable-next-line no-use-before-define
264
288
  collectPatternBindings(argument, out);
265
289
  };
@@ -303,9 +327,7 @@ const collectArrayPatternBindings = (
303
327
  pattern: AstNode,
304
328
  out: Set<string>
305
329
  ): void => {
306
- const { elements } = pattern as unknown as {
307
- elements?: readonly (AstNode | null)[];
308
- };
330
+ const elements = getNodeElements(pattern);
309
331
  if (!elements) {
310
332
  return;
311
333
  }
@@ -321,9 +343,7 @@ const collectObjectPatternBindings = (
321
343
  pattern: AstNode,
322
344
  out: Set<string>
323
345
  ): void => {
324
- const { properties } = pattern as unknown as {
325
- properties?: readonly AstNode[];
326
- };
346
+ const properties = getNodeProperties(pattern);
327
347
  if (!properties) {
328
348
  return;
329
349
  }
@@ -333,7 +353,7 @@ const collectObjectPatternBindings = (
333
353
  collectPatternBindings(prop, out);
334
354
  } else {
335
355
  // Property node: value holds the binding pattern.
336
- const { value } = prop as unknown as { value?: AstNode };
356
+ const value = getNodeValueNode(prop);
337
357
  // eslint-disable-next-line no-use-before-define
338
358
  collectPatternBindings(value, out);
339
359
  }
@@ -380,16 +400,12 @@ const collectVariableDeclarationBindings = (
380
400
  if (!declNode || declNode.type !== 'VariableDeclaration') {
381
401
  return;
382
402
  }
383
- const declarators = (
384
- declNode as unknown as {
385
- declarations?: readonly AstNode[];
386
- }
387
- ).declarations;
403
+ const declarators = getNodeDeclarations(declNode);
388
404
  if (!declarators) {
389
405
  return;
390
406
  }
391
407
  for (const d of declarators) {
392
- const { id } = d as unknown as { id?: AstNode };
408
+ const id = getNodeId(d);
393
409
  collectPatternBindings(id, out);
394
410
  }
395
411
  };
@@ -400,7 +416,7 @@ const getVariableDeclarationKind = (
400
416
  if (!declNode || declNode.type !== 'VariableDeclaration') {
401
417
  return null;
402
418
  }
403
- return (declNode as unknown as { kind?: string }).kind ?? null;
419
+ return getNodeKind(declNode) ?? null;
404
420
  };
405
421
 
406
422
  /** True if declaration is `var` (function/program-scoped, hoistable). */
@@ -426,7 +442,7 @@ interface FunctionScopeBindings {
426
442
 
427
443
  const collectParamBindings = (scope: AstNode): Set<string> => {
428
444
  const paramBindings = new Set<string>();
429
- const { params } = scope as unknown as { params?: readonly AstNode[] };
445
+ const params = getNodeParams(scope);
430
446
  if (params) {
431
447
  for (const param of params) {
432
448
  collectPatternBindings(param, paramBindings);
@@ -436,7 +452,7 @@ const collectParamBindings = (scope: AstNode): Set<string> => {
436
452
  };
437
453
 
438
454
  const addHoistedVarsFromBody = (scope: AstNode, out: Set<string>): void => {
439
- const { body } = scope as unknown as { body?: AstNode };
455
+ const body = getNodeBodyNode(scope);
440
456
  if (!(body && isAstLike(body))) {
441
457
  return;
442
458
  }
@@ -462,7 +478,7 @@ const collectFunctionScopeBindings = (scope: AstNode): Set<string> =>
462
478
 
463
479
  const collectCatchScopeBindings = (scope: AstNode): Set<string> => {
464
480
  const bindings = new Set<string>();
465
- const { param } = scope as unknown as { param?: AstNode };
481
+ const param = getNodeParam(scope);
466
482
  collectPatternBindings(param, bindings);
467
483
  return bindings;
468
484
  };
@@ -470,10 +486,10 @@ const collectCatchScopeBindings = (scope: AstNode): Set<string> => {
470
486
  const collectForScopeBindings = (scope: AstNode): Set<string> => {
471
487
  const bindings = new Set<string>();
472
488
  if (scope.type === 'ForStatement') {
473
- const { init } = scope as unknown as { init?: AstNode };
489
+ const init = getNodeInit(scope);
474
490
  collectBlockScopedDeclaratorBindings(init, bindings);
475
491
  } else {
476
- const { left } = scope as unknown as { left?: AstNode };
492
+ const left = getNodeLeft(scope);
477
493
  collectBlockScopedDeclaratorBindings(left, bindings);
478
494
  }
479
495
  return bindings;
@@ -483,7 +499,7 @@ const addFunctionDeclarationName = (stmt: AstNode, out: Set<string>): void => {
483
499
  if (stmt.type !== 'FunctionDeclaration') {
484
500
  return;
485
501
  }
486
- const { id } = stmt as unknown as { id?: AstNode };
502
+ const id = getNodeId(stmt);
487
503
  const fnName = identifierName(id);
488
504
  if (fnName) {
489
505
  out.add(fnName);
@@ -494,7 +510,7 @@ const addClassDeclarationName = (stmt: AstNode, out: Set<string>): void => {
494
510
  if (stmt.type !== 'ClassDeclaration') {
495
511
  return;
496
512
  }
497
- const { id } = stmt as unknown as { id?: AstNode };
513
+ const id = getNodeId(stmt);
498
514
  const className = identifierName(id);
499
515
  if (className) {
500
516
  out.add(className);
@@ -517,7 +533,7 @@ const collectBlockScopedStatementListBindings = (
517
533
 
518
534
  const collectBlockStatementBindings = (scope: AstNode): Set<string> => {
519
535
  const bindings = new Set<string>();
520
- const { body } = scope as unknown as { body?: readonly AstNode[] };
536
+ const body = getNodeBodyStatements(scope);
521
537
  collectBlockScopedStatementListBindings(body, bindings);
522
538
  // Static initializer blocks own their own VariableEnvironment (per ES spec),
523
539
  // so `var` declarations inside them do not escape into the enclosing class
@@ -707,10 +723,8 @@ const extractPlainIdentifierAssignmentName = (
707
723
  if (!parent || parent.type !== 'AssignmentExpression') {
708
724
  return null;
709
725
  }
710
- const { operator, left } = parent as unknown as {
711
- operator?: string;
712
- left?: AstNode;
713
- };
726
+ const operator = getNodeOperator(parent);
727
+ const left = getNodeLeft(parent);
714
728
  // Only plain `=` assignments to a bare identifier. Member-expression LHS
715
729
  // (`obj.result = blaze(...)`) is a property write, not a bare binding we
716
730
  // can track by name.
@@ -759,10 +773,8 @@ const isAssignmentToParamName = (node: AstNode): boolean => {
759
773
  if (node.type !== 'AssignmentExpression') {
760
774
  return false;
761
775
  }
762
- const { operator, left } = node as unknown as {
763
- operator?: string;
764
- left?: AstNode;
765
- };
776
+ const operator = getNodeOperator(node);
777
+ const left = getNodeLeft(node);
766
778
  return operator === '=' && left?.type === 'Identifier';
767
779
  };
768
780
 
@@ -834,24 +846,19 @@ type CarrierChildExtractor = (
834
846
  ) => readonly (AstNode | undefined)[];
835
847
 
836
848
  const CARRIER_CHILDREN: Record<string, CarrierChildExtractor> = {
837
- ConditionalExpression: (expr) => {
838
- const { consequent, alternate } = expr as unknown as {
839
- consequent?: AstNode;
840
- alternate?: AstNode;
841
- };
842
- return [consequent, alternate];
843
- },
849
+ ConditionalExpression: (expr) => [
850
+ getNodeConsequent(expr),
851
+ getNodeAlternate(expr),
852
+ ],
844
853
  LogicalExpression: (expr) => {
845
- const { left, right } = expr as unknown as {
846
- left?: AstNode;
847
- right?: AstNode;
848
- };
854
+ const left = getNodeLeft(expr);
855
+ const right = getNodeRight(expr);
849
856
  return [left, right];
850
857
  },
851
858
  };
852
859
 
853
860
  const unwrapTransparentWrapper = (expr: AstNode): AstNode | undefined =>
854
- (expr as unknown as { expression?: AstNode }).expression;
861
+ getNodeExpression(expr);
855
862
 
856
863
  // biome-ignore lint/style/useConst: hoisted for recursive call
857
864
  // eslint-disable-next-line func-style
@@ -893,11 +900,9 @@ const extractIdentifierAssignment = (
893
900
  if (node.type !== 'AssignmentExpression') {
894
901
  return null;
895
902
  }
896
- const { operator, left, right } = node as unknown as {
897
- operator?: string;
898
- left?: AstNode;
899
- right?: AstNode;
900
- };
903
+ const operator = getNodeOperator(node);
904
+ const left = getNodeLeft(node);
905
+ const right = getNodeRight(node);
901
906
  if (!(operator && left) || left.type !== 'Identifier') {
902
907
  return null;
903
908
  }
@@ -1012,15 +1017,12 @@ const propertyDestructuresResultAccessor = (prop: AstNode): boolean => {
1012
1017
  if (prop.type === 'RestElement') {
1013
1018
  return false;
1014
1019
  }
1015
- const { key } = prop as unknown as { key?: AstNode };
1016
- const keyName = identifierName(key);
1020
+ const keyName = identifierName(getNodeKey(prop));
1017
1021
  return keyName !== null && RESULT_ACCESSOR_PROPERTIES.has(keyName);
1018
1022
  };
1019
1023
 
1020
1024
  const objectPatternHasResultAccessorKey = (pattern: AstNode): boolean => {
1021
- const { properties } = pattern as unknown as {
1022
- properties?: readonly AstNode[];
1023
- };
1025
+ const properties = getNodeProperties(pattern);
1024
1026
  return properties?.some(propertyDestructuresResultAccessor) ?? false;
1025
1027
  };
1026
1028
 
@@ -1037,7 +1039,7 @@ const getDestructuredResultAccessorDeclarator = (
1037
1039
  if (!parent || parent.type !== 'VariableDeclarator') {
1038
1040
  return null;
1039
1041
  }
1040
- const { id } = parent as unknown as { id?: AstNode };
1042
+ const id = getNodeId(parent);
1041
1043
  if (!id || id.type !== 'ObjectPattern') {
1042
1044
  return null;
1043
1045
  }
@@ -1138,7 +1140,7 @@ function walkWithScopes(node: AstNode, state: AnalyzeState): void {
1138
1140
 
1139
1141
  const collectProgramBindings = (ast: AstNode): Set<string> => {
1140
1142
  const bindings = new Set<string>();
1141
- const programBody = (ast as unknown as { body?: readonly AstNode[] }).body;
1143
+ const programBody = getNodeBodyStatements(ast);
1142
1144
  // Top-level `let`/`const`/function declarations.
1143
1145
  collectBlockScopedStatementListBindings(programBody, bindings);
1144
1146
  // Top-level `var`s are program-scoped; also hoist any `var`s nested