@rcrsr/rill-cli 0.7.2 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/dist/cli-shared.d.ts.map +1 -1
  2. package/dist/cli-shared.js +3 -1
  3. package/dist/cli-shared.js.map +1 -1
  4. package/package.json +16 -1
  5. package/src/check/config.ts +0 -202
  6. package/src/check/fixer.ts +0 -174
  7. package/src/check/index.ts +0 -39
  8. package/src/check/rules/anti-patterns.ts +0 -585
  9. package/src/check/rules/closures.ts +0 -445
  10. package/src/check/rules/collections.ts +0 -437
  11. package/src/check/rules/conditionals.ts +0 -155
  12. package/src/check/rules/flow.ts +0 -262
  13. package/src/check/rules/formatting.ts +0 -811
  14. package/src/check/rules/helpers.ts +0 -89
  15. package/src/check/rules/index.ts +0 -140
  16. package/src/check/rules/loops.ts +0 -372
  17. package/src/check/rules/naming.ts +0 -242
  18. package/src/check/rules/strings.ts +0 -104
  19. package/src/check/rules/types.ts +0 -213
  20. package/src/check/types.ts +0 -163
  21. package/src/check/validator.ts +0 -136
  22. package/src/check/visitor.ts +0 -338
  23. package/src/cli-check.ts +0 -456
  24. package/src/cli-error-enrichment.ts +0 -274
  25. package/src/cli-error-formatter.ts +0 -313
  26. package/src/cli-eval.ts +0 -145
  27. package/src/cli-exec.ts +0 -408
  28. package/src/cli-explain.ts +0 -76
  29. package/src/cli-lsp-diagnostic.ts +0 -132
  30. package/src/cli-module-loader.ts +0 -101
  31. package/src/cli-shared.ts +0 -187
  32. package/tests/check/cli-check.test.ts +0 -189
  33. package/tests/check/config.test.ts +0 -350
  34. package/tests/check/fixer.test.ts +0 -373
  35. package/tests/check/format-diagnostics.test.ts +0 -327
  36. package/tests/check/rules/anti-patterns.test.ts +0 -467
  37. package/tests/check/rules/closures.test.ts +0 -192
  38. package/tests/check/rules/collections.test.ts +0 -380
  39. package/tests/check/rules/conditionals.test.ts +0 -185
  40. package/tests/check/rules/flow.test.ts +0 -250
  41. package/tests/check/rules/formatting.test.ts +0 -755
  42. package/tests/check/rules/loops.test.ts +0 -334
  43. package/tests/check/rules/naming.test.ts +0 -336
  44. package/tests/check/rules/strings.test.ts +0 -129
  45. package/tests/check/rules/types.test.ts +0 -233
  46. package/tests/check/validator.test.ts +0 -444
  47. package/tests/check/visitor.test.ts +0 -171
  48. package/tests/cli/check.test.ts +0 -801
  49. package/tests/cli/error-enrichment.test.ts +0 -510
  50. package/tests/cli/error-formatter.test.ts +0 -631
  51. package/tests/cli/eval.test.ts +0 -85
  52. package/tests/cli/exec.test.ts +0 -537
  53. package/tests/cli-explain.test.ts +0 -249
  54. package/tests/cli-lsp-diagnostic.test.ts +0 -202
  55. package/tests/cli-shared.test.ts +0 -439
  56. package/tsconfig.json +0 -9
  57. package/tsconfig.tsbuildinfo +0 -1
@@ -1,510 +0,0 @@
1
- /**
2
- * Tests for CLI Error Enrichment Functions
3
- * Covers: IR-4, IR-6, IR-8, EC-3, EC-4, EC-6, EC-7, EC-9, EC-10, IC-1
4
- */
5
-
6
- import { describe, it, expect } from 'vitest';
7
- import {
8
- extractSnippet,
9
- suggestSimilarNames,
10
- enrichError,
11
- } from '../../src/cli-error-enrichment.js';
12
- import type {
13
- SourceSpan,
14
- SourceLocation,
15
- } from '@rcrsr/rill';
16
- import { RuntimeError } from '@rcrsr/rill';
17
-
18
- describe('extractSnippet', () => {
19
- describe('IR-6: Context lines and line numbering', () => {
20
- it('extracts 2 context lines before and after by default', () => {
21
- const source = 'line1\nline2\nline3\nERROR\nline5\nline6\nline7';
22
- const span: SourceSpan = {
23
- start: { line: 4, column: 0, offset: 18 },
24
- end: { line: 4, column: 5, offset: 23 },
25
- };
26
-
27
- const snippet = extractSnippet(source, span);
28
-
29
- expect(snippet.lines).toHaveLength(5); // 2 before + error line + 2 after
30
- expect(snippet.lines[0]).toEqual({
31
- lineNumber: 2,
32
- content: 'line2',
33
- isErrorLine: false,
34
- });
35
- expect(snippet.lines[1]).toEqual({
36
- lineNumber: 3,
37
- content: 'line3',
38
- isErrorLine: false,
39
- });
40
- expect(snippet.lines[2]).toEqual({
41
- lineNumber: 4,
42
- content: 'ERROR',
43
- isErrorLine: true,
44
- });
45
- expect(snippet.lines[3]).toEqual({
46
- lineNumber: 5,
47
- content: 'line5',
48
- isErrorLine: false,
49
- });
50
- expect(snippet.lines[4]).toEqual({
51
- lineNumber: 6,
52
- content: 'line6',
53
- isErrorLine: false,
54
- });
55
- expect(snippet.highlightSpan).toBe(span);
56
- });
57
-
58
- it('handles configurable context lines', () => {
59
- const source = 'L1\nL2\nL3\nERROR\nL5\nL6\nL7';
60
- const span: SourceSpan = {
61
- start: { line: 4, column: 0, offset: 9 },
62
- end: { line: 4, column: 5, offset: 14 },
63
- };
64
-
65
- const snippet = extractSnippet(source, span, 1);
66
-
67
- expect(snippet.lines).toHaveLength(3); // 1 before + error line + 1 after
68
- expect(snippet.lines[0]?.lineNumber).toBe(3);
69
- expect(snippet.lines[1]?.lineNumber).toBe(4);
70
- expect(snippet.lines[2]?.lineNumber).toBe(5);
71
- });
72
-
73
- it('handles line 1 edge case (no negative lines)', () => {
74
- const source = 'ERROR\nline2\nline3';
75
- const span: SourceSpan = {
76
- start: { line: 1, column: 0, offset: 0 },
77
- end: { line: 1, column: 5, offset: 5 },
78
- };
79
-
80
- const snippet = extractSnippet(source, span);
81
-
82
- expect(snippet.lines).toHaveLength(3); // error line + 2 after (no before)
83
- expect(snippet.lines[0]).toEqual({
84
- lineNumber: 1,
85
- content: 'ERROR',
86
- isErrorLine: true,
87
- });
88
- expect(snippet.lines[1]?.lineNumber).toBe(2);
89
- expect(snippet.lines[2]?.lineNumber).toBe(3);
90
- });
91
-
92
- it('handles last line edge case', () => {
93
- const source = 'line1\nline2\nline3\nERROR';
94
- const span: SourceSpan = {
95
- start: { line: 4, column: 0, offset: 18 },
96
- end: { line: 4, column: 5, offset: 23 },
97
- };
98
-
99
- const snippet = extractSnippet(source, span);
100
-
101
- expect(snippet.lines).toHaveLength(3); // 2 before + error line (no after)
102
- expect(snippet.lines[0]?.lineNumber).toBe(2);
103
- expect(snippet.lines[1]?.lineNumber).toBe(3);
104
- expect(snippet.lines[2]).toEqual({
105
- lineNumber: 4,
106
- content: 'ERROR',
107
- isErrorLine: true,
108
- });
109
- });
110
-
111
- it('uses 1-based line numbers', () => {
112
- const source = 'first\nsecond\nthird';
113
- const span: SourceSpan = {
114
- start: { line: 1, column: 0, offset: 0 },
115
- end: { line: 1, column: 5, offset: 5 },
116
- };
117
-
118
- const snippet = extractSnippet(source, span);
119
-
120
- expect(snippet.lines[0]?.lineNumber).toBe(1);
121
- expect(snippet.lines[0]?.isErrorLine).toBe(true);
122
- });
123
- });
124
-
125
- describe('EC-6: Span outside source bounds throws RangeError', () => {
126
- it('throws when span line exceeds source', () => {
127
- const source = 'line1\nline2';
128
- const span: SourceSpan = {
129
- start: { line: 10, column: 0, offset: 999 },
130
- end: { line: 10, column: 5, offset: 1004 },
131
- };
132
-
133
- expect(() => extractSnippet(source, span)).toThrow(RangeError);
134
- expect(() => extractSnippet(source, span)).toThrow(
135
- 'Span exceeds source bounds'
136
- );
137
- });
138
-
139
- it('throws when span line is 0', () => {
140
- const source = 'line1\nline2';
141
- const span: SourceSpan = {
142
- start: { line: 0, column: 0, offset: 0 },
143
- end: { line: 0, column: 5, offset: 5 },
144
- };
145
-
146
- expect(() => extractSnippet(source, span)).toThrow(RangeError);
147
- expect(() => extractSnippet(source, span)).toThrow(
148
- 'Span exceeds source bounds'
149
- );
150
- });
151
- });
152
-
153
- describe('EC-7: Empty source returns empty snippet', () => {
154
- it('returns empty snippet for empty source', () => {
155
- const source = '';
156
- const span: SourceSpan = {
157
- start: { line: 1, column: 0, offset: 0 },
158
- end: { line: 1, column: 0, offset: 0 },
159
- };
160
-
161
- const snippet = extractSnippet(source, span);
162
-
163
- expect(snippet.lines).toEqual([]);
164
- expect(snippet.highlightSpan).toBe(span);
165
- });
166
- });
167
-
168
- describe('IC-1: Multi-line span handling', () => {
169
- it('handles multi-line error spans', () => {
170
- const source = 'line1\nline2\nERROR_START\nERROR_END\nline5';
171
- const span: SourceSpan = {
172
- start: { line: 3, column: 0, offset: 12 },
173
- end: { line: 4, column: 9, offset: 33 },
174
- };
175
-
176
- const snippet = extractSnippet(source, span);
177
-
178
- // Should mark both lines as error lines
179
- const errorLines = snippet.lines.filter((l) => l.isErrorLine);
180
- expect(errorLines).toHaveLength(2);
181
- expect(errorLines[0]?.lineNumber).toBe(3);
182
- expect(errorLines[1]?.lineNumber).toBe(4);
183
- });
184
- });
185
- });
186
-
187
- describe('suggestSimilarNames', () => {
188
- describe('IR-8: Edit distance and suggestion limits', () => {
189
- it('returns names with edit distance <= 2', () => {
190
- const target = 'test';
191
- const candidates = [
192
- 'test', // distance 0
193
- 'tost', // distance 1
194
- 'tast', // distance 1
195
- 'toast', // distance 2
196
- 'testing', // distance 3 (excluded)
197
- 'best', // distance 1
198
- ];
199
-
200
- const suggestions = suggestSimilarNames(target, candidates);
201
-
202
- // Max 3 suggestions, all must be within distance 2
203
- expect(suggestions.length).toBeLessThanOrEqual(3);
204
- for (const suggestion of suggestions) {
205
- expect(candidates).toContain(suggestion);
206
- }
207
- expect(suggestions).not.toContain('testing'); // distance 3 excluded
208
- expect(suggestions[0]).toBe('test'); // exact match first
209
- });
210
-
211
- it('returns maximum 3 suggestions', () => {
212
- const target = 'x';
213
- const candidates = ['a', 'b', 'c', 'd', 'e']; // All have distance 1
214
-
215
- const suggestions = suggestSimilarNames(target, candidates);
216
-
217
- expect(suggestions).toHaveLength(3);
218
- });
219
-
220
- it('sorts by ascending distance, then alphabetically', () => {
221
- const target = 'test';
222
- const candidates = [
223
- 'zest', // distance 1
224
- 'best', // distance 1
225
- 'toast', // distance 2
226
- 'roast', // distance 2
227
- 'test', // distance 0
228
- ];
229
-
230
- const suggestions = suggestSimilarNames(target, candidates);
231
-
232
- expect(suggestions[0]).toBe('test'); // distance 0
233
- expect(suggestions[1]).toBe('best'); // distance 1, alphabetically before 'zest'
234
- expect(suggestions[2]).toBe('zest'); // distance 1
235
- // Only 3 suggestions, so 'toast' and 'roast' excluded
236
- });
237
-
238
- it('prioritizes exact matches', () => {
239
- const target = 'value';
240
- const candidates = ['value', 'values', 'valve'];
241
-
242
- const suggestions = suggestSimilarNames(target, candidates);
243
-
244
- expect(suggestions[0]).toBe('value'); // exact match first
245
- });
246
- });
247
-
248
- describe('EC-9: Empty target returns []', () => {
249
- it('returns empty array for empty target', () => {
250
- const suggestions = suggestSimilarNames('', ['test', 'value']);
251
-
252
- expect(suggestions).toEqual([]);
253
- });
254
- });
255
-
256
- describe('EC-10: Empty candidates returns []', () => {
257
- it('returns empty array for empty candidates', () => {
258
- const suggestions = suggestSimilarNames('test', []);
259
-
260
- expect(suggestions).toEqual([]);
261
- });
262
- });
263
-
264
- describe('AC-10: Undefined $valeu with $value in scope suggests correction', () => {
265
- it('suggests $value when $valeu is undefined (typo scenario)', () => {
266
- const target = 'valeu'; // Typo: missing 'e'
267
- const candidates = ['value', 'values', 'valid'];
268
-
269
- const suggestions = suggestSimilarNames(target, candidates);
270
-
271
- expect(suggestions.length).toBeGreaterThan(0);
272
- expect(suggestions).toContain('value'); // Edit distance 1 (e->u substitution)
273
- // This simulates: "Variable $valeu is not defined. Did you mean $value?"
274
- });
275
- });
276
-
277
- describe('AC-11: Dict key error shows Available keys', () => {
278
- it('provides list of available keys for dict key errors', () => {
279
- // This test demonstrates the pattern for dict key suggestions
280
- const attemptedKey = 'nam'; // Typo: should be 'name'
281
- const availableKeys = ['name', 'age', 'email'];
282
-
283
- const suggestions = suggestSimilarNames(attemptedKey, availableKeys);
284
-
285
- expect(suggestions.length).toBeGreaterThan(0);
286
- expect(suggestions).toContain('name'); // Edit distance 1
287
- // This simulates: "Key 'nam' not found. Available keys: name, age, email"
288
- });
289
-
290
- it('formats available keys as comma-separated list', () => {
291
- const availableKeys = ['a', 'b', 'c'];
292
- const formattedKeys = availableKeys.join(', ');
293
-
294
- expect(formattedKeys).toBe('a, b, c');
295
- // This demonstrates the format: "Available keys: a, b, c"
296
- });
297
- });
298
-
299
- describe('Edge cases', () => {
300
- it('handles single character targets', () => {
301
- const suggestions = suggestSimilarNames('x', ['x', 'y', 'xy', 'xyz']);
302
-
303
- expect(suggestions.length).toBeLessThanOrEqual(3);
304
- expect(suggestions).toContain('x'); // exact match
305
- // 'y' has distance 1, 'xy' has distance 1, 'xyz' has distance 2
306
- });
307
-
308
- it('excludes names with distance > 2', () => {
309
- const target = 'cat';
310
- const candidates = ['cat', 'bat', 'hat', 'chat', 'chats', 'elephant'];
311
-
312
- const suggestions = suggestSimilarNames(target, candidates);
313
-
314
- expect(suggestions.length).toBeLessThanOrEqual(3);
315
- expect(suggestions[0]).toBe('cat'); // exact match first
316
- expect(suggestions).not.toContain('chats'); // distance 3 (c->c, h->a, a->t, t->s, +s = 3)
317
- expect(suggestions).not.toContain('elephant'); // distance > 2
318
- // Remaining should be within distance 2
319
- });
320
- });
321
- });
322
-
323
- describe('AC-13/EC-3: Invalid UTF-8 source handling', () => {
324
- it('handles invalid UTF-8 gracefully in extractSnippet', () => {
325
- // JavaScript strings are always valid Unicode/UTF-16
326
- // Invalid UTF-8 bytes become replacement characters (�) when decoded
327
- // This test demonstrates behavior with malformed Unicode
328
- const invalidSource = 'line1\nline2\u{FFFD}\nline3'; // U+FFFD is replacement character
329
- const span: SourceSpan = {
330
- start: { line: 2, column: 0, offset: 6 },
331
- end: { line: 2, column: 5, offset: 11 },
332
- };
333
-
334
- // Should not throw TypeError - extractSnippet handles strings
335
- const snippet = extractSnippet(invalidSource, span);
336
-
337
- expect(snippet.lines).toHaveLength(3);
338
- expect(snippet.lines[1]?.content).toContain('\u{FFFD}');
339
- });
340
-
341
- it('validates source is a string', () => {
342
- const span: SourceSpan = {
343
- start: { line: 1, column: 0, offset: 0 },
344
- end: { line: 1, column: 5, offset: 5 },
345
- };
346
-
347
- // TypeScript prevents this, but at runtime source must be a string
348
- expect(() => extractSnippet(123 as unknown as string, span)).toThrow(
349
- TypeError
350
- );
351
- });
352
- });
353
-
354
- describe('enrichError', () => {
355
- describe('IR-4: enrichError returns EnrichedError with all fields populated', () => {
356
- it('enriches error with source snippet', () => {
357
- const source = 'line1\nline2\nerror line\nline4\nline5';
358
- const location: SourceLocation = { line: 3, column: 0, offset: 12 };
359
- const error = new RuntimeError('RILL-R001', 'Test error', location, {
360
- key: 'value',
361
- });
362
-
363
- const enriched = enrichError(error, source);
364
-
365
- expect(enriched.errorId).toBe('RILL-R001');
366
- expect(enriched.message).toBe('Test error');
367
- expect(enriched.span).toEqual({
368
- start: location,
369
- end: location,
370
- });
371
- expect(enriched.context).toEqual({ key: 'value' });
372
- expect(enriched.sourceSnippet).toBeDefined();
373
- expect(enriched.sourceSnippet?.lines).toHaveLength(5);
374
- expect(enriched.sourceSnippet?.lines[2]?.content).toBe('error line');
375
- expect(enriched.sourceSnippet?.lines[2]?.isErrorLine).toBe(true);
376
- });
377
-
378
- it('enriches error with suggestions when scope provided', () => {
379
- const source = 'line1\nline2';
380
- const location: SourceLocation = { line: 1, column: 0, offset: 0 };
381
- const error = new RuntimeError(
382
- 'RILL-R001',
383
- 'Variable not defined',
384
- location,
385
- {
386
- name: 'valeu',
387
- }
388
- );
389
-
390
- const enriched = enrichError(error, source, {
391
- variableNames: ['value', 'valid'],
392
- functionNames: ['test'],
393
- });
394
-
395
- expect(enriched.suggestions).toBeDefined();
396
- expect(enriched.suggestions).toContain('value');
397
- });
398
-
399
- it('includes helpUrl when present in error', () => {
400
- const source = 'test';
401
- const location: SourceLocation = { line: 1, column: 0, offset: 0 };
402
- const error = new RuntimeError('RILL-R001', 'Test error', location);
403
- // Set helpUrl via error data
404
- Object.defineProperty(error, 'helpUrl', {
405
- value: 'https://example.com/help',
406
- });
407
-
408
- const enriched = enrichError(error, source);
409
-
410
- expect(enriched.helpUrl).toBe('https://example.com/help');
411
- });
412
-
413
- it('handles error without location', () => {
414
- const source = 'test source';
415
- const error = new RuntimeError('RILL-R001', 'Test error');
416
-
417
- const enriched = enrichError(error, source);
418
-
419
- expect(enriched.errorId).toBe('RILL-R001');
420
- expect(enriched.message).toBe('Test error');
421
- expect(enriched.span).toBeUndefined();
422
- expect(enriched.sourceSnippet).toBeUndefined();
423
- });
424
-
425
- it('handles empty source', () => {
426
- const source = '';
427
- const location: SourceLocation = { line: 1, column: 0, offset: 0 };
428
- const error = new RuntimeError('RILL-R001', 'Test error', location);
429
-
430
- const enriched = enrichError(error, source);
431
-
432
- expect(enriched.errorId).toBe('RILL-R001');
433
- expect(enriched.sourceSnippet).toBeUndefined();
434
- });
435
-
436
- it('strips location suffix from message', () => {
437
- const source = 'test';
438
- const location: SourceLocation = { line: 1, column: 5, offset: 5 };
439
- // RuntimeError constructor adds " at 1:5" automatically when location is provided
440
- const error = new RuntimeError('RILL-R001', 'Test error', location);
441
-
442
- const enriched = enrichError(error, source);
443
-
444
- // enrichError strips the location suffix that was added by constructor
445
- expect(enriched.message).toBe('Test error');
446
- });
447
- });
448
-
449
- describe('EC-3: Invalid source encoding', () => {
450
- it('throws TypeError when source is not a string', () => {
451
- const location: SourceLocation = { line: 1, column: 0, offset: 0 };
452
- const error = new RuntimeError('RILL-R001', 'Test error', location);
453
-
454
- expect(() => enrichError(error, 123 as unknown as string)).toThrow(
455
- TypeError
456
- );
457
- expect(() => enrichError(error, 123 as unknown as string)).toThrow(
458
- 'Source must be valid UTF-8'
459
- );
460
- });
461
-
462
- it('throws TypeError when source is null', () => {
463
- const location: SourceLocation = { line: 1, column: 0, offset: 0 };
464
- const error = new RuntimeError('RILL-R001', 'Test error', location);
465
-
466
- expect(() => enrichError(error, null as unknown as string)).toThrow(
467
- TypeError
468
- );
469
- expect(() => enrichError(error, null as unknown as string)).toThrow(
470
- 'Source must be valid UTF-8'
471
- );
472
- });
473
-
474
- it('throws TypeError when source is undefined', () => {
475
- const location: SourceLocation = { line: 1, column: 0, offset: 0 };
476
- const error = new RuntimeError('RILL-R001', 'Test error', location);
477
-
478
- expect(() => enrichError(error, undefined as unknown as string)).toThrow(
479
- TypeError
480
- );
481
- expect(() => enrichError(error, undefined as unknown as string)).toThrow(
482
- 'Source must be valid UTF-8'
483
- );
484
- });
485
- });
486
-
487
- describe('EC-4: Null error', () => {
488
- it('throws TypeError when error is null', () => {
489
- const source = 'test source';
490
-
491
- expect(() =>
492
- enrichError(null as unknown as RuntimeError, source)
493
- ).toThrow(TypeError);
494
- expect(() =>
495
- enrichError(null as unknown as RuntimeError, source)
496
- ).toThrow('Error is required');
497
- });
498
-
499
- it('throws TypeError when error is undefined', () => {
500
- const source = 'test source';
501
-
502
- expect(() =>
503
- enrichError(undefined as unknown as RuntimeError, source)
504
- ).toThrow(TypeError);
505
- expect(() =>
506
- enrichError(undefined as unknown as RuntimeError, source)
507
- ).toThrow('Error is required');
508
- });
509
- });
510
- });