Skip to content

Commit e251944

Browse files
committed
veb: fix error lines for html templates via source to source mapping
1 parent 48000f4 commit e251944

6 files changed

Lines changed: 207 additions & 12 deletions

File tree

‎vlib/v/ast/ast.v‎

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1079,6 +1079,13 @@ pub mut:
10791079
len int
10801080
}
10811081

1082+
// TemplateLineInfo maps a generated code line to the original template location
1083+
pub struct TemplateLineInfo {
1084+
pub:
1085+
tmpl_path string // path to the template file (for @include support)
1086+
tmpl_line int // 0-based line number in the template
1087+
}
1088+
10821089
// Each V source file is represented by one File structure.
10831090
// When the V compiler runs, the parser will fill an []File.
10841091
// That array is then passed to V's checker.
@@ -1113,9 +1120,10 @@ pub mut:
11131120
notices []errors.Notice // all the checker notices in the file
11141121
call_stack []errors.CallStackItem // call stack for this file (used for template errors)
11151122
generic_fns []&FnDecl
1116-
global_labels []string // from `asm { .globl labelname }`
1117-
template_paths []string // all the .html/.md files that were processed with $tmpl
1118-
unique_prefix string // a hash of the `.path` field, used for making anon fn generation unique
1123+
global_labels []string // from `asm { .globl labelname }`
1124+
template_paths []string // all the .html/.md files that were processed with $tmpl
1125+
template_line_map []TemplateLineInfo // maps generated line -> original template location
1126+
unique_prefix string // a hash of the `.path` field, used for making anon fn generation unique
11191127
//
11201128
is_parse_text bool // true for files, produced by parse_text
11211129
is_template_text bool // true for files, produced by parse_comptime

‎vlib/v/checker/comptime.v‎

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ module checker
55
import os
66
import v.ast
77
import v.pref
8+
import v.token
89
import v.util
910
import v.pkgconfig
1011
import v.type_resolver
@@ -118,6 +119,70 @@ fn (mut c Checker) comptime_call(mut node ast.ComptimeCall) ast.Type {
118119
mut c2 := new_checker(c.table, pref2)
119120
c2.comptime_call_pos = node.pos.pos
120121
c2.check(mut node.veb_tmpl)
122+
123+
// Cache template file content for error display using the relative path
124+
// The template_paths contains full paths, but errors use relative paths from node.veb_tmpl.path
125+
if node.veb_tmpl.template_paths.len > 0 {
126+
// Cache the main template file
127+
main_template_path := node.veb_tmpl.template_paths[0]
128+
if content := os.read_file(main_template_path) {
129+
util.set_source_for_path(node.veb_tmpl.path, content)
130+
}
131+
}
132+
133+
// Fix error line numbers using template line mapping
134+
line_map := node.veb_tmpl.template_line_map
135+
if line_map.len > 0 {
136+
for i, err in c2.errors {
137+
if err.pos.line_nr >= 0 && err.pos.line_nr < line_map.len {
138+
line_info := line_map[err.pos.line_nr]
139+
c2.errors[i] = errors.Error{
140+
message: err.message
141+
details: err.details
142+
file_path: err.file_path
143+
pos: token.Pos{
144+
...err.pos
145+
line_nr: line_info.tmpl_line
146+
}
147+
reporter: err.reporter
148+
call_stack: err.call_stack
149+
}
150+
}
151+
}
152+
for i, warn in c2.warnings {
153+
if warn.pos.line_nr >= 0 && warn.pos.line_nr < line_map.len {
154+
line_info := line_map[warn.pos.line_nr]
155+
c2.warnings[i] = errors.Warning{
156+
message: warn.message
157+
details: warn.details
158+
file_path: warn.file_path
159+
pos: token.Pos{
160+
...warn.pos
161+
line_nr: line_info.tmpl_line
162+
}
163+
reporter: warn.reporter
164+
call_stack: warn.call_stack
165+
}
166+
}
167+
}
168+
for i, notice in c2.notices {
169+
if notice.pos.line_nr >= 0 && notice.pos.line_nr < line_map.len {
170+
line_info := line_map[notice.pos.line_nr]
171+
c2.notices[i] = errors.Notice{
172+
message: notice.message
173+
details: notice.details
174+
file_path: notice.file_path
175+
pos: token.Pos{
176+
...notice.pos
177+
line_nr: line_info.tmpl_line
178+
}
179+
reporter: notice.reporter
180+
call_stack: notice.call_stack
181+
}
182+
}
183+
}
184+
}
185+
121186
c.warnings << c2.warnings
122187
c.errors << c2.errors
123188
c.notices << c2.notices

‎vlib/v/checker/tests/template_call_position.out‎

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
1-
template_call_position_test.txt:11:2: error: undefined ident: `unknown_var` (veb action: main__main)
1+
template_call_position_test.txt:3:2: error: undefined ident: `unknown_var` (veb action: main__main)
2+
1 | Hello World!
3+
2 | This is a template file.
4+
3 | @unknown_var
5+
| ~~~~~~~~~~~
26
called from vlib/v/checker/tests/template_call_position.vv:4:2
3-
2 |
7+
2 |
48
3 | fn main() {
59
4 | $tmpl('template_call_position_test.txt')
610
| ^
711
5 | }
8-
template_call_position_test.txt:11:2: error: expression does not return a value (veb action: main__main)
12+
template_call_position_test.txt:3:2: error: expression does not return a value (veb action: main__main)
13+
1 | Hello World!
14+
2 | This is a template file.
15+
3 | @unknown_var
16+
| ~~~~~~~~~~~
917
called from vlib/v/checker/tests/template_call_position.vv:4:2
10-
2 |
18+
2 |
1119
3 | fn main() {
1220
4 | $tmpl('template_call_position_test.txt')
1321
| ^

‎vlib/v/parser/comptime.v‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,9 @@ fn (mut p Parser) comptime_call() ast.ComptimeCall {
383383
pos: start_pos
384384
},
385385
]
386+
// Transfer template paths and line mapping from parser to file for error reporting
387+
file.template_paths = p.template_paths
388+
file.template_line_map = p.template_line_map
386389
return ast.ComptimeCall{
387390
scope: unsafe { nil }
388391
is_vweb: true

‎vlib/v/parser/parser.v‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -130,11 +130,12 @@ pub mut:
130130
opened_scopes int
131131
max_opened_scopes int = 100 // values above 300 risk stack overflow
132132

133-
errors []errors.Error
134-
warnings []errors.Warning
135-
notices []errors.Notice
136-
template_paths []string // record all compiled $tmpl files; needed for `v watch run webserver.v`
137-
content ParseContentKind
133+
errors []errors.Error
134+
warnings []errors.Warning
135+
notices []errors.Notice
136+
template_paths []string // record all compiled $tmpl files; needed for `v watch run webserver.v`
137+
template_line_map []ast.TemplateLineInfo // line mapping for current template compilation
138+
content ParseContentKind
138139
}
139140

140141
enum ParseContentKind {

‎vlib/v/parser/tmpl.v‎

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// that can be found in the LICENSE file.
44
module parser
55

6+
import v.ast
67
import v.token
78
import v.errors
89
import os
@@ -223,13 +224,26 @@ pub fn (mut p Parser) compile_template_file(template_file string, fn_name string
223224
lstartlength := lines.len * 30
224225
tmpl_str_start := "\tsb_${fn_name}.write_string('"
225226
mut source := strings.new_builder(1000)
227+
228+
// Reset line mapping for this template compilation
229+
p.template_line_map = []
230+
226231
source.writeln('
227232
import strings
228233
// === veb html template for file: ${template_file} ===
229234
fn veb_tmpl_${fn_name}() string {
230235
mut sb_${fn_name} := strings.new_builder(${lstartlength})\n
231236
232237
')
238+
// Header adds 8 lines (0: empty, 1: import, 2: comment, 3: fn, 4: builder, 5: empty from \n escape, 6: empty from literal, 7: empty from writeln)
239+
// Pre-fill the line map with placeholder entries for header lines
240+
for _ in 0 .. 8 {
241+
p.template_line_map << ast.TemplateLineInfo{
242+
tmpl_path: template_file
243+
tmpl_line: 0
244+
}
245+
}
246+
233247
source.write_string(tmpl_str_start)
234248

235249
mut state := State.simple
@@ -325,35 +339,91 @@ fn veb_tmpl_${fn_name}() string {
325339
}
326340
if line.contains('@if ') {
327341
source.writeln(tmpl_str_end)
342+
// tmpl_str_end contains '\n', so writeln creates 2 lines: ')' and empty
343+
p.template_line_map << ast.TemplateLineInfo{
344+
tmpl_path: template_file
345+
tmpl_line: tline_number
346+
}
347+
p.template_line_map << ast.TemplateLineInfo{
348+
tmpl_path: template_file
349+
tmpl_line: tline_number
350+
}
328351
pos := line.index('@if') or { continue }
329352
source.writeln('if ' + line[pos + 4..] + '{')
353+
p.template_line_map << ast.TemplateLineInfo{
354+
tmpl_path: template_file
355+
tmpl_line: tline_number
356+
}
330357
source.write_string(tmpl_str_start)
331358
continue
332359
}
333360
if line.contains('@end') {
334361
source.writeln(tmpl_str_end)
362+
// tmpl_str_end contains '\n', so writeln creates 2 lines: ')' and empty
363+
p.template_line_map << ast.TemplateLineInfo{
364+
tmpl_path: template_file
365+
tmpl_line: tline_number
366+
}
367+
p.template_line_map << ast.TemplateLineInfo{
368+
tmpl_path: template_file
369+
tmpl_line: tline_number
370+
}
335371
source.writeln('}')
372+
p.template_line_map << ast.TemplateLineInfo{
373+
tmpl_path: template_file
374+
tmpl_line: tline_number
375+
}
336376
source.write_string(tmpl_str_start)
337377
continue
338378
}
339379
if line.contains('@else') {
340380
source.writeln(tmpl_str_end)
381+
// tmpl_str_end contains '\n', so writeln creates 2 lines: ')' and empty
382+
p.template_line_map << ast.TemplateLineInfo{
383+
tmpl_path: template_file
384+
tmpl_line: tline_number
385+
}
386+
p.template_line_map << ast.TemplateLineInfo{
387+
tmpl_path: template_file
388+
tmpl_line: tline_number
389+
}
341390
pos := line.index('@else') or { continue }
342391
source.writeln('}' + line[pos + 1..] + '{')
392+
p.template_line_map << ast.TemplateLineInfo{
393+
tmpl_path: template_file
394+
tmpl_line: tline_number
395+
}
343396
// source.writeln(' } else { ')
344397
source.write_string(tmpl_str_start)
345398
continue
346399
}
347400
if line.contains('@for') {
348401
source.writeln(tmpl_str_end)
402+
// tmpl_str_end contains '\n', so writeln creates 2 lines: ')' and empty
403+
p.template_line_map << ast.TemplateLineInfo{
404+
tmpl_path: template_file
405+
tmpl_line: tline_number
406+
}
407+
p.template_line_map << ast.TemplateLineInfo{
408+
tmpl_path: template_file
409+
tmpl_line: tline_number
410+
}
349411
pos := line.index('@for') or { continue }
350412
source.writeln('for ' + line[pos + 4..] + '{')
413+
p.template_line_map << ast.TemplateLineInfo{
414+
tmpl_path: template_file
415+
tmpl_line: tline_number
416+
}
351417
source.write_string(tmpl_str_start)
352418
continue
353419
}
354420
if state == .simple {
355421
// by default, just copy 1:1
356422
source.writeln(insert_template_code(fn_name, tmpl_str_start, line))
423+
p.template_line_map << ast.TemplateLineInfo{
424+
tmpl_path: template_file
425+
tmpl_line: tline_number
426+
}
357427
continue
358428
}
359429
// in_write = false
@@ -365,13 +435,21 @@ fn veb_tmpl_${fn_name}() string {
365435
source.write_string('<script src="')
366436
source.write_string(line[pos + 5..line.len - 1])
367437
source.writeln('"></script>')
438+
p.template_line_map << ast.TemplateLineInfo{
439+
tmpl_path: template_file
440+
tmpl_line: tline_number
441+
}
368442
continue
369443
}
370444
if line.contains('@css ') {
371445
pos := line.index('@css') or { continue }
372446
source.write_string('<link href="')
373447
source.write_string(line[pos + 6..line.len - 1])
374448
source.writeln('" rel="stylesheet" type="text/css">')
449+
p.template_line_map << ast.TemplateLineInfo{
450+
tmpl_path: template_file
451+
tmpl_line: tline_number
452+
}
375453
continue
376454
}
377455
}
@@ -383,6 +461,10 @@ fn veb_tmpl_${fn_name}() string {
383461
// `span.header {` => `<span class='header'>`
384462
class := line.find_between('span.', '{').trim_space()
385463
source.writeln('<span class="${class}">')
464+
p.template_line_map << ast.TemplateLineInfo{
465+
tmpl_path: template_file
466+
tmpl_line: tline_number
467+
}
386468
in_span = true
387469
continue
388470
} else if line_t.starts_with('.') && line.ends_with('{') {
@@ -391,11 +473,19 @@ fn veb_tmpl_${fn_name}() string {
391473
trimmed := line.trim_space()
392474
source.write_string(strings.repeat(`\t`, line.len - trimmed.len)) // add the necessary indent to keep <div><div><div> code clean
393475
source.writeln('<div class="${class}">')
476+
p.template_line_map << ast.TemplateLineInfo{
477+
tmpl_path: template_file
478+
tmpl_line: tline_number
479+
}
394480
continue
395481
} else if line_t.starts_with('#') && line.ends_with('{') {
396482
// `#header {` => `<div id='header'>`
397483
class := line.find_between('#', '{').trim_space()
398484
source.writeln('<div id="${class}">')
485+
p.template_line_map << ast.TemplateLineInfo{
486+
tmpl_path: template_file
487+
tmpl_line: tline_number
488+
}
399489
continue
400490
} else if line_t == '}' {
401491
source.write_string(strings.repeat(`\t`, line.len - line_t.len)) // add the necessary indent to keep <div><div><div> code clean
@@ -405,12 +495,20 @@ fn veb_tmpl_${fn_name}() string {
405495
} else {
406496
source.writeln('</div>')
407497
}
498+
p.template_line_map << ast.TemplateLineInfo{
499+
tmpl_path: template_file
500+
tmpl_line: tline_number
501+
}
408502
continue
409503
}
410504
}
411505
.js {
412506
// if line.contains('//V_TEMPLATE') {
413507
source.writeln(insert_template_code(fn_name, tmpl_str_start, line))
508+
p.template_line_map << ast.TemplateLineInfo{
509+
tmpl_path: template_file
510+
tmpl_line: tline_number
511+
}
414512
//} else {
415513
// replace `$` to `\$` at first to escape JavaScript template literal syntax
416514
// source.writeln(line.replace(r'$', r'\$').replace(r'$$', r'@').replace(r'.$',
@@ -422,6 +520,10 @@ fn veb_tmpl_${fn_name}() string {
422520
// disable template variable declaration in inline stylesheet
423521
// because of some CSS rules prefixed with `@`.
424522
source.writeln(line.replace(r'.$', r'.@').replace(r"'", r"\'"))
523+
p.template_line_map << ast.TemplateLineInfo{
524+
tmpl_path: template_file
525+
tmpl_line: tline_number
526+
}
425527
continue
426528
}
427529
else {}
@@ -459,10 +561,18 @@ fn veb_tmpl_${fn_name}() string {
459561
}
460562
// println(source.str())
461563
source.writeln(insert_template_code(fn_name, tmpl_str_start, line_))
564+
p.template_line_map << ast.TemplateLineInfo{
565+
tmpl_path: template_file
566+
tmpl_line: tline_number
567+
}
462568
// exit(0)
463569
} else {
464570
// by default, just copy 1:1
465571
source.writeln(insert_template_code(fn_name, tmpl_str_start, line))
572+
p.template_line_map << ast.TemplateLineInfo{
573+
tmpl_path: template_file
574+
tmpl_line: tline_number
575+
}
466576
}
467577
}
468578

0 commit comments

Comments
 (0)