@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.
Files changed (171) hide show
  1. package/LICENSE +21 -0
  2. package/dist/check/config.d.ts +20 -0
  3. package/dist/check/config.d.ts.map +1 -0
  4. package/dist/check/config.js +151 -0
  5. package/dist/check/config.js.map +1 -0
  6. package/dist/check/fixer.d.ts +39 -0
  7. package/dist/check/fixer.d.ts.map +1 -0
  8. package/dist/check/fixer.js +119 -0
  9. package/dist/check/fixer.js.map +1 -0
  10. package/dist/check/index.d.ts +10 -0
  11. package/dist/check/index.d.ts.map +1 -0
  12. package/dist/check/index.js +21 -0
  13. package/dist/check/index.js.map +1 -0
  14. package/dist/check/rules/anti-patterns.d.ts +65 -0
  15. package/dist/check/rules/anti-patterns.d.ts.map +1 -0
  16. package/dist/check/rules/anti-patterns.js +481 -0
  17. package/dist/check/rules/anti-patterns.js.map +1 -0
  18. package/dist/check/rules/closures.d.ts +66 -0
  19. package/dist/check/rules/closures.d.ts.map +1 -0
  20. package/dist/check/rules/closures.js +370 -0
  21. package/dist/check/rules/closures.js.map +1 -0
  22. package/dist/check/rules/collections.d.ts +90 -0
  23. package/dist/check/rules/collections.d.ts.map +1 -0
  24. package/dist/check/rules/collections.js +373 -0
  25. package/dist/check/rules/collections.js.map +1 -0
  26. package/dist/check/rules/conditionals.d.ts +41 -0
  27. package/dist/check/rules/conditionals.d.ts.map +1 -0
  28. package/dist/check/rules/conditionals.js +134 -0
  29. package/dist/check/rules/conditionals.js.map +1 -0
  30. package/dist/check/rules/flow.d.ts +46 -0
  31. package/dist/check/rules/flow.d.ts.map +1 -0
  32. package/dist/check/rules/flow.js +206 -0
  33. package/dist/check/rules/flow.js.map +1 -0
  34. package/dist/check/rules/formatting.d.ts +143 -0
  35. package/dist/check/rules/formatting.d.ts.map +1 -0
  36. package/dist/check/rules/formatting.js +656 -0
  37. package/dist/check/rules/formatting.js.map +1 -0
  38. package/dist/check/rules/helpers.d.ts +26 -0
  39. package/dist/check/rules/helpers.d.ts.map +1 -0
  40. package/dist/check/rules/helpers.js +66 -0
  41. package/dist/check/rules/helpers.js.map +1 -0
  42. package/dist/check/rules/index.d.ts +21 -0
  43. package/dist/check/rules/index.d.ts.map +1 -0
  44. package/dist/check/rules/index.js +78 -0
  45. package/dist/check/rules/index.js.map +1 -0
  46. package/dist/check/rules/loops.d.ts +77 -0
  47. package/dist/check/rules/loops.d.ts.map +1 -0
  48. package/dist/check/rules/loops.js +310 -0
  49. package/dist/check/rules/loops.js.map +1 -0
  50. package/dist/check/rules/naming.d.ts +21 -0
  51. package/dist/check/rules/naming.d.ts.map +1 -0
  52. package/dist/check/rules/naming.js +174 -0
  53. package/dist/check/rules/naming.js.map +1 -0
  54. package/dist/check/rules/strings.d.ts +28 -0
  55. package/dist/check/rules/strings.d.ts.map +1 -0
  56. package/dist/check/rules/strings.js +79 -0
  57. package/dist/check/rules/strings.js.map +1 -0
  58. package/dist/check/rules/types.d.ts +41 -0
  59. package/dist/check/rules/types.d.ts.map +1 -0
  60. package/dist/check/rules/types.js +167 -0
  61. package/dist/check/rules/types.js.map +1 -0
  62. package/dist/check/types.d.ts +112 -0
  63. package/dist/check/types.d.ts.map +1 -0
  64. package/dist/check/types.js +6 -0
  65. package/dist/check/types.js.map +1 -0
  66. package/dist/check/validator.d.ts +18 -0
  67. package/dist/check/validator.d.ts.map +1 -0
  68. package/dist/check/validator.js +110 -0
  69. package/dist/check/validator.js.map +1 -0
  70. package/dist/check/visitor.d.ts +33 -0
  71. package/dist/check/visitor.d.ts.map +1 -0
  72. package/dist/check/visitor.js +259 -0
  73. package/dist/check/visitor.js.map +1 -0
  74. package/dist/cli-check.d.ts +43 -0
  75. package/dist/cli-check.d.ts.map +1 -0
  76. package/dist/cli-check.js +366 -0
  77. package/dist/cli-check.js.map +1 -0
  78. package/dist/cli-error-enrichment.d.ts +73 -0
  79. package/dist/cli-error-enrichment.d.ts.map +1 -0
  80. package/dist/cli-error-enrichment.js +205 -0
  81. package/dist/cli-error-enrichment.js.map +1 -0
  82. package/dist/cli-error-formatter.d.ts +45 -0
  83. package/dist/cli-error-formatter.d.ts.map +1 -0
  84. package/dist/cli-error-formatter.js +218 -0
  85. package/dist/cli-error-formatter.js.map +1 -0
  86. package/dist/cli-eval.d.ts +15 -0
  87. package/dist/cli-eval.d.ts.map +1 -0
  88. package/dist/cli-eval.js +116 -0
  89. package/dist/cli-eval.js.map +1 -0
  90. package/dist/cli-exec.d.ts +58 -0
  91. package/dist/cli-exec.d.ts.map +1 -0
  92. package/dist/cli-exec.js +326 -0
  93. package/dist/cli-exec.js.map +1 -0
  94. package/dist/cli-explain.d.ts +24 -0
  95. package/dist/cli-explain.d.ts.map +1 -0
  96. package/dist/cli-explain.js +68 -0
  97. package/dist/cli-explain.js.map +1 -0
  98. package/dist/cli-lsp-diagnostic.d.ts +35 -0
  99. package/dist/cli-lsp-diagnostic.d.ts.map +1 -0
  100. package/dist/cli-lsp-diagnostic.js +98 -0
  101. package/dist/cli-lsp-diagnostic.js.map +1 -0
  102. package/dist/cli-module-loader.d.ts +19 -0
  103. package/dist/cli-module-loader.d.ts.map +1 -0
  104. package/dist/cli-module-loader.js +83 -0
  105. package/dist/cli-module-loader.js.map +1 -0
  106. package/dist/cli-shared.d.ts +62 -0
  107. package/dist/cli-shared.d.ts.map +1 -0
  108. package/dist/cli-shared.js +158 -0
  109. package/dist/cli-shared.js.map +1 -0
  110. package/dist/cli.d.ts +13 -0
  111. package/dist/cli.d.ts.map +1 -0
  112. package/dist/cli.js +62 -0
  113. package/dist/cli.js.map +1 -0
  114. package/dist/test-internal-import.d.ts +2 -0
  115. package/dist/test-internal-import.d.ts.map +1 -0
  116. package/dist/test-internal-import.js +7 -0
  117. package/dist/test-internal-import.js.map +1 -0
  118. package/package.json +24 -0
  119. package/src/check/config.ts +202 -0
  120. package/src/check/fixer.ts +174 -0
  121. package/src/check/index.ts +39 -0
  122. package/src/check/rules/anti-patterns.ts +585 -0
  123. package/src/check/rules/closures.ts +445 -0
  124. package/src/check/rules/collections.ts +437 -0
  125. package/src/check/rules/conditionals.ts +155 -0
  126. package/src/check/rules/flow.ts +262 -0
  127. package/src/check/rules/formatting.ts +811 -0
  128. package/src/check/rules/helpers.ts +89 -0
  129. package/src/check/rules/index.ts +140 -0
  130. package/src/check/rules/loops.ts +372 -0
  131. package/src/check/rules/naming.ts +242 -0
  132. package/src/check/rules/strings.ts +104 -0
  133. package/src/check/rules/types.ts +214 -0
  134. package/src/check/types.ts +163 -0
  135. package/src/check/validator.ts +136 -0
  136. package/src/check/visitor.ts +338 -0
  137. package/src/cli-check.ts +456 -0
  138. package/src/cli-error-enrichment.ts +274 -0
  139. package/src/cli-error-formatter.ts +313 -0
  140. package/src/cli-eval.ts +145 -0
  141. package/src/cli-exec.ts +408 -0
  142. package/src/cli-explain.ts +76 -0
  143. package/src/cli-lsp-diagnostic.ts +132 -0
  144. package/src/cli-module-loader.ts +101 -0
  145. package/src/cli-shared.ts +187 -0
  146. package/tests/check/cli-check.test.ts +189 -0
  147. package/tests/check/config.test.ts +350 -0
  148. package/tests/check/fixer.test.ts +373 -0
  149. package/tests/check/format-diagnostics.test.ts +327 -0
  150. package/tests/check/rules/anti-patterns.test.ts +467 -0
  151. package/tests/check/rules/closures.test.ts +192 -0
  152. package/tests/check/rules/collections.test.ts +380 -0
  153. package/tests/check/rules/conditionals.test.ts +185 -0
  154. package/tests/check/rules/flow.test.ts +250 -0
  155. package/tests/check/rules/formatting.test.ts +755 -0
  156. package/tests/check/rules/loops.test.ts +334 -0
  157. package/tests/check/rules/naming.test.ts +336 -0
  158. package/tests/check/rules/strings.test.ts +129 -0
  159. package/tests/check/rules/types.test.ts +257 -0
  160. package/tests/check/validator.test.ts +444 -0
  161. package/tests/check/visitor.test.ts +171 -0
  162. package/tests/cli/check.test.ts +801 -0
  163. package/tests/cli/error-enrichment.test.ts +510 -0
  164. package/tests/cli/error-formatter.test.ts +631 -0
  165. package/tests/cli/eval.test.ts +85 -0
  166. package/tests/cli/exec.test.ts +537 -0
  167. package/tests/cli-explain.test.ts +249 -0
  168. package/tests/cli-lsp-diagnostic.test.ts +202 -0
  169. package/tests/cli-shared.test.ts +439 -0
  170. package/tsconfig.json +9 -0
  171. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,242 @@
1
+ /**
2
+ * Naming Convention Rules
3
+ * Enforces snake_case naming for variables, parameters, and dict keys.
4
+ */
5
+
6
+ import type {
7
+ ValidationRule,
8
+ Diagnostic,
9
+ Fix,
10
+ ValidationContext,
11
+ FixContext,
12
+ } from '../types.js';
13
+ import type {
14
+ ASTNode,
15
+ ClosureParamNode,
16
+ DictEntryNode,
17
+ CaptureNode,
18
+ SourceLocation,
19
+ } from '@rcrsr/rill';
20
+ import { extractContextLine } from './helpers.js';
21
+
22
+ // ============================================================
23
+ // NAMING VALIDATION
24
+ // ============================================================
25
+
26
+ /**
27
+ * Check if a name follows snake_case convention.
28
+ * Valid: user_name, item_list, is_valid, x, count
29
+ * Invalid: userName, ItemList, user-name, user.name
30
+ */
31
+ function isSnakeCase(name: string): boolean {
32
+ // Empty string is invalid
33
+ if (!name) return false;
34
+
35
+ // Must match: lowercase letters, numbers, underscores only
36
+ // Must start with letter or underscore
37
+ // No consecutive underscores, no trailing underscore
38
+ const snakeCasePattern = /^[a-z_][a-z0-9_]*$/;
39
+ if (!snakeCasePattern.test(name)) return false;
40
+
41
+ // Reject consecutive underscores
42
+ if (name.includes('__')) return false;
43
+
44
+ // Reject trailing underscore (unless single underscore)
45
+ if (name.length > 1 && name.endsWith('_')) return false;
46
+
47
+ return true;
48
+ }
49
+
50
+ /**
51
+ * Convert a name to snake_case.
52
+ * Handles camelCase, PascalCase, kebab-case, and mixed formats.
53
+ */
54
+ function toSnakeCase(name: string): string {
55
+ return (
56
+ name
57
+ // Handle consecutive uppercase (XMLParser -> xml_parser)
58
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2')
59
+ // Insert underscore before uppercase letters (camelCase -> camel_case)
60
+ .replace(/([a-z0-9])([A-Z])/g, '$1_$2')
61
+ // Replace hyphens and dots with underscores
62
+ .replace(/[-.\s]+/g, '_')
63
+ // Convert to lowercase
64
+ .toLowerCase()
65
+ // Remove consecutive underscores
66
+ .replace(/_+/g, '_')
67
+ // Remove leading/trailing underscores
68
+ .replace(/^_+|_+$/g, '')
69
+ );
70
+ }
71
+
72
+ /**
73
+ * Create a diagnostic for snake_case violation.
74
+ */
75
+ function createNamingDiagnostic(
76
+ location: SourceLocation,
77
+ name: string,
78
+ kind: string,
79
+ context: ValidationContext,
80
+ fix: Fix | null
81
+ ): Diagnostic {
82
+ const message = `${kind} '${name}' should use snake_case (e.g., '${toSnakeCase(name)}')`;
83
+
84
+ return {
85
+ location,
86
+ severity: 'error',
87
+ code: 'NAMING_SNAKE_CASE',
88
+ message,
89
+ context: extractContextLine(location.line, context.source),
90
+ fix,
91
+ };
92
+ }
93
+
94
+ // ============================================================
95
+ // NAMING_SNAKE_CASE RULE
96
+ // ============================================================
97
+
98
+ /**
99
+ * Validates that variable definitions, parameters, and dict keys use snake_case.
100
+ *
101
+ * Checks definition sites only (not variable usage):
102
+ * - Captures: => $user_name, => $item_list, => $is_valid
103
+ * - Closure params: |user_name, count| { }
104
+ * - Dict keys: [user_name: "Alice", is_active: true]
105
+ *
106
+ * Exceptions:
107
+ * - Single-letter names are valid (common for loop variables)
108
+ *
109
+ * References:
110
+ * - docs/guide-conventions.md:10-53
111
+ */
112
+ export const NAMING_SNAKE_CASE: ValidationRule = {
113
+ code: 'NAMING_SNAKE_CASE',
114
+ category: 'naming',
115
+ severity: 'error',
116
+ nodeTypes: ['ClosureParam', 'DictEntry', 'Capture'],
117
+
118
+ validate(node: ASTNode, context: ValidationContext): Diagnostic[] {
119
+ switch (node.type) {
120
+ case 'ClosureParam': {
121
+ const paramNode = node as ClosureParamNode;
122
+ const name = paramNode.name;
123
+
124
+ if (!isSnakeCase(name)) {
125
+ const fix = this.fix?.(node, context) ?? null;
126
+ return [
127
+ createNamingDiagnostic(
128
+ paramNode.span.start,
129
+ name,
130
+ 'Parameter',
131
+ context,
132
+ fix
133
+ ),
134
+ ];
135
+ }
136
+ return [];
137
+ }
138
+
139
+ case 'DictEntry': {
140
+ const entryNode = node as DictEntryNode;
141
+ const key = entryNode.key;
142
+
143
+ // Skip multi-key entries (tuple keys) - only validate string keys
144
+ if (typeof key !== 'string') {
145
+ return [];
146
+ }
147
+
148
+ if (!isSnakeCase(key)) {
149
+ const fix = this.fix?.(node, context) ?? null;
150
+ return [
151
+ createNamingDiagnostic(
152
+ entryNode.span.start,
153
+ key,
154
+ 'Dict key',
155
+ context,
156
+ fix
157
+ ),
158
+ ];
159
+ }
160
+ return [];
161
+ }
162
+
163
+ case 'Capture': {
164
+ const captureNode = node as CaptureNode;
165
+ const name = captureNode.name;
166
+
167
+ if (!isSnakeCase(name)) {
168
+ const fix = this.fix?.(node, context) ?? null;
169
+ return [
170
+ createNamingDiagnostic(
171
+ captureNode.span.start,
172
+ name,
173
+ 'Captured variable',
174
+ context,
175
+ fix
176
+ ),
177
+ ];
178
+ }
179
+ return [];
180
+ }
181
+
182
+ default:
183
+ return [];
184
+ }
185
+ },
186
+
187
+ fix(node: ASTNode, context: FixContext): Fix | null {
188
+ let name: string | null = null;
189
+ let range = node.span;
190
+
191
+ // Extract name and determine replacement range
192
+ switch (node.type) {
193
+ case 'ClosureParam': {
194
+ const paramNode = node as ClosureParamNode;
195
+ name = paramNode.name;
196
+ break;
197
+ }
198
+
199
+ case 'DictEntry': {
200
+ const entryNode = node as DictEntryNode;
201
+ // Skip multi-key entries (tuple keys) - only fix string keys
202
+ if (typeof entryNode.key === 'string') {
203
+ name = entryNode.key;
204
+ // For dict entries, replace only the key portion before the colon
205
+ }
206
+ break;
207
+ }
208
+
209
+ case 'Capture': {
210
+ const captureNode = node as CaptureNode;
211
+ name = captureNode.name;
212
+ break;
213
+ }
214
+
215
+ default:
216
+ return null;
217
+ }
218
+
219
+ if (!name || isSnakeCase(name)) {
220
+ return null;
221
+ }
222
+
223
+ const snakeCaseName = toSnakeCase(name);
224
+
225
+ // Get original source text for this node
226
+ const sourceText = context.source.substring(
227
+ range.start.offset,
228
+ range.end.offset
229
+ );
230
+
231
+ // Replace the original name with snake_case version
232
+ // This preserves $ prefix for variables and : for dict entries
233
+ const replacement = sourceText.replace(name, snakeCaseName);
234
+
235
+ return {
236
+ description: `Rename '${name}' to '${snakeCaseName}'`,
237
+ applicable: true,
238
+ range,
239
+ replacement,
240
+ };
241
+ },
242
+ };
@@ -0,0 +1,104 @@
1
+ /**
2
+ * String Handling Convention Rules
3
+ * Enforces string handling best practices from docs/guide-conventions.md:318-352.
4
+ */
5
+
6
+ import type {
7
+ ValidationRule,
8
+ Diagnostic,
9
+ ValidationContext,
10
+ } from '../types.js';
11
+ import type { ASTNode, StringLiteralNode, BinaryExprNode } from '@rcrsr/rill';
12
+ import { extractContextLine } from './helpers.js';
13
+
14
+ // ============================================================
15
+ // USE_EMPTY_METHOD RULE
16
+ // ============================================================
17
+
18
+ /**
19
+ * Enforces .empty method for emptiness checks.
20
+ * Direct string comparison with "" is not idiomatic and may not work
21
+ * correctly in all contexts. Use .empty method instead.
22
+ *
23
+ * Detection:
24
+ * - BinaryExpr with == or != operator
25
+ * - One side is empty string literal ""
26
+ * - Suggests using .empty method
27
+ *
28
+ * Valid patterns:
29
+ * - $str -> .empty (check if empty)
30
+ * - $str -> .empty ? "yes" ! "no" (conditional)
31
+ *
32
+ * Discouraged:
33
+ * - $str == "" (direct comparison)
34
+ * - $str != "" (direct comparison)
35
+ *
36
+ * References:
37
+ * - docs/guide-conventions.md:333-345
38
+ */
39
+ export const USE_EMPTY_METHOD: ValidationRule = {
40
+ code: 'USE_EMPTY_METHOD',
41
+ category: 'strings',
42
+ severity: 'warning',
43
+ nodeTypes: ['BinaryExpr'],
44
+
45
+ validate(node: ASTNode, context: ValidationContext): Diagnostic[] {
46
+ const binaryNode = node as BinaryExprNode;
47
+
48
+ // Only check equality operators
49
+ if (binaryNode.op !== '==' && binaryNode.op !== '!=') {
50
+ return [];
51
+ }
52
+
53
+ // left and right are PostfixExpr - check their primaries
54
+ const { left, right } = binaryNode;
55
+
56
+ const leftIsEmpty =
57
+ left.type === 'PostfixExpr' && isEmptyStringLiteral(left.primary);
58
+ const rightIsEmpty =
59
+ right.type === 'PostfixExpr' && isEmptyStringLiteral(right.primary);
60
+
61
+ if (leftIsEmpty || rightIsEmpty) {
62
+ const suggestedMethod = binaryNode.op === '==' ? '.empty' : '.empty -> !';
63
+
64
+ return [
65
+ {
66
+ location: binaryNode.span.start,
67
+ severity: 'warning',
68
+ code: 'USE_EMPTY_METHOD',
69
+ message: `Use ${suggestedMethod} for emptiness checks instead of comparing with ""`,
70
+ context: extractContextLine(
71
+ binaryNode.span.start.line,
72
+ context.source
73
+ ),
74
+ fix: null, // Auto-fix would require expression reconstruction
75
+ },
76
+ ];
77
+ }
78
+
79
+ return [];
80
+ },
81
+ };
82
+
83
+ /**
84
+ * Check if a node is an empty string literal.
85
+ */
86
+ function isEmptyStringLiteral(node: ASTNode): boolean {
87
+ if (node.type !== 'StringLiteral') {
88
+ return false;
89
+ }
90
+
91
+ const stringNode = node as StringLiteralNode;
92
+
93
+ // Check if all parts are empty strings (no interpolations)
94
+ if (stringNode.parts.length === 0) {
95
+ return true;
96
+ }
97
+
98
+ if (stringNode.parts.length === 1) {
99
+ const part = stringNode.parts[0];
100
+ return typeof part === 'string' && part === '';
101
+ }
102
+
103
+ return false;
104
+ }
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Type Safety Convention Rules
3
+ * Enforces type annotation best practices from docs/guide-conventions.md:288-316.
4
+ */
5
+
6
+ import type {
7
+ ValidationRule,
8
+ Diagnostic,
9
+ Fix,
10
+ ValidationContext,
11
+ FixContext,
12
+ } from '../types.js';
13
+ import type { ASTNode, HostCallNode, TypeAssertionNode } from '@rcrsr/rill';
14
+ import { extractContextLine } from './helpers.js';
15
+
16
+ // ============================================================
17
+ // UNNECESSARY_ASSERTION RULE
18
+ // ============================================================
19
+
20
+ /**
21
+ * Detects redundant type assertions on literal values.
22
+ * Type assertions are for validation, not conversion. Asserting a literal's
23
+ * type is unnecessary because the type is already known at parse time.
24
+ *
25
+ * Redundant patterns:
26
+ * - 5:number (number literal with number assertion)
27
+ * - "hello":string (string literal with string assertion)
28
+ * - true:bool (bool literal with bool assertion)
29
+ *
30
+ * Valid patterns:
31
+ * - parseJson($input):dict (external input validation)
32
+ * - $userInput:string (runtime validation)
33
+ *
34
+ * References:
35
+ * - docs/guide-conventions.md:305-315
36
+ */
37
+ export const UNNECESSARY_ASSERTION: ValidationRule = {
38
+ code: 'UNNECESSARY_ASSERTION',
39
+ category: 'types',
40
+ severity: 'info',
41
+ nodeTypes: ['TypeAssertion'],
42
+
43
+ validate(node: ASTNode, context: ValidationContext): Diagnostic[] {
44
+ const assertionNode = node as TypeAssertionNode;
45
+ const operand = assertionNode.operand;
46
+
47
+ // Bare assertions (:type) are valid - they check pipe value
48
+ if (!operand) {
49
+ return [];
50
+ }
51
+
52
+ // operand is PostfixExprNode - check the primary
53
+ const primary = operand.primary;
54
+
55
+ // Check if primary is a literal
56
+ const isLiteral =
57
+ primary.type === 'NumberLiteral' ||
58
+ primary.type === 'StringLiteral' ||
59
+ primary.type === 'BoolLiteral';
60
+
61
+ if (!isLiteral) {
62
+ return [];
63
+ }
64
+
65
+ // Check if the assertion matches the literal type
66
+ const literalType = getLiteralType(primary);
67
+ const assertedType = assertionNode.typeName;
68
+
69
+ if (literalType === assertedType) {
70
+ const fix = this.fix?.(node, context) ?? null;
71
+
72
+ return [
73
+ {
74
+ location: assertionNode.span.start,
75
+ severity: 'info',
76
+ code: 'UNNECESSARY_ASSERTION',
77
+ message: `Type assertion on ${literalType} literal is unnecessary`,
78
+ context: extractContextLine(
79
+ assertionNode.span.start.line,
80
+ context.source
81
+ ),
82
+ fix,
83
+ },
84
+ ];
85
+ }
86
+
87
+ return [];
88
+ },
89
+
90
+ fix(node: ASTNode, context: FixContext): Fix | null {
91
+ const assertionNode = node as TypeAssertionNode;
92
+ const operand = assertionNode.operand;
93
+
94
+ if (!operand) {
95
+ return null;
96
+ }
97
+
98
+ // Find the end of the type assertion (:type part)
99
+ const assertionSource = context.source.substring(
100
+ assertionNode.span.start.offset,
101
+ assertionNode.span.end.offset
102
+ );
103
+
104
+ // The type assertion is "literal:type" - we want to keep only "literal"
105
+ // Find the : character position
106
+ const colonIndex = assertionSource.indexOf(':');
107
+ if (colonIndex === -1) {
108
+ return null;
109
+ }
110
+
111
+ // Calculate the actual end of ":type" part
112
+ const typeStart = assertionNode.span.start.offset + colonIndex;
113
+ const typeEnd = typeStart + 1 + assertionNode.typeName.length;
114
+
115
+ return {
116
+ description: 'Remove unnecessary type assertion',
117
+ applicable: true,
118
+ range: {
119
+ start: { ...assertionNode.span.start, offset: typeStart },
120
+ end: { ...assertionNode.span.start, offset: typeEnd },
121
+ },
122
+ replacement: '',
123
+ };
124
+ },
125
+ };
126
+
127
+ /**
128
+ * Get the type name of a literal node.
129
+ */
130
+ function getLiteralType(
131
+ node: ASTNode
132
+ ): 'string' | 'number' | 'bool' | 'list' | 'dict' | null {
133
+ switch (node.type) {
134
+ case 'NumberLiteral':
135
+ return 'number';
136
+ case 'StringLiteral':
137
+ return 'string';
138
+ case 'BoolLiteral':
139
+ return 'bool';
140
+ case 'Tuple':
141
+ return 'list';
142
+ case 'Dict':
143
+ return 'dict';
144
+ default:
145
+ return null;
146
+ }
147
+ }
148
+
149
+ // ============================================================
150
+ // VALIDATE_EXTERNAL RULE
151
+ // ============================================================
152
+
153
+ /**
154
+ * Recommends type assertions for external input validation.
155
+ * External inputs (from host functions, user input, parsed data) should be
156
+ * validated with type assertions to ensure type safety.
157
+ *
158
+ * Detection heuristics:
159
+ * - Host function calls (HostCall nodes)
160
+ * - Parsing functions (parse_json, parse_xml, etc.)
161
+ * - Variables from external sources ($ARGS, $ENV)
162
+ *
163
+ * This is an informational rule - not all external data needs assertions,
164
+ * but it's a good practice for critical paths.
165
+ *
166
+ * References:
167
+ * - docs/guide-conventions.md:307-311
168
+ */
169
+ export const VALIDATE_EXTERNAL: ValidationRule = {
170
+ code: 'VALIDATE_EXTERNAL',
171
+ category: 'types',
172
+ severity: 'info',
173
+ nodeTypes: ['HostCall'],
174
+
175
+ validate(node: ASTNode, context: ValidationContext): Diagnostic[] {
176
+ const hostCallNode = node as HostCallNode;
177
+ const functionName = hostCallNode.name;
178
+
179
+ // Skip namespaced functions (ns::func) - these are trusted host APIs
180
+ if (functionName.includes('::')) {
181
+ return [];
182
+ }
183
+
184
+ // Check if this is a parsing or external data function
185
+ const isExternalDataFunction =
186
+ functionName.startsWith('parse_') ||
187
+ functionName.includes('fetch') ||
188
+ functionName.includes('read') ||
189
+ functionName.includes('load');
190
+
191
+ if (!isExternalDataFunction) {
192
+ return [];
193
+ }
194
+
195
+ // Skip if this HostCall is already wrapped in a TypeAssertion
196
+ if (context.assertedHostCalls.has(node)) {
197
+ return [];
198
+ }
199
+
200
+ return [
201
+ {
202
+ location: hostCallNode.span.start,
203
+ severity: 'info',
204
+ code: 'VALIDATE_EXTERNAL',
205
+ message: `Consider validating external input with type assertion: ${functionName}():type`,
206
+ context: extractContextLine(
207
+ hostCallNode.span.start.line,
208
+ context.source
209
+ ),
210
+ fix: null, // Cannot auto-fix - requires developer judgment
211
+ },
212
+ ];
213
+ },
214
+ };