@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,631 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for CLI Error Formatter
|
|
3
|
+
* Covers: IR-5, IR-7, EC-5, EC-8, IC-2
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect } from 'vitest';
|
|
7
|
+
import {
|
|
8
|
+
formatError,
|
|
9
|
+
renderCaretUnderline,
|
|
10
|
+
type EnrichedError,
|
|
11
|
+
type FormatOptions,
|
|
12
|
+
type CallFrame,
|
|
13
|
+
} from '../../src/cli-error-formatter.js';
|
|
14
|
+
import type {
|
|
15
|
+
SourceSpan,
|
|
16
|
+
SourceLocation,
|
|
17
|
+
} from '@rcrsr/rill';
|
|
18
|
+
import type { SourceSnippet } from '../../src/cli-error-enrichment.js';
|
|
19
|
+
|
|
20
|
+
describe('formatError', () => {
|
|
21
|
+
describe('IR-5: Human format with header and location', () => {
|
|
22
|
+
it('formats error with header and location', () => {
|
|
23
|
+
const error: EnrichedError = {
|
|
24
|
+
errorId: 'RILL-R005',
|
|
25
|
+
message: 'Variable foo is not defined',
|
|
26
|
+
span: {
|
|
27
|
+
start: { line: 5, column: 10, offset: 50 },
|
|
28
|
+
end: { line: 5, column: 13, offset: 53 },
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const options: FormatOptions = {
|
|
33
|
+
format: 'human',
|
|
34
|
+
verbose: false,
|
|
35
|
+
includeCallStack: false,
|
|
36
|
+
maxCallStackDepth: 10,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const result = formatError(error, options);
|
|
40
|
+
|
|
41
|
+
expect(result).toContain('error[RILL-R005]: Variable foo is not defined');
|
|
42
|
+
expect(result).toContain(' --> 5:10');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('formats error with source snippet and caret underline', () => {
|
|
46
|
+
const error: EnrichedError = {
|
|
47
|
+
errorId: 'RILL-R005',
|
|
48
|
+
message: 'Variable foo is not defined',
|
|
49
|
+
span: {
|
|
50
|
+
start: { line: 3, column: 2, offset: 20 },
|
|
51
|
+
end: { line: 3, column: 6, offset: 24 },
|
|
52
|
+
},
|
|
53
|
+
sourceSnippet: {
|
|
54
|
+
lines: [
|
|
55
|
+
{ lineNumber: 1, content: '"start" => $begin', isErrorLine: false },
|
|
56
|
+
{
|
|
57
|
+
lineNumber: 2,
|
|
58
|
+
content: '$begin -> .upper => $upper',
|
|
59
|
+
isErrorLine: false,
|
|
60
|
+
},
|
|
61
|
+
{ lineNumber: 3, content: '$foo -> .len', isErrorLine: true },
|
|
62
|
+
{ lineNumber: 4, content: 'end', isErrorLine: false },
|
|
63
|
+
],
|
|
64
|
+
highlightSpan: {
|
|
65
|
+
start: { line: 3, column: 2, offset: 20 },
|
|
66
|
+
end: { line: 3, column: 6, offset: 24 },
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const options: FormatOptions = {
|
|
72
|
+
format: 'human',
|
|
73
|
+
verbose: false,
|
|
74
|
+
includeCallStack: false,
|
|
75
|
+
maxCallStackDepth: 10,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const result = formatError(error, options);
|
|
79
|
+
|
|
80
|
+
expect(result).toContain('error[RILL-R005]');
|
|
81
|
+
expect(result).toContain(' --> 3:2');
|
|
82
|
+
expect(result).toContain(' |');
|
|
83
|
+
expect(result).toContain(' 1 | "start" => $begin');
|
|
84
|
+
expect(result).toContain(' 2 | $begin -> .upper => $upper');
|
|
85
|
+
expect(result).toContain(' 3 | $foo -> .len');
|
|
86
|
+
expect(result).toContain(' | ^^^^');
|
|
87
|
+
expect(result).toContain(' 4 | end');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('formats error with suggestions', () => {
|
|
91
|
+
const error: EnrichedError = {
|
|
92
|
+
errorId: 'RILL-R005',
|
|
93
|
+
message: 'Variable foo is not defined',
|
|
94
|
+
span: {
|
|
95
|
+
start: { line: 3, column: 0, offset: 18 },
|
|
96
|
+
end: { line: 3, column: 4, offset: 22 },
|
|
97
|
+
},
|
|
98
|
+
suggestions: ['Did you mean `$begin`?', 'Try declaring it first'],
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const options: FormatOptions = {
|
|
102
|
+
format: 'human',
|
|
103
|
+
verbose: false,
|
|
104
|
+
includeCallStack: false,
|
|
105
|
+
maxCallStackDepth: 10,
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const result = formatError(error, options);
|
|
109
|
+
|
|
110
|
+
expect(result).toContain(' = help: Did you mean `$begin`?');
|
|
111
|
+
expect(result).toContain(' = help: Try declaring it first');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('includes help URL when verbose', () => {
|
|
115
|
+
const error: EnrichedError = {
|
|
116
|
+
errorId: 'RILL-R005',
|
|
117
|
+
message: 'Variable foo is not defined',
|
|
118
|
+
helpUrl: 'https://example.com/errors/R005',
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const options: FormatOptions = {
|
|
122
|
+
format: 'human',
|
|
123
|
+
verbose: true,
|
|
124
|
+
includeCallStack: false,
|
|
125
|
+
maxCallStackDepth: 10,
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const result = formatError(error, options);
|
|
129
|
+
|
|
130
|
+
expect(result).toContain(' = see: https://example.com/errors/R005');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('excludes help URL when not verbose', () => {
|
|
134
|
+
const error: EnrichedError = {
|
|
135
|
+
errorId: 'RILL-R005',
|
|
136
|
+
message: 'Variable foo is not defined',
|
|
137
|
+
helpUrl: 'https://example.com/errors/R005',
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const options: FormatOptions = {
|
|
141
|
+
format: 'human',
|
|
142
|
+
verbose: false,
|
|
143
|
+
includeCallStack: false,
|
|
144
|
+
maxCallStackDepth: 10,
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const result = formatError(error, options);
|
|
148
|
+
|
|
149
|
+
expect(result).not.toContain('https://example.com/errors/R005');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('formats error with call stack when enabled', () => {
|
|
153
|
+
const callStack: CallFrame[] = [
|
|
154
|
+
{
|
|
155
|
+
location: {
|
|
156
|
+
start: { line: 10, column: 5, offset: 100 },
|
|
157
|
+
end: { line: 10, column: 15, offset: 110 },
|
|
158
|
+
},
|
|
159
|
+
functionName: 'myFunction',
|
|
160
|
+
context: 'in each body',
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
location: {
|
|
164
|
+
start: { line: 5, column: 0, offset: 50 },
|
|
165
|
+
end: { line: 5, column: 10, offset: 60 },
|
|
166
|
+
},
|
|
167
|
+
functionName: 'outer',
|
|
168
|
+
},
|
|
169
|
+
];
|
|
170
|
+
|
|
171
|
+
const error: EnrichedError = {
|
|
172
|
+
errorId: 'RILL-R001',
|
|
173
|
+
message: 'Runtime error occurred',
|
|
174
|
+
callStack,
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const options: FormatOptions = {
|
|
178
|
+
format: 'human',
|
|
179
|
+
verbose: false,
|
|
180
|
+
includeCallStack: true,
|
|
181
|
+
maxCallStackDepth: 10,
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const result = formatError(error, options);
|
|
185
|
+
|
|
186
|
+
expect(result).toContain('Call stack:');
|
|
187
|
+
expect(result).toContain(' 1. myFunction (in each body) at 10:5');
|
|
188
|
+
expect(result).toContain(' 2. outer at 5:0');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('limits call stack depth', () => {
|
|
192
|
+
const callStack: CallFrame[] = [
|
|
193
|
+
{
|
|
194
|
+
location: {
|
|
195
|
+
start: { line: 1, column: 0, offset: 0 },
|
|
196
|
+
end: { line: 1, column: 5, offset: 5 },
|
|
197
|
+
},
|
|
198
|
+
functionName: 'fn1',
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
location: {
|
|
202
|
+
start: { line: 2, column: 0, offset: 10 },
|
|
203
|
+
end: { line: 2, column: 5, offset: 15 },
|
|
204
|
+
},
|
|
205
|
+
functionName: 'fn2',
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
location: {
|
|
209
|
+
start: { line: 3, column: 0, offset: 20 },
|
|
210
|
+
end: { line: 3, column: 5, offset: 25 },
|
|
211
|
+
},
|
|
212
|
+
functionName: 'fn3',
|
|
213
|
+
},
|
|
214
|
+
];
|
|
215
|
+
|
|
216
|
+
const error: EnrichedError = {
|
|
217
|
+
errorId: 'RILL-R001',
|
|
218
|
+
message: 'Runtime error',
|
|
219
|
+
callStack,
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const options: FormatOptions = {
|
|
223
|
+
format: 'human',
|
|
224
|
+
verbose: false,
|
|
225
|
+
includeCallStack: true,
|
|
226
|
+
maxCallStackDepth: 2,
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const result = formatError(error, options);
|
|
230
|
+
|
|
231
|
+
expect(result).toContain(' 1. fn1');
|
|
232
|
+
expect(result).toContain(' 2. fn2');
|
|
233
|
+
expect(result).toContain(' ... 1 more frames');
|
|
234
|
+
expect(result).not.toContain('fn3');
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
describe('IR-5: JSON format with LSP structure', () => {
|
|
239
|
+
it('formats error as JSON with LSP diagnostic structure', () => {
|
|
240
|
+
const error: EnrichedError = {
|
|
241
|
+
errorId: 'RILL-R005',
|
|
242
|
+
message: 'Variable foo is not defined',
|
|
243
|
+
span: {
|
|
244
|
+
start: { line: 5, column: 10, offset: 50 },
|
|
245
|
+
end: { line: 5, column: 13, offset: 53 },
|
|
246
|
+
},
|
|
247
|
+
suggestions: ['Did you mean `$begin`?'],
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const options: FormatOptions = {
|
|
251
|
+
format: 'json',
|
|
252
|
+
verbose: false,
|
|
253
|
+
includeCallStack: false,
|
|
254
|
+
maxCallStackDepth: 10,
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const result = formatError(error, options);
|
|
258
|
+
const diagnostic = JSON.parse(result);
|
|
259
|
+
|
|
260
|
+
expect(diagnostic.errorId).toBe('RILL-R005');
|
|
261
|
+
expect(diagnostic.severity).toBe(1); // Error
|
|
262
|
+
expect(diagnostic.message).toBe('Variable foo is not defined');
|
|
263
|
+
expect(diagnostic.source).toBe('rill');
|
|
264
|
+
expect(diagnostic.code).toBe('RILL-R005');
|
|
265
|
+
expect(diagnostic.range).toEqual({
|
|
266
|
+
start: { line: 4, character: 10 }, // LSP uses 0-based lines
|
|
267
|
+
end: { line: 4, character: 13 },
|
|
268
|
+
});
|
|
269
|
+
expect(diagnostic.suggestions).toEqual(['Did you mean `$begin`?']);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('includes call stack in JSON format when enabled', () => {
|
|
273
|
+
const callStack: CallFrame[] = [
|
|
274
|
+
{
|
|
275
|
+
location: {
|
|
276
|
+
start: { line: 10, column: 5, offset: 100 },
|
|
277
|
+
end: { line: 10, column: 15, offset: 110 },
|
|
278
|
+
},
|
|
279
|
+
functionName: 'myFunction',
|
|
280
|
+
context: 'in each body',
|
|
281
|
+
},
|
|
282
|
+
];
|
|
283
|
+
|
|
284
|
+
const error: EnrichedError = {
|
|
285
|
+
errorId: 'RILL-R001',
|
|
286
|
+
message: 'Runtime error',
|
|
287
|
+
callStack,
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
const options: FormatOptions = {
|
|
291
|
+
format: 'json',
|
|
292
|
+
verbose: false,
|
|
293
|
+
includeCallStack: true,
|
|
294
|
+
maxCallStackDepth: 10,
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
const result = formatError(error, options);
|
|
298
|
+
const diagnostic = JSON.parse(result);
|
|
299
|
+
|
|
300
|
+
expect(diagnostic.callStack).toHaveLength(1);
|
|
301
|
+
expect(diagnostic.callStack[0]).toEqual({
|
|
302
|
+
location: {
|
|
303
|
+
start: { line: 9, character: 5 }, // 0-based
|
|
304
|
+
end: { line: 9, character: 15 },
|
|
305
|
+
},
|
|
306
|
+
functionName: 'myFunction',
|
|
307
|
+
context: 'in each body',
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('excludes call stack when not enabled', () => {
|
|
312
|
+
const callStack: CallFrame[] = [
|
|
313
|
+
{
|
|
314
|
+
location: {
|
|
315
|
+
start: { line: 10, column: 5, offset: 100 },
|
|
316
|
+
end: { line: 10, column: 15, offset: 110 },
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
];
|
|
320
|
+
|
|
321
|
+
const error: EnrichedError = {
|
|
322
|
+
errorId: 'RILL-R001',
|
|
323
|
+
message: 'Runtime error',
|
|
324
|
+
callStack,
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
const options: FormatOptions = {
|
|
328
|
+
format: 'json',
|
|
329
|
+
verbose: false,
|
|
330
|
+
includeCallStack: false,
|
|
331
|
+
maxCallStackDepth: 10,
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
const result = formatError(error, options);
|
|
335
|
+
const diagnostic = JSON.parse(result);
|
|
336
|
+
|
|
337
|
+
expect(diagnostic.callStack).toBeUndefined();
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it('includes help URL in JSON when verbose', () => {
|
|
341
|
+
const error: EnrichedError = {
|
|
342
|
+
errorId: 'RILL-R005',
|
|
343
|
+
message: 'Variable foo is not defined',
|
|
344
|
+
helpUrl: 'https://example.com/errors/R005',
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
const options: FormatOptions = {
|
|
348
|
+
format: 'json',
|
|
349
|
+
verbose: true,
|
|
350
|
+
includeCallStack: false,
|
|
351
|
+
maxCallStackDepth: 10,
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
const result = formatError(error, options);
|
|
355
|
+
const diagnostic = JSON.parse(result);
|
|
356
|
+
|
|
357
|
+
expect(diagnostic.helpUrl).toBe('https://example.com/errors/R005');
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
describe('IR-5: Compact format single line', () => {
|
|
362
|
+
it('formats error as single line', () => {
|
|
363
|
+
const error: EnrichedError = {
|
|
364
|
+
errorId: 'RILL-R005',
|
|
365
|
+
message: 'Variable foo is not defined',
|
|
366
|
+
span: {
|
|
367
|
+
start: { line: 5, column: 10, offset: 50 },
|
|
368
|
+
end: { line: 5, column: 13, offset: 53 },
|
|
369
|
+
},
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
const options: FormatOptions = {
|
|
373
|
+
format: 'compact',
|
|
374
|
+
verbose: false,
|
|
375
|
+
includeCallStack: false,
|
|
376
|
+
maxCallStackDepth: 10,
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
const result = formatError(error, options);
|
|
380
|
+
|
|
381
|
+
expect(result).toBe('[RILL-R005] Variable foo is not defined at 5:10');
|
|
382
|
+
expect(result).not.toContain('\n');
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it('includes first suggestion as hint', () => {
|
|
386
|
+
const error: EnrichedError = {
|
|
387
|
+
errorId: 'RILL-R005',
|
|
388
|
+
message: 'Variable foo is not defined',
|
|
389
|
+
suggestions: ['Did you mean `$begin`?', 'Another suggestion'],
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
const options: FormatOptions = {
|
|
393
|
+
format: 'compact',
|
|
394
|
+
verbose: false,
|
|
395
|
+
includeCallStack: false,
|
|
396
|
+
maxCallStackDepth: 10,
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
const result = formatError(error, options);
|
|
400
|
+
|
|
401
|
+
expect(result).toBe(
|
|
402
|
+
'[RILL-R005] Variable foo is not defined (hint: Did you mean `$begin`?)'
|
|
403
|
+
);
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it('formats without location when span absent', () => {
|
|
407
|
+
const error: EnrichedError = {
|
|
408
|
+
errorId: 'RILL-R001',
|
|
409
|
+
message: 'Runtime error',
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
const options: FormatOptions = {
|
|
413
|
+
format: 'compact',
|
|
414
|
+
verbose: false,
|
|
415
|
+
includeCallStack: false,
|
|
416
|
+
maxCallStackDepth: 10,
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
const result = formatError(error, options);
|
|
420
|
+
|
|
421
|
+
expect(result).toBe('[RILL-R001] Runtime error');
|
|
422
|
+
});
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
describe('EC-5: Unknown format throws TypeError', () => {
|
|
426
|
+
it('throws TypeError for unknown format', () => {
|
|
427
|
+
const error: EnrichedError = {
|
|
428
|
+
errorId: 'RILL-R001',
|
|
429
|
+
message: 'Error message',
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
const options: FormatOptions = {
|
|
433
|
+
format: 'xml' as 'human', // Force invalid format
|
|
434
|
+
verbose: false,
|
|
435
|
+
includeCallStack: false,
|
|
436
|
+
maxCallStackDepth: 10,
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
expect(() => formatError(error, options)).toThrow(TypeError);
|
|
440
|
+
expect(() => formatError(error, options)).toThrow('Unknown format: xml');
|
|
441
|
+
});
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
describe('renderCaretUnderline', () => {
|
|
446
|
+
describe('IR-7: Single char shows ^', () => {
|
|
447
|
+
it('renders single caret for single character span', () => {
|
|
448
|
+
const span: SourceSpan = {
|
|
449
|
+
start: { line: 1, column: 5, offset: 5 },
|
|
450
|
+
end: { line: 1, column: 6, offset: 6 },
|
|
451
|
+
};
|
|
452
|
+
const lineContent = 'hello world';
|
|
453
|
+
|
|
454
|
+
const result = renderCaretUnderline(span, lineContent);
|
|
455
|
+
|
|
456
|
+
expect(result).toBe(' ^');
|
|
457
|
+
});
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
describe('IR-7: Multi-char shows ^^^^^', () => {
|
|
461
|
+
it('renders multiple carets for multi-character span', () => {
|
|
462
|
+
const span: SourceSpan = {
|
|
463
|
+
start: { line: 1, column: 2, offset: 2 },
|
|
464
|
+
end: { line: 1, column: 6, offset: 6 },
|
|
465
|
+
};
|
|
466
|
+
const lineContent = '$foo -> .len';
|
|
467
|
+
|
|
468
|
+
const result = renderCaretUnderline(span, lineContent);
|
|
469
|
+
|
|
470
|
+
expect(result).toBe(' ^^^^'); // 2 spaces + 4 carets
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
it('handles zero-width span as single caret', () => {
|
|
474
|
+
const span: SourceSpan = {
|
|
475
|
+
start: { line: 1, column: 3, offset: 3 },
|
|
476
|
+
end: { line: 1, column: 3, offset: 3 },
|
|
477
|
+
};
|
|
478
|
+
const lineContent = 'hello';
|
|
479
|
+
|
|
480
|
+
const result = renderCaretUnderline(span, lineContent);
|
|
481
|
+
|
|
482
|
+
expect(result).toBe(' ^'); // 3 spaces + 1 caret (minimum)
|
|
483
|
+
});
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
describe('IR-7: Multi-line span continues on first line', () => {
|
|
487
|
+
it('renders carets to end of line for multi-line span', () => {
|
|
488
|
+
const span: SourceSpan = {
|
|
489
|
+
start: { line: 1, column: 6, offset: 6 },
|
|
490
|
+
end: { line: 3, column: 2, offset: 25 },
|
|
491
|
+
};
|
|
492
|
+
const lineContent = 'hello world';
|
|
493
|
+
|
|
494
|
+
const result = renderCaretUnderline(span, lineContent);
|
|
495
|
+
|
|
496
|
+
expect(result).toBe(' ^^^^^'); // 6 spaces + 5 carets (from col 6 to end)
|
|
497
|
+
});
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
describe('EC-8: Invalid span throws RangeError', () => {
|
|
501
|
+
it('throws when start line after end line', () => {
|
|
502
|
+
const span: SourceSpan = {
|
|
503
|
+
start: { line: 5, column: 0, offset: 50 },
|
|
504
|
+
end: { line: 3, column: 0, offset: 30 },
|
|
505
|
+
};
|
|
506
|
+
const lineContent = 'content';
|
|
507
|
+
|
|
508
|
+
expect(() => renderCaretUnderline(span, lineContent)).toThrow(RangeError);
|
|
509
|
+
expect(() => renderCaretUnderline(span, lineContent)).toThrow(
|
|
510
|
+
'Span start must precede end'
|
|
511
|
+
);
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
it('throws when start column after end column on same line', () => {
|
|
515
|
+
const span: SourceSpan = {
|
|
516
|
+
start: { line: 1, column: 10, offset: 10 },
|
|
517
|
+
end: { line: 1, column: 5, offset: 5 },
|
|
518
|
+
};
|
|
519
|
+
const lineContent = 'hello world';
|
|
520
|
+
|
|
521
|
+
expect(() => renderCaretUnderline(span, lineContent)).toThrow(RangeError);
|
|
522
|
+
expect(() => renderCaretUnderline(span, lineContent)).toThrow(
|
|
523
|
+
'Span start must precede end'
|
|
524
|
+
);
|
|
525
|
+
});
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
describe('AC-20: Error at final character renders correctly', () => {
|
|
529
|
+
it('renders caret at last character position', () => {
|
|
530
|
+
const lineContent = 'hello world';
|
|
531
|
+
const lastCharColumn = lineContent.length - 1; // Column of 'd'
|
|
532
|
+
|
|
533
|
+
const span: SourceSpan = {
|
|
534
|
+
start: { line: 1, column: lastCharColumn, offset: lastCharColumn },
|
|
535
|
+
end: {
|
|
536
|
+
line: 1,
|
|
537
|
+
column: lineContent.length,
|
|
538
|
+
offset: lineContent.length,
|
|
539
|
+
},
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
const result = renderCaretUnderline(span, lineContent);
|
|
543
|
+
|
|
544
|
+
expect(result).toBe(' ^'); // 10 spaces + 1 caret
|
|
545
|
+
expect(result.length).toBe(lineContent.length); // Underline should align
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
it('renders caret at very end of line (past last char)', () => {
|
|
549
|
+
const lineContent = 'test';
|
|
550
|
+
const span: SourceSpan = {
|
|
551
|
+
start: { line: 1, column: 4, offset: 4 }, // After 't' (column 4)
|
|
552
|
+
end: { line: 1, column: 4, offset: 4 },
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
const result = renderCaretUnderline(span, lineContent);
|
|
556
|
+
|
|
557
|
+
expect(result).toBe(' ^'); // 4 spaces + 1 caret
|
|
558
|
+
});
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
describe('Edge cases', () => {
|
|
562
|
+
it('handles span at start of line', () => {
|
|
563
|
+
const span: SourceSpan = {
|
|
564
|
+
start: { line: 1, column: 0, offset: 0 },
|
|
565
|
+
end: { line: 1, column: 5, offset: 5 },
|
|
566
|
+
};
|
|
567
|
+
const lineContent = 'hello';
|
|
568
|
+
|
|
569
|
+
const result = renderCaretUnderline(span, lineContent);
|
|
570
|
+
|
|
571
|
+
expect(result).toBe('^^^^^'); // No padding, 5 carets
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
it('handles empty line content', () => {
|
|
575
|
+
const span: SourceSpan = {
|
|
576
|
+
start: { line: 1, column: 0, offset: 0 },
|
|
577
|
+
end: { line: 1, column: 0, offset: 0 },
|
|
578
|
+
};
|
|
579
|
+
const lineContent = '';
|
|
580
|
+
|
|
581
|
+
const result = renderCaretUnderline(span, lineContent);
|
|
582
|
+
|
|
583
|
+
expect(result).toBe('^'); // Single caret (minimum)
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
it('handles span extending beyond line content', () => {
|
|
587
|
+
const span: SourceSpan = {
|
|
588
|
+
start: { line: 1, column: 3, offset: 3 },
|
|
589
|
+
end: { line: 1, column: 10, offset: 10 },
|
|
590
|
+
};
|
|
591
|
+
const lineContent = 'hi'; // Only 2 chars, but span goes to col 10
|
|
592
|
+
|
|
593
|
+
const result = renderCaretUnderline(span, lineContent);
|
|
594
|
+
|
|
595
|
+
expect(result).toBe(' ^^^^^^^'); // 3 spaces + 7 carets
|
|
596
|
+
});
|
|
597
|
+
});
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
describe('IC-2: Type definitions present', () => {
|
|
601
|
+
it('exports EnrichedError type', () => {
|
|
602
|
+
const error: EnrichedError = {
|
|
603
|
+
errorId: 'RILL-R001',
|
|
604
|
+
message: 'Test',
|
|
605
|
+
};
|
|
606
|
+
|
|
607
|
+
expect(error.errorId).toBe('RILL-R001');
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
it('exports FormatOptions type', () => {
|
|
611
|
+
const options: FormatOptions = {
|
|
612
|
+
format: 'human',
|
|
613
|
+
verbose: false,
|
|
614
|
+
includeCallStack: false,
|
|
615
|
+
maxCallStackDepth: 10,
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
expect(options.format).toBe('human');
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
it('exports CallFrame type', () => {
|
|
622
|
+
const frame: CallFrame = {
|
|
623
|
+
location: {
|
|
624
|
+
start: { line: 1, column: 0, offset: 0 },
|
|
625
|
+
end: { line: 1, column: 5, offset: 5 },
|
|
626
|
+
},
|
|
627
|
+
};
|
|
628
|
+
|
|
629
|
+
expect(frame.location.start.line).toBe(1);
|
|
630
|
+
});
|
|
631
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rill CLI Tests: rill-eval command
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, it } from 'vitest';
|
|
6
|
+
import {
|
|
7
|
+
ParseError,
|
|
8
|
+
RuntimeError,
|
|
9
|
+
callable,
|
|
10
|
+
isCallable,
|
|
11
|
+
} from '@rcrsr/rill';
|
|
12
|
+
import { formatOutput } from '../../src/cli-shared.js';
|
|
13
|
+
import { evaluateExpression } from '../../src/cli-eval.js';
|
|
14
|
+
|
|
15
|
+
describe('rill-eval', () => {
|
|
16
|
+
describe('evaluateExpression', () => {
|
|
17
|
+
it('evaluates string methods', async () => {
|
|
18
|
+
expect((await evaluateExpression('"hello".len')).value).toBe(5);
|
|
19
|
+
expect((await evaluateExpression('"hello".upper')).value).toBe('HELLO');
|
|
20
|
+
expect((await evaluateExpression('" hi ".trim')).value).toBe('hi');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('evaluates arithmetic', async () => {
|
|
24
|
+
expect((await evaluateExpression('5 + 3')).value).toBe(8);
|
|
25
|
+
expect((await evaluateExpression('10 - 4')).value).toBe(6);
|
|
26
|
+
expect((await evaluateExpression('6 * 7')).value).toBe(42);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('evaluates pipes', async () => {
|
|
30
|
+
expect((await evaluateExpression('"hello" -> .upper')).value).toBe(
|
|
31
|
+
'HELLO'
|
|
32
|
+
);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('evaluates collections', async () => {
|
|
36
|
+
expect((await evaluateExpression('[1, 2, 3] -> .len')).value).toBe(3);
|
|
37
|
+
expect(
|
|
38
|
+
(await evaluateExpression('[1, 2, 3] -> map |x|($x * 2)')).value
|
|
39
|
+
).toEqual([2, 4, 6]);
|
|
40
|
+
expect((await evaluateExpression('[a: 1].a')).value).toBe(1);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('evaluates closures', async () => {
|
|
44
|
+
const result = await evaluateExpression('|x| { $x }');
|
|
45
|
+
expect(isCallable(result.value)).toBe(true);
|
|
46
|
+
expect(formatOutput(result.value)).toBe('[closure]');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('handles empty values', async () => {
|
|
50
|
+
expect((await evaluateExpression('""')).value).toBe('');
|
|
51
|
+
expect((await evaluateExpression('[]')).value).toEqual([]);
|
|
52
|
+
expect((await evaluateExpression('0')).value).toBe(0);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('throws parse errors', async () => {
|
|
56
|
+
await expect(evaluateExpression('{')).rejects.toThrow(ParseError);
|
|
57
|
+
await expect(evaluateExpression('|x| x }')).rejects.toThrow(ParseError);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('throws runtime errors', async () => {
|
|
61
|
+
await expect(evaluateExpression('$undefined')).rejects.toThrow(
|
|
62
|
+
RuntimeError
|
|
63
|
+
);
|
|
64
|
+
await expect(evaluateExpression('"string" + 5')).rejects.toThrow(
|
|
65
|
+
RuntimeError
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('preserves error details', async () => {
|
|
70
|
+
try {
|
|
71
|
+
await evaluateExpression('$missing');
|
|
72
|
+
} catch (err) {
|
|
73
|
+
expect(err).toBeInstanceOf(RuntimeError);
|
|
74
|
+
expect((err as RuntimeError).errorId).toBe('RILL-R005');
|
|
75
|
+
expect((err as RuntimeError).location?.line).toBe(1);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('formatOutput for eval results', () => {
|
|
81
|
+
it('formats closures from expressions', () => {
|
|
82
|
+
expect(formatOutput(callable(() => 'x'))).toBe('[closure]');
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
});
|