@rcrsr/rill-cli 0.6.0
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/LICENSE +21 -0
- package/dist/check/config.d.ts +20 -0
- package/dist/check/config.d.ts.map +1 -0
- package/dist/check/config.js +151 -0
- package/dist/check/config.js.map +1 -0
- package/dist/check/fixer.d.ts +39 -0
- package/dist/check/fixer.d.ts.map +1 -0
- package/dist/check/fixer.js +119 -0
- package/dist/check/fixer.js.map +1 -0
- package/dist/check/index.d.ts +10 -0
- package/dist/check/index.d.ts.map +1 -0
- package/dist/check/index.js +21 -0
- package/dist/check/index.js.map +1 -0
- package/dist/check/rules/anti-patterns.d.ts +65 -0
- package/dist/check/rules/anti-patterns.d.ts.map +1 -0
- package/dist/check/rules/anti-patterns.js +481 -0
- package/dist/check/rules/anti-patterns.js.map +1 -0
- package/dist/check/rules/closures.d.ts +66 -0
- package/dist/check/rules/closures.d.ts.map +1 -0
- package/dist/check/rules/closures.js +370 -0
- package/dist/check/rules/closures.js.map +1 -0
- package/dist/check/rules/collections.d.ts +90 -0
- package/dist/check/rules/collections.d.ts.map +1 -0
- package/dist/check/rules/collections.js +373 -0
- package/dist/check/rules/collections.js.map +1 -0
- package/dist/check/rules/conditionals.d.ts +41 -0
- package/dist/check/rules/conditionals.d.ts.map +1 -0
- package/dist/check/rules/conditionals.js +134 -0
- package/dist/check/rules/conditionals.js.map +1 -0
- package/dist/check/rules/flow.d.ts +46 -0
- package/dist/check/rules/flow.d.ts.map +1 -0
- package/dist/check/rules/flow.js +206 -0
- package/dist/check/rules/flow.js.map +1 -0
- package/dist/check/rules/formatting.d.ts +143 -0
- package/dist/check/rules/formatting.d.ts.map +1 -0
- package/dist/check/rules/formatting.js +656 -0
- package/dist/check/rules/formatting.js.map +1 -0
- package/dist/check/rules/helpers.d.ts +26 -0
- package/dist/check/rules/helpers.d.ts.map +1 -0
- package/dist/check/rules/helpers.js +66 -0
- package/dist/check/rules/helpers.js.map +1 -0
- package/dist/check/rules/index.d.ts +21 -0
- package/dist/check/rules/index.d.ts.map +1 -0
- package/dist/check/rules/index.js +78 -0
- package/dist/check/rules/index.js.map +1 -0
- package/dist/check/rules/loops.d.ts +77 -0
- package/dist/check/rules/loops.d.ts.map +1 -0
- package/dist/check/rules/loops.js +310 -0
- package/dist/check/rules/loops.js.map +1 -0
- package/dist/check/rules/naming.d.ts +21 -0
- package/dist/check/rules/naming.d.ts.map +1 -0
- package/dist/check/rules/naming.js +174 -0
- package/dist/check/rules/naming.js.map +1 -0
- package/dist/check/rules/strings.d.ts +28 -0
- package/dist/check/rules/strings.d.ts.map +1 -0
- package/dist/check/rules/strings.js +79 -0
- package/dist/check/rules/strings.js.map +1 -0
- package/dist/check/rules/types.d.ts +41 -0
- package/dist/check/rules/types.d.ts.map +1 -0
- package/dist/check/rules/types.js +167 -0
- package/dist/check/rules/types.js.map +1 -0
- package/dist/check/types.d.ts +112 -0
- package/dist/check/types.d.ts.map +1 -0
- package/dist/check/types.js +6 -0
- package/dist/check/types.js.map +1 -0
- package/dist/check/validator.d.ts +18 -0
- package/dist/check/validator.d.ts.map +1 -0
- package/dist/check/validator.js +110 -0
- package/dist/check/validator.js.map +1 -0
- package/dist/check/visitor.d.ts +33 -0
- package/dist/check/visitor.d.ts.map +1 -0
- package/dist/check/visitor.js +259 -0
- package/dist/check/visitor.js.map +1 -0
- package/dist/cli-check.d.ts +43 -0
- package/dist/cli-check.d.ts.map +1 -0
- package/dist/cli-check.js +366 -0
- package/dist/cli-check.js.map +1 -0
- package/dist/cli-error-enrichment.d.ts +73 -0
- package/dist/cli-error-enrichment.d.ts.map +1 -0
- package/dist/cli-error-enrichment.js +205 -0
- package/dist/cli-error-enrichment.js.map +1 -0
- package/dist/cli-error-formatter.d.ts +45 -0
- package/dist/cli-error-formatter.d.ts.map +1 -0
- package/dist/cli-error-formatter.js +218 -0
- package/dist/cli-error-formatter.js.map +1 -0
- package/dist/cli-eval.d.ts +15 -0
- package/dist/cli-eval.d.ts.map +1 -0
- package/dist/cli-eval.js +116 -0
- package/dist/cli-eval.js.map +1 -0
- package/dist/cli-exec.d.ts +58 -0
- package/dist/cli-exec.d.ts.map +1 -0
- package/dist/cli-exec.js +326 -0
- package/dist/cli-exec.js.map +1 -0
- package/dist/cli-explain.d.ts +24 -0
- package/dist/cli-explain.d.ts.map +1 -0
- package/dist/cli-explain.js +68 -0
- package/dist/cli-explain.js.map +1 -0
- package/dist/cli-lsp-diagnostic.d.ts +35 -0
- package/dist/cli-lsp-diagnostic.d.ts.map +1 -0
- package/dist/cli-lsp-diagnostic.js +98 -0
- package/dist/cli-lsp-diagnostic.js.map +1 -0
- package/dist/cli-module-loader.d.ts +19 -0
- package/dist/cli-module-loader.d.ts.map +1 -0
- package/dist/cli-module-loader.js +83 -0
- package/dist/cli-module-loader.js.map +1 -0
- package/dist/cli-shared.d.ts +62 -0
- package/dist/cli-shared.d.ts.map +1 -0
- package/dist/cli-shared.js +158 -0
- package/dist/cli-shared.js.map +1 -0
- package/dist/cli.d.ts +13 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +62 -0
- package/dist/cli.js.map +1 -0
- package/dist/test-internal-import.d.ts +2 -0
- package/dist/test-internal-import.d.ts.map +1 -0
- package/dist/test-internal-import.js +7 -0
- package/dist/test-internal-import.js.map +1 -0
- package/package.json +24 -0
- package/src/check/config.ts +202 -0
- package/src/check/fixer.ts +174 -0
- package/src/check/index.ts +39 -0
- package/src/check/rules/anti-patterns.ts +585 -0
- package/src/check/rules/closures.ts +445 -0
- package/src/check/rules/collections.ts +437 -0
- package/src/check/rules/conditionals.ts +155 -0
- package/src/check/rules/flow.ts +262 -0
- package/src/check/rules/formatting.ts +811 -0
- package/src/check/rules/helpers.ts +89 -0
- package/src/check/rules/index.ts +140 -0
- package/src/check/rules/loops.ts +372 -0
- package/src/check/rules/naming.ts +242 -0
- package/src/check/rules/strings.ts +104 -0
- package/src/check/rules/types.ts +214 -0
- package/src/check/types.ts +163 -0
- package/src/check/validator.ts +136 -0
- package/src/check/visitor.ts +338 -0
- package/src/cli-check.ts +456 -0
- package/src/cli-error-enrichment.ts +274 -0
- package/src/cli-error-formatter.ts +313 -0
- package/src/cli-eval.ts +145 -0
- package/src/cli-exec.ts +408 -0
- package/src/cli-explain.ts +76 -0
- package/src/cli-lsp-diagnostic.ts +132 -0
- package/src/cli-module-loader.ts +101 -0
- package/src/cli-shared.ts +187 -0
- package/tests/check/cli-check.test.ts +189 -0
- package/tests/check/config.test.ts +350 -0
- package/tests/check/fixer.test.ts +373 -0
- package/tests/check/format-diagnostics.test.ts +327 -0
- package/tests/check/rules/anti-patterns.test.ts +467 -0
- package/tests/check/rules/closures.test.ts +192 -0
- package/tests/check/rules/collections.test.ts +380 -0
- package/tests/check/rules/conditionals.test.ts +185 -0
- package/tests/check/rules/flow.test.ts +250 -0
- package/tests/check/rules/formatting.test.ts +755 -0
- package/tests/check/rules/loops.test.ts +334 -0
- package/tests/check/rules/naming.test.ts +336 -0
- package/tests/check/rules/strings.test.ts +129 -0
- package/tests/check/rules/types.test.ts +257 -0
- package/tests/check/validator.test.ts +444 -0
- package/tests/check/visitor.test.ts +171 -0
- package/tests/cli/check.test.ts +801 -0
- package/tests/cli/error-enrichment.test.ts +510 -0
- package/tests/cli/error-formatter.test.ts +631 -0
- package/tests/cli/eval.test.ts +85 -0
- package/tests/cli/exec.test.ts +537 -0
- package/tests/cli-explain.test.ts +249 -0
- package/tests/cli-lsp-diagnostic.test.ts +202 -0
- package/tests/cli-shared.test.ts +439 -0
- package/tsconfig.json +9 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check Types
|
|
3
|
+
* Type definitions for the rill-check static analysis tool.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
SourceLocation,
|
|
8
|
+
SourceSpan,
|
|
9
|
+
ScriptNode,
|
|
10
|
+
NodeType,
|
|
11
|
+
ASTNode,
|
|
12
|
+
} from '@rcrsr/rill';
|
|
13
|
+
|
|
14
|
+
// ============================================================
|
|
15
|
+
// SEVERITY AND RULE STATE
|
|
16
|
+
// ============================================================
|
|
17
|
+
|
|
18
|
+
/** Diagnostic severity levels */
|
|
19
|
+
export type Severity = 'error' | 'warning' | 'info';
|
|
20
|
+
|
|
21
|
+
/** Rule state configuration */
|
|
22
|
+
export type RuleState = 'on' | 'off' | 'warn';
|
|
23
|
+
|
|
24
|
+
// ============================================================
|
|
25
|
+
// DIAGNOSTIC DATA
|
|
26
|
+
// ============================================================
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Fix suggestion for a diagnostic.
|
|
30
|
+
* Provides automated fix information that can be applied to source code.
|
|
31
|
+
*/
|
|
32
|
+
export interface Fix {
|
|
33
|
+
/** Human-readable description of what the fix does */
|
|
34
|
+
readonly description: string;
|
|
35
|
+
/** Whether the fix can be safely applied automatically */
|
|
36
|
+
readonly applicable: boolean;
|
|
37
|
+
/** Source range to replace */
|
|
38
|
+
readonly range: SourceSpan;
|
|
39
|
+
/** Replacement text */
|
|
40
|
+
readonly replacement: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* A single diagnostic issue found during validation.
|
|
45
|
+
* Represents errors, warnings, or informational messages from static analysis.
|
|
46
|
+
*/
|
|
47
|
+
export interface Diagnostic {
|
|
48
|
+
/** Location of the issue in source */
|
|
49
|
+
readonly location: SourceLocation;
|
|
50
|
+
/** Severity level */
|
|
51
|
+
readonly severity: Severity;
|
|
52
|
+
/** Rule code (e.g., NAMING_SNAKE_CASE) */
|
|
53
|
+
readonly code: string;
|
|
54
|
+
/** Human-readable description */
|
|
55
|
+
readonly message: string;
|
|
56
|
+
/** Source line containing the issue */
|
|
57
|
+
readonly context: string;
|
|
58
|
+
/** Optional automatic fix */
|
|
59
|
+
readonly fix: Fix | null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ============================================================
|
|
63
|
+
// CHECK CONFIGURATION
|
|
64
|
+
// ============================================================
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Configuration for check rules and severity overrides.
|
|
68
|
+
* Controls which rules are active and at what severity level.
|
|
69
|
+
*/
|
|
70
|
+
export interface CheckConfig {
|
|
71
|
+
/** Per-rule enable/disable/warn state */
|
|
72
|
+
readonly rules: Record<string, RuleState>;
|
|
73
|
+
/** Severity overrides by rule code */
|
|
74
|
+
readonly severity: Record<string, Severity>;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ============================================================
|
|
78
|
+
// VALIDATION CONTEXT
|
|
79
|
+
// ============================================================
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Context for validation passes.
|
|
83
|
+
* Tracks source, AST, configuration, and accumulated diagnostics.
|
|
84
|
+
*/
|
|
85
|
+
export interface ValidationContext {
|
|
86
|
+
/** Original source text */
|
|
87
|
+
readonly source: string;
|
|
88
|
+
/** Parsed AST */
|
|
89
|
+
readonly ast: ScriptNode;
|
|
90
|
+
/** Active configuration */
|
|
91
|
+
readonly config: CheckConfig;
|
|
92
|
+
/** Accumulated diagnostics */
|
|
93
|
+
readonly diagnostics: Diagnostic[];
|
|
94
|
+
/** Variable definitions for collision detection */
|
|
95
|
+
readonly variables: Map<string, SourceLocation>;
|
|
96
|
+
/** HostCall nodes that are wrapped in type assertions */
|
|
97
|
+
readonly assertedHostCalls: Set<ASTNode>;
|
|
98
|
+
/** Closure scope IDs for variables (maps variable name to closure AST node) */
|
|
99
|
+
readonly variableScopes: Map<string, ASTNode | null>;
|
|
100
|
+
/** Current closure scope stack during traversal */
|
|
101
|
+
readonly scopeStack: ASTNode[];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Context for fix generation.
|
|
106
|
+
* Provides access to source and AST for computing fix replacements.
|
|
107
|
+
*/
|
|
108
|
+
export interface FixContext {
|
|
109
|
+
/** Original source text */
|
|
110
|
+
readonly source: string;
|
|
111
|
+
/** Parsed AST */
|
|
112
|
+
readonly ast: ScriptNode;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ============================================================
|
|
116
|
+
// VALIDATION RULES
|
|
117
|
+
// ============================================================
|
|
118
|
+
|
|
119
|
+
/** Rule category for grouping and organization */
|
|
120
|
+
export type RuleCategory =
|
|
121
|
+
| 'naming'
|
|
122
|
+
| 'flow'
|
|
123
|
+
| 'collections'
|
|
124
|
+
| 'loops'
|
|
125
|
+
| 'conditionals'
|
|
126
|
+
| 'closures'
|
|
127
|
+
| 'types'
|
|
128
|
+
| 'strings'
|
|
129
|
+
| 'errors'
|
|
130
|
+
| 'formatting'
|
|
131
|
+
| 'anti-patterns';
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Validation rule interface.
|
|
135
|
+
* Rules are stateless - all context passed via ValidationContext.
|
|
136
|
+
* Rules return diagnostics, never throw.
|
|
137
|
+
* fix() must preserve semantics (no behavior changes).
|
|
138
|
+
*/
|
|
139
|
+
export interface ValidationRule {
|
|
140
|
+
/** Unique rule code (e.g., NAMING_SNAKE_CASE) */
|
|
141
|
+
readonly code: string;
|
|
142
|
+
|
|
143
|
+
/** Rule category for grouping */
|
|
144
|
+
readonly category: RuleCategory;
|
|
145
|
+
|
|
146
|
+
/** Default severity level */
|
|
147
|
+
readonly severity: Severity;
|
|
148
|
+
|
|
149
|
+
/** Node types this rule applies to */
|
|
150
|
+
readonly nodeTypes: NodeType[];
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Validate a node, returning diagnostics for violations.
|
|
154
|
+
* Called for each node matching nodeTypes.
|
|
155
|
+
*/
|
|
156
|
+
validate(node: ASTNode, context: ValidationContext): Diagnostic[];
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Optionally generate a fix for a diagnostic.
|
|
160
|
+
* Returns null if fix not applicable.
|
|
161
|
+
*/
|
|
162
|
+
fix?(node: ASTNode, context: FixContext): Fix | null;
|
|
163
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Script Validator
|
|
3
|
+
* Orchestrates validation by traversing AST and invoking enabled rules.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
ScriptNode,
|
|
8
|
+
ASTNode,
|
|
9
|
+
CaptureNode,
|
|
10
|
+
TypeAssertionNode,
|
|
11
|
+
} from '@rcrsr/rill';
|
|
12
|
+
import type { CheckConfig, Diagnostic, ValidationContext } from './types.js';
|
|
13
|
+
import { visitNode, type RuleVisitor } from './visitor.js';
|
|
14
|
+
import { VALIDATION_RULES } from './rules/index.js';
|
|
15
|
+
|
|
16
|
+
// ============================================================
|
|
17
|
+
// VALIDATION ORCHESTRATOR
|
|
18
|
+
// ============================================================
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Validate script AST against all enabled rules.
|
|
22
|
+
* Traverses AST using visitor pattern, invoking enabled rules for matching nodes.
|
|
23
|
+
* Returns diagnostics sorted by line number, then column.
|
|
24
|
+
*
|
|
25
|
+
* @param ast - Parsed script AST to validate
|
|
26
|
+
* @param source - Original source text for context extraction
|
|
27
|
+
* @param config - Configuration determining which rules are active
|
|
28
|
+
* @returns Array of diagnostics sorted by location
|
|
29
|
+
*/
|
|
30
|
+
export function validateScript(
|
|
31
|
+
ast: ScriptNode,
|
|
32
|
+
source: string,
|
|
33
|
+
config: CheckConfig
|
|
34
|
+
): Diagnostic[] {
|
|
35
|
+
// Create validation context
|
|
36
|
+
const context: ValidationContext = {
|
|
37
|
+
source,
|
|
38
|
+
ast,
|
|
39
|
+
config,
|
|
40
|
+
diagnostics: [],
|
|
41
|
+
variables: new Map(),
|
|
42
|
+
assertedHostCalls: new Set(),
|
|
43
|
+
variableScopes: new Map(),
|
|
44
|
+
scopeStack: [],
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Create visitor that invokes enabled rules
|
|
48
|
+
const visitor: RuleVisitor = {
|
|
49
|
+
enter(node: ASTNode, ctx: ValidationContext): void {
|
|
50
|
+
// Track closure scope entry
|
|
51
|
+
if (node.type === 'Closure') {
|
|
52
|
+
ctx.scopeStack.push(node);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Track HostCall nodes wrapped in TypeAssertion BEFORE rules check
|
|
56
|
+
if (node.type === 'TypeAssertion') {
|
|
57
|
+
const operand = (node as TypeAssertionNode).operand;
|
|
58
|
+
if (operand?.primary.type === 'HostCall') {
|
|
59
|
+
ctx.assertedHostCalls.add(operand.primary);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// For each enabled rule that applies to this node type
|
|
64
|
+
for (const rule of VALIDATION_RULES) {
|
|
65
|
+
// Skip if rule not enabled
|
|
66
|
+
if (!isRuleEnabled(rule.code, ctx.config)) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Skip if rule doesn't apply to this node type
|
|
71
|
+
if (!rule.nodeTypes.includes(node.type)) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Invoke rule validation and accumulate diagnostics
|
|
76
|
+
const ruleDiagnostics = rule.validate(node, ctx);
|
|
77
|
+
ctx.diagnostics.push(...ruleDiagnostics);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Track variable captures AFTER rules check (for reassignment detection)
|
|
81
|
+
if (node.type === 'Capture') {
|
|
82
|
+
const captureNode = node as CaptureNode;
|
|
83
|
+
if (!ctx.variables.has(captureNode.name)) {
|
|
84
|
+
ctx.variables.set(captureNode.name, node.span.start);
|
|
85
|
+
// Track which closure scope this variable belongs to
|
|
86
|
+
const currentScope =
|
|
87
|
+
ctx.scopeStack.length > 0
|
|
88
|
+
? ctx.scopeStack[ctx.scopeStack.length - 1]
|
|
89
|
+
: null;
|
|
90
|
+
ctx.variableScopes.set(captureNode.name, currentScope ?? null);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
exit(node: ASTNode, ctx: ValidationContext): void {
|
|
96
|
+
// Track closure scope exit
|
|
97
|
+
if (node.type === 'Closure') {
|
|
98
|
+
ctx.scopeStack.pop();
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// Traverse AST with visitor
|
|
104
|
+
visitNode(ast, context, visitor);
|
|
105
|
+
|
|
106
|
+
// Sort diagnostics by location (line first, then column)
|
|
107
|
+
return sortDiagnostics(context.diagnostics);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ============================================================
|
|
111
|
+
// HELPERS
|
|
112
|
+
// ============================================================
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Check if a rule is enabled based on configuration.
|
|
116
|
+
* Rules are enabled if state is 'on' or 'warn'.
|
|
117
|
+
*/
|
|
118
|
+
function isRuleEnabled(ruleCode: string, config: CheckConfig): boolean {
|
|
119
|
+
const state = config.rules[ruleCode];
|
|
120
|
+
return state === 'on' || state === 'warn';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Sort diagnostics by line number first, then column number.
|
|
125
|
+
* Stable sort preserves original order for diagnostics at same location.
|
|
126
|
+
*/
|
|
127
|
+
function sortDiagnostics(diagnostics: Diagnostic[]): Diagnostic[] {
|
|
128
|
+
return [...diagnostics].sort((a, b) => {
|
|
129
|
+
// Sort by line first
|
|
130
|
+
if (a.location.line !== b.location.line) {
|
|
131
|
+
return a.location.line - b.location.line;
|
|
132
|
+
}
|
|
133
|
+
// Then by column
|
|
134
|
+
return a.location.column - b.location.column;
|
|
135
|
+
});
|
|
136
|
+
}
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AST Visitor
|
|
3
|
+
* Recursive traversal with enter/exit callbacks for validation rules.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ASTNode } from '@rcrsr/rill';
|
|
7
|
+
import type { ValidationContext } from './types.js';
|
|
8
|
+
|
|
9
|
+
// ============================================================
|
|
10
|
+
// VISITOR INTERFACE
|
|
11
|
+
// ============================================================
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Visitor pattern interface for AST traversal.
|
|
15
|
+
* Provides enter/exit callbacks invoked before and after visiting children.
|
|
16
|
+
*/
|
|
17
|
+
export interface RuleVisitor {
|
|
18
|
+
/**
|
|
19
|
+
* Called before visiting node's children.
|
|
20
|
+
* Use for pre-order traversal validation.
|
|
21
|
+
*/
|
|
22
|
+
enter(node: ASTNode, context: ValidationContext): void;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Called after visiting node's children.
|
|
26
|
+
* Use for post-order traversal validation.
|
|
27
|
+
*/
|
|
28
|
+
exit(node: ASTNode, context: ValidationContext): void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ============================================================
|
|
32
|
+
// VISITOR FUNCTION
|
|
33
|
+
// ============================================================
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Recursively visit AST nodes with enter/exit callbacks.
|
|
37
|
+
* Handles all 46 node types from ASTNode union.
|
|
38
|
+
*
|
|
39
|
+
* Traversal order:
|
|
40
|
+
* 1. visitor.enter(node)
|
|
41
|
+
* 2. Recurse into children
|
|
42
|
+
* 3. visitor.exit(node)
|
|
43
|
+
*/
|
|
44
|
+
export function visitNode(
|
|
45
|
+
node: ASTNode,
|
|
46
|
+
context: ValidationContext,
|
|
47
|
+
visitor: RuleVisitor
|
|
48
|
+
): void {
|
|
49
|
+
// Enter callback before children
|
|
50
|
+
visitor.enter(node, context);
|
|
51
|
+
|
|
52
|
+
// Recurse based on node type
|
|
53
|
+
switch (node.type) {
|
|
54
|
+
case 'Script':
|
|
55
|
+
if (node.frontmatter) {
|
|
56
|
+
visitNode(node.frontmatter, context, visitor);
|
|
57
|
+
}
|
|
58
|
+
for (const stmt of node.statements) {
|
|
59
|
+
visitNode(stmt, context, visitor);
|
|
60
|
+
}
|
|
61
|
+
break;
|
|
62
|
+
|
|
63
|
+
case 'Frontmatter':
|
|
64
|
+
// Leaf node - no children
|
|
65
|
+
break;
|
|
66
|
+
|
|
67
|
+
case 'Statement':
|
|
68
|
+
visitNode(node.expression, context, visitor);
|
|
69
|
+
break;
|
|
70
|
+
|
|
71
|
+
case 'AnnotatedStatement':
|
|
72
|
+
for (const arg of node.annotations) {
|
|
73
|
+
visitNode(arg, context, visitor);
|
|
74
|
+
}
|
|
75
|
+
visitNode(node.statement, context, visitor);
|
|
76
|
+
break;
|
|
77
|
+
|
|
78
|
+
case 'NamedArg':
|
|
79
|
+
visitNode(node.value, context, visitor);
|
|
80
|
+
break;
|
|
81
|
+
|
|
82
|
+
case 'SpreadArg':
|
|
83
|
+
visitNode(node.expression, context, visitor);
|
|
84
|
+
break;
|
|
85
|
+
|
|
86
|
+
case 'PipeChain':
|
|
87
|
+
visitNode(node.head, context, visitor);
|
|
88
|
+
for (const pipe of node.pipes) {
|
|
89
|
+
visitNode(pipe, context, visitor);
|
|
90
|
+
}
|
|
91
|
+
if (node.terminator) {
|
|
92
|
+
visitNode(node.terminator, context, visitor);
|
|
93
|
+
}
|
|
94
|
+
break;
|
|
95
|
+
|
|
96
|
+
case 'PostfixExpr':
|
|
97
|
+
visitNode(node.primary, context, visitor);
|
|
98
|
+
for (const method of node.methods) {
|
|
99
|
+
visitNode(method, context, visitor);
|
|
100
|
+
}
|
|
101
|
+
break;
|
|
102
|
+
|
|
103
|
+
case 'BinaryExpr':
|
|
104
|
+
visitNode(node.left, context, visitor);
|
|
105
|
+
visitNode(node.right, context, visitor);
|
|
106
|
+
break;
|
|
107
|
+
|
|
108
|
+
case 'UnaryExpr':
|
|
109
|
+
visitNode(node.operand, context, visitor);
|
|
110
|
+
break;
|
|
111
|
+
|
|
112
|
+
case 'GroupedExpr':
|
|
113
|
+
visitNode(node.expression, context, visitor);
|
|
114
|
+
break;
|
|
115
|
+
|
|
116
|
+
case 'StringLiteral':
|
|
117
|
+
for (const part of node.parts) {
|
|
118
|
+
if (typeof part !== 'string') {
|
|
119
|
+
visitNode(part, context, visitor);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
break;
|
|
123
|
+
|
|
124
|
+
case 'Interpolation':
|
|
125
|
+
visitNode(node.expression, context, visitor);
|
|
126
|
+
break;
|
|
127
|
+
|
|
128
|
+
case 'NumberLiteral':
|
|
129
|
+
case 'BoolLiteral':
|
|
130
|
+
// Leaf nodes - no children
|
|
131
|
+
break;
|
|
132
|
+
|
|
133
|
+
case 'Tuple':
|
|
134
|
+
for (const element of node.elements) {
|
|
135
|
+
visitNode(element, context, visitor);
|
|
136
|
+
}
|
|
137
|
+
break;
|
|
138
|
+
|
|
139
|
+
case 'ListSpread':
|
|
140
|
+
visitNode(node.expression, context, visitor);
|
|
141
|
+
break;
|
|
142
|
+
|
|
143
|
+
case 'Dict':
|
|
144
|
+
for (const entry of node.entries) {
|
|
145
|
+
visitNode(entry, context, visitor);
|
|
146
|
+
}
|
|
147
|
+
break;
|
|
148
|
+
|
|
149
|
+
case 'DictEntry':
|
|
150
|
+
visitNode(node.value, context, visitor);
|
|
151
|
+
break;
|
|
152
|
+
|
|
153
|
+
case 'Closure':
|
|
154
|
+
for (const param of node.params) {
|
|
155
|
+
visitNode(param, context, visitor);
|
|
156
|
+
}
|
|
157
|
+
visitNode(node.body, context, visitor);
|
|
158
|
+
break;
|
|
159
|
+
|
|
160
|
+
case 'ClosureParam':
|
|
161
|
+
if (node.defaultValue) {
|
|
162
|
+
visitNode(node.defaultValue, context, visitor);
|
|
163
|
+
}
|
|
164
|
+
break;
|
|
165
|
+
|
|
166
|
+
case 'Variable':
|
|
167
|
+
if (node.defaultValue) {
|
|
168
|
+
visitNode(node.defaultValue, context, visitor);
|
|
169
|
+
}
|
|
170
|
+
break;
|
|
171
|
+
|
|
172
|
+
case 'HostCall':
|
|
173
|
+
for (const arg of node.args) {
|
|
174
|
+
visitNode(arg, context, visitor);
|
|
175
|
+
}
|
|
176
|
+
break;
|
|
177
|
+
|
|
178
|
+
case 'ClosureCall':
|
|
179
|
+
for (const arg of node.args) {
|
|
180
|
+
visitNode(arg, context, visitor);
|
|
181
|
+
}
|
|
182
|
+
break;
|
|
183
|
+
|
|
184
|
+
case 'MethodCall':
|
|
185
|
+
for (const arg of node.args) {
|
|
186
|
+
visitNode(arg, context, visitor);
|
|
187
|
+
}
|
|
188
|
+
break;
|
|
189
|
+
|
|
190
|
+
case 'Invoke':
|
|
191
|
+
for (const arg of node.args) {
|
|
192
|
+
visitNode(arg, context, visitor);
|
|
193
|
+
}
|
|
194
|
+
break;
|
|
195
|
+
|
|
196
|
+
case 'PipeInvoke':
|
|
197
|
+
for (const arg of node.args) {
|
|
198
|
+
visitNode(arg, context, visitor);
|
|
199
|
+
}
|
|
200
|
+
break;
|
|
201
|
+
|
|
202
|
+
case 'Conditional':
|
|
203
|
+
if (node.input) {
|
|
204
|
+
visitNode(node.input, context, visitor);
|
|
205
|
+
}
|
|
206
|
+
if (node.condition) {
|
|
207
|
+
visitNode(node.condition, context, visitor);
|
|
208
|
+
}
|
|
209
|
+
visitNode(node.thenBranch, context, visitor);
|
|
210
|
+
if (node.elseBranch) {
|
|
211
|
+
visitNode(node.elseBranch, context, visitor);
|
|
212
|
+
}
|
|
213
|
+
break;
|
|
214
|
+
|
|
215
|
+
case 'WhileLoop':
|
|
216
|
+
visitNode(node.condition, context, visitor);
|
|
217
|
+
visitNode(node.body, context, visitor);
|
|
218
|
+
break;
|
|
219
|
+
|
|
220
|
+
case 'DoWhileLoop':
|
|
221
|
+
if (node.input) {
|
|
222
|
+
visitNode(node.input, context, visitor);
|
|
223
|
+
}
|
|
224
|
+
visitNode(node.body, context, visitor);
|
|
225
|
+
visitNode(node.condition, context, visitor);
|
|
226
|
+
break;
|
|
227
|
+
|
|
228
|
+
case 'Block':
|
|
229
|
+
for (const stmt of node.statements) {
|
|
230
|
+
visitNode(stmt, context, visitor);
|
|
231
|
+
}
|
|
232
|
+
break;
|
|
233
|
+
|
|
234
|
+
case 'EachExpr':
|
|
235
|
+
visitNode(node.body, context, visitor);
|
|
236
|
+
if (node.accumulator) {
|
|
237
|
+
visitNode(node.accumulator, context, visitor);
|
|
238
|
+
}
|
|
239
|
+
break;
|
|
240
|
+
|
|
241
|
+
case 'MapExpr':
|
|
242
|
+
visitNode(node.body, context, visitor);
|
|
243
|
+
break;
|
|
244
|
+
|
|
245
|
+
case 'FoldExpr':
|
|
246
|
+
visitNode(node.body, context, visitor);
|
|
247
|
+
if (node.accumulator) {
|
|
248
|
+
visitNode(node.accumulator, context, visitor);
|
|
249
|
+
}
|
|
250
|
+
break;
|
|
251
|
+
|
|
252
|
+
case 'FilterExpr':
|
|
253
|
+
visitNode(node.body, context, visitor);
|
|
254
|
+
break;
|
|
255
|
+
|
|
256
|
+
case 'ClosureChain':
|
|
257
|
+
visitNode(node.target, context, visitor);
|
|
258
|
+
break;
|
|
259
|
+
|
|
260
|
+
case 'Destructure':
|
|
261
|
+
for (const element of node.elements) {
|
|
262
|
+
visitNode(element, context, visitor);
|
|
263
|
+
}
|
|
264
|
+
break;
|
|
265
|
+
|
|
266
|
+
case 'DestructPattern':
|
|
267
|
+
if (node.nested) {
|
|
268
|
+
visitNode(node.nested, context, visitor);
|
|
269
|
+
}
|
|
270
|
+
break;
|
|
271
|
+
|
|
272
|
+
case 'Slice':
|
|
273
|
+
if (node.start) {
|
|
274
|
+
visitNode(node.start, context, visitor);
|
|
275
|
+
}
|
|
276
|
+
if (node.stop) {
|
|
277
|
+
visitNode(node.stop, context, visitor);
|
|
278
|
+
}
|
|
279
|
+
if (node.step) {
|
|
280
|
+
visitNode(node.step, context, visitor);
|
|
281
|
+
}
|
|
282
|
+
break;
|
|
283
|
+
|
|
284
|
+
case 'Spread':
|
|
285
|
+
if (node.operand) {
|
|
286
|
+
visitNode(node.operand, context, visitor);
|
|
287
|
+
}
|
|
288
|
+
break;
|
|
289
|
+
|
|
290
|
+
case 'TypeAssertion':
|
|
291
|
+
if (node.operand) {
|
|
292
|
+
visitNode(node.operand, context, visitor);
|
|
293
|
+
}
|
|
294
|
+
break;
|
|
295
|
+
|
|
296
|
+
case 'TypeCheck':
|
|
297
|
+
if (node.operand) {
|
|
298
|
+
visitNode(node.operand, context, visitor);
|
|
299
|
+
}
|
|
300
|
+
break;
|
|
301
|
+
|
|
302
|
+
case 'Assert':
|
|
303
|
+
visitNode(node.condition, context, visitor);
|
|
304
|
+
if (node.message) {
|
|
305
|
+
visitNode(node.message, context, visitor);
|
|
306
|
+
}
|
|
307
|
+
break;
|
|
308
|
+
|
|
309
|
+
case 'Capture':
|
|
310
|
+
case 'Break':
|
|
311
|
+
case 'Return':
|
|
312
|
+
case 'Pass':
|
|
313
|
+
// Leaf nodes - no children
|
|
314
|
+
break;
|
|
315
|
+
|
|
316
|
+
case 'RecoveryError':
|
|
317
|
+
// Recovery error node - no children to visit
|
|
318
|
+
break;
|
|
319
|
+
|
|
320
|
+
case 'Error':
|
|
321
|
+
// Error statement node - visit message if present
|
|
322
|
+
if (node.message) {
|
|
323
|
+
visitNode(node.message, context, visitor);
|
|
324
|
+
}
|
|
325
|
+
break;
|
|
326
|
+
|
|
327
|
+
default: {
|
|
328
|
+
// Exhaustive check: if we reach here, a node type is missing
|
|
329
|
+
const _exhaustive: never = node;
|
|
330
|
+
throw new Error(
|
|
331
|
+
`Unhandled node type in visitor: ${(_exhaustive as ASTNode).type}`
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Exit callback after children
|
|
337
|
+
visitor.exit(node, context);
|
|
338
|
+
}
|