@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,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
|
+
};
|