tova 0.2.9 → 0.3.1
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/bin/tova.js +1404 -114
- package/package.json +3 -1
- package/src/analyzer/analyzer.js +882 -695
- package/src/analyzer/client-analyzer.js +191 -0
- package/src/analyzer/server-analyzer.js +467 -0
- package/src/analyzer/types.js +20 -4
- package/src/codegen/base-codegen.js +473 -111
- package/src/codegen/client-codegen.js +109 -46
- package/src/codegen/codegen.js +65 -5
- package/src/codegen/server-codegen.js +297 -38
- package/src/diagnostics/error-codes.js +255 -0
- package/src/diagnostics/formatter.js +150 -28
- package/src/docs/generator.js +390 -0
- package/src/lexer/lexer.js +306 -64
- package/src/lexer/tokens.js +19 -0
- package/src/lsp/server.js +935 -53
- package/src/parser/ast.js +81 -368
- package/src/parser/client-ast.js +138 -0
- package/src/parser/client-parser.js +504 -0
- package/src/parser/parser.js +492 -1056
- package/src/parser/server-ast.js +240 -0
- package/src/parser/server-parser.js +602 -0
- package/src/runtime/array-proto.js +32 -0
- package/src/runtime/embedded.js +1 -1
- package/src/runtime/reactivity.js +239 -42
- package/src/stdlib/advanced-collections.js +81 -0
- package/src/stdlib/inline.js +556 -13
- package/src/version.js +1 -1
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
// Tova Diagnostic Error Codes Registry
|
|
2
|
+
// Every diagnostic has a unique code: E### for errors, W### for warnings
|
|
3
|
+
|
|
4
|
+
// ─── Error Codes ─────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
export const ErrorCode = {
|
|
7
|
+
// === Syntax / Parse Errors (E001–E099) ===
|
|
8
|
+
E001: { code: 'E001', title: 'Unexpected token', category: 'syntax' },
|
|
9
|
+
E002: { code: 'E002', title: 'Unterminated string', category: 'syntax' },
|
|
10
|
+
E003: { code: 'E003', title: 'Expected closing delimiter', category: 'syntax' },
|
|
11
|
+
E004: { code: 'E004', title: 'Invalid number literal', category: 'syntax' },
|
|
12
|
+
E005: { code: 'E005', title: 'Unexpected character', category: 'syntax' },
|
|
13
|
+
E006: { code: 'E006', title: 'Unterminated comment', category: 'syntax' },
|
|
14
|
+
E007: { code: 'E007', title: 'Expected expression', category: 'syntax' },
|
|
15
|
+
E008: { code: 'E008', title: 'Mismatched JSX tag', category: 'syntax' },
|
|
16
|
+
E009: { code: 'E009', title: 'Invalid operator', category: 'syntax' },
|
|
17
|
+
E010: { code: 'E010', title: 'Max nesting depth exceeded', category: 'syntax' },
|
|
18
|
+
|
|
19
|
+
// === Type Errors (E100–E199) ===
|
|
20
|
+
E100: { code: 'E100', title: 'Type mismatch', category: 'type' },
|
|
21
|
+
E101: { code: 'E101', title: 'Return type mismatch', category: 'type' },
|
|
22
|
+
E102: { code: 'E102', title: 'Cannot assign to type', category: 'type' },
|
|
23
|
+
E103: { code: 'E103', title: 'Invalid argument type', category: 'type' },
|
|
24
|
+
E104: { code: 'E104', title: 'Incompatible operand types', category: 'type' },
|
|
25
|
+
E105: { code: 'E105', title: 'Cannot apply operator to type', category: 'type' },
|
|
26
|
+
|
|
27
|
+
// === Scope / Definition Errors (E200–E299) ===
|
|
28
|
+
E200: { code: 'E200', title: 'Undefined variable', category: 'scope' },
|
|
29
|
+
E201: { code: 'E201', title: 'Duplicate definition', category: 'scope' },
|
|
30
|
+
E202: { code: 'E202', title: 'Cannot reassign immutable', category: 'scope' },
|
|
31
|
+
E203: { code: 'E203', title: 'Invalid redeclaration', category: 'scope' },
|
|
32
|
+
|
|
33
|
+
// === Context Errors (E300–E399) ===
|
|
34
|
+
E300: { code: 'E300', title: 'Invalid context: await', category: 'context' },
|
|
35
|
+
E301: { code: 'E301', title: 'Invalid context: return', category: 'context' },
|
|
36
|
+
E302: { code: 'E302', title: 'Invalid context: client-only', category: 'context' },
|
|
37
|
+
E303: { code: 'E303', title: 'Invalid context: server-only', category: 'context' },
|
|
38
|
+
E304: { code: 'E304', title: 'Invalid context: function-only', category: 'context' },
|
|
39
|
+
|
|
40
|
+
// === Import Errors (E400–E499) ===
|
|
41
|
+
E400: { code: 'E400', title: 'Invalid import', category: 'import' },
|
|
42
|
+
E401: { code: 'E401', title: 'Circular import', category: 'import' },
|
|
43
|
+
|
|
44
|
+
// === Pattern Match Errors (E500–E599) ===
|
|
45
|
+
E500: { code: 'E500', title: 'Invalid pattern', category: 'match' },
|
|
46
|
+
|
|
47
|
+
// === Trait / Impl Errors (E600–E699) ===
|
|
48
|
+
E600: { code: 'E600', title: 'Missing trait method', category: 'trait' },
|
|
49
|
+
E601: { code: 'E601', title: 'Trait method signature mismatch', category: 'trait' },
|
|
50
|
+
E602: { code: 'E602', title: 'Unknown trait', category: 'trait' },
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// ─── Warning Codes ───────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
export const WarningCode = {
|
|
56
|
+
// === Unused (W001–W099) ===
|
|
57
|
+
W001: { code: 'W001', title: 'Unused variable', category: 'unused' },
|
|
58
|
+
W002: { code: 'W002', title: 'Unused function', category: 'unused' },
|
|
59
|
+
W003: { code: 'W003', title: 'Unused import', category: 'unused' },
|
|
60
|
+
|
|
61
|
+
// === Style (W100–W199) ===
|
|
62
|
+
W100: { code: 'W100', title: 'Naming convention violation', category: 'style' },
|
|
63
|
+
W101: { code: 'W101', title: 'Variable shadows outer', category: 'style' },
|
|
64
|
+
|
|
65
|
+
// === Potential Bugs (W200–W299) ===
|
|
66
|
+
W200: { code: 'W200', title: 'Non-exhaustive match', category: 'match' },
|
|
67
|
+
W201: { code: 'W201', title: 'Unreachable code', category: 'logic' },
|
|
68
|
+
W202: { code: 'W202', title: 'Condition always true', category: 'logic' },
|
|
69
|
+
W203: { code: 'W203', title: 'Condition always false', category: 'logic' },
|
|
70
|
+
W204: { code: 'W204', title: 'Potential data loss', category: 'type' },
|
|
71
|
+
W205: { code: 'W205', title: 'Missing return on some paths', category: 'logic' },
|
|
72
|
+
W206: { code: 'W206', title: 'Non-Tova keyword used', category: 'style' },
|
|
73
|
+
W207: { code: 'W207', title: 'Unreachable match arm', category: 'match' },
|
|
74
|
+
W208: { code: 'W208', title: 'Defer outside function', category: 'context' },
|
|
75
|
+
|
|
76
|
+
// === Trait Conformance (W300–W399) ===
|
|
77
|
+
W300: { code: 'W300', title: 'Missing trait method', category: 'trait' },
|
|
78
|
+
W301: { code: 'W301', title: 'Trait parameter mismatch', category: 'trait' },
|
|
79
|
+
W302: { code: 'W302', title: 'Trait return type mismatch', category: 'trait' },
|
|
80
|
+
W303: { code: 'W303', title: 'Unknown derive trait', category: 'trait' },
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// ─── Lookup maps ─────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
const _allCodes = new Map();
|
|
86
|
+
for (const entry of Object.values(ErrorCode)) _allCodes.set(entry.code, entry);
|
|
87
|
+
for (const entry of Object.values(WarningCode)) _allCodes.set(entry.code, entry);
|
|
88
|
+
|
|
89
|
+
export function lookupCode(code) {
|
|
90
|
+
return _allCodes.get(code) || null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function isErrorCode(code) {
|
|
94
|
+
return code.startsWith('E');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function isWarningCode(code) {
|
|
98
|
+
return code.startsWith('W');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ─── tova-ignore comment parsing ─────────────────────────────
|
|
102
|
+
|
|
103
|
+
const IGNORE_PATTERN = /\/\/\s*tova-ignore\s+((?:[EW]\d{3}(?:\s*,\s*)?)+)/;
|
|
104
|
+
|
|
105
|
+
export function parseIgnoreComment(line) {
|
|
106
|
+
const match = line.match(IGNORE_PATTERN);
|
|
107
|
+
if (!match) return null;
|
|
108
|
+
return match[1].split(',').map(c => c.trim()).filter(c => c);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ─── Explanation text for --explain flag ─────────────────────
|
|
112
|
+
|
|
113
|
+
const EXPLANATIONS = {
|
|
114
|
+
E001: `
|
|
115
|
+
Unexpected token in source code.
|
|
116
|
+
|
|
117
|
+
This error occurs when the parser encounters a token it doesn't expect
|
|
118
|
+
in the current context.
|
|
119
|
+
|
|
120
|
+
Example:
|
|
121
|
+
fn foo() {
|
|
122
|
+
x = 1 +
|
|
123
|
+
} // error: unexpected '}', expected expression after '+'
|
|
124
|
+
|
|
125
|
+
Fix: complete the expression or remove the trailing operator.
|
|
126
|
+
`,
|
|
127
|
+
E100: `
|
|
128
|
+
Type mismatch between expected and actual types.
|
|
129
|
+
|
|
130
|
+
This error occurs when a value of one type is used where a different
|
|
131
|
+
type is expected.
|
|
132
|
+
|
|
133
|
+
Example:
|
|
134
|
+
fn add(a: Int, b: Int) -> Int {
|
|
135
|
+
return a + b
|
|
136
|
+
}
|
|
137
|
+
add("hello", 5) // error: expected Int, got String
|
|
138
|
+
|
|
139
|
+
Fix: ensure the value matches the expected type, or use a conversion
|
|
140
|
+
function like to_int(), to_string(), etc.
|
|
141
|
+
`,
|
|
142
|
+
E200: `
|
|
143
|
+
Reference to an undefined variable or function.
|
|
144
|
+
|
|
145
|
+
This error occurs when you use a name that hasn't been defined in the
|
|
146
|
+
current scope or any parent scope.
|
|
147
|
+
|
|
148
|
+
Example:
|
|
149
|
+
print(foo) // error: 'foo' is not defined
|
|
150
|
+
|
|
151
|
+
Fix: define the variable before using it, or check for typos.
|
|
152
|
+
`,
|
|
153
|
+
E202: `
|
|
154
|
+
Attempt to reassign an immutable variable.
|
|
155
|
+
|
|
156
|
+
In Tova, variables bound with '=' are immutable by default. Use 'var'
|
|
157
|
+
to create a mutable variable.
|
|
158
|
+
|
|
159
|
+
Example:
|
|
160
|
+
x = 5
|
|
161
|
+
x = 10 // error: cannot reassign immutable variable 'x'
|
|
162
|
+
|
|
163
|
+
var y = 5
|
|
164
|
+
y = 10 // ok: 'var' makes it mutable
|
|
165
|
+
|
|
166
|
+
Fix: change the declaration to 'var x = 5' if you need mutability.
|
|
167
|
+
`,
|
|
168
|
+
E300: `
|
|
169
|
+
'await' used outside an async function.
|
|
170
|
+
|
|
171
|
+
The 'await' keyword can only be used inside functions declared with
|
|
172
|
+
the 'async' keyword.
|
|
173
|
+
|
|
174
|
+
Example:
|
|
175
|
+
fn fetch_data() {
|
|
176
|
+
data = await fetch("/api") // error: await outside async
|
|
177
|
+
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async fn fetch_data() {
|
|
181
|
+
data = await fetch("/api") // ok
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
Fix: add 'async' to the enclosing function declaration.
|
|
185
|
+
`,
|
|
186
|
+
E302: `
|
|
187
|
+
Client-only feature used outside a client block.
|
|
188
|
+
|
|
189
|
+
Features like 'state', 'computed', 'effect', 'component', and 'store'
|
|
190
|
+
can only be used inside a client { } block.
|
|
191
|
+
|
|
192
|
+
Example:
|
|
193
|
+
state count = 0 // error: 'state' outside client block
|
|
194
|
+
|
|
195
|
+
client {
|
|
196
|
+
state count = 0 // ok
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
Fix: move the code inside a client { } block.
|
|
200
|
+
`,
|
|
201
|
+
E303: `
|
|
202
|
+
Server-only feature used outside a server block.
|
|
203
|
+
|
|
204
|
+
Features like 'route', 'middleware', 'ws', 'db', 'auth', etc.
|
|
205
|
+
can only be used inside a server { } block.
|
|
206
|
+
|
|
207
|
+
Example:
|
|
208
|
+
route GET "/api/users" => get_users // error: outside server block
|
|
209
|
+
|
|
210
|
+
server {
|
|
211
|
+
route GET "/api/users" => get_users // ok
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
Fix: move the code inside a server { } block.
|
|
215
|
+
`,
|
|
216
|
+
W001: `
|
|
217
|
+
A variable is declared but never used.
|
|
218
|
+
|
|
219
|
+
This warning helps catch typos and dead code.
|
|
220
|
+
|
|
221
|
+
Example:
|
|
222
|
+
fn foo() {
|
|
223
|
+
x = 5 // warning: 'x' declared but never used
|
|
224
|
+
return 10
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
Fix: remove the variable, or prefix with _ to suppress: _x = 5
|
|
228
|
+
`,
|
|
229
|
+
W200: `
|
|
230
|
+
A match expression doesn't cover all possible variants.
|
|
231
|
+
|
|
232
|
+
This can lead to runtime errors if an unmatched variant is encountered.
|
|
233
|
+
|
|
234
|
+
Example:
|
|
235
|
+
type Color = Red | Green | Blue
|
|
236
|
+
match color {
|
|
237
|
+
Red => "red"
|
|
238
|
+
Green => "green"
|
|
239
|
+
} // warning: missing 'Blue' variant
|
|
240
|
+
|
|
241
|
+
Fix: add the missing variants, or add a wildcard: _ => "other"
|
|
242
|
+
`,
|
|
243
|
+
W204: `
|
|
244
|
+
Implicit narrowing conversion that may lose data.
|
|
245
|
+
|
|
246
|
+
Example:
|
|
247
|
+
x: Int = 3.14 // warning: assigning Float to Int loses decimal
|
|
248
|
+
|
|
249
|
+
Fix: use an explicit conversion like floor(), round(), or to_int().
|
|
250
|
+
`,
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
export function getExplanation(code) {
|
|
254
|
+
return EXPLANATIONS[code] || null;
|
|
255
|
+
}
|
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
// Rich error message formatter for the Tova language
|
|
2
|
-
// Produces Rust/Elm-style error messages with source context and
|
|
2
|
+
// Produces Rust/Elm-style error messages with source context, carets, and fix suggestions
|
|
3
|
+
|
|
4
|
+
import { parseIgnoreComment } from './error-codes.js';
|
|
5
|
+
|
|
6
|
+
// ─── ANSI color helpers (terminal only) ──────────────────────
|
|
7
|
+
|
|
8
|
+
const _isTTY = typeof process !== 'undefined' && process.stderr && process.stderr.isTTY;
|
|
9
|
+
|
|
10
|
+
const _c = {
|
|
11
|
+
red: (s) => _isTTY ? `\x1b[31m${s}\x1b[0m` : s,
|
|
12
|
+
yellow: (s) => _isTTY ? `\x1b[33m${s}\x1b[0m` : s,
|
|
13
|
+
cyan: (s) => _isTTY ? `\x1b[36m${s}\x1b[0m` : s,
|
|
14
|
+
green: (s) => _isTTY ? `\x1b[32m${s}\x1b[0m` : s,
|
|
15
|
+
blue: (s) => _isTTY ? `\x1b[34m${s}\x1b[0m` : s,
|
|
16
|
+
dim: (s) => _isTTY ? `\x1b[2m${s}\x1b[0m` : s,
|
|
17
|
+
bold: (s) => _isTTY ? `\x1b[1m${s}\x1b[0m` : s,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// ─── DiagnosticFormatter ─────────────────────────────────────
|
|
3
21
|
|
|
4
22
|
export class DiagnosticFormatter {
|
|
5
23
|
constructor(source, filename = '<stdin>') {
|
|
@@ -8,24 +26,46 @@ export class DiagnosticFormatter {
|
|
|
8
26
|
this.lines = source.split('\n');
|
|
9
27
|
}
|
|
10
28
|
|
|
11
|
-
|
|
12
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Format a diagnostic with full source context.
|
|
31
|
+
* @param {string} level - 'error' | 'warning' | 'info' | 'hint'
|
|
32
|
+
* @param {string} message - Human-readable message
|
|
33
|
+
* @param {object} loc - { line, column }
|
|
34
|
+
* @param {object} [opts] - { hint, code, length, fix }
|
|
35
|
+
* hint: string — suggestion text
|
|
36
|
+
* code: string — error code like 'E100'
|
|
37
|
+
* length: number — underline length (default 1)
|
|
38
|
+
* fix: { description, replacement } — auto-fix suggestion
|
|
39
|
+
*/
|
|
40
|
+
format(level, message, loc, opts = {}) {
|
|
41
|
+
// Backwards compat: opts can be a string (hint)
|
|
42
|
+
if (typeof opts === 'string') opts = { hint: opts };
|
|
43
|
+
|
|
44
|
+
const hint = opts.hint || null;
|
|
45
|
+
const code = opts.code || null;
|
|
46
|
+
const length = opts.length || 0;
|
|
47
|
+
const fix = opts.fix || null;
|
|
48
|
+
|
|
49
|
+
const line = loc.line || 1;
|
|
13
50
|
const column = loc.column || 1;
|
|
14
51
|
const lineNum = Math.max(1, Math.min(line, this.lines.length));
|
|
15
|
-
const gutterWidth = String(lineNum + 1).length;
|
|
52
|
+
const gutterWidth = Math.max(String(lineNum + 1).length, 3);
|
|
16
53
|
|
|
17
54
|
let output = '';
|
|
18
55
|
|
|
19
|
-
// Header
|
|
20
|
-
const levelStr = level === 'error' ? 'error'
|
|
21
|
-
|
|
22
|
-
|
|
56
|
+
// ── Header ──
|
|
57
|
+
const levelStr = level === 'error' ? _c.red(_c.bold('error'))
|
|
58
|
+
: level === 'warning' ? _c.yellow(_c.bold('warning'))
|
|
59
|
+
: _c.blue(_c.bold(level));
|
|
60
|
+
const codeStr = code ? _c.dim(`[${code}]`) + ' ' : '';
|
|
61
|
+
output += `${levelStr}: ${codeStr}${_c.bold(message)}\n`;
|
|
62
|
+
output += `${' '.repeat(gutterWidth)}${_c.blue(' -->')} ${this.filename}:${lineNum}:${column}\n`;
|
|
23
63
|
|
|
24
|
-
// Context lines
|
|
64
|
+
// ── Context lines ──
|
|
25
65
|
const startLine = Math.max(1, lineNum - 2);
|
|
26
66
|
const endLine = Math.min(this.lines.length, lineNum + 1);
|
|
27
67
|
|
|
28
|
-
output += `${' '.repeat(gutterWidth)}
|
|
68
|
+
output += `${' '.repeat(gutterWidth)} ${_c.blue('|')}\n`;
|
|
29
69
|
|
|
30
70
|
for (let i = startLine; i <= endLine; i++) {
|
|
31
71
|
const lineContent = this.lines[i - 1] || '';
|
|
@@ -33,53 +73,79 @@ export class DiagnosticFormatter {
|
|
|
33
73
|
|
|
34
74
|
if (i === lineNum) {
|
|
35
75
|
// The error line
|
|
36
|
-
output += `${lineStr} | ${lineContent}\n`;
|
|
37
|
-
//
|
|
76
|
+
output += `${_c.blue(lineStr)} ${_c.blue('|')} ${lineContent}\n`;
|
|
77
|
+
// Underline with carets
|
|
38
78
|
const caretPad = ' '.repeat(Math.max(0, column - 1));
|
|
39
|
-
|
|
79
|
+
const underlineLen = Math.max(1, length || _guessLength(lineContent, column));
|
|
80
|
+
const underline = '^'.repeat(underlineLen);
|
|
81
|
+
const caretColor = level === 'error' ? _c.red : _c.yellow;
|
|
82
|
+
output += `${' '.repeat(gutterWidth)} ${_c.blue('|')} ${caretPad}${caretColor(underline)}\n`;
|
|
40
83
|
} else {
|
|
41
|
-
output += `${lineStr} | ${lineContent}\n`;
|
|
84
|
+
output += `${_c.dim(lineStr)} ${_c.blue('|')} ${lineContent}\n`;
|
|
42
85
|
}
|
|
43
86
|
}
|
|
44
87
|
|
|
45
|
-
output += `${' '.repeat(gutterWidth)}
|
|
88
|
+
output += `${' '.repeat(gutterWidth)} ${_c.blue('|')}\n`;
|
|
46
89
|
|
|
47
|
-
// Hint
|
|
90
|
+
// ── Hint ──
|
|
48
91
|
if (hint) {
|
|
49
|
-
output += `${' '.repeat(gutterWidth)} = hint: ${hint}\n`;
|
|
92
|
+
output += `${' '.repeat(gutterWidth)} ${_c.blue('=')} ${_c.cyan('hint')}: ${hint}\n`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ── Fix suggestion ──
|
|
96
|
+
if (fix) {
|
|
97
|
+
output += `${' '.repeat(gutterWidth)} ${_c.blue('=')} ${_c.green('fix')}: ${fix.description}\n`;
|
|
98
|
+
if (fix.replacement !== undefined) {
|
|
99
|
+
output += `${' '.repeat(gutterWidth)} ${_c.green('|')} ${_c.green(fix.replacement)}\n`;
|
|
100
|
+
}
|
|
50
101
|
}
|
|
51
102
|
|
|
52
103
|
return output;
|
|
53
104
|
}
|
|
54
105
|
|
|
55
|
-
formatError(message, loc,
|
|
56
|
-
|
|
106
|
+
formatError(message, loc, hintOrOpts = null) {
|
|
107
|
+
const opts = typeof hintOrOpts === 'string' ? { hint: hintOrOpts } : (hintOrOpts || {});
|
|
108
|
+
return this.format('error', message, loc, opts);
|
|
57
109
|
}
|
|
58
110
|
|
|
59
|
-
formatWarning(message, loc,
|
|
60
|
-
|
|
111
|
+
formatWarning(message, loc, hintOrOpts = null) {
|
|
112
|
+
const opts = typeof hintOrOpts === 'string' ? { hint: hintOrOpts } : (hintOrOpts || {});
|
|
113
|
+
return this.format('warning', message, loc, opts);
|
|
61
114
|
}
|
|
62
115
|
}
|
|
63
116
|
|
|
64
|
-
//
|
|
117
|
+
// Guess a reasonable underline length from the token at the given column
|
|
118
|
+
function _guessLength(lineContent, column) {
|
|
119
|
+
const rest = lineContent.slice(column - 1);
|
|
120
|
+
// Try to find a word boundary
|
|
121
|
+
const wordMatch = rest.match(/^(\w+|[^\s]+)/);
|
|
122
|
+
if (wordMatch) return wordMatch[1].length;
|
|
123
|
+
return 1;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ─── Format arrays of diagnostics ────────────────────────────
|
|
127
|
+
|
|
65
128
|
export function formatDiagnostics(source, filename, errors, warnings = []) {
|
|
66
129
|
const formatter = new DiagnosticFormatter(source, filename);
|
|
130
|
+
const ignoredCodes = _collectIgnoredCodes(source);
|
|
67
131
|
let output = '';
|
|
68
132
|
|
|
69
133
|
for (const err of errors) {
|
|
134
|
+
if (err.code && ignoredCodes.has(err.code)) continue;
|
|
70
135
|
output += formatter.formatError(
|
|
71
136
|
err.message,
|
|
72
137
|
{ line: err.line, column: err.column },
|
|
73
|
-
err.hint
|
|
138
|
+
{ hint: err.hint, code: err.code, length: err.length, fix: err.fix }
|
|
74
139
|
);
|
|
75
140
|
output += '\n';
|
|
76
141
|
}
|
|
77
142
|
|
|
78
143
|
for (const warn of warnings) {
|
|
144
|
+
if (warn.code && ignoredCodes.has(warn.code)) continue;
|
|
79
145
|
output += formatter.formatWarning(
|
|
80
146
|
warn.message,
|
|
81
147
|
{ line: warn.line, column: warn.column },
|
|
82
|
-
warn.hint
|
|
148
|
+
{ hint: warn.hint, code: warn.code, length: warn.length, fix: warn.fix }
|
|
83
149
|
);
|
|
84
150
|
output += '\n';
|
|
85
151
|
}
|
|
@@ -87,7 +153,20 @@ export function formatDiagnostics(source, filename, errors, warnings = []) {
|
|
|
87
153
|
return output;
|
|
88
154
|
}
|
|
89
155
|
|
|
90
|
-
//
|
|
156
|
+
// Collect all tova-ignore codes from source comments
|
|
157
|
+
function _collectIgnoredCodes(source) {
|
|
158
|
+
const codes = new Set();
|
|
159
|
+
for (const line of source.split('\n')) {
|
|
160
|
+
const parsed = parseIgnoreComment(line);
|
|
161
|
+
if (parsed) {
|
|
162
|
+
for (const c of parsed) codes.add(c);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return codes;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ─── Helper: extract location from typical Tova error messages ──
|
|
169
|
+
|
|
91
170
|
export function parseErrorLocation(errorMessage) {
|
|
92
171
|
const match = errorMessage.match(/^(.+?):(\d+):(\d+)\s*[—-]\s*(.+)/);
|
|
93
172
|
if (match) {
|
|
@@ -101,14 +180,47 @@ export function parseErrorLocation(errorMessage) {
|
|
|
101
180
|
return null;
|
|
102
181
|
}
|
|
103
182
|
|
|
104
|
-
// Create a rich error from a Tova parse/analysis error
|
|
183
|
+
// ─── Create a rich error from a Tova parse/analysis error ────
|
|
184
|
+
|
|
105
185
|
export function richError(source, error, filename = '<stdin>') {
|
|
106
186
|
const formatter = new DiagnosticFormatter(source, filename);
|
|
107
187
|
|
|
188
|
+
// If error carries structured errors, format them all
|
|
189
|
+
if (error.errors && Array.isArray(error.errors)) {
|
|
190
|
+
let output = '';
|
|
191
|
+
for (const e of error.errors) {
|
|
192
|
+
if (e.line && e.column) {
|
|
193
|
+
output += formatter.formatError(
|
|
194
|
+
e.message,
|
|
195
|
+
{ line: e.line, column: e.column },
|
|
196
|
+
{ hint: e.hint, code: e.code, length: e.length, fix: e.fix }
|
|
197
|
+
);
|
|
198
|
+
output += '\n';
|
|
199
|
+
} else if (e.loc) {
|
|
200
|
+
output += formatter.formatError(
|
|
201
|
+
e.message.replace(/^.+?:\d+:\d+\s*[—-]\s*/, ''),
|
|
202
|
+
{ line: e.loc.line, column: e.loc.column },
|
|
203
|
+
{ hint: e.hint, code: e.code }
|
|
204
|
+
);
|
|
205
|
+
output += '\n';
|
|
206
|
+
} else {
|
|
207
|
+
// Try to parse location from message
|
|
208
|
+
const loc = parseErrorLocation(e.message);
|
|
209
|
+
if (loc) {
|
|
210
|
+
output += formatter.formatError(loc.message, { line: loc.line, column: loc.column });
|
|
211
|
+
output += '\n';
|
|
212
|
+
} else {
|
|
213
|
+
output += formatter.formatError(e.message, { line: 1, column: 1 });
|
|
214
|
+
output += '\n';
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return output || error.message;
|
|
219
|
+
}
|
|
220
|
+
|
|
108
221
|
// Try to extract location from error message
|
|
109
222
|
const loc = parseErrorLocation(error.message);
|
|
110
223
|
if (loc) {
|
|
111
|
-
// Try to detect hint
|
|
112
224
|
let hint = null;
|
|
113
225
|
if (loc.message.includes("Expected '}'")) {
|
|
114
226
|
hint = "check for a matching opening '{' above";
|
|
@@ -118,7 +230,7 @@ export function richError(source, error, filename = '<stdin>') {
|
|
|
118
230
|
hint = "check for a matching opening '[' above";
|
|
119
231
|
}
|
|
120
232
|
|
|
121
|
-
return formatter.formatError(loc.message, { line: loc.line, column: loc.column }, hint);
|
|
233
|
+
return formatter.formatError(loc.message, { line: loc.line, column: loc.column }, { hint });
|
|
122
234
|
}
|
|
123
235
|
|
|
124
236
|
// Fallback: try to parse the error for analysis errors
|
|
@@ -137,3 +249,13 @@ export function richError(source, error, filename = '<stdin>') {
|
|
|
137
249
|
|
|
138
250
|
return error.message;
|
|
139
251
|
}
|
|
252
|
+
|
|
253
|
+
// ─── Summary line for CLI output ─────────────────────────────
|
|
254
|
+
|
|
255
|
+
export function formatSummary(errorCount, warningCount) {
|
|
256
|
+
const parts = [];
|
|
257
|
+
if (errorCount > 0) parts.push(_c.red(`${errorCount} error${errorCount === 1 ? '' : 's'}`));
|
|
258
|
+
if (warningCount > 0) parts.push(_c.yellow(`${warningCount} warning${warningCount === 1 ? '' : 's'}`));
|
|
259
|
+
if (parts.length === 0) return _c.green('no errors');
|
|
260
|
+
return parts.join(', ') + ' emitted';
|
|
261
|
+
}
|