@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,373 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fixer Tests
|
|
3
|
+
* Tests for fix application with collision detection.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect } from 'vitest';
|
|
7
|
+
import { applyFixes, type ApplyResult } from '../../src/check/fixer.js';
|
|
8
|
+
import type {
|
|
9
|
+
Diagnostic,
|
|
10
|
+
Fix,
|
|
11
|
+
ValidationContext,
|
|
12
|
+
} from '../../src/check/types.js';
|
|
13
|
+
import { parse } from '@rcrsr/rill';
|
|
14
|
+
import { createDefaultConfig } from '../../src/check/config.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Helper to create a diagnostic with a fix.
|
|
18
|
+
*/
|
|
19
|
+
function createDiagnostic(
|
|
20
|
+
code: string,
|
|
21
|
+
line: number,
|
|
22
|
+
column: number,
|
|
23
|
+
startOffset: number,
|
|
24
|
+
endOffset: number,
|
|
25
|
+
replacement: string,
|
|
26
|
+
applicable = true
|
|
27
|
+
): Diagnostic {
|
|
28
|
+
return {
|
|
29
|
+
location: { line, column, offset: startOffset },
|
|
30
|
+
severity: 'error',
|
|
31
|
+
code,
|
|
32
|
+
message: `Test diagnostic for ${code}`,
|
|
33
|
+
context: 'test context',
|
|
34
|
+
fix: {
|
|
35
|
+
description: `Fix ${code}`,
|
|
36
|
+
applicable,
|
|
37
|
+
range: {
|
|
38
|
+
start: { line, column, offset: startOffset },
|
|
39
|
+
end: {
|
|
40
|
+
line,
|
|
41
|
+
column: column + (endOffset - startOffset),
|
|
42
|
+
offset: endOffset,
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
replacement,
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Helper to create a minimal validation context.
|
|
52
|
+
*/
|
|
53
|
+
function createContext(source: string): ValidationContext {
|
|
54
|
+
const ast = parse(source);
|
|
55
|
+
return {
|
|
56
|
+
source,
|
|
57
|
+
ast,
|
|
58
|
+
config: createDefaultConfig(),
|
|
59
|
+
diagnostics: [],
|
|
60
|
+
variables: new Map(),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
describe('applyFixes', () => {
|
|
65
|
+
describe('basic fix application', () => {
|
|
66
|
+
it('returns original source when no diagnostics provided', () => {
|
|
67
|
+
const source = '"hello"';
|
|
68
|
+
const context = createContext(source);
|
|
69
|
+
const result = applyFixes(source, [], context);
|
|
70
|
+
|
|
71
|
+
expect(result.modified).toBe(source);
|
|
72
|
+
expect(result.applied).toBe(0);
|
|
73
|
+
expect(result.skipped).toBe(0);
|
|
74
|
+
expect(result.skippedReasons).toEqual([]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('returns original source when no fixes available', () => {
|
|
78
|
+
const source = '"hello"';
|
|
79
|
+
const context = createContext(source);
|
|
80
|
+
const diagnostics: Diagnostic[] = [
|
|
81
|
+
{
|
|
82
|
+
location: { line: 1, column: 1, offset: 0 },
|
|
83
|
+
severity: 'error',
|
|
84
|
+
code: 'TEST_NO_FIX',
|
|
85
|
+
message: 'Test',
|
|
86
|
+
context: 'test',
|
|
87
|
+
fix: null,
|
|
88
|
+
},
|
|
89
|
+
];
|
|
90
|
+
const result = applyFixes(source, diagnostics, context);
|
|
91
|
+
|
|
92
|
+
expect(result.modified).toBe(source);
|
|
93
|
+
expect(result.applied).toBe(0);
|
|
94
|
+
expect(result.skipped).toBe(0);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('returns original source when all fixes are not applicable', () => {
|
|
98
|
+
const source = '"hello"';
|
|
99
|
+
const context = createContext(source);
|
|
100
|
+
const diagnostics = [
|
|
101
|
+
createDiagnostic('TEST_1', 1, 1, 0, 7, '"world"', false),
|
|
102
|
+
];
|
|
103
|
+
const result = applyFixes(source, diagnostics, context);
|
|
104
|
+
|
|
105
|
+
expect(result.modified).toBe(source);
|
|
106
|
+
expect(result.applied).toBe(0);
|
|
107
|
+
expect(result.skipped).toBe(0);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('applies a single fix successfully', () => {
|
|
111
|
+
const source = '"hello"';
|
|
112
|
+
const context = createContext(source);
|
|
113
|
+
const diagnostics = [createDiagnostic('TEST_1', 1, 1, 0, 7, '"world"')];
|
|
114
|
+
const result = applyFixes(source, diagnostics, context);
|
|
115
|
+
|
|
116
|
+
expect(result.modified).toBe('"world"');
|
|
117
|
+
expect(result.applied).toBe(1);
|
|
118
|
+
expect(result.skipped).toBe(0);
|
|
119
|
+
expect(result.skippedReasons).toEqual([]);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('applies multiple non-overlapping fixes', () => {
|
|
123
|
+
const source = '"a" -> "b" -> "c"';
|
|
124
|
+
const context = createContext(source);
|
|
125
|
+
const diagnostics = [
|
|
126
|
+
// Replace "a" with "x"
|
|
127
|
+
createDiagnostic('TEST_1', 1, 1, 0, 3, '"x"'),
|
|
128
|
+
// Replace "b" with "y"
|
|
129
|
+
createDiagnostic('TEST_2', 1, 8, 7, 10, '"y"'),
|
|
130
|
+
// Replace "c" with "z"
|
|
131
|
+
createDiagnostic('TEST_3', 1, 15, 14, 17, '"z"'),
|
|
132
|
+
];
|
|
133
|
+
const result = applyFixes(source, diagnostics, context);
|
|
134
|
+
|
|
135
|
+
expect(result.modified).toBe('"x" -> "y" -> "z"');
|
|
136
|
+
expect(result.applied).toBe(3);
|
|
137
|
+
expect(result.skipped).toBe(0);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe('fix ordering', () => {
|
|
142
|
+
it('applies fixes in reverse position order to avoid offset shifts', () => {
|
|
143
|
+
const source = '1 + 2 + 3';
|
|
144
|
+
const context = createContext(source);
|
|
145
|
+
const diagnostics = [
|
|
146
|
+
// These fixes are provided in forward order
|
|
147
|
+
createDiagnostic('TEST_1', 1, 1, 0, 1, '10'), // Replace "1"
|
|
148
|
+
createDiagnostic('TEST_2', 1, 5, 4, 5, '20'), // Replace "2"
|
|
149
|
+
createDiagnostic('TEST_3', 1, 9, 8, 9, '30'), // Replace "3"
|
|
150
|
+
];
|
|
151
|
+
const result = applyFixes(source, diagnostics, context);
|
|
152
|
+
|
|
153
|
+
// Should apply from end to start
|
|
154
|
+
expect(result.modified).toBe('10 + 20 + 30');
|
|
155
|
+
expect(result.applied).toBe(3);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('handles fixes at different positions correctly', () => {
|
|
159
|
+
const source = '[a: 1, b: 2]';
|
|
160
|
+
const context = createContext(source);
|
|
161
|
+
const diagnostics = [
|
|
162
|
+
createDiagnostic('TEST_1', 1, 2, 1, 2, 'x'), // "a" -> "x"
|
|
163
|
+
createDiagnostic('TEST_2', 1, 8, 7, 8, 'y'), // "b" -> "y"
|
|
164
|
+
];
|
|
165
|
+
const result = applyFixes(source, diagnostics, context);
|
|
166
|
+
|
|
167
|
+
expect(result.modified).toBe('[x: 1, y: 2]');
|
|
168
|
+
expect(result.applied).toBe(2);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe('collision detection [EC-5]', () => {
|
|
173
|
+
it('skips overlapping fixes with reason', () => {
|
|
174
|
+
const source = '"hello world"';
|
|
175
|
+
const context = createContext(source);
|
|
176
|
+
const diagnostics = [
|
|
177
|
+
// First fix: replace entire string
|
|
178
|
+
createDiagnostic('TEST_1', 1, 1, 0, 13, '"goodbye"'),
|
|
179
|
+
// Second fix: replace part of string (overlaps with first)
|
|
180
|
+
createDiagnostic('TEST_2', 1, 2, 1, 6, 'HELLO'),
|
|
181
|
+
];
|
|
182
|
+
const result = applyFixes(source, diagnostics, context);
|
|
183
|
+
|
|
184
|
+
// First fix applied (appears last in sorted order)
|
|
185
|
+
expect(result.modified).toBe('"goodbye"');
|
|
186
|
+
expect(result.applied).toBe(1);
|
|
187
|
+
expect(result.skipped).toBe(1);
|
|
188
|
+
expect(result.skippedReasons).toHaveLength(1);
|
|
189
|
+
expect(result.skippedReasons[0]).toEqual({
|
|
190
|
+
code: 'TEST_2',
|
|
191
|
+
reason: 'Fix range overlaps with another fix',
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('skips multiple overlapping fixes', () => {
|
|
196
|
+
const source = '"hello"';
|
|
197
|
+
const context = createContext(source);
|
|
198
|
+
const diagnostics = [
|
|
199
|
+
createDiagnostic('TEST_1', 1, 1, 0, 7, '"a"'),
|
|
200
|
+
createDiagnostic('TEST_2', 1, 1, 0, 7, '"b"'),
|
|
201
|
+
createDiagnostic('TEST_3', 1, 1, 0, 7, '"c"'),
|
|
202
|
+
];
|
|
203
|
+
const result = applyFixes(source, diagnostics, context);
|
|
204
|
+
|
|
205
|
+
// Only one fix applied, others skipped
|
|
206
|
+
expect(result.applied).toBe(1);
|
|
207
|
+
expect(result.skipped).toBe(2);
|
|
208
|
+
expect(result.skippedReasons).toHaveLength(2);
|
|
209
|
+
expect(
|
|
210
|
+
result.skippedReasons.every(
|
|
211
|
+
(r) => r.reason === 'Fix range overlaps with another fix'
|
|
212
|
+
)
|
|
213
|
+
).toBe(true);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('applies non-overlapping fixes and skips overlapping ones', () => {
|
|
217
|
+
const source = '"a" -> "b" -> "c"';
|
|
218
|
+
const context = createContext(source);
|
|
219
|
+
const diagnostics = [
|
|
220
|
+
createDiagnostic('TEST_1', 1, 1, 0, 3, '"x"'), // "a" -> "x" (ok)
|
|
221
|
+
createDiagnostic('TEST_2', 1, 2, 1, 2, 'X'), // Overlaps with TEST_1
|
|
222
|
+
createDiagnostic('TEST_3', 1, 8, 7, 10, '"y"'), // "b" -> "y" (ok)
|
|
223
|
+
];
|
|
224
|
+
const result = applyFixes(source, diagnostics, context);
|
|
225
|
+
|
|
226
|
+
expect(result.modified).toBe('"x" -> "y" -> "c"');
|
|
227
|
+
expect(result.applied).toBe(2);
|
|
228
|
+
expect(result.skipped).toBe(1);
|
|
229
|
+
expect(result.skippedReasons[0]?.code).toBe('TEST_2');
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('detects adjacent but non-overlapping ranges correctly', () => {
|
|
233
|
+
const source = '"ab"';
|
|
234
|
+
const context = createContext(source);
|
|
235
|
+
const diagnostics = [
|
|
236
|
+
createDiagnostic('TEST_1', 1, 2, 1, 2, 'X'), // "a" -> "X"
|
|
237
|
+
createDiagnostic('TEST_2', 1, 3, 2, 3, 'Y'), // "b" -> "Y"
|
|
238
|
+
];
|
|
239
|
+
const result = applyFixes(source, diagnostics, context);
|
|
240
|
+
|
|
241
|
+
// Adjacent ranges should NOT overlap
|
|
242
|
+
expect(result.modified).toBe('"XY"');
|
|
243
|
+
expect(result.applied).toBe(2);
|
|
244
|
+
expect(result.skipped).toBe(0);
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
describe('parse verification [EC-6]', () => {
|
|
249
|
+
it('throws when fix creates invalid syntax', () => {
|
|
250
|
+
const source = '"hello"';
|
|
251
|
+
const context = createContext(source);
|
|
252
|
+
const diagnostics = [
|
|
253
|
+
// This creates invalid syntax (operator without operands)
|
|
254
|
+
createDiagnostic('TEST_BAD', 1, 1, 0, 7, '1 + +'),
|
|
255
|
+
];
|
|
256
|
+
|
|
257
|
+
expect(() => applyFixes(source, diagnostics, context)).toThrow(
|
|
258
|
+
'Fix would create invalid syntax'
|
|
259
|
+
);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('throws when multiple fixes together create invalid syntax', () => {
|
|
263
|
+
const source = '1 + 2';
|
|
264
|
+
const context = createContext(source);
|
|
265
|
+
const diagnostics = [
|
|
266
|
+
createDiagnostic('TEST_1', 1, 1, 0, 1, '+'), // Replace "1" with "+"
|
|
267
|
+
createDiagnostic('TEST_2', 1, 5, 4, 5, '+'), // Replace "2" with "+"
|
|
268
|
+
];
|
|
269
|
+
|
|
270
|
+
// Results in "+ + +" which is invalid
|
|
271
|
+
expect(() => applyFixes(source, diagnostics, context)).toThrow(
|
|
272
|
+
'Fix would create invalid syntax'
|
|
273
|
+
);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('succeeds when all fixes create valid syntax', () => {
|
|
277
|
+
const source = '"a" -> "b"';
|
|
278
|
+
const context = createContext(source);
|
|
279
|
+
const diagnostics = [
|
|
280
|
+
createDiagnostic('TEST_1', 1, 1, 0, 3, '"x"'),
|
|
281
|
+
createDiagnostic('TEST_2', 1, 8, 7, 10, '"y"'),
|
|
282
|
+
];
|
|
283
|
+
|
|
284
|
+
const result = applyFixes(source, diagnostics, context);
|
|
285
|
+
expect(result.modified).toBe('"x" -> "y"');
|
|
286
|
+
|
|
287
|
+
// Verify it actually parses
|
|
288
|
+
expect(() => parse(result.modified)).not.toThrow();
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
describe('edge cases', () => {
|
|
293
|
+
it('handles empty source', () => {
|
|
294
|
+
const source = '';
|
|
295
|
+
const context = createContext(source);
|
|
296
|
+
const result = applyFixes(source, [], context);
|
|
297
|
+
|
|
298
|
+
expect(result.modified).toBe('');
|
|
299
|
+
expect(result.applied).toBe(0);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('handles fix that replaces entire source', () => {
|
|
303
|
+
const source = '"old"';
|
|
304
|
+
const context = createContext(source);
|
|
305
|
+
const diagnostics = [createDiagnostic('TEST_1', 1, 1, 0, 5, '"new"')];
|
|
306
|
+
const result = applyFixes(source, diagnostics, context);
|
|
307
|
+
|
|
308
|
+
expect(result.modified).toBe('"new"');
|
|
309
|
+
expect(result.applied).toBe(1);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it('handles fix with empty replacement (deletion)', () => {
|
|
313
|
+
const source = '1 + 2';
|
|
314
|
+
const context = createContext(source);
|
|
315
|
+
const diagnostics = [
|
|
316
|
+
createDiagnostic('TEST_1', 1, 1, 0, 1, ''), // Remove "1"
|
|
317
|
+
];
|
|
318
|
+
|
|
319
|
+
// This would create invalid syntax ( + 2)
|
|
320
|
+
expect(() => applyFixes(source, diagnostics, context)).toThrow(
|
|
321
|
+
'Fix would create invalid syntax'
|
|
322
|
+
);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('handles fix with multi-line replacement', () => {
|
|
326
|
+
const source = '1';
|
|
327
|
+
const context = createContext(source);
|
|
328
|
+
const diagnostics = [createDiagnostic('TEST_1', 1, 1, 0, 1, '{\n 2\n}')];
|
|
329
|
+
const result = applyFixes(source, diagnostics, context);
|
|
330
|
+
|
|
331
|
+
expect(result.modified).toBe('{\n 2\n}');
|
|
332
|
+
expect(result.applied).toBe(1);
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
describe('return value structure', () => {
|
|
337
|
+
it('returns correct structure with all fields', () => {
|
|
338
|
+
const source = '"hello"';
|
|
339
|
+
const context = createContext(source);
|
|
340
|
+
const diagnostics = [createDiagnostic('TEST_1', 1, 1, 0, 7, '"world"')];
|
|
341
|
+
const result = applyFixes(source, diagnostics, context);
|
|
342
|
+
|
|
343
|
+
expect(result).toHaveProperty('modified');
|
|
344
|
+
expect(result).toHaveProperty('applied');
|
|
345
|
+
expect(result).toHaveProperty('skipped');
|
|
346
|
+
expect(result).toHaveProperty('skippedReasons');
|
|
347
|
+
expect(typeof result.modified).toBe('string');
|
|
348
|
+
expect(typeof result.applied).toBe('number');
|
|
349
|
+
expect(typeof result.skipped).toBe('number');
|
|
350
|
+
expect(Array.isArray(result.skippedReasons)).toBe(true);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it('includes all skipped reasons with correct structure', () => {
|
|
354
|
+
const source = '"test"';
|
|
355
|
+
const context = createContext(source);
|
|
356
|
+
const diagnostics = [
|
|
357
|
+
createDiagnostic('RULE_A', 1, 1, 0, 6, '"a"'),
|
|
358
|
+
createDiagnostic('RULE_B', 1, 1, 0, 6, '"b"'),
|
|
359
|
+
createDiagnostic('RULE_C', 1, 1, 0, 6, '"c"'),
|
|
360
|
+
];
|
|
361
|
+
const result = applyFixes(source, diagnostics, context);
|
|
362
|
+
|
|
363
|
+
expect(result.skipped).toBe(2);
|
|
364
|
+
expect(result.skippedReasons).toHaveLength(2);
|
|
365
|
+
result.skippedReasons.forEach((reason) => {
|
|
366
|
+
expect(reason).toHaveProperty('code');
|
|
367
|
+
expect(reason).toHaveProperty('reason');
|
|
368
|
+
expect(typeof reason.code).toBe('string');
|
|
369
|
+
expect(typeof reason.reason).toBe('string');
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
});
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for formatDiagnostics function
|
|
3
|
+
* Validates text and JSON formatting with verbose mode
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect } from 'vitest';
|
|
7
|
+
import { formatDiagnostics } from '../../src/cli-check.js';
|
|
8
|
+
import type { Diagnostic } from '../../src/check/index.js';
|
|
9
|
+
|
|
10
|
+
describe('formatDiagnostics', () => {
|
|
11
|
+
describe('text format', () => {
|
|
12
|
+
it('formats single diagnostic with all fields', () => {
|
|
13
|
+
const diagnostics: Diagnostic[] = [
|
|
14
|
+
{
|
|
15
|
+
location: { line: 1, column: 5, offset: 4 },
|
|
16
|
+
severity: 'error',
|
|
17
|
+
code: 'NAMING_SNAKE_CASE',
|
|
18
|
+
message: 'Variable names must use snake_case',
|
|
19
|
+
context: 'badName => $value',
|
|
20
|
+
fix: null,
|
|
21
|
+
},
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
const result = formatDiagnostics('file.rill', diagnostics, 'text', false);
|
|
25
|
+
|
|
26
|
+
expect(result).toBe(
|
|
27
|
+
'file.rill:1:5: error: Variable names must use snake_case (NAMING_SNAKE_CASE)'
|
|
28
|
+
);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('formats multiple diagnostics', () => {
|
|
32
|
+
const diagnostics: Diagnostic[] = [
|
|
33
|
+
{
|
|
34
|
+
location: { line: 1, column: 5, offset: 4 },
|
|
35
|
+
severity: 'error',
|
|
36
|
+
code: 'NAMING_SNAKE_CASE',
|
|
37
|
+
message: 'Variable names must use snake_case',
|
|
38
|
+
context: 'badName => $value',
|
|
39
|
+
fix: null,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
location: { line: 3, column: 10, offset: 25 },
|
|
43
|
+
severity: 'warning',
|
|
44
|
+
code: 'NO_EMPTY_BLOCKS',
|
|
45
|
+
message: 'Block is empty',
|
|
46
|
+
context: '{ }',
|
|
47
|
+
fix: null,
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
const result = formatDiagnostics('test.rill', diagnostics, 'text', false);
|
|
52
|
+
|
|
53
|
+
expect(result).toBe(
|
|
54
|
+
'test.rill:1:5: error: Variable names must use snake_case (NAMING_SNAKE_CASE)\n' +
|
|
55
|
+
'test.rill:3:10: warning: Block is empty (NO_EMPTY_BLOCKS)'
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('formats empty diagnostics array', () => {
|
|
60
|
+
const result = formatDiagnostics('empty.rill', [], 'text', false);
|
|
61
|
+
expect(result).toBe('');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('formats different severity levels', () => {
|
|
65
|
+
const diagnostics: Diagnostic[] = [
|
|
66
|
+
{
|
|
67
|
+
location: { line: 1, column: 1, offset: 0 },
|
|
68
|
+
severity: 'error',
|
|
69
|
+
code: 'ERROR_CODE',
|
|
70
|
+
message: 'Error message',
|
|
71
|
+
context: 'line 1',
|
|
72
|
+
fix: null,
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
location: { line: 2, column: 1, offset: 10 },
|
|
76
|
+
severity: 'warning',
|
|
77
|
+
code: 'WARNING_CODE',
|
|
78
|
+
message: 'Warning message',
|
|
79
|
+
context: 'line 2',
|
|
80
|
+
fix: null,
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
location: { line: 3, column: 1, offset: 20 },
|
|
84
|
+
severity: 'info',
|
|
85
|
+
code: 'INFO_CODE',
|
|
86
|
+
message: 'Info message',
|
|
87
|
+
context: 'line 3',
|
|
88
|
+
fix: null,
|
|
89
|
+
},
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
const result = formatDiagnostics('file.rill', diagnostics, 'text', false);
|
|
93
|
+
|
|
94
|
+
expect(result).toContain('error: Error message');
|
|
95
|
+
expect(result).toContain('warning: Warning message');
|
|
96
|
+
expect(result).toContain('info: Info message');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('ignores verbose flag in text mode', () => {
|
|
100
|
+
const diagnostics: Diagnostic[] = [
|
|
101
|
+
{
|
|
102
|
+
location: { line: 1, column: 5, offset: 4 },
|
|
103
|
+
severity: 'error',
|
|
104
|
+
code: 'TEST_CODE',
|
|
105
|
+
message: 'Test message',
|
|
106
|
+
context: 'test context',
|
|
107
|
+
fix: null,
|
|
108
|
+
},
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
const withoutVerbose = formatDiagnostics(
|
|
112
|
+
'file.rill',
|
|
113
|
+
diagnostics,
|
|
114
|
+
'text',
|
|
115
|
+
false
|
|
116
|
+
);
|
|
117
|
+
const withVerbose = formatDiagnostics(
|
|
118
|
+
'file.rill',
|
|
119
|
+
diagnostics,
|
|
120
|
+
'text',
|
|
121
|
+
true
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
expect(withoutVerbose).toBe(withVerbose);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe('json format', () => {
|
|
129
|
+
it('formats single diagnostic with required fields', () => {
|
|
130
|
+
const diagnostics: Diagnostic[] = [
|
|
131
|
+
{
|
|
132
|
+
location: { line: 1, column: 5, offset: 4 },
|
|
133
|
+
severity: 'error',
|
|
134
|
+
code: 'NAMING_SNAKE_CASE',
|
|
135
|
+
message: 'Variable names must use snake_case',
|
|
136
|
+
context: 'badName => $value',
|
|
137
|
+
fix: null,
|
|
138
|
+
},
|
|
139
|
+
];
|
|
140
|
+
|
|
141
|
+
const result = formatDiagnostics('file.rill', diagnostics, 'json', false);
|
|
142
|
+
const parsed = JSON.parse(result);
|
|
143
|
+
|
|
144
|
+
expect(parsed).toEqual({
|
|
145
|
+
file: 'file.rill',
|
|
146
|
+
errors: [
|
|
147
|
+
{
|
|
148
|
+
location: { line: 1, column: 5, offset: 4 },
|
|
149
|
+
severity: 'error',
|
|
150
|
+
code: 'NAMING_SNAKE_CASE',
|
|
151
|
+
message: 'Variable names must use snake_case',
|
|
152
|
+
context: 'badName => $value',
|
|
153
|
+
},
|
|
154
|
+
],
|
|
155
|
+
summary: {
|
|
156
|
+
total: 1,
|
|
157
|
+
errors: 1,
|
|
158
|
+
warnings: 0,
|
|
159
|
+
info: 0,
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('includes fix when present', () => {
|
|
165
|
+
const diagnostics: Diagnostic[] = [
|
|
166
|
+
{
|
|
167
|
+
location: { line: 1, column: 5, offset: 4 },
|
|
168
|
+
severity: 'error',
|
|
169
|
+
code: 'NAMING_SNAKE_CASE',
|
|
170
|
+
message: 'Variable names must use snake_case',
|
|
171
|
+
context: 'badName => $value',
|
|
172
|
+
fix: {
|
|
173
|
+
description: 'Rename to snake_case',
|
|
174
|
+
applicable: true,
|
|
175
|
+
range: {
|
|
176
|
+
start: { line: 1, column: 1, offset: 0 },
|
|
177
|
+
end: { line: 1, column: 8, offset: 7 },
|
|
178
|
+
},
|
|
179
|
+
replacement: 'bad_name',
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
];
|
|
183
|
+
|
|
184
|
+
const result = formatDiagnostics('file.rill', diagnostics, 'json', false);
|
|
185
|
+
const parsed = JSON.parse(result);
|
|
186
|
+
|
|
187
|
+
expect(parsed.errors[0].fix).toEqual({
|
|
188
|
+
description: 'Rename to snake_case',
|
|
189
|
+
applicable: true,
|
|
190
|
+
range: {
|
|
191
|
+
start: { line: 1, column: 1, offset: 0 },
|
|
192
|
+
end: { line: 1, column: 8, offset: 7 },
|
|
193
|
+
},
|
|
194
|
+
replacement: 'bad_name',
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('calculates summary counts correctly', () => {
|
|
199
|
+
const diagnostics: Diagnostic[] = [
|
|
200
|
+
{
|
|
201
|
+
location: { line: 1, column: 1, offset: 0 },
|
|
202
|
+
severity: 'error',
|
|
203
|
+
code: 'ERROR_1',
|
|
204
|
+
message: 'Error 1',
|
|
205
|
+
context: 'ctx1',
|
|
206
|
+
fix: null,
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
location: { line: 2, column: 1, offset: 10 },
|
|
210
|
+
severity: 'error',
|
|
211
|
+
code: 'ERROR_2',
|
|
212
|
+
message: 'Error 2',
|
|
213
|
+
context: 'ctx2',
|
|
214
|
+
fix: null,
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
location: { line: 3, column: 1, offset: 20 },
|
|
218
|
+
severity: 'warning',
|
|
219
|
+
code: 'WARNING_1',
|
|
220
|
+
message: 'Warning 1',
|
|
221
|
+
context: 'ctx3',
|
|
222
|
+
fix: null,
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
location: { line: 4, column: 1, offset: 30 },
|
|
226
|
+
severity: 'info',
|
|
227
|
+
code: 'INFO_1',
|
|
228
|
+
message: 'Info 1',
|
|
229
|
+
context: 'ctx4',
|
|
230
|
+
fix: null,
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
location: { line: 5, column: 1, offset: 40 },
|
|
234
|
+
severity: 'info',
|
|
235
|
+
code: 'INFO_2',
|
|
236
|
+
message: 'Info 2',
|
|
237
|
+
context: 'ctx5',
|
|
238
|
+
fix: null,
|
|
239
|
+
},
|
|
240
|
+
];
|
|
241
|
+
|
|
242
|
+
const result = formatDiagnostics('file.rill', diagnostics, 'json', false);
|
|
243
|
+
const parsed = JSON.parse(result);
|
|
244
|
+
|
|
245
|
+
expect(parsed.summary).toEqual({
|
|
246
|
+
total: 5,
|
|
247
|
+
errors: 2,
|
|
248
|
+
warnings: 1,
|
|
249
|
+
info: 2,
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('excludes category when verbose is false', () => {
|
|
254
|
+
const diagnostics: Diagnostic[] = [
|
|
255
|
+
{
|
|
256
|
+
location: { line: 1, column: 5, offset: 4 },
|
|
257
|
+
severity: 'error',
|
|
258
|
+
code: 'NAMING_SNAKE_CASE',
|
|
259
|
+
message: 'Variable names must use snake_case',
|
|
260
|
+
context: 'badName => $value',
|
|
261
|
+
fix: null,
|
|
262
|
+
},
|
|
263
|
+
];
|
|
264
|
+
|
|
265
|
+
const result = formatDiagnostics('file.rill', diagnostics, 'json', false);
|
|
266
|
+
const parsed = JSON.parse(result);
|
|
267
|
+
|
|
268
|
+
expect(parsed.errors[0]).not.toHaveProperty('category');
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('includes category when verbose is true and rule exists', () => {
|
|
272
|
+
// Note: This test will work once VALIDATION_RULES is populated
|
|
273
|
+
// For now, it tests that category is absent when rule not found
|
|
274
|
+
const diagnostics: Diagnostic[] = [
|
|
275
|
+
{
|
|
276
|
+
location: { line: 1, column: 5, offset: 4 },
|
|
277
|
+
severity: 'error',
|
|
278
|
+
code: 'UNKNOWN_CODE',
|
|
279
|
+
message: 'Unknown rule',
|
|
280
|
+
context: 'test',
|
|
281
|
+
fix: null,
|
|
282
|
+
},
|
|
283
|
+
];
|
|
284
|
+
|
|
285
|
+
const result = formatDiagnostics('file.rill', diagnostics, 'json', true);
|
|
286
|
+
const parsed = JSON.parse(result);
|
|
287
|
+
|
|
288
|
+
// Category should not be present if rule not in VALIDATION_RULES
|
|
289
|
+
expect(parsed.errors[0]).not.toHaveProperty('category');
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('formats empty diagnostics array as valid JSON', () => {
|
|
293
|
+
const result = formatDiagnostics('empty.rill', [], 'json', false);
|
|
294
|
+
const parsed = JSON.parse(result);
|
|
295
|
+
|
|
296
|
+
expect(parsed).toEqual({
|
|
297
|
+
file: 'empty.rill',
|
|
298
|
+
errors: [],
|
|
299
|
+
summary: {
|
|
300
|
+
total: 0,
|
|
301
|
+
errors: 0,
|
|
302
|
+
warnings: 0,
|
|
303
|
+
info: 0,
|
|
304
|
+
},
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('produces valid JSON with proper indentation', () => {
|
|
309
|
+
const diagnostics: Diagnostic[] = [
|
|
310
|
+
{
|
|
311
|
+
location: { line: 1, column: 1, offset: 0 },
|
|
312
|
+
severity: 'error',
|
|
313
|
+
code: 'TEST_CODE',
|
|
314
|
+
message: 'Test message',
|
|
315
|
+
context: 'test',
|
|
316
|
+
fix: null,
|
|
317
|
+
},
|
|
318
|
+
];
|
|
319
|
+
|
|
320
|
+
const result = formatDiagnostics('file.rill', diagnostics, 'json', false);
|
|
321
|
+
|
|
322
|
+
// Should be pretty-printed with 2-space indent
|
|
323
|
+
expect(result).toContain('{\n "file"');
|
|
324
|
+
expect(() => JSON.parse(result)).not.toThrow();
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
});
|