Skip to content

Commit a74d263

Browse files
committed
Improve error template highlighting when backtrace lineno is wrong
1 parent 58b2d73 commit a74d263

File tree

3 files changed

+56
-49
lines changed

3 files changed

+56
-49
lines changed

actionview/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
* Improve error highlighting of multi-line methods in ERB templates or
2+
templates where the error occurs within a do-end block.
3+
4+
*Martin Emde*
5+
16
* Fix a crash in ERB template error highlighting when the error occurs on a
27
line in the compiled template that is past the end of the source template.
38

actionview/lib/action_view/template/handlers/erb.rb

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,17 +40,19 @@ def handles_encoding?
4040

4141
# Translate an error location returned by ErrorHighlight to the correct
4242
# source location inside the template.
43-
def translate_location(spot, backtrace_location, source)
44-
# Tokenize the source line
43+
def translate_location(spot, _backtrace_location, source)
44+
compiled = spot[:script_lines]
45+
highlight = compiled[spot[:first_lineno] - 1]&.byteslice((spot[:first_column] - 1)...spot[:last_column])
46+
return nil if highlight.blank?
47+
4548
source_lines = source.lines
46-
return nil if source_lines.size < backtrace_location.lineno
47-
tokens = ::ERB::Util.tokenize(source_lines[backtrace_location.lineno - 1])
48-
new_first_column = find_offset(spot[:snippet], tokens, spot[:first_column])
49-
lineno_delta = spot[:first_lineno] - backtrace_location.lineno
49+
lineno_delta = find_lineno_offset(compiled, source_lines, highlight, spot[:first_lineno])
50+
51+
tokens = ::ERB::Util.tokenize(source_lines[spot[:first_lineno] - lineno_delta - 1])
52+
column_delta = find_offset(spot[:snippet], tokens, spot[:first_column])
53+
5054
spot[:first_lineno] -= lineno_delta
5155
spot[:last_lineno] -= lineno_delta
52-
53-
column_delta = spot[:first_column] - new_first_column
5456
spot[:first_column] -= column_delta
5557
spot[:last_column] -= column_delta
5658
spot[:script_lines] = source_lines
@@ -107,6 +109,28 @@ def valid_encoding(string, encoding)
107109
raise WrongEncodingError.new(string, string.encoding)
108110
end
109111

112+
# Return the offset between the error lineno and the source lineno.
113+
# Searches in reverse from the backtrace lineno so we have a better
114+
# chance of finding the correct line
115+
#
116+
# The compiled template is likely to be longer than the source.
117+
# Use the difference between the compiled and source sizes to
118+
# determine the earliest line that could contain the highlight.
119+
def find_lineno_offset(compiled, source_lines, highlight, error_lineno)
120+
first_index = error_lineno - 1 - compiled.size + source_lines.size
121+
first_index = 0 if first_index < 0
122+
123+
last_index = error_lineno - 1
124+
last_index = source_lines.size - 1 if last_index >= source_lines.size
125+
126+
last_index.downto(first_index) do |line_index|
127+
next unless source_lines[line_index].include?(highlight)
128+
return error_lineno - 1 - line_index
129+
end
130+
131+
raise LocationParsingError, "Couldn't find code snippet"
132+
end
133+
110134
# Find which token in the source template spans the byte range that
111135
# contains the error_column, then return the offset compared to the
112136
# original source template.
@@ -137,7 +161,7 @@ def find_offset(compiled, source_tokens, error_column)
137161
matched_str = true
138162

139163
if name == :CODE && compiled.pos <= error_column && compiled.pos + str.bytesize >= error_column
140-
return error_column - compiled.pos + offset
164+
return compiled.pos - offset
141165
end
142166

143167
compiled.pos += str.bytesize

actionview/test/template/template_test.rb

Lines changed: 18 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,8 @@ def render(implicit_locals: [], **locals)
6767

6868
def spot_highlight(compiled, highlight, first_column: nil, **options)
6969
# rindex by default since our tests usually put the highlight last
70-
first_column ||= compiled.rindex(highlight) || 999
71-
last_column = first_column + highlight.size
70+
first_column ||= compiled.byterindex(highlight) || 999
71+
last_column = first_column + highlight.bytesize
7272
spot = {
7373
first_column:, last_column:, snippet: compiled,
7474
first_lineno: 1, last_lineno: 1, script_lines: compiled.lines,
@@ -332,121 +332,99 @@ def test_template_translate_location
332332
source = "<%= nomethoderror %>"
333333
compiled = "'.freeze; @output_buffer.append= nomethoderror ; @output_buffer.safe_append='\n"
334334

335-
backtrace_location = Data.define(:lineno).new(lineno: 1)
336335
spot = spot_highlight(compiled, highlight)
337336
expected = spot_highlight(source, highlight, snippet: compiled)
338337

339-
assert_equal expected, new_template(source).translate_location(backtrace_location, spot)
338+
assert_equal expected, new_template(source).translate_location(nil, spot)
340339
end
341340

342-
# merely tests for the case where the backtrace and spot disagree about lineno
343-
def test_template_translate_location_lineno_offset
344-
highlight = "nomethoderror"
345-
source = "<%= nomethoderror %>"
346-
compiled = "'.freeze; @output_buffer.append= nomethoderror ; @output_buffer.safe_append='"
347-
348-
backtrace_location = Data.define(:lineno).new(lineno: 1)
349-
spot = spot_highlight(compiled, highlight, first_lineno: 2, last_lineno: 2)
350-
expected = spot_highlight(source, highlight, snippet: compiled)
351-
352-
assert_equal expected, new_template(source).translate_location(backtrace_location, spot)
353-
end
354-
355-
# We are testing the failure case here. `find_offset` doesn't correctly handle the case
356-
# where the line number is not the same in the backtrace and template.
357341
def test_template_translate_location_with_multiline_code_source
358342
highlight = "nomethoderror"
359343
source = "<%=\ngood(\n nomethoderror\n) %>"
360344
extracted_line = " nomethoderror\n"
361-
compiled = "ValidatedOutputBuffer.wrap(@output_buffer, ({}), ' \ngood(\n nomethoderror\n" \
362-
") '.freeze, true).safe_none_append=( \ngood(\n nomethoderror\n) );\n@output_buffer"
345+
compiled = "ValidatedOutputBuffer.wrap(@output_buffer, ({}), '\ngood(\n nomethoderror\n) '.freeze, true).safe_none_append=(\ngood(\n nomethoderror\n) );\n@output_buffer"
363346

364-
backtrace_location = Data.define(:lineno).new(lineno: 6)
365347
spot = spot_highlight(compiled, highlight, first_column: 1, first_lineno: 6, last_lineno: 6, snippet: extracted_line)
348+
expected = spot_highlight(source, highlight, first_column: 1, first_lineno: 3, last_lineno: 3, snippet: extracted_line)
366349

367-
assert_equal spot, new_template(source).translate_location(backtrace_location, spot)
350+
assert_equal expected, new_template(source).translate_location(nil, spot)
368351
end
369352

370353
def test_template_translate_location_with_multibye_string_before_highlight
371-
highlight = "nomethoderror"
372-
source = String.new("\u{a5}<%= nomethoderror %>", encoding: Encoding::UTF_8) # yen symbol
373-
compiled = String.new("\u{a5}'.freeze; @output_buffer.append= nomethoderror ; @output_buffer.safe_append='\n", encoding: Encoding::UTF_8)
354+
highlight = "nope"
355+
# ensure the byte offset is enough to make us miss the highlight if wrong
356+
multibyte = String.new("\u{a5}\u{a5}\u{a5}\u{a5}\u{a5}\u{a5}\u{a5}", encoding: Encoding::UTF_8) # yen symbol
357+
source = "#{multibyte}<%= nope %>"
358+
compiled = "#{multibyte}'.freeze; @output_buffer.append= nope ; @output_buffer.safe_append='\n"
374359

375-
backtrace_location = Data.define(:lineno).new(lineno: 1)
376360
spot = spot_highlight(compiled, highlight)
377361
expected = spot_highlight(source, highlight, snippet: compiled)
378362

379-
assert_equal expected, new_template(source).translate_location(backtrace_location, spot)
363+
assert_equal expected, new_template(source).translate_location(nil, spot)
380364
end
381365

382366
def test_template_translate_location_no_match_in_compiled
383367
highlight = "nomatch"
384368
source = "<%= nomatch %>"
385369
compiled = "this source does not contain the highlight, so the original spot is returned"
386370

387-
backtrace_location = Data.define(:lineno).new(lineno: 1)
388371
spot = spot_highlight(compiled, highlight, first_column: 50)
389372

390-
assert_equal spot, new_template(source).translate_location(backtrace_location, spot)
373+
assert_equal spot, new_template(source).translate_location(nil, spot)
391374
end
392375

393376
def test_template_translate_location_text_includes_highlight
394377
highlight = "nomethoderror"
395378
source = " nomethoderror <%= nomethoderror %>"
396379
compiled = " nomethoderror '.freeze; @output_buffer.append= nomethoderror ; @output_buffer.safe_append='\n"
397380

398-
backtrace_location = Data.define(:lineno).new(lineno: 1)
399381
spot = spot_highlight(compiled, highlight)
400382
expected = spot_highlight(source, highlight, snippet: compiled)
401383

402-
assert_equal expected, new_template(source).translate_location(backtrace_location, spot)
384+
assert_equal expected, new_template(source).translate_location(nil, spot)
403385
end
404386

405387
def test_template_translate_location_space_separated_erb_tags
406388
highlight = "nomethoderror"
407389
source = "<%= goodcode %> <%= nomethoderror %>"
408390
compiled = "'.freeze; @output_buffer.append= goodcode ; @output_buffer.safe_append=' '.freeze; @output_buffer.append= nomethoderror ; @output_buffer.safe_append='\n"
409391

410-
backtrace_location = Data.define(:lineno).new(lineno: 1)
411392
spot = spot_highlight(compiled, highlight)
412393
expected = spot_highlight(source, highlight, snippet: compiled)
413394

414-
assert_equal expected, new_template(source).translate_location(backtrace_location, spot)
395+
assert_equal expected, new_template(source).translate_location(nil, spot)
415396
end
416397

417398
def test_template_translate_location_consecutive_erb_tags
418399
highlight = "nomethoderror"
419400
source = "<%= goodcode %><%= nomethoderror %>"
420401
compiled = "'.freeze; @output_buffer.append= goodcode ; @output_buffer.append= nomethoderror ; @output_buffer.safe_append='\n"
421402

422-
backtrace_location = Data.define(:lineno).new(lineno: 1)
423403
spot = spot_highlight(compiled, highlight)
424404
expected = spot_highlight(source, highlight, snippet: compiled)
425405

426-
assert_equal expected, new_template(source).translate_location(backtrace_location, spot)
406+
assert_equal expected, new_template(source).translate_location(nil, spot)
427407
end
428408

429409
def test_template_translate_location_repeated_highlight_in_compiled_template
430410
highlight = "nomethoderror"
431411
source = "<%= nomethoderror %>"
432412
compiled = "ValidatedOutputBuffer.wrap(@output_buffer, ({}), ' nomethoderror '.freeze, true).safe_none_append= nomethoderror ; @output_buffer.safe_append='\n"
433413

434-
backtrace_location = Data.define(:lineno).new(lineno: 1)
435414
spot = spot_highlight(compiled, highlight)
436415
expected = spot_highlight(source, highlight, snippet: compiled)
437416

438-
assert_equal expected, new_template(source).translate_location(backtrace_location, spot)
417+
assert_equal expected, new_template(source).translate_location(nil, spot)
439418
end
440419

441420
def test_template_translate_location_flaky_pathological_template
442421
highlight = "flakymethod"
443422
source = "<%= flakymethod %> flakymethod <%= flakymethod " # fails on second call, no tailing %>
444423
compiled = "ValidatedOutputBuffer.wrap(@output_buffer, ({}), ' flakymethod '.freeze, true).safe_none_append=( flakymethod );@output_buffer.safe_append=' flakymethod '.freeze;ValidatedOutputBuffer.wrap(@output_buffer, ({}), ' flakymethod '.freeze, true).safe_none_append=( flakymethod "
445424

446-
backtrace_location = Data.define(:lineno).new(lineno: 1)
447425
spot = spot_highlight(compiled, highlight)
448426
expected = spot_highlight(source, highlight, snippet: compiled)
449427

450-
assert_equal expected, new_template(source).translate_location(backtrace_location, spot)
428+
assert_equal expected, new_template(source).translate_location(nil, spot)
451429
end
452430
end

0 commit comments

Comments
 (0)