@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
package/src/rules/ast.ts CHANGED
@@ -12,6 +12,12 @@ import { fileURLToPath } from 'node:url';
12
12
  import { DRAFT_ID_PREFIX, intentValues } from '@ontrails/core';
13
13
  import type { Intent } from '@ontrails/core';
14
14
  import { parseSync } from 'oxc-parser';
15
+ import { ScopeTracker, walk as walkWithOxc } from 'oxc-walker';
16
+ import type {
17
+ ScopeTrackerNode,
18
+ WalkerCallbackContext,
19
+ WalkOptions,
20
+ } from 'oxc-walker';
15
21
 
16
22
  // ---------------------------------------------------------------------------
17
23
  // Types (minimal, avoiding full @oxc-project/types dep)
@@ -21,20 +27,640 @@ export interface AstNode {
21
27
  readonly type: string;
22
28
  readonly start: number;
23
29
  readonly end: number;
24
- readonly key?: { readonly name?: string };
30
+ readonly parent?: AstNode | null;
31
+ readonly key?: unknown;
25
32
  readonly value?: unknown;
26
33
  readonly body?: AstNode | readonly AstNode[];
27
34
  readonly [key: string]: unknown;
28
35
  }
29
36
 
30
- interface StringLiteralNode extends AstNode {
37
+ export interface ProgramNode extends AstNode {
38
+ readonly type: 'Program';
39
+ readonly body?: readonly AstNode[];
40
+ }
41
+
42
+ export interface IdentifierNode extends AstNode {
43
+ readonly type: 'Identifier';
44
+ readonly name?: string;
45
+ }
46
+
47
+ export interface StringLiteralNode extends AstNode {
31
48
  readonly type: 'Literal' | 'StringLiteral';
32
49
  readonly value?: unknown;
33
50
  }
34
51
 
35
- const isAstNode = (value: unknown): value is AstNode =>
52
+ export interface CallExpressionNode extends AstNode {
53
+ readonly type: 'CallExpression' | 'NewExpression';
54
+ readonly arguments?: readonly AstNode[];
55
+ readonly callee?: AstNode;
56
+ }
57
+
58
+ export interface MemberExpressionNode extends AstNode {
59
+ readonly type: 'MemberExpression' | 'StaticMemberExpression';
60
+ readonly computed?: boolean;
61
+ readonly object?: AstNode;
62
+ readonly property?: AstNode;
63
+ }
64
+
65
+ export interface ImportDeclarationNode extends AstNode {
66
+ readonly type: 'ImportDeclaration';
67
+ readonly source?: AstNode;
68
+ readonly specifiers?: readonly AstNode[];
69
+ }
70
+
71
+ export interface ImportSpecifierNode extends AstNode {
72
+ readonly type:
73
+ | 'ImportDefaultSpecifier'
74
+ | 'ImportNamespaceSpecifier'
75
+ | 'ImportSpecifier';
76
+ readonly imported?: AstNode;
77
+ readonly local?: AstNode;
78
+ }
79
+
80
+ export interface ExportDeclarationNode extends AstNode {
81
+ readonly type:
82
+ | 'ExportAllDeclaration'
83
+ | 'ExportDefaultDeclaration'
84
+ | 'ExportNamedDeclaration';
85
+ readonly declaration?: AstNode;
86
+ readonly source?: AstNode;
87
+ readonly specifiers?: readonly AstNode[];
88
+ }
89
+
90
+ export interface ExportSpecifierNode extends AstNode {
91
+ readonly type: 'ExportSpecifier';
92
+ readonly exportKind?: string;
93
+ readonly exported?: AstNode;
94
+ readonly local?: AstNode;
95
+ }
96
+
97
+ export interface VariableDeclarationNode extends AstNode {
98
+ readonly type: 'VariableDeclaration';
99
+ readonly declarations?: readonly AstNode[];
100
+ readonly kind?: string;
101
+ }
102
+
103
+ export interface VariableDeclaratorNode extends AstNode {
104
+ readonly type: 'VariableDeclarator';
105
+ readonly id?: AstNode;
106
+ readonly init?: AstNode;
107
+ }
108
+
109
+ export interface DeclarationWithIdNode extends AstNode {
110
+ readonly type:
111
+ | 'ClassDeclaration'
112
+ | 'FunctionDeclaration'
113
+ | 'InterfaceDeclaration'
114
+ | 'TSInterfaceDeclaration'
115
+ | 'TSTypeAliasDeclaration';
116
+ readonly id?: AstNode;
117
+ }
118
+
119
+ export interface ClassMemberNode extends AstNode {
120
+ readonly type: 'MethodDefinition' | 'PropertyDefinition';
121
+ readonly computed?: boolean;
122
+ readonly key?: AstNode;
123
+ readonly value?: AstNode;
124
+ }
125
+
126
+ export interface ArrayExpressionNode extends AstNode {
127
+ readonly type: 'ArrayExpression';
128
+ readonly elements?: readonly (AstNode | null)[];
129
+ }
130
+
131
+ export interface ObjectExpressionNode extends AstNode {
132
+ readonly type: 'ObjectExpression' | 'ObjectPattern';
133
+ readonly properties?: readonly AstNode[];
134
+ }
135
+
136
+ export interface PropertyNode extends AstNode {
137
+ readonly type: 'Property';
138
+ readonly computed?: boolean;
139
+ readonly key?: AstNode;
140
+ readonly value?: AstNode;
141
+ }
142
+
143
+ export interface RestElementNode extends AstNode {
144
+ readonly type: 'RestElement';
145
+ readonly argument?: AstNode;
146
+ }
147
+
148
+ export interface AssignmentPatternNode extends AstNode {
149
+ readonly type: 'AssignmentPattern';
150
+ readonly left?: AstNode;
151
+ readonly right?: AstNode;
152
+ }
153
+
154
+ export interface ExpressionStatementNode extends AstNode {
155
+ readonly type: 'ExpressionStatement';
156
+ readonly expression?: AstNode;
157
+ }
158
+
159
+ export interface UnaryExpressionNode extends AstNode {
160
+ readonly type: 'AwaitExpression' | 'ChainExpression' | 'UnaryExpression';
161
+ readonly argument?: AstNode;
162
+ }
163
+
164
+ export interface BinaryExpressionNode extends AstNode {
165
+ readonly type:
166
+ | 'AssignmentExpression'
167
+ | 'BinaryExpression'
168
+ | 'ConditionalExpression'
169
+ | 'LogicalExpression';
170
+ readonly alternate?: AstNode;
171
+ readonly consequent?: AstNode;
172
+ readonly left?: AstNode;
173
+ readonly operator?: string;
174
+ readonly right?: AstNode;
175
+ readonly test?: AstNode;
176
+ }
177
+
178
+ export interface FunctionLikeNode extends AstNode {
179
+ readonly type:
180
+ | 'ArrowFunctionExpression'
181
+ | 'FunctionDeclaration'
182
+ | 'FunctionExpression';
183
+ readonly body?: AstNode;
184
+ readonly params?: readonly AstNode[];
185
+ }
186
+
187
+ export interface BlockStatementNode extends AstNode {
188
+ readonly type: 'BlockStatement' | 'StaticBlock';
189
+ readonly body?: readonly AstNode[];
190
+ }
191
+
192
+ export interface ReturnStatementNode extends AstNode {
193
+ readonly type: 'ReturnStatement';
194
+ readonly argument?: AstNode;
195
+ }
196
+
197
+ export type CuratedAstNode =
198
+ | ArrayExpressionNode
199
+ | AssignmentPatternNode
200
+ | BinaryExpressionNode
201
+ | BlockStatementNode
202
+ | CallExpressionNode
203
+ | ClassMemberNode
204
+ | DeclarationWithIdNode
205
+ | ExportDeclarationNode
206
+ | ExportSpecifierNode
207
+ | ExpressionStatementNode
208
+ | FunctionLikeNode
209
+ | IdentifierNode
210
+ | ImportDeclarationNode
211
+ | ImportSpecifierNode
212
+ | MemberExpressionNode
213
+ | ObjectExpressionNode
214
+ | ProgramNode
215
+ | PropertyNode
216
+ | RestElementNode
217
+ | ReturnStatementNode
218
+ | StringLiteralNode
219
+ | UnaryExpressionNode
220
+ | VariableDeclarationNode
221
+ | VariableDeclaratorNode;
222
+
223
+ export interface AstParentContext {
224
+ readonly index: number | null;
225
+ readonly key: string | number | symbol | null | undefined;
226
+ readonly parent: AstNode | null;
227
+ }
228
+
229
+ export interface AstScopeDeclaration {
230
+ readonly end: number;
231
+ readonly node: AstNode;
232
+ readonly start: number;
233
+ readonly type: string;
234
+ }
235
+
236
+ export interface AstScopeContext extends AstParentContext {
237
+ readonly currentScope: string;
238
+ readonly getDeclaration: (name: string) => AstScopeDeclaration | null;
239
+ readonly isDeclared: (name: string) => boolean;
240
+ readonly isCurrentScopeUnder: (scope: string) => boolean;
241
+ }
242
+
243
+ export interface SourceEdit {
244
+ readonly end: number;
245
+ readonly replacement: string;
246
+ readonly start: number;
247
+ }
248
+
249
+ export interface SourceLocation {
250
+ readonly column: number;
251
+ readonly line: number;
252
+ }
253
+
254
+ export interface AstParseDiagnosticLabel {
255
+ readonly end: number;
256
+ readonly message: string | null;
257
+ readonly start: number;
258
+ }
259
+
260
+ export interface AstParseDiagnostic {
261
+ readonly helpMessage: string | null;
262
+ readonly labels: readonly AstParseDiagnosticLabel[];
263
+ readonly message: string;
264
+ readonly severity: string;
265
+ }
266
+
267
+ export interface AstParseResult {
268
+ readonly ast: AstNode | null;
269
+ readonly diagnostics: readonly AstParseDiagnostic[];
270
+ }
271
+
272
+ export interface AstFieldProjection {
273
+ readonly alternate?: AstNode;
274
+ readonly argument?: AstNode;
275
+ readonly arguments?: readonly AstNode[];
276
+ readonly body?: AstNode | readonly AstNode[];
277
+ readonly callee?: AstNode;
278
+ readonly cases?: readonly AstNode[];
279
+ readonly computed?: boolean;
280
+ readonly consequent?: AstNode;
281
+ readonly declaration?: AstNode;
282
+ readonly declarations?: readonly AstNode[];
283
+ readonly discriminant?: AstNode;
284
+ readonly elements?: readonly (AstNode | null)[];
285
+ readonly exportKind?: string;
286
+ readonly exported?: AstNode;
287
+ readonly expression?: AstNode;
288
+ readonly id?: AstNode;
289
+ readonly imported?: AstNode;
290
+ readonly init?: AstNode;
291
+ readonly key?: AstNode;
292
+ readonly kind?: string;
293
+ readonly left?: AstNode;
294
+ readonly local?: AstNode;
295
+ readonly name?: string;
296
+ readonly object?: AstNode;
297
+ readonly operator?: string;
298
+ readonly param?: AstNode;
299
+ readonly params?: readonly AstNode[];
300
+ readonly properties?: readonly AstNode[];
301
+ readonly property?: AstNode;
302
+ readonly returnType?: AstNode;
303
+ readonly right?: AstNode;
304
+ readonly source?: AstNode;
305
+ readonly specifiers?: readonly AstNode[];
306
+ readonly superClass?: AstNode;
307
+ readonly test?: AstNode;
308
+ readonly typeAnnotation?: AstNode;
309
+ readonly value?: unknown;
310
+ }
311
+
312
+ export const isAstNode = (value: unknown): value is AstNode =>
36
313
  Boolean(value && typeof value === 'object' && (value as AstNode).type);
37
314
 
315
+ const isNodeType = <TNode extends CuratedAstNode>(
316
+ node: AstNode | null | undefined,
317
+ types: readonly string[]
318
+ ): node is TNode =>
319
+ node !== null && node !== undefined && types.includes(node.type);
320
+
321
+ export const isProgram = (
322
+ node: AstNode | null | undefined
323
+ ): node is ProgramNode => isNodeType<ProgramNode>(node, ['Program']);
324
+
325
+ export const isIdentifier = (
326
+ node: AstNode | null | undefined
327
+ ): node is IdentifierNode => isNodeType<IdentifierNode>(node, ['Identifier']);
328
+
329
+ export const isCallExpression = (
330
+ node: AstNode | null | undefined
331
+ ): node is CallExpressionNode =>
332
+ isNodeType<CallExpressionNode>(node, ['CallExpression', 'NewExpression']);
333
+
334
+ export const isMemberExpression = (
335
+ node: AstNode | null | undefined
336
+ ): node is MemberExpressionNode =>
337
+ isNodeType<MemberExpressionNode>(node, [
338
+ 'MemberExpression',
339
+ 'StaticMemberExpression',
340
+ ]);
341
+
342
+ export const isImportDeclaration = (
343
+ node: AstNode | null | undefined
344
+ ): node is ImportDeclarationNode =>
345
+ isNodeType<ImportDeclarationNode>(node, ['ImportDeclaration']);
346
+
347
+ export const isImportSpecifier = (
348
+ node: AstNode | null | undefined
349
+ ): node is ImportSpecifierNode =>
350
+ isNodeType<ImportSpecifierNode>(node, [
351
+ 'ImportDefaultSpecifier',
352
+ 'ImportNamespaceSpecifier',
353
+ 'ImportSpecifier',
354
+ ]);
355
+
356
+ export const isExportDeclaration = (
357
+ node: AstNode | null | undefined
358
+ ): node is ExportDeclarationNode =>
359
+ isNodeType<ExportDeclarationNode>(node, [
360
+ 'ExportAllDeclaration',
361
+ 'ExportDefaultDeclaration',
362
+ 'ExportNamedDeclaration',
363
+ ]);
364
+
365
+ export const isExportNamedDeclaration = (
366
+ node: AstNode | null | undefined
367
+ ): node is ExportDeclarationNode & {
368
+ readonly type: 'ExportNamedDeclaration';
369
+ } =>
370
+ isNodeType<
371
+ ExportDeclarationNode & { readonly type: 'ExportNamedDeclaration' }
372
+ >(node, ['ExportNamedDeclaration']);
373
+
374
+ export const isExportDefaultDeclaration = (
375
+ node: AstNode | null | undefined
376
+ ): node is ExportDeclarationNode & {
377
+ readonly type: 'ExportDefaultDeclaration';
378
+ } =>
379
+ isNodeType<
380
+ ExportDeclarationNode & { readonly type: 'ExportDefaultDeclaration' }
381
+ >(node, ['ExportDefaultDeclaration']);
382
+
383
+ export const isExportAllDeclaration = (
384
+ node: AstNode | null | undefined
385
+ ): node is ExportDeclarationNode & { readonly type: 'ExportAllDeclaration' } =>
386
+ isNodeType<ExportDeclarationNode & { readonly type: 'ExportAllDeclaration' }>(
387
+ node,
388
+ ['ExportAllDeclaration']
389
+ );
390
+
391
+ export const isExportSpecifier = (
392
+ node: AstNode | null | undefined
393
+ ): node is ExportSpecifierNode =>
394
+ isNodeType<ExportSpecifierNode>(node, ['ExportSpecifier']);
395
+
396
+ export const isVariableDeclaration = (
397
+ node: AstNode | null | undefined
398
+ ): node is VariableDeclarationNode =>
399
+ isNodeType<VariableDeclarationNode>(node, ['VariableDeclaration']);
400
+
401
+ export const isVariableDeclarator = (
402
+ node: AstNode | null | undefined
403
+ ): node is VariableDeclaratorNode =>
404
+ isNodeType<VariableDeclaratorNode>(node, ['VariableDeclarator']);
405
+
406
+ export const isDeclarationWithId = (
407
+ node: AstNode | null | undefined
408
+ ): node is DeclarationWithIdNode =>
409
+ isNodeType<DeclarationWithIdNode>(node, [
410
+ 'ClassDeclaration',
411
+ 'FunctionDeclaration',
412
+ 'InterfaceDeclaration',
413
+ 'TSInterfaceDeclaration',
414
+ 'TSTypeAliasDeclaration',
415
+ ]);
416
+
417
+ export const isClassMember = (
418
+ node: AstNode | null | undefined
419
+ ): node is ClassMemberNode =>
420
+ isNodeType<ClassMemberNode>(node, ['MethodDefinition', 'PropertyDefinition']);
421
+
422
+ export const isArrayExpression = (
423
+ node: AstNode | null | undefined
424
+ ): node is ArrayExpressionNode =>
425
+ isNodeType<ArrayExpressionNode>(node, ['ArrayExpression']);
426
+
427
+ export const isObjectExpression = (
428
+ node: AstNode | null | undefined
429
+ ): node is ObjectExpressionNode =>
430
+ isNodeType<ObjectExpressionNode>(node, ['ObjectExpression', 'ObjectPattern']);
431
+
432
+ export const isProperty = (
433
+ node: AstNode | null | undefined
434
+ ): node is PropertyNode => isNodeType<PropertyNode>(node, ['Property']);
435
+
436
+ export const isRestElement = (
437
+ node: AstNode | null | undefined
438
+ ): node is RestElementNode =>
439
+ isNodeType<RestElementNode>(node, ['RestElement']);
440
+
441
+ export const isAssignmentPattern = (
442
+ node: AstNode | null | undefined
443
+ ): node is AssignmentPatternNode =>
444
+ isNodeType<AssignmentPatternNode>(node, ['AssignmentPattern']);
445
+
446
+ export const isExpressionStatement = (
447
+ node: AstNode | null | undefined
448
+ ): node is ExpressionStatementNode =>
449
+ isNodeType<ExpressionStatementNode>(node, ['ExpressionStatement']);
450
+
451
+ export const isUnaryExpression = (
452
+ node: AstNode | null | undefined
453
+ ): node is UnaryExpressionNode =>
454
+ isNodeType<UnaryExpressionNode>(node, [
455
+ 'AwaitExpression',
456
+ 'ChainExpression',
457
+ 'UnaryExpression',
458
+ ]);
459
+
460
+ export const isBinaryExpression = (
461
+ node: AstNode | null | undefined
462
+ ): node is BinaryExpressionNode =>
463
+ isNodeType<BinaryExpressionNode>(node, [
464
+ 'AssignmentExpression',
465
+ 'BinaryExpression',
466
+ 'ConditionalExpression',
467
+ 'LogicalExpression',
468
+ ]);
469
+
470
+ export const isFunctionLike = (
471
+ node: AstNode | null | undefined
472
+ ): node is FunctionLikeNode =>
473
+ isNodeType<FunctionLikeNode>(node, [
474
+ 'ArrowFunctionExpression',
475
+ 'FunctionDeclaration',
476
+ 'FunctionExpression',
477
+ ]);
478
+
479
+ export const isBlockStatement = (
480
+ node: AstNode | null | undefined
481
+ ): node is BlockStatementNode =>
482
+ isNodeType<BlockStatementNode>(node, ['BlockStatement', 'StaticBlock']);
483
+
484
+ export const isReturnStatement = (
485
+ node: AstNode | null | undefined
486
+ ): node is ReturnStatementNode =>
487
+ isNodeType<ReturnStatementNode>(node, ['ReturnStatement']);
488
+
489
+ const projectAstFields = (
490
+ node: AstNode | null | undefined
491
+ ): AstFieldProjection | null => (node ? (node as AstFieldProjection) : null);
492
+
493
+ export const getNodeAlternate = (
494
+ node: AstNode | null | undefined
495
+ ): AstNode | undefined => projectAstFields(node)?.alternate;
496
+
497
+ export const getNodeArgument = (
498
+ node: AstNode | null | undefined
499
+ ): AstNode | undefined => projectAstFields(node)?.argument;
500
+
501
+ export const getNodeArguments = (
502
+ node: AstNode | null | undefined
503
+ ): readonly AstNode[] => (isCallExpression(node) ? (node.arguments ?? []) : []);
504
+
505
+ export const getNodeBody = (
506
+ node: AstNode | null | undefined
507
+ ): AstNode | readonly AstNode[] | undefined => projectAstFields(node)?.body;
508
+
509
+ export const getNodeBodyNode = (
510
+ node: AstNode | null | undefined
511
+ ): AstNode | undefined => {
512
+ const body = getNodeBody(node);
513
+ return isAstNode(body) ? body : undefined;
514
+ };
515
+
516
+ export const getNodeBodyStatements = (
517
+ node: AstNode | null | undefined
518
+ ): readonly AstNode[] => {
519
+ const body = getNodeBody(node);
520
+ return Array.isArray(body) ? body : [];
521
+ };
522
+
523
+ export const getNodeCallee = (
524
+ node: AstNode | null | undefined
525
+ ): AstNode | undefined => (isCallExpression(node) ? node.callee : undefined);
526
+
527
+ export const getNodeCases = (
528
+ node: AstNode | null | undefined
529
+ ): readonly AstNode[] => projectAstFields(node)?.cases ?? [];
530
+
531
+ export const getNodeComputed = (
532
+ node: AstNode | null | undefined
533
+ ): boolean | undefined => projectAstFields(node)?.computed;
534
+
535
+ export const getNodeConsequent = (
536
+ node: AstNode | null | undefined
537
+ ): AstNode | undefined => projectAstFields(node)?.consequent;
538
+
539
+ export const getNodeDeclaration = (
540
+ node: AstNode | null | undefined
541
+ ): AstNode | undefined => projectAstFields(node)?.declaration;
542
+
543
+ export const getNodeDeclarations = (
544
+ node: AstNode | null | undefined
545
+ ): readonly AstNode[] =>
546
+ isVariableDeclaration(node) ? (node.declarations ?? []) : [];
547
+
548
+ export const getNodeDiscriminant = (
549
+ node: AstNode | null | undefined
550
+ ): AstNode | undefined => projectAstFields(node)?.discriminant;
551
+
552
+ export const getNodeElements = (
553
+ node: AstNode | null | undefined
554
+ ): readonly (AstNode | null)[] => projectAstFields(node)?.elements ?? [];
555
+
556
+ export const getNodeExported = (
557
+ node: AstNode | null | undefined
558
+ ): AstNode | undefined => projectAstFields(node)?.exported;
559
+
560
+ export const getNodeExportKind = (
561
+ node: AstNode | null | undefined
562
+ ): string | undefined => projectAstFields(node)?.exportKind;
563
+
564
+ export const getNodeExpression = (
565
+ node: AstNode | null | undefined
566
+ ): AstNode | undefined => projectAstFields(node)?.expression;
567
+
568
+ export const getNodeId = (
569
+ node: AstNode | null | undefined
570
+ ): AstNode | undefined => projectAstFields(node)?.id;
571
+
572
+ export const getNodeImported = (
573
+ node: AstNode | null | undefined
574
+ ): AstNode | undefined => projectAstFields(node)?.imported;
575
+
576
+ export const getNodeInit = (
577
+ node: AstNode | null | undefined
578
+ ): AstNode | undefined => projectAstFields(node)?.init;
579
+
580
+ export const getNodeKey = (
581
+ node: AstNode | null | undefined
582
+ ): AstNode | undefined => projectAstFields(node)?.key;
583
+
584
+ export const getNodeKind = (
585
+ node: AstNode | null | undefined
586
+ ): string | undefined => projectAstFields(node)?.kind;
587
+
588
+ export const getNodeLeft = (
589
+ node: AstNode | null | undefined
590
+ ): AstNode | undefined => projectAstFields(node)?.left;
591
+
592
+ export const getNodeLocal = (
593
+ node: AstNode | null | undefined
594
+ ): AstNode | undefined => projectAstFields(node)?.local;
595
+
596
+ export const getNodeName = (
597
+ node: AstNode | null | undefined
598
+ ): string | undefined => projectAstFields(node)?.name;
599
+
600
+ export const getNodeObject = (
601
+ node: AstNode | null | undefined
602
+ ): AstNode | undefined => (isMemberExpression(node) ? node.object : undefined);
603
+
604
+ export const getNodeOperator = (
605
+ node: AstNode | null | undefined
606
+ ): string | undefined => projectAstFields(node)?.operator;
607
+
608
+ export const getNodeParam = (
609
+ node: AstNode | null | undefined
610
+ ): AstNode | undefined => projectAstFields(node)?.param;
611
+
612
+ export const getNodeParams = (
613
+ node: AstNode | null | undefined
614
+ ): readonly AstNode[] => (isFunctionLike(node) ? (node.params ?? []) : []);
615
+
616
+ export const getNodeProperties = (
617
+ node: AstNode | null | undefined
618
+ ): readonly AstNode[] =>
619
+ isObjectExpression(node) ? (node.properties ?? []) : [];
620
+
621
+ export const getNodeProperty = (
622
+ node: AstNode | null | undefined
623
+ ): AstNode | undefined =>
624
+ isMemberExpression(node) ? node.property : undefined;
625
+
626
+ export const getNodeReturnType = (
627
+ node: AstNode | null | undefined
628
+ ): AstNode | undefined => projectAstFields(node)?.returnType;
629
+
630
+ export const getNodeRight = (
631
+ node: AstNode | null | undefined
632
+ ): AstNode | undefined => projectAstFields(node)?.right;
633
+
634
+ export const getNodeSource = (
635
+ node: AstNode | null | undefined
636
+ ): AstNode | undefined => projectAstFields(node)?.source;
637
+
638
+ export const getNodeSpecifiers = (
639
+ node: AstNode | null | undefined
640
+ ): readonly AstNode[] => projectAstFields(node)?.specifiers ?? [];
641
+
642
+ export const getNodeSuperClass = (
643
+ node: AstNode | null | undefined
644
+ ): AstNode | undefined => projectAstFields(node)?.superClass;
645
+
646
+ export const getNodeTest = (
647
+ node: AstNode | null | undefined
648
+ ): AstNode | undefined => projectAstFields(node)?.test;
649
+
650
+ export const getNodeTypeAnnotation = (
651
+ node: AstNode | null | undefined
652
+ ): AstNode | undefined => projectAstFields(node)?.typeAnnotation;
653
+
654
+ export const getNodeValue = (node: AstNode | null | undefined): unknown =>
655
+ projectAstFields(node)?.value;
656
+
657
+ export const getNodeValueNode = (
658
+ node: AstNode | null | undefined
659
+ ): AstNode | undefined => {
660
+ const value = getNodeValue(node);
661
+ return isAstNode(value) ? value : undefined;
662
+ };
663
+
38
664
  // ---------------------------------------------------------------------------
39
665
  // Parser
40
666
  // ---------------------------------------------------------------------------
@@ -49,6 +675,46 @@ export const parse = (filePath: string, sourceCode: string): AstNode | null => {
49
675
  }
50
676
  };
51
677
 
678
+ /**
679
+ * Parse TypeScript source and surface parser diagnostics. OXC can recover a
680
+ * partial program for malformed input, so rewrite tooling should use this
681
+ * helper when applying edits would be unsafe after syntax errors.
682
+ */
683
+ export const parseWithDiagnostics = (
684
+ filePath: string,
685
+ sourceCode: string
686
+ ): AstParseResult => {
687
+ try {
688
+ const result = parseSync(filePath, sourceCode, { sourceType: 'module' });
689
+ return {
690
+ ast: result.program as unknown as AstNode,
691
+ diagnostics: result.errors.map((error) => ({
692
+ helpMessage: error.helpMessage,
693
+ labels: error.labels.map((label) => ({
694
+ end: label.end,
695
+ message: label.message,
696
+ start: label.start,
697
+ })),
698
+ message: error.message,
699
+ severity: error.severity,
700
+ })),
701
+ };
702
+ } catch (error) {
703
+ return {
704
+ ast: null,
705
+ diagnostics: [
706
+ {
707
+ helpMessage: null,
708
+ labels: [],
709
+ message:
710
+ error instanceof Error ? error.message : 'Unable to parse source.',
711
+ severity: 'Error',
712
+ },
713
+ ],
714
+ };
715
+ }
716
+ };
717
+
52
718
  // ---------------------------------------------------------------------------
53
719
  // Walker
54
720
  // ---------------------------------------------------------------------------
@@ -83,6 +749,94 @@ export const walk: WalkFn = (node, visit) => {
83
749
  walkChildren(n, visit, walk);
84
750
  };
85
751
 
752
+ const toAstParentContext = (
753
+ parent: unknown,
754
+ ctx: WalkerCallbackContext
755
+ ): AstParentContext => ({
756
+ index: ctx.index,
757
+ key: ctx.key,
758
+ parent: isAstNode(parent) ? parent : null,
759
+ });
760
+
761
+ const toScopeDeclaration = (
762
+ declaration: ScopeTrackerNode | null
763
+ ): AstScopeDeclaration | null => {
764
+ if (!declaration) {
765
+ return null;
766
+ }
767
+
768
+ return {
769
+ end: declaration.end,
770
+ node: declaration.node as unknown as AstNode,
771
+ start: declaration.start,
772
+ type: declaration.type,
773
+ };
774
+ };
775
+
776
+ const walkWithOxcFacade = (
777
+ node: unknown,
778
+ enter: (node: AstNode, context: AstParentContext) => void,
779
+ scopeTracker?: ScopeTracker
780
+ ): void => {
781
+ if (!isAstNode(node)) {
782
+ return;
783
+ }
784
+
785
+ const options: Partial<WalkOptions> = {
786
+ enter(candidate, parent, ctx) {
787
+ if (!isAstNode(candidate)) {
788
+ return;
789
+ }
790
+ enter(candidate, toAstParentContext(parent, ctx));
791
+ },
792
+ };
793
+
794
+ if (scopeTracker) {
795
+ options.scopeTracker = scopeTracker;
796
+ }
797
+
798
+ walkWithOxc(node as never, options);
799
+ };
800
+
801
+ /**
802
+ * Walk an AST node tree with parent, key, and index context for each visited
803
+ * node. This is the supported Warden facade over `oxc-walker` for rules and
804
+ * regrades that need structural context.
805
+ */
806
+ export const walkWithParents = (
807
+ node: unknown,
808
+ visit: (node: AstNode, context: AstParentContext) => void
809
+ ): void => {
810
+ walkWithOxcFacade(node, visit);
811
+ };
812
+
813
+ /**
814
+ * Walk an AST node tree with parent context and a scope query facade. The
815
+ * concrete `oxc-walker` tracker stays behind this helper so rule authors can
816
+ * ask Warden-shaped questions without depending on walker internals.
817
+ */
818
+ export const walkWithScopeContext = (
819
+ node: unknown,
820
+ visit: (node: AstNode, context: AstScopeContext) => void
821
+ ): void => {
822
+ const scopeTracker = new ScopeTracker();
823
+
824
+ walkWithOxcFacade(
825
+ node,
826
+ (candidate, context) => {
827
+ visit(candidate, {
828
+ ...context,
829
+ currentScope: scopeTracker.getCurrentScope(),
830
+ getDeclaration: (name) =>
831
+ toScopeDeclaration(scopeTracker.getDeclaration(name)),
832
+ isCurrentScopeUnder: (scope) => scopeTracker.isCurrentScopeUnder(scope),
833
+ isDeclared: (name) => scopeTracker.isDeclared(name),
834
+ });
835
+ },
836
+ scopeTracker
837
+ );
838
+ };
839
+
86
840
  const NESTED_SCOPE_TYPES = new Set([
87
841
  'ArrowFunctionExpression',
88
842
  'FunctionExpression',
@@ -135,6 +889,81 @@ export const offsetToLine = (sourceCode: string, offset: number): number => {
135
889
  return line;
136
890
  };
137
891
 
892
+ /** Find the byte offset's line and column (1-based) in source code. */
893
+ export const offsetToLineColumn = (
894
+ sourceCode: string,
895
+ offset: number
896
+ ): SourceLocation => {
897
+ let line = 1;
898
+ let column = 1;
899
+ const limit = Math.min(Math.max(offset, 0), sourceCode.length);
900
+
901
+ for (let i = 0; i < limit; i += 1) {
902
+ if (sourceCode[i] === '\n') {
903
+ line += 1;
904
+ column = 1;
905
+ } else {
906
+ column += 1;
907
+ }
908
+ }
909
+
910
+ return { column, line };
911
+ };
912
+
913
+ export const createSourceEdit = (
914
+ start: number,
915
+ end: number,
916
+ replacement: string
917
+ ): SourceEdit => ({ end, replacement, start });
918
+
919
+ export const validateSourceEdits = (
920
+ edits: readonly SourceEdit[],
921
+ sourceLength?: number
922
+ ): readonly SourceEdit[] => {
923
+ const ordered = [...edits].toSorted(
924
+ (left, right) => left.start - right.start
925
+ );
926
+ for (let i = 0; i < ordered.length; i += 1) {
927
+ const edit = ordered[i];
928
+ if (!edit) {
929
+ continue;
930
+ }
931
+ if (
932
+ !Number.isSafeInteger(edit.start) ||
933
+ !Number.isSafeInteger(edit.end) ||
934
+ edit.start < 0 ||
935
+ edit.end < edit.start ||
936
+ (sourceLength !== undefined && edit.end > sourceLength)
937
+ ) {
938
+ throw new Error(`Invalid source edit range ${edit.start}-${edit.end}.`);
939
+ }
940
+
941
+ const previous = ordered[i - 1];
942
+ if (previous && edit.start < previous.end) {
943
+ throw new Error(
944
+ `Overlapping source edits ${previous.start}-${previous.end} and ${edit.start}-${edit.end}.`
945
+ );
946
+ }
947
+ }
948
+
949
+ return ordered;
950
+ };
951
+
952
+ export const applySourceEdits = (
953
+ sourceCode: string,
954
+ edits: readonly SourceEdit[]
955
+ ): string => {
956
+ validateSourceEdits(edits, sourceCode.length);
957
+
958
+ return [...edits]
959
+ .toSorted((left, right) => right.start - left.start)
960
+ .reduce(
961
+ (output, edit) =>
962
+ output.slice(0, edit.start) + edit.replacement + output.slice(edit.end),
963
+ sourceCode
964
+ );
965
+ };
966
+
138
967
  /** Get the name of an Identifier node, or null. */
139
968
  export const identifierName = (node: AstNode | undefined): string | null => {
140
969
  if (node?.type !== 'Identifier') {
@@ -1865,7 +2694,9 @@ const extractImportSpecifierAlias = (
1865
2694
  };
1866
2695
 
1867
2696
  /**
1868
- * Collect `import { foo as bar } from '...'` and `import bar from '...'`
2697
+ * Collect `import {
2698
+ foo as bar
2699
+ } from '...';` and `import bar from '...'`
1869
2700
  * specifier mappings keyed by local binding name. The value is the original
1870
2701
  * exported name for named imports. Default imports map to themselves because
1871
2702
  * the exported name cannot be recovered statically — callers should fall
@@ -2421,8 +3252,8 @@ const extractBlazeFromConfig = (config: AstNode): AstNode[] => {
2421
3252
  }
2422
3253
  for (const prop of properties) {
2423
3254
  if (
2424
- prop.type === 'Property' &&
2425
- prop.key?.name === 'blaze' &&
3255
+ isProperty(prop) &&
3256
+ identifierName(prop.key) === 'blaze' &&
2426
3257
  isAstNode(prop.value)
2427
3258
  ) {
2428
3259
  bodies.push(prop.value);