@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,811 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formatting Rules
|
|
3
|
+
* Enforces style conventions from docs/guide-conventions.md:465-662.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
ValidationRule,
|
|
8
|
+
Diagnostic,
|
|
9
|
+
ValidationContext,
|
|
10
|
+
FixContext,
|
|
11
|
+
Fix,
|
|
12
|
+
} from '../types.js';
|
|
13
|
+
import type {
|
|
14
|
+
ASTNode,
|
|
15
|
+
BinaryExprNode,
|
|
16
|
+
PipeChainNode,
|
|
17
|
+
CaptureNode,
|
|
18
|
+
ClosureNode,
|
|
19
|
+
SourceSpan,
|
|
20
|
+
PostfixExprNode,
|
|
21
|
+
VariableNode,
|
|
22
|
+
BracketAccess,
|
|
23
|
+
MethodCallNode,
|
|
24
|
+
HostCallNode,
|
|
25
|
+
ClosureCallNode,
|
|
26
|
+
} from '@rcrsr/rill';
|
|
27
|
+
import { extractContextLine, isBareReference } from './helpers.js';
|
|
28
|
+
|
|
29
|
+
// ============================================================
|
|
30
|
+
// SPACING_OPERATOR RULE
|
|
31
|
+
// ============================================================
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Enforces space on both sides of operators.
|
|
35
|
+
* Operators like +, -, ->, =>, ==, etc. should have spaces on both sides.
|
|
36
|
+
*
|
|
37
|
+
* Detection:
|
|
38
|
+
* - Extract operator text from source using source spans
|
|
39
|
+
* - Check if space exists before/after operator
|
|
40
|
+
*
|
|
41
|
+
* References:
|
|
42
|
+
* - docs/guide-conventions.md:467-482
|
|
43
|
+
*/
|
|
44
|
+
export const SPACING_OPERATOR: ValidationRule = {
|
|
45
|
+
code: 'SPACING_OPERATOR',
|
|
46
|
+
category: 'formatting',
|
|
47
|
+
severity: 'info',
|
|
48
|
+
nodeTypes: ['BinaryExpr', 'PipeChain', 'Capture'],
|
|
49
|
+
|
|
50
|
+
validate(node: ASTNode, context: ValidationContext): Diagnostic[] {
|
|
51
|
+
const diagnostics: Diagnostic[] = [];
|
|
52
|
+
|
|
53
|
+
if (node.type === 'BinaryExpr') {
|
|
54
|
+
const binaryNode = node as BinaryExprNode;
|
|
55
|
+
const operator = binaryNode.op;
|
|
56
|
+
|
|
57
|
+
// Check spacing around operator in source
|
|
58
|
+
const violation = checkOperatorSpacing(
|
|
59
|
+
operator,
|
|
60
|
+
binaryNode.span,
|
|
61
|
+
context.source
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
if (violation) {
|
|
65
|
+
diagnostics.push({
|
|
66
|
+
location: binaryNode.span.start,
|
|
67
|
+
severity: 'info',
|
|
68
|
+
code: 'SPACING_OPERATOR',
|
|
69
|
+
message: `Operator '${operator}' should have spaces on both sides`,
|
|
70
|
+
context: extractContextLine(
|
|
71
|
+
binaryNode.span.start.line,
|
|
72
|
+
context.source
|
|
73
|
+
),
|
|
74
|
+
fix: null, // Complex to fix without AST reconstruction
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (node.type === 'PipeChain') {
|
|
80
|
+
const pipeNode = node as PipeChainNode;
|
|
81
|
+
// Check -> operators between pipes
|
|
82
|
+
const violation = checkPipeSpacing(pipeNode.span, context.source);
|
|
83
|
+
|
|
84
|
+
if (violation) {
|
|
85
|
+
diagnostics.push({
|
|
86
|
+
location: pipeNode.span.start,
|
|
87
|
+
severity: 'info',
|
|
88
|
+
code: 'SPACING_OPERATOR',
|
|
89
|
+
message: "Pipe operator '->' should have spaces on both sides",
|
|
90
|
+
context: extractContextLine(pipeNode.span.start.line, context.source),
|
|
91
|
+
fix: null,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (node.type === 'Capture') {
|
|
97
|
+
const captureNode = node as CaptureNode;
|
|
98
|
+
// Check => operator
|
|
99
|
+
const violation = checkCaptureSpacing(captureNode.span, context.source);
|
|
100
|
+
|
|
101
|
+
if (violation) {
|
|
102
|
+
diagnostics.push({
|
|
103
|
+
location: captureNode.span.start,
|
|
104
|
+
severity: 'info',
|
|
105
|
+
code: 'SPACING_OPERATOR',
|
|
106
|
+
message: "Capture operator '=>' should have spaces on both sides",
|
|
107
|
+
context: extractContextLine(
|
|
108
|
+
captureNode.span.start.line,
|
|
109
|
+
context.source
|
|
110
|
+
),
|
|
111
|
+
fix: null,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return diagnostics;
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Check if operator has proper spacing in source.
|
|
122
|
+
*/
|
|
123
|
+
function checkOperatorSpacing(
|
|
124
|
+
operator: string,
|
|
125
|
+
span: SourceSpan,
|
|
126
|
+
source: string
|
|
127
|
+
): boolean {
|
|
128
|
+
const text = extractSpanText(span, source);
|
|
129
|
+
|
|
130
|
+
// Look for operator without spaces
|
|
131
|
+
const patterns = [
|
|
132
|
+
new RegExp(`\\S${escapeRegex(operator)}`), // No space before
|
|
133
|
+
new RegExp(`${escapeRegex(operator)}\\S`), // No space after
|
|
134
|
+
];
|
|
135
|
+
|
|
136
|
+
return patterns.some((pattern) => pattern.test(text));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Check pipe operator spacing.
|
|
141
|
+
*/
|
|
142
|
+
function checkPipeSpacing(span: SourceSpan, source: string): boolean {
|
|
143
|
+
const text = extractSpanText(span, source);
|
|
144
|
+
|
|
145
|
+
// Check for -> without spaces
|
|
146
|
+
return /\S->/.test(text) || /->[\S&&[^\s]]/.test(text);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Check capture operator spacing.
|
|
151
|
+
*/
|
|
152
|
+
function checkCaptureSpacing(span: SourceSpan, source: string): boolean {
|
|
153
|
+
const text = extractSpanText(span, source);
|
|
154
|
+
|
|
155
|
+
// Check for => without spaces
|
|
156
|
+
return /\S=>/.test(text) || /=>\S/.test(text);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ============================================================
|
|
160
|
+
// SPACING_BRACES RULE
|
|
161
|
+
// ============================================================
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Enforces space after { and before } in blocks.
|
|
165
|
+
* Braces for blocks, closures, etc. should have internal spacing.
|
|
166
|
+
*
|
|
167
|
+
* Detection:
|
|
168
|
+
* - Extract brace content from source
|
|
169
|
+
* - Check if opening { has space after, closing } has space before
|
|
170
|
+
*
|
|
171
|
+
* References:
|
|
172
|
+
* - docs/guide-conventions.md:497-508
|
|
173
|
+
*/
|
|
174
|
+
export const SPACING_BRACES: ValidationRule = {
|
|
175
|
+
code: 'SPACING_BRACES',
|
|
176
|
+
category: 'formatting',
|
|
177
|
+
severity: 'info',
|
|
178
|
+
nodeTypes: ['Block', 'Closure'],
|
|
179
|
+
|
|
180
|
+
validate(node: ASTNode, context: ValidationContext): Diagnostic[] {
|
|
181
|
+
const diagnostics: Diagnostic[] = [];
|
|
182
|
+
const span = node.span;
|
|
183
|
+
const lines = context.source.split('\n');
|
|
184
|
+
|
|
185
|
+
const openLine = lines[span.start.line - 1] ?? '';
|
|
186
|
+
const closeLine = lines[span.end.line - 1] ?? '';
|
|
187
|
+
|
|
188
|
+
// Check for opening brace without space after
|
|
189
|
+
// Only examine the opening line (from the { onward)
|
|
190
|
+
// Use ^ anchor to only check the block's opening brace, not string interpolation
|
|
191
|
+
const openFrom = openLine.substring(span.start.column - 1);
|
|
192
|
+
if (/^\{[^\s\n]/.test(openFrom)) {
|
|
193
|
+
diagnostics.push({
|
|
194
|
+
location: span.start,
|
|
195
|
+
severity: 'info',
|
|
196
|
+
code: 'SPACING_BRACES',
|
|
197
|
+
message: 'Space required after opening brace {',
|
|
198
|
+
context: extractContextLine(span.start.line, context.source),
|
|
199
|
+
fix: null,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Check for closing brace without space before
|
|
204
|
+
// span.end.column is 1-indexed and points AFTER the }, so:
|
|
205
|
+
// - } is at 0-index: span.end.column - 2
|
|
206
|
+
// - Character before } is at 0-index: span.end.column - 3
|
|
207
|
+
const charBeforeClose = closeLine[span.end.column - 3];
|
|
208
|
+
const isCloseOnOwnLine = /^\s*$/.test(
|
|
209
|
+
closeLine.substring(0, span.end.column - 2)
|
|
210
|
+
);
|
|
211
|
+
if (charBeforeClose && !/\s/.test(charBeforeClose) && !isCloseOnOwnLine) {
|
|
212
|
+
diagnostics.push({
|
|
213
|
+
location: span.end,
|
|
214
|
+
severity: 'info',
|
|
215
|
+
code: 'SPACING_BRACES',
|
|
216
|
+
message: 'Space required before closing brace }',
|
|
217
|
+
context: extractContextLine(span.end.line, context.source),
|
|
218
|
+
fix: null,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return diagnostics;
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
// ============================================================
|
|
227
|
+
// SPACING_BRACKETS RULE
|
|
228
|
+
// ============================================================
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Enforces no inner spaces for indexing brackets.
|
|
232
|
+
* Array/dict indexing should use $list[0] not $list[ 0 ].
|
|
233
|
+
*
|
|
234
|
+
* Detection:
|
|
235
|
+
* - PostfixExpr nodes with index access
|
|
236
|
+
* - Check for spaces inside brackets
|
|
237
|
+
*
|
|
238
|
+
* References:
|
|
239
|
+
* - docs/guide-conventions.md:526-535
|
|
240
|
+
*/
|
|
241
|
+
export const SPACING_BRACKETS: ValidationRule = {
|
|
242
|
+
code: 'SPACING_BRACKETS',
|
|
243
|
+
category: 'formatting',
|
|
244
|
+
severity: 'info',
|
|
245
|
+
nodeTypes: ['PostfixExpr'],
|
|
246
|
+
|
|
247
|
+
validate(node: ASTNode, context: ValidationContext): Diagnostic[] {
|
|
248
|
+
const diagnostics: Diagnostic[] = [];
|
|
249
|
+
const postfixNode = node as PostfixExprNode;
|
|
250
|
+
|
|
251
|
+
// Only process if primary is a Variable (contains accessChain)
|
|
252
|
+
if (postfixNode.primary.type !== 'Variable') {
|
|
253
|
+
return diagnostics;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const variableNode = postfixNode.primary as VariableNode;
|
|
257
|
+
|
|
258
|
+
// Check each BracketAccess in the accessChain
|
|
259
|
+
for (const access of variableNode.accessChain) {
|
|
260
|
+
// Skip non-bracket accesses
|
|
261
|
+
if (!('accessKind' in access) || access.accessKind !== 'bracket') {
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const bracketAccess = access as BracketAccess;
|
|
266
|
+
|
|
267
|
+
// Skip if span is missing or invalid (EC-3, EC-4)
|
|
268
|
+
if (!isValidSpan(bracketAccess.span)) {
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Extract text from bracket span
|
|
273
|
+
const text = extractSpanText(bracketAccess.span, context.source);
|
|
274
|
+
|
|
275
|
+
// Check for space after opening bracket: /\[\s/
|
|
276
|
+
// Check for space before closing bracket: /\s\]/
|
|
277
|
+
const hasSpaceAfterOpen = /\[\s/.test(text);
|
|
278
|
+
const hasSpaceBeforeClose = /\s\]/.test(text);
|
|
279
|
+
|
|
280
|
+
if (hasSpaceAfterOpen || hasSpaceBeforeClose) {
|
|
281
|
+
// Extract content between brackets for error message
|
|
282
|
+
const content = text.substring(1, text.length - 1).trim();
|
|
283
|
+
|
|
284
|
+
diagnostics.push({
|
|
285
|
+
location: bracketAccess.span.start,
|
|
286
|
+
severity: 'info',
|
|
287
|
+
code: 'SPACING_BRACKETS',
|
|
288
|
+
message: `No spaces inside brackets: remove spaces around ${content}`,
|
|
289
|
+
context: extractContextLine(
|
|
290
|
+
bracketAccess.span.start.line,
|
|
291
|
+
context.source
|
|
292
|
+
),
|
|
293
|
+
fix: null,
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return diagnostics;
|
|
299
|
+
},
|
|
300
|
+
|
|
301
|
+
fix(node: ASTNode, context: FixContext): Fix | null {
|
|
302
|
+
const postfixNode = node as PostfixExprNode;
|
|
303
|
+
|
|
304
|
+
// Only process if primary is a Variable (contains accessChain)
|
|
305
|
+
if (postfixNode.primary.type !== 'Variable') {
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const variableNode = postfixNode.primary as VariableNode;
|
|
310
|
+
|
|
311
|
+
// Find the first BracketAccess with spacing violation
|
|
312
|
+
for (const access of variableNode.accessChain) {
|
|
313
|
+
// Skip non-bracket accesses
|
|
314
|
+
if (!('accessKind' in access) || access.accessKind !== 'bracket') {
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const bracketAccess = access as BracketAccess;
|
|
319
|
+
|
|
320
|
+
// Skip if span is missing or invalid
|
|
321
|
+
if (!isValidSpan(bracketAccess.span)) {
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Extract text from bracket span
|
|
326
|
+
const text = extractSpanText(bracketAccess.span, context.source);
|
|
327
|
+
|
|
328
|
+
// Check for spacing violations
|
|
329
|
+
const hasSpaceAfterOpen = /\[\s/.test(text);
|
|
330
|
+
const hasSpaceBeforeClose = /\s\]/.test(text);
|
|
331
|
+
|
|
332
|
+
if (hasSpaceAfterOpen || hasSpaceBeforeClose) {
|
|
333
|
+
// Build replacement text by removing inner spaces
|
|
334
|
+
// Replace [ followed by whitespace with [
|
|
335
|
+
// Replace whitespace followed by ] with ]
|
|
336
|
+
const replacement = text.replace(/\[\s+/g, '[').replace(/\s+\]/g, ']');
|
|
337
|
+
|
|
338
|
+
return {
|
|
339
|
+
description: 'Remove spaces inside brackets',
|
|
340
|
+
applicable: true,
|
|
341
|
+
range: bracketAccess.span,
|
|
342
|
+
replacement,
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// No fixable violation found
|
|
348
|
+
return null;
|
|
349
|
+
},
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
// ============================================================
|
|
353
|
+
// SPACING_CLOSURE RULE
|
|
354
|
+
// ============================================================
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Enforces no space before pipe, space after in closures.
|
|
358
|
+
* Closure parameters: |x| not | x |.
|
|
359
|
+
*
|
|
360
|
+
* Detection:
|
|
361
|
+
* - Extract closure parameter section from source
|
|
362
|
+
* - Check spacing around pipes
|
|
363
|
+
*
|
|
364
|
+
* References:
|
|
365
|
+
* - docs/guide-conventions.md:549-560
|
|
366
|
+
*/
|
|
367
|
+
export const SPACING_CLOSURE: ValidationRule = {
|
|
368
|
+
code: 'SPACING_CLOSURE',
|
|
369
|
+
category: 'formatting',
|
|
370
|
+
severity: 'info',
|
|
371
|
+
nodeTypes: ['Closure'],
|
|
372
|
+
|
|
373
|
+
validate(node: ASTNode, context: ValidationContext): Diagnostic[] {
|
|
374
|
+
const diagnostics: Diagnostic[] = [];
|
|
375
|
+
const closureNode = node as ClosureNode;
|
|
376
|
+
const text = extractSpanText(closureNode.span, context.source);
|
|
377
|
+
|
|
378
|
+
// Check for space before opening pipe
|
|
379
|
+
if (/\s\|/.test(text.substring(0, text.indexOf('|') + 1))) {
|
|
380
|
+
diagnostics.push({
|
|
381
|
+
location: closureNode.span.start,
|
|
382
|
+
severity: 'info',
|
|
383
|
+
code: 'SPACING_CLOSURE',
|
|
384
|
+
message: 'No space before opening pipe in closure parameters',
|
|
385
|
+
context: extractContextLine(
|
|
386
|
+
closureNode.span.start.line,
|
|
387
|
+
context.source
|
|
388
|
+
),
|
|
389
|
+
fix: null,
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Check for missing space after params (only if params exist)
|
|
394
|
+
if (closureNode.params.length > 0) {
|
|
395
|
+
// Look for pattern |params|( or |params|{ without space
|
|
396
|
+
const afterPipeIdx = text.lastIndexOf(
|
|
397
|
+
'|',
|
|
398
|
+
text.indexOf('{') || text.indexOf('(')
|
|
399
|
+
);
|
|
400
|
+
if (afterPipeIdx !== -1) {
|
|
401
|
+
const afterPipe = text.substring(afterPipeIdx + 1, afterPipeIdx + 2);
|
|
402
|
+
if (
|
|
403
|
+
afterPipe &&
|
|
404
|
+
/[^\s]/.test(afterPipe) &&
|
|
405
|
+
afterPipe !== '{' &&
|
|
406
|
+
afterPipe !== '('
|
|
407
|
+
) {
|
|
408
|
+
// This is complex - skip for now as it requires better parsing
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return diagnostics;
|
|
414
|
+
},
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
// ============================================================
|
|
418
|
+
// INDENT_CONTINUATION RULE
|
|
419
|
+
// ============================================================
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Enforces 2-space indent for continued lines.
|
|
423
|
+
* Pipe chains should indent continuation lines by 2 spaces.
|
|
424
|
+
*
|
|
425
|
+
* Detection:
|
|
426
|
+
* - Multi-line pipe chains
|
|
427
|
+
* - Check indentation of continuation lines
|
|
428
|
+
*
|
|
429
|
+
* References:
|
|
430
|
+
* - docs/guide-conventions.md:636-662
|
|
431
|
+
*/
|
|
432
|
+
export const INDENT_CONTINUATION: ValidationRule = {
|
|
433
|
+
code: 'INDENT_CONTINUATION',
|
|
434
|
+
category: 'formatting',
|
|
435
|
+
severity: 'info',
|
|
436
|
+
nodeTypes: ['PipeChain'],
|
|
437
|
+
|
|
438
|
+
validate(node: ASTNode, context: ValidationContext): Diagnostic[] {
|
|
439
|
+
const diagnostics: Diagnostic[] = [];
|
|
440
|
+
const pipeNode = node as PipeChainNode;
|
|
441
|
+
|
|
442
|
+
// EC-5: Single-line chain - Return []
|
|
443
|
+
if (pipeNode.span.start.line === pipeNode.span.end.line) {
|
|
444
|
+
return [];
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Extract full text and check continuation indentation
|
|
448
|
+
const text = extractSpanText(pipeNode.span, context.source);
|
|
449
|
+
const lines = text.split('\n');
|
|
450
|
+
|
|
451
|
+
// KNOWN LIMITATION: This rule validates multi-line pipe chains where the pipe
|
|
452
|
+
// operator (`->`) and its target appear on the same line. The parser requires
|
|
453
|
+
// pipe targets to be on the same line as the `->` operator, so patterns like
|
|
454
|
+
// `value ->\n .method()` are invalid. See tests/language/statement-boundaries.test.ts:211-215
|
|
455
|
+
// for authoritative language behavior.
|
|
456
|
+
if (lines.length > 1) {
|
|
457
|
+
// Check each continuation line (skip first line which establishes baseline)
|
|
458
|
+
for (let i = 1; i < lines.length; i++) {
|
|
459
|
+
const line = lines[i];
|
|
460
|
+
|
|
461
|
+
// EC-6: Empty continuation line - Skip line
|
|
462
|
+
if (!line) continue;
|
|
463
|
+
|
|
464
|
+
const indent = line.match(/^(\s*)/)?.[1] || '';
|
|
465
|
+
|
|
466
|
+
// Continuation = line starting with -> (after whitespace)
|
|
467
|
+
// Should have at least 2 spaces for continuation
|
|
468
|
+
if (line.trim().startsWith('->') && indent.length < 2) {
|
|
469
|
+
diagnostics.push({
|
|
470
|
+
location: {
|
|
471
|
+
line: pipeNode.span.start.line + i,
|
|
472
|
+
column: 1,
|
|
473
|
+
offset: 0,
|
|
474
|
+
},
|
|
475
|
+
severity: 'info',
|
|
476
|
+
code: 'INDENT_CONTINUATION',
|
|
477
|
+
message: 'Continuation lines should be indented by 2 spaces',
|
|
478
|
+
context: line.trim(),
|
|
479
|
+
fix: null,
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
return diagnostics;
|
|
486
|
+
},
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
// ============================================================
|
|
490
|
+
// IMPLICIT_DOLLAR_METHOD RULE
|
|
491
|
+
// ============================================================
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Detect explicit $.method() patterns replaceable with .method.
|
|
495
|
+
*
|
|
496
|
+
* Flags method calls where the receiver is a bare $ (pipe variable).
|
|
497
|
+
* The implicit form .method is preferred when $ represents the current
|
|
498
|
+
* piped value (e.g., in blocks, closures, conditionals).
|
|
499
|
+
*
|
|
500
|
+
* Detection:
|
|
501
|
+
* - MethodCallNode with non-null receiverSpan
|
|
502
|
+
* - Receiver is bare $ (zero-width or single-char span)
|
|
503
|
+
* - Method call is first in chain (receiverSpan.end.offset <= 1)
|
|
504
|
+
*
|
|
505
|
+
* Note: Cannot use isBareReference() helper here because MethodCallNode.receiverSpan
|
|
506
|
+
* is a SourceSpan (position range), not an ExpressionNode. The helper requires
|
|
507
|
+
* an AST node to traverse. Instead, we detect bare $ by checking:
|
|
508
|
+
* 1. receiverSpan is zero-width (start == end) or single-char
|
|
509
|
+
* 2. Character at offset is '$'
|
|
510
|
+
* 3. Next character is '.' (not a variable name continuation)
|
|
511
|
+
*
|
|
512
|
+
* Examples:
|
|
513
|
+
* - $.upper() -> .upper
|
|
514
|
+
* - $.len -> .len
|
|
515
|
+
* - $.trim().upper() -> First method flagged, second is chained (not bare $)
|
|
516
|
+
*
|
|
517
|
+
* Not flagged:
|
|
518
|
+
* - .upper (receiverSpan is null)
|
|
519
|
+
* - $var.method() (receiverSpan is not bare $)
|
|
520
|
+
* - $.trim().upper() second method (receiverSpan covers $.trim())
|
|
521
|
+
*
|
|
522
|
+
* References:
|
|
523
|
+
* - docs/guide-conventions.md:587-598
|
|
524
|
+
*/
|
|
525
|
+
export const IMPLICIT_DOLLAR_METHOD: ValidationRule = {
|
|
526
|
+
code: 'IMPLICIT_DOLLAR_METHOD',
|
|
527
|
+
category: 'formatting',
|
|
528
|
+
severity: 'info',
|
|
529
|
+
nodeTypes: ['MethodCall'],
|
|
530
|
+
|
|
531
|
+
validate(node: ASTNode, context: ValidationContext): Diagnostic[] {
|
|
532
|
+
const methodNode = node as MethodCallNode;
|
|
533
|
+
|
|
534
|
+
// EC-7: No receiverSpan means implicit receiver (already correct form)
|
|
535
|
+
if (methodNode.receiverSpan === null) {
|
|
536
|
+
return [];
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Detect bare $ receiver by analyzing the receiverSpan
|
|
540
|
+
// For bare $, the span is either:
|
|
541
|
+
// 1. Zero-width (start.offset == end.offset) at the $ character
|
|
542
|
+
// 2. Single-char span covering just $
|
|
543
|
+
const receiverSpan = methodNode.receiverSpan;
|
|
544
|
+
const spanLength = receiverSpan.end.offset - receiverSpan.start.offset;
|
|
545
|
+
|
|
546
|
+
// EC-8: Receiver is not bare $ if span is longer than 1 character
|
|
547
|
+
// This filters out chains like $.trim().upper() where second method
|
|
548
|
+
// has receiverSpan covering "$.trim()."
|
|
549
|
+
if (spanLength > 1) {
|
|
550
|
+
return [];
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Check that the character at the span is '$' and not part of a variable name
|
|
554
|
+
const offset = receiverSpan.start.offset;
|
|
555
|
+
const charAtOffset = context.source[offset];
|
|
556
|
+
const nextChar = context.source[offset + 1];
|
|
557
|
+
|
|
558
|
+
// Must be '$' followed by '.' (method call)
|
|
559
|
+
// This distinguishes $.method() from $var.method()
|
|
560
|
+
if (charAtOffset !== '$' || nextChar !== '.') {
|
|
561
|
+
return [];
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Generate diagnostic for bare $ receiver
|
|
565
|
+
const suggestedCode =
|
|
566
|
+
methodNode.args.length === 0
|
|
567
|
+
? `.${methodNode.name}`
|
|
568
|
+
: `.${methodNode.name}()`;
|
|
569
|
+
|
|
570
|
+
return [
|
|
571
|
+
{
|
|
572
|
+
code: 'IMPLICIT_DOLLAR_METHOD',
|
|
573
|
+
message: `Prefer implicit '${suggestedCode}' over explicit '$.${methodNode.name}()'`,
|
|
574
|
+
severity: 'info',
|
|
575
|
+
location: {
|
|
576
|
+
line: methodNode.span.start.line,
|
|
577
|
+
column: methodNode.span.start.column,
|
|
578
|
+
offset: methodNode.span.start.offset,
|
|
579
|
+
},
|
|
580
|
+
context: extractContextLine(methodNode.span.start.line, context.source),
|
|
581
|
+
fix: null,
|
|
582
|
+
},
|
|
583
|
+
];
|
|
584
|
+
},
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
// ============================================================
|
|
588
|
+
// IMPLICIT_DOLLAR_FUNCTION RULE
|
|
589
|
+
// ============================================================
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Prefer foo over foo($) for global function calls.
|
|
593
|
+
* When single argument is bare $, prefer implicit form.
|
|
594
|
+
*
|
|
595
|
+
* Detection:
|
|
596
|
+
* - HostCall with single argument that is bare $
|
|
597
|
+
*
|
|
598
|
+
* References:
|
|
599
|
+
* - docs/guide-conventions.md:599-607
|
|
600
|
+
*/
|
|
601
|
+
export const IMPLICIT_DOLLAR_FUNCTION: ValidationRule = {
|
|
602
|
+
code: 'IMPLICIT_DOLLAR_FUNCTION',
|
|
603
|
+
category: 'formatting',
|
|
604
|
+
severity: 'info',
|
|
605
|
+
nodeTypes: ['HostCall'],
|
|
606
|
+
|
|
607
|
+
validate(node: ASTNode, context: ValidationContext): Diagnostic[] {
|
|
608
|
+
const hostCallNode = node as HostCallNode;
|
|
609
|
+
|
|
610
|
+
// EC-9: Zero args - Return []
|
|
611
|
+
if (hostCallNode.args.length === 0) {
|
|
612
|
+
return [];
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// EC-10: Multiple args - Return []
|
|
616
|
+
if (hostCallNode.args.length > 1) {
|
|
617
|
+
return [];
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// EC-11: Single arg not bare $ - Return []
|
|
621
|
+
const singleArg = hostCallNode.args[0];
|
|
622
|
+
if (!isBareReference(singleArg)) {
|
|
623
|
+
return [];
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// Generate diagnostic for bare $ argument
|
|
627
|
+
return [
|
|
628
|
+
{
|
|
629
|
+
code: 'IMPLICIT_DOLLAR_FUNCTION',
|
|
630
|
+
message: `Prefer pipe syntax '-> ${hostCallNode.name}' over explicit '${hostCallNode.name}($)'`,
|
|
631
|
+
severity: 'info',
|
|
632
|
+
location: {
|
|
633
|
+
line: hostCallNode.span.start.line,
|
|
634
|
+
column: hostCallNode.span.start.column,
|
|
635
|
+
offset: hostCallNode.span.start.offset,
|
|
636
|
+
},
|
|
637
|
+
context: extractContextLine(
|
|
638
|
+
hostCallNode.span.start.line,
|
|
639
|
+
context.source
|
|
640
|
+
),
|
|
641
|
+
fix: null,
|
|
642
|
+
},
|
|
643
|
+
];
|
|
644
|
+
},
|
|
645
|
+
};
|
|
646
|
+
|
|
647
|
+
// ============================================================
|
|
648
|
+
// IMPLICIT_DOLLAR_CLOSURE RULE
|
|
649
|
+
// ============================================================
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* Prefer $fn over $fn($) for closure invocation.
|
|
653
|
+
* When single argument is bare $, prefer implicit form.
|
|
654
|
+
*
|
|
655
|
+
* Detection:
|
|
656
|
+
* - ClosureCall with single argument that is bare $
|
|
657
|
+
*
|
|
658
|
+
* References:
|
|
659
|
+
* - docs/guide-conventions.md:608-615
|
|
660
|
+
*/
|
|
661
|
+
export const IMPLICIT_DOLLAR_CLOSURE: ValidationRule = {
|
|
662
|
+
code: 'IMPLICIT_DOLLAR_CLOSURE',
|
|
663
|
+
category: 'formatting',
|
|
664
|
+
severity: 'info',
|
|
665
|
+
nodeTypes: ['ClosureCall'],
|
|
666
|
+
|
|
667
|
+
validate(node: ASTNode, context: ValidationContext): Diagnostic[] {
|
|
668
|
+
const closureCallNode = node as ClosureCallNode;
|
|
669
|
+
|
|
670
|
+
// EC-12: Zero args - Return []
|
|
671
|
+
if (closureCallNode.args.length === 0) {
|
|
672
|
+
return [];
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// EC-13: Multiple args - Return []
|
|
676
|
+
if (closureCallNode.args.length > 1) {
|
|
677
|
+
return [];
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// EC-14: Single arg not bare $ - Return []
|
|
681
|
+
const singleArg = closureCallNode.args[0];
|
|
682
|
+
if (!isBareReference(singleArg)) {
|
|
683
|
+
return [];
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// Build closure name with access chain for display
|
|
687
|
+
const closureName =
|
|
688
|
+
closureCallNode.accessChain.length > 0
|
|
689
|
+
? `$${closureCallNode.name}.${closureCallNode.accessChain.join('.')}`
|
|
690
|
+
: `$${closureCallNode.name}`;
|
|
691
|
+
|
|
692
|
+
// Generate diagnostic for bare $ argument
|
|
693
|
+
return [
|
|
694
|
+
{
|
|
695
|
+
code: 'IMPLICIT_DOLLAR_CLOSURE',
|
|
696
|
+
message: `Prefer pipe syntax '-> ${closureName}' over explicit '${closureName}($)'`,
|
|
697
|
+
severity: 'info',
|
|
698
|
+
location: {
|
|
699
|
+
line: closureCallNode.span.start.line,
|
|
700
|
+
column: closureCallNode.span.start.column,
|
|
701
|
+
offset: closureCallNode.span.start.offset,
|
|
702
|
+
},
|
|
703
|
+
context: extractContextLine(
|
|
704
|
+
closureCallNode.span.start.line,
|
|
705
|
+
context.source
|
|
706
|
+
),
|
|
707
|
+
fix: null,
|
|
708
|
+
},
|
|
709
|
+
];
|
|
710
|
+
},
|
|
711
|
+
};
|
|
712
|
+
|
|
713
|
+
// ============================================================
|
|
714
|
+
// THROWAWAY_CAPTURE RULE
|
|
715
|
+
// ============================================================
|
|
716
|
+
|
|
717
|
+
/**
|
|
718
|
+
* Warns on capture-only-to-continue patterns.
|
|
719
|
+
* Capturing a value just to use it immediately in the next line is unnecessary.
|
|
720
|
+
*
|
|
721
|
+
* Detection:
|
|
722
|
+
* - Capture node followed by immediate use of that variable only
|
|
723
|
+
* - Variable not referenced later in the script
|
|
724
|
+
*
|
|
725
|
+
* References:
|
|
726
|
+
* - docs/guide-conventions.md:617-634
|
|
727
|
+
*/
|
|
728
|
+
export const THROWAWAY_CAPTURE: ValidationRule = {
|
|
729
|
+
code: 'THROWAWAY_CAPTURE',
|
|
730
|
+
category: 'formatting',
|
|
731
|
+
severity: 'info',
|
|
732
|
+
nodeTypes: ['Capture'],
|
|
733
|
+
|
|
734
|
+
validate(_node: ASTNode, _context: ValidationContext): Diagnostic[] {
|
|
735
|
+
// [DEBT] Stubbed - Requires full script analysis across statement boundaries
|
|
736
|
+
// Must track: 1) All captures 2) All variable references 3) Single-use detection
|
|
737
|
+
return [];
|
|
738
|
+
},
|
|
739
|
+
};
|
|
740
|
+
|
|
741
|
+
// ============================================================
|
|
742
|
+
// HELPER FUNCTIONS
|
|
743
|
+
// ============================================================
|
|
744
|
+
|
|
745
|
+
/**
|
|
746
|
+
* Validate that a SourceSpan has valid coordinates.
|
|
747
|
+
* Returns false if span, start, or end are missing,
|
|
748
|
+
* or if line/column values are less than 1.
|
|
749
|
+
*
|
|
750
|
+
* Exported for testing purposes to enable direct unit testing
|
|
751
|
+
* of edge cases (null spans, invalid coordinates).
|
|
752
|
+
*/
|
|
753
|
+
export function isValidSpan(span: SourceSpan | null | undefined): boolean {
|
|
754
|
+
if (!span) {
|
|
755
|
+
return false;
|
|
756
|
+
}
|
|
757
|
+
if (!span.start || !span.end) {
|
|
758
|
+
return false;
|
|
759
|
+
}
|
|
760
|
+
if (
|
|
761
|
+
span.start.line < 1 ||
|
|
762
|
+
span.start.column < 1 ||
|
|
763
|
+
span.end.line < 1 ||
|
|
764
|
+
span.end.column < 1
|
|
765
|
+
) {
|
|
766
|
+
return false;
|
|
767
|
+
}
|
|
768
|
+
return true;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/**
|
|
772
|
+
* Extract text from source using span coordinates.
|
|
773
|
+
*/
|
|
774
|
+
function extractSpanText(span: SourceSpan, source: string): string {
|
|
775
|
+
const lines = source.split('\n');
|
|
776
|
+
|
|
777
|
+
if (span.start.line === span.end.line) {
|
|
778
|
+
// Single line
|
|
779
|
+
const line = lines[span.start.line - 1];
|
|
780
|
+
if (!line) return '';
|
|
781
|
+
return line.substring(span.start.column - 1, span.end.column - 1);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// Multi-line
|
|
785
|
+
const result: string[] = [];
|
|
786
|
+
|
|
787
|
+
for (let i = span.start.line - 1; i < span.end.line; i++) {
|
|
788
|
+
const line = lines[i];
|
|
789
|
+
if (!line) continue;
|
|
790
|
+
|
|
791
|
+
if (i === span.start.line - 1) {
|
|
792
|
+
// First line: from start column to end
|
|
793
|
+
result.push(line.substring(span.start.column - 1));
|
|
794
|
+
} else if (i === span.end.line - 1) {
|
|
795
|
+
// Last line: from start to end column
|
|
796
|
+
result.push(line.substring(0, span.end.column - 1));
|
|
797
|
+
} else {
|
|
798
|
+
// Middle lines: full line
|
|
799
|
+
result.push(line);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
return result.join('\n');
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
/**
|
|
807
|
+
* Escape special regex characters.
|
|
808
|
+
*/
|
|
809
|
+
function escapeRegex(str: string): string {
|
|
810
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
811
|
+
}
|