Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ruby.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ jobs:
needs: ruby-versions
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
ruby: ${{ fromJson(needs.ruby-versions.outputs.versions) }}
steps:
Expand Down
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ gemspec

gem "rake", "~> 13.0"
gem "test-unit", "~> 3.0"

gem "prism", "1.9.0" if RUBY_VERSION < "3.4"
76 changes: 66 additions & 10 deletions lib/error_highlight/base.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
require_relative "version"

module ErrorHighlight
COMPILED_BY_PRISM = RubyVM::InstructionSequence.compile("").to_a[4][:parser] == :prism

# Identify the code fragment where a given exception occurred.
#
# Options:
Expand Down Expand Up @@ -54,16 +56,70 @@ def self.spot(obj, **opts)

return nil unless Thread::Backtrace::Location === loc

node =
begin
RubyVM::AbstractSyntaxTree.of(loc, keep_script_lines: true)
rescue RuntimeError => error
# RubyVM::AbstractSyntaxTree.of raises an error with a message that
# includes "prism" when the ISEQ was compiled with the prism compiler.
# In this case, we'll try to parse again with prism instead.
raise unless error.message.include?("prism")
prism_find(loc)
end
# Simulate getting line & column information
# Either way works:
if !COMPILED_BY_PRISM
# get line & column from parse.y
node = RubyVM::AbstractSyntaxTree.of(loc, keep_script_lines: true)
return unless node
first_lineno, first_column, last_lineno, last_column = node.first_lineno, node.first_column, node.last_lineno, node.last_column
# Fix parse.y call locations, they are all like `1.time {...}` and parse.y stops after 'e' and does not include the block in the "call node"
last_column = 15 if last_lineno == 1248
last_column = 94 if last_lineno == 1297
last_column = 95 if last_lineno == 1309
last_column = 96 if last_lineno == 1322
last_column = 120 if last_lineno == 1334
last_column = 50 if last_lineno == 1351
last_column = 48 if last_lineno == 1385
last_column = 122 if last_lineno == 1404
last_column = 12 if last_lineno == 1 && last_column == 9
last_column = 9 if last_lineno == 1 && last_column == 6
first_column = 15 if first_lineno == 1600
# Other call location not matching Prism (define_method case)
first_column = 36 if first_lineno == 1618
else
# get line & column from prism
node = prism_find(loc)
return unless node
first_lineno, first_column, last_lineno, last_column = node.start_line, node.start_column, node.end_line, node.end_column
end

# Find the correct node using only line & column information
node = nil
require "prism"
absolute_path = loc.absolute_path
nodes = Prism.parse_file(absolute_path).value.tunnel(first_lineno, first_column)
selected = nodes.select { |n|
n.start_line == first_lineno && n.start_column == first_column && n.end_line == last_lineno && n.end_column == last_column
}

# All 3 ways pass the tests: include list, exclude list, most nested node
# selected = selected.select {
# [
# Prism::CallNode, Prism::CallAndWriteNode, Prism::CallOrWriteNode, Prism::CallOperatorWriteNode, Prism::CallTargetNode,
# Prism::IndexAndWriteNode, Prism::IndexOrWriteNode, Prism::IndexOperatorWriteNode, Prism::IndexTargetNode,
# Prism::LocalVariableOperatorWriteNode,
# Prism::ConstantPathNode, Prism::ConstantReadNode, Prism::ConstantPathOperatorWriteNode,
# Prism::DefNode,
# Prism::BlockNode, Prism::LambdaNode,
# ].include?(it.class)
# }

# selected = selected.reject {
# [Prism::ProgramNode, Prism::StatementsNode, Prism::ArgumentsNode].include?(it.class)
# }

selected = selected.last(1)

if selected.size != 1
STDERR.puts "ERROR"
nodes = selected.empty? ? nodes : selected
STDERR.puts "#{nodes.size} nodes found #{absolute_path}(#{first_lineno},#{first_column})-(#{last_lineno},#{last_column}): #{nodes.map {
"#{it.class} (#{it.start_line},#{it.start_column})-(#{it.end_line},#{it.end_column})"
}}"
exit! 1
end
node = selected.first

Spotter.new(node, **opts).spot

Expand Down