@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,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI Error Enrichment
|
|
3
|
+
* Functions for extracting source snippets and suggesting similar names
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { SourceSpan, RillError, CallFrame } from '@rcrsr/rill';
|
|
7
|
+
|
|
8
|
+
// ============================================================
|
|
9
|
+
// PUBLIC TYPES
|
|
10
|
+
// ============================================================
|
|
11
|
+
|
|
12
|
+
export interface SourceSnippet {
|
|
13
|
+
readonly lines: SnippetLine[];
|
|
14
|
+
readonly highlightSpan: SourceSpan;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface SnippetLine {
|
|
18
|
+
readonly lineNumber: number;
|
|
19
|
+
readonly content: string;
|
|
20
|
+
readonly isErrorLine: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ScopeInfo {
|
|
24
|
+
readonly variableNames: string[];
|
|
25
|
+
readonly functionNames: string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface EnrichedError {
|
|
29
|
+
readonly errorId: string;
|
|
30
|
+
readonly message: string;
|
|
31
|
+
readonly span?: SourceSpan | undefined;
|
|
32
|
+
readonly context?: Record<string, unknown> | undefined;
|
|
33
|
+
readonly callStack?: CallFrame[] | undefined;
|
|
34
|
+
readonly sourceSnippet?: SourceSnippet | undefined;
|
|
35
|
+
readonly suggestions?: string[] | undefined;
|
|
36
|
+
readonly helpUrl?: string | undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ============================================================
|
|
40
|
+
// SOURCE SNIPPET EXTRACTION
|
|
41
|
+
// ============================================================
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Extract source lines around error location.
|
|
45
|
+
*
|
|
46
|
+
* Constraints:
|
|
47
|
+
* - Context lines: 2 before, 2 after (configurable)
|
|
48
|
+
* - Line numbers: 1-based
|
|
49
|
+
* - Handles edge cases: line 1, last line
|
|
50
|
+
*
|
|
51
|
+
* @param source - Full source text
|
|
52
|
+
* @param span - Error location span
|
|
53
|
+
* @param contextLines - Number of context lines before/after (default: 2)
|
|
54
|
+
* @returns Snippet with context lines
|
|
55
|
+
* @throws {RangeError} When span exceeds source bounds
|
|
56
|
+
*/
|
|
57
|
+
export function extractSnippet(
|
|
58
|
+
source: string,
|
|
59
|
+
span: SourceSpan,
|
|
60
|
+
contextLines: number = 2
|
|
61
|
+
): SourceSnippet {
|
|
62
|
+
// EC-7: Empty source returns empty snippet
|
|
63
|
+
if (source === '') {
|
|
64
|
+
return { lines: [], highlightSpan: span };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const lines = source.split('\n');
|
|
68
|
+
const totalLines = lines.length;
|
|
69
|
+
|
|
70
|
+
// EC-6: Validate span is within bounds (1-based line numbers)
|
|
71
|
+
if (span.start.line < 1 || span.start.line > totalLines) {
|
|
72
|
+
throw new RangeError('Span exceeds source bounds');
|
|
73
|
+
}
|
|
74
|
+
if (span.end.line < 1 || span.end.line > totalLines) {
|
|
75
|
+
throw new RangeError('Span exceeds source bounds');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Calculate context range
|
|
79
|
+
const errorStartLine = span.start.line;
|
|
80
|
+
const errorEndLine = span.end.line;
|
|
81
|
+
const firstLine = Math.max(1, errorStartLine - contextLines);
|
|
82
|
+
const lastLine = Math.min(totalLines, errorEndLine + contextLines);
|
|
83
|
+
|
|
84
|
+
// Build snippet lines
|
|
85
|
+
const snippetLines: SnippetLine[] = [];
|
|
86
|
+
for (let lineNum = firstLine; lineNum <= lastLine; lineNum++) {
|
|
87
|
+
const isErrorLine = lineNum >= errorStartLine && lineNum <= errorEndLine;
|
|
88
|
+
snippetLines.push({
|
|
89
|
+
lineNumber: lineNum,
|
|
90
|
+
content: lines[lineNum - 1] ?? '', // Convert 1-based to 0-based index
|
|
91
|
+
isErrorLine,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
lines: snippetLines,
|
|
97
|
+
highlightSpan: span,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ============================================================
|
|
102
|
+
// NAME SUGGESTION
|
|
103
|
+
// ============================================================
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Find similar names using fuzzy matching.
|
|
107
|
+
*
|
|
108
|
+
* Constraints:
|
|
109
|
+
* - Edit distance threshold: <= 2
|
|
110
|
+
* - Max suggestions: 3
|
|
111
|
+
* - Sort: ascending by distance, then alphabetically
|
|
112
|
+
*
|
|
113
|
+
* @param target - Name to match against
|
|
114
|
+
* @param candidates - Available names
|
|
115
|
+
* @returns Up to 3 similar names, sorted by distance then alphabetically
|
|
116
|
+
*/
|
|
117
|
+
export function suggestSimilarNames(
|
|
118
|
+
target: string,
|
|
119
|
+
candidates: string[]
|
|
120
|
+
): string[] {
|
|
121
|
+
// EC-9: Empty target returns []
|
|
122
|
+
if (target === '') {
|
|
123
|
+
return [];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// EC-10: Empty candidates returns []
|
|
127
|
+
if (candidates.length === 0) {
|
|
128
|
+
return [];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Calculate edit distance for each candidate
|
|
132
|
+
const candidatesWithDistance = candidates
|
|
133
|
+
.map((candidate) => ({
|
|
134
|
+
name: candidate,
|
|
135
|
+
distance: levenshteinDistance(target, candidate),
|
|
136
|
+
}))
|
|
137
|
+
.filter((item) => item.distance <= 2); // IR-8: Edit distance threshold
|
|
138
|
+
|
|
139
|
+
// Sort: ascending by distance, then alphabetically
|
|
140
|
+
candidatesWithDistance.sort((a, b) => {
|
|
141
|
+
if (a.distance !== b.distance) {
|
|
142
|
+
return a.distance - b.distance;
|
|
143
|
+
}
|
|
144
|
+
return a.name.localeCompare(b.name);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// IR-8: Max 3 suggestions
|
|
148
|
+
return candidatesWithDistance.slice(0, 3).map((item) => item.name);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Calculate Levenshtein distance between two strings.
|
|
153
|
+
* Uses dynamic programming with O(m*n) time and O(min(m,n)) space.
|
|
154
|
+
*
|
|
155
|
+
* @param a - First string
|
|
156
|
+
* @param b - Second string
|
|
157
|
+
* @returns Edit distance (number of operations to transform a into b)
|
|
158
|
+
*/
|
|
159
|
+
function levenshteinDistance(a: string, b: string): number {
|
|
160
|
+
// Ensure a is the shorter string for space optimization
|
|
161
|
+
if (a.length > b.length) {
|
|
162
|
+
[a, b] = [b, a];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const m = a.length;
|
|
166
|
+
const n = b.length;
|
|
167
|
+
|
|
168
|
+
// Early exit for empty strings
|
|
169
|
+
if (m === 0) return n;
|
|
170
|
+
if (n === 0) return m;
|
|
171
|
+
|
|
172
|
+
// Use rolling array optimization (only need previous row)
|
|
173
|
+
let prevRow = Array.from({ length: m + 1 }, (_, i) => i);
|
|
174
|
+
let currRow = new Array<number>(m + 1);
|
|
175
|
+
|
|
176
|
+
for (let j = 1; j <= n; j++) {
|
|
177
|
+
currRow[0] = j;
|
|
178
|
+
|
|
179
|
+
for (let i = 1; i <= m; i++) {
|
|
180
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
181
|
+
currRow[i] = Math.min(
|
|
182
|
+
prevRow[i]! + 1, // deletion
|
|
183
|
+
currRow[i - 1]! + 1, // insertion
|
|
184
|
+
prevRow[i - 1]! + cost // substitution
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Swap rows for next iteration
|
|
189
|
+
[prevRow, currRow] = [currRow, prevRow];
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return prevRow[m] ?? 0;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ============================================================
|
|
196
|
+
// ERROR ENRICHMENT
|
|
197
|
+
// ============================================================
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Enrich RillError with source snippets and suggestions.
|
|
201
|
+
*
|
|
202
|
+
* Constraints:
|
|
203
|
+
* - Source must be valid UTF-8
|
|
204
|
+
* - Snippet extraction: 2 lines before, error line, 2 after
|
|
205
|
+
* - Fuzzy matching: edit distance <= 2
|
|
206
|
+
* - Max 3 suggestions
|
|
207
|
+
*
|
|
208
|
+
* @param error - RillError to enrich
|
|
209
|
+
* @param source - Full source text
|
|
210
|
+
* @param scope - Optional scope information for suggestions
|
|
211
|
+
* @returns Enriched error with snippet and suggestions
|
|
212
|
+
* @throws {TypeError} When source is not a string or error is null
|
|
213
|
+
*/
|
|
214
|
+
export function enrichError(
|
|
215
|
+
error: RillError,
|
|
216
|
+
source: string,
|
|
217
|
+
scope?: ScopeInfo
|
|
218
|
+
): EnrichedError {
|
|
219
|
+
// EC-4: Null error
|
|
220
|
+
if (!error) {
|
|
221
|
+
throw new TypeError('Error is required');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// EC-3: Invalid source encoding (JavaScript strings are always valid UTF-16)
|
|
225
|
+
if (typeof source !== 'string') {
|
|
226
|
+
throw new TypeError('Source must be valid UTF-8');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Extract span from error location
|
|
230
|
+
let span: SourceSpan | undefined;
|
|
231
|
+
if (error.location) {
|
|
232
|
+
// Create a minimal span from the location (single character)
|
|
233
|
+
span = {
|
|
234
|
+
start: error.location,
|
|
235
|
+
end: error.location,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Extract source snippet if we have a span
|
|
240
|
+
let sourceSnippet: SourceSnippet | undefined;
|
|
241
|
+
if (span && source !== '') {
|
|
242
|
+
try {
|
|
243
|
+
sourceSnippet = extractSnippet(source, span);
|
|
244
|
+
} catch {
|
|
245
|
+
// If snippet extraction fails (e.g., invalid span), skip it
|
|
246
|
+
sourceSnippet = undefined;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Generate suggestions if scope info is provided
|
|
251
|
+
let suggestions: string[] | undefined;
|
|
252
|
+
if (scope && error.context) {
|
|
253
|
+
// Look for undefined variable names in context
|
|
254
|
+
const undefinedName = error.context['name'] as string | undefined;
|
|
255
|
+
if (undefinedName && typeof undefinedName === 'string') {
|
|
256
|
+
const candidates = [...scope.variableNames, ...scope.functionNames];
|
|
257
|
+
const similarNames = suggestSimilarNames(undefinedName, candidates);
|
|
258
|
+
if (similarNames.length > 0) {
|
|
259
|
+
suggestions = similarNames;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
errorId: error.errorId,
|
|
266
|
+
message: error.message.replace(/ at \d+:\d+$/, ''), // Strip location suffix
|
|
267
|
+
span,
|
|
268
|
+
context: error.context,
|
|
269
|
+
callStack: undefined, // Call stack not part of base RillError
|
|
270
|
+
sourceSnippet,
|
|
271
|
+
suggestions,
|
|
272
|
+
helpUrl: error.helpUrl,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI Error Formatter
|
|
3
|
+
* Format enriched errors for human-readable, JSON, or compact output
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { SourceSpan, CallFrame } from '@rcrsr/rill';
|
|
7
|
+
import type { EnrichedError } from './cli-error-enrichment.js';
|
|
8
|
+
|
|
9
|
+
// ============================================================
|
|
10
|
+
// PUBLIC TYPES
|
|
11
|
+
// ============================================================
|
|
12
|
+
|
|
13
|
+
export type { CallFrame, EnrichedError };
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Format options for error output.
|
|
17
|
+
*/
|
|
18
|
+
export interface FormatOptions {
|
|
19
|
+
readonly format: 'human' | 'json' | 'compact';
|
|
20
|
+
readonly verbose: boolean;
|
|
21
|
+
readonly includeCallStack: boolean;
|
|
22
|
+
readonly maxCallStackDepth: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ============================================================
|
|
26
|
+
// ERROR FORMATTING
|
|
27
|
+
// ============================================================
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Format enriched error for output.
|
|
31
|
+
*
|
|
32
|
+
* Constraints:
|
|
33
|
+
* - Human format: multi-line with snippet and caret underline
|
|
34
|
+
* - JSON format: LSP Diagnostic compatible
|
|
35
|
+
* - Compact format: single line for CI output
|
|
36
|
+
*
|
|
37
|
+
* @param error - Enriched error with context
|
|
38
|
+
* @param options - Format options
|
|
39
|
+
* @returns Formatted error string
|
|
40
|
+
* @throws {TypeError} Unknown format
|
|
41
|
+
*/
|
|
42
|
+
export function formatError(
|
|
43
|
+
error: EnrichedError,
|
|
44
|
+
options: FormatOptions
|
|
45
|
+
): string {
|
|
46
|
+
// EC-5: Unknown format throws TypeError
|
|
47
|
+
if (
|
|
48
|
+
options.format !== 'human' &&
|
|
49
|
+
options.format !== 'json' &&
|
|
50
|
+
options.format !== 'compact'
|
|
51
|
+
) {
|
|
52
|
+
throw new TypeError(`Unknown format: ${options.format}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (options.format === 'json') {
|
|
56
|
+
return formatErrorJson(error, options);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (options.format === 'compact') {
|
|
60
|
+
return formatErrorCompact(error);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return formatErrorHuman(error, options);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Format error in human-readable format.
|
|
68
|
+
*
|
|
69
|
+
* Output format:
|
|
70
|
+
* ```
|
|
71
|
+
* error[RILL-R005]: Variable foo is not defined
|
|
72
|
+
* --> script.rill:5:10
|
|
73
|
+
* |
|
|
74
|
+
* 3 | "start" => $begin
|
|
75
|
+
* 4 | $begin -> .upper => $upper
|
|
76
|
+
* 5 | $foo -> .len
|
|
77
|
+
* | ^^^^ undefined variable
|
|
78
|
+
* |
|
|
79
|
+
* = help: Did you mean `$begin`?
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
function formatErrorHuman(
|
|
83
|
+
error: EnrichedError,
|
|
84
|
+
options: FormatOptions
|
|
85
|
+
): string {
|
|
86
|
+
const lines: string[] = [];
|
|
87
|
+
|
|
88
|
+
// Header: error[RILL-XXXX]: message
|
|
89
|
+
lines.push(`error[${error.errorId}]: ${error.message}`);
|
|
90
|
+
|
|
91
|
+
// Location: --> script.rill:5:10
|
|
92
|
+
if (error.span) {
|
|
93
|
+
const location = `${error.span.start.line}:${error.span.start.column}`;
|
|
94
|
+
lines.push(` --> ${location}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Source snippet with caret underline
|
|
98
|
+
if (error.sourceSnippet && error.sourceSnippet.lines.length > 0) {
|
|
99
|
+
lines.push(' |');
|
|
100
|
+
|
|
101
|
+
// Calculate padding width for line numbers
|
|
102
|
+
const maxLineNumber = Math.max(
|
|
103
|
+
...error.sourceSnippet.lines.map((l) => l.lineNumber)
|
|
104
|
+
);
|
|
105
|
+
const lineNumberWidth = String(maxLineNumber).length;
|
|
106
|
+
|
|
107
|
+
for (const line of error.sourceSnippet.lines) {
|
|
108
|
+
const lineNumStr = String(line.lineNumber).padStart(lineNumberWidth, ' ');
|
|
109
|
+
lines.push(` ${lineNumStr} | ${line.content}`);
|
|
110
|
+
|
|
111
|
+
// Add caret underline for error lines
|
|
112
|
+
if (line.isErrorLine && error.span) {
|
|
113
|
+
const caret = renderCaretUnderline(error.span, line.content);
|
|
114
|
+
const padding = ' '.repeat(lineNumberWidth);
|
|
115
|
+
lines.push(` ${padding} | ${caret}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
lines.push(' |');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Suggestions: = help: Did you mean `$begin`?
|
|
122
|
+
if (error.suggestions && error.suggestions.length > 0) {
|
|
123
|
+
for (const suggestion of error.suggestions) {
|
|
124
|
+
lines.push(` = help: ${suggestion}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Help URL
|
|
129
|
+
if (options.verbose && error.helpUrl) {
|
|
130
|
+
lines.push(` = see: ${error.helpUrl}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Call stack
|
|
134
|
+
if (
|
|
135
|
+
options.includeCallStack &&
|
|
136
|
+
error.callStack &&
|
|
137
|
+
error.callStack.length > 0
|
|
138
|
+
) {
|
|
139
|
+
lines.push('');
|
|
140
|
+
lines.push('Call stack:');
|
|
141
|
+
const depth = Math.min(error.callStack.length, options.maxCallStackDepth);
|
|
142
|
+
for (let i = 0; i < depth; i++) {
|
|
143
|
+
const frame = error.callStack[i]!;
|
|
144
|
+
const location = `${frame.location.start.line}:${frame.location.start.column}`;
|
|
145
|
+
const name = frame.functionName ?? '<anonymous>';
|
|
146
|
+
const context = frame.context ? ` (${frame.context})` : '';
|
|
147
|
+
lines.push(` ${i + 1}. ${name}${context} at ${location}`);
|
|
148
|
+
}
|
|
149
|
+
if (error.callStack.length > depth) {
|
|
150
|
+
lines.push(` ... ${error.callStack.length - depth} more frames`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return lines.join('\n');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Format error in JSON format (LSP Diagnostic compatible).
|
|
159
|
+
*/
|
|
160
|
+
function formatErrorJson(error: EnrichedError, options: FormatOptions): string {
|
|
161
|
+
const diagnostic: {
|
|
162
|
+
errorId: string;
|
|
163
|
+
severity: number;
|
|
164
|
+
message: string;
|
|
165
|
+
range?: {
|
|
166
|
+
start: { line: number; character: number };
|
|
167
|
+
end: { line: number; character: number };
|
|
168
|
+
};
|
|
169
|
+
source: string;
|
|
170
|
+
code: string;
|
|
171
|
+
suggestions?: string[];
|
|
172
|
+
callStack?: Array<{
|
|
173
|
+
location: {
|
|
174
|
+
start: { line: number; character: number };
|
|
175
|
+
end: { line: number; character: number };
|
|
176
|
+
};
|
|
177
|
+
functionName?: string | undefined;
|
|
178
|
+
context?: string | undefined;
|
|
179
|
+
}>;
|
|
180
|
+
helpUrl?: string;
|
|
181
|
+
} = {
|
|
182
|
+
errorId: error.errorId,
|
|
183
|
+
severity: 1, // Error severity in LSP (1 = Error, 2 = Warning, 3 = Information, 4 = Hint)
|
|
184
|
+
message: error.message,
|
|
185
|
+
source: 'rill',
|
|
186
|
+
code: error.errorId,
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
if (error.span) {
|
|
190
|
+
diagnostic.range = {
|
|
191
|
+
start: {
|
|
192
|
+
line: error.span.start.line - 1, // LSP uses 0-based line numbers
|
|
193
|
+
character: error.span.start.column,
|
|
194
|
+
},
|
|
195
|
+
end: {
|
|
196
|
+
line: error.span.end.line - 1, // LSP uses 0-based line numbers
|
|
197
|
+
character: error.span.end.column,
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (error.suggestions && error.suggestions.length > 0) {
|
|
203
|
+
diagnostic.suggestions = error.suggestions;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (
|
|
207
|
+
options.includeCallStack &&
|
|
208
|
+
error.callStack &&
|
|
209
|
+
error.callStack.length > 0
|
|
210
|
+
) {
|
|
211
|
+
const depth = Math.min(error.callStack.length, options.maxCallStackDepth);
|
|
212
|
+
diagnostic.callStack = error.callStack.slice(0, depth).map((frame) => {
|
|
213
|
+
const callFrame: {
|
|
214
|
+
location: {
|
|
215
|
+
start: { line: number; character: number };
|
|
216
|
+
end: { line: number; character: number };
|
|
217
|
+
};
|
|
218
|
+
functionName?: string | undefined;
|
|
219
|
+
context?: string | undefined;
|
|
220
|
+
} = {
|
|
221
|
+
location: {
|
|
222
|
+
start: {
|
|
223
|
+
line: frame.location.start.line - 1,
|
|
224
|
+
character: frame.location.start.column,
|
|
225
|
+
},
|
|
226
|
+
end: {
|
|
227
|
+
line: frame.location.end.line - 1,
|
|
228
|
+
character: frame.location.end.column,
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
if (frame.functionName !== undefined) {
|
|
233
|
+
callFrame.functionName = frame.functionName;
|
|
234
|
+
}
|
|
235
|
+
if (frame.context !== undefined) {
|
|
236
|
+
callFrame.context = frame.context;
|
|
237
|
+
}
|
|
238
|
+
return callFrame;
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (options.verbose && error.helpUrl) {
|
|
243
|
+
diagnostic.helpUrl = error.helpUrl;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return JSON.stringify(diagnostic, null, 2);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Format error in compact format (single line for CI).
|
|
251
|
+
*/
|
|
252
|
+
function formatErrorCompact(error: EnrichedError): string {
|
|
253
|
+
const parts: string[] = [`[${error.errorId}]`, error.message];
|
|
254
|
+
|
|
255
|
+
if (error.span) {
|
|
256
|
+
parts.push(`at ${error.span.start.line}:${error.span.start.column}`);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (error.suggestions && error.suggestions.length > 0) {
|
|
260
|
+
parts.push(`(hint: ${error.suggestions[0]})`);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return parts.join(' ');
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ============================================================
|
|
267
|
+
// CARET UNDERLINE
|
|
268
|
+
// ============================================================
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Render caret underline for error span.
|
|
272
|
+
*
|
|
273
|
+
* Constraints:
|
|
274
|
+
* - Single-char: single ^
|
|
275
|
+
* - Multi-char same line: ^^^^^ (length = span width)
|
|
276
|
+
* - Multi-line: ^^^^^ (continues) on first line only
|
|
277
|
+
*
|
|
278
|
+
* @param span - Error span
|
|
279
|
+
* @param lineContent - Content of the line
|
|
280
|
+
* @returns Caret underline string
|
|
281
|
+
* @throws {RangeError} Invalid span (start after end)
|
|
282
|
+
*/
|
|
283
|
+
export function renderCaretUnderline(
|
|
284
|
+
span: SourceSpan,
|
|
285
|
+
lineContent: string
|
|
286
|
+
): string {
|
|
287
|
+
// EC-8: Invalid span throws RangeError
|
|
288
|
+
if (
|
|
289
|
+
span.start.line > span.end.line ||
|
|
290
|
+
(span.start.line === span.end.line && span.start.column > span.end.column)
|
|
291
|
+
) {
|
|
292
|
+
throw new RangeError('Span start must precede end');
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Calculate the width of the underline
|
|
296
|
+
const startColumn = span.start.column;
|
|
297
|
+
let endColumn: number;
|
|
298
|
+
|
|
299
|
+
if (span.start.line === span.end.line) {
|
|
300
|
+
// Single-line span: underline from start to end
|
|
301
|
+
endColumn = span.end.column;
|
|
302
|
+
} else {
|
|
303
|
+
// Multi-line span: underline to end of first line
|
|
304
|
+
endColumn = lineContent.length;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Build underline: spaces before, carets for the span
|
|
308
|
+
const padding = ' '.repeat(startColumn);
|
|
309
|
+
const caretCount = Math.max(1, endColumn - startColumn);
|
|
310
|
+
const carets = '^'.repeat(caretCount);
|
|
311
|
+
|
|
312
|
+
return padding + carets;
|
|
313
|
+
}
|