Skip to content

Commit 8646fd0

Browse files
committed
Annotate syntax error without require
Currently dead_end works by monkey patching require. This causes confusion and problems as other tools are not expecting this. For example zombocom/derailed_benchmarks#204 and #124. This PR utilizes the new SyntaxError#detailed_message as introduced in ruby/ruby#5516 that will be released in Ruby 3.2. That means that developers using dead_end with Ruby 3.2+ will experience more consistent behavior. ## Limitations As pointed out in #31 the current version of dead_end only works if the developer requires dead_end and then invokes `require`. This behavior is still not fixed for Ruby 3.2+ ``` $ ruby -v ruby 3.2.0preview1 (2022-04-03 master f801386f0c) [x86_64-darwin20] $ cat monkeypatch.rb SyntaxError.prepend Module.new { def detailed_message(highlight: nil, **) message = super message += "Monkeypatch worked\n" message end } # require_relative "bad.rb" # Note that i am commenting # out the require, but leaving # in the monkeypatch ⛄️ 3.2.0 🚀 /tmp $ cat bad.rb def lol_i-am-a-synt^xerror ⛄️ 3.2.0 🚀 /tmp $ ruby -r./monkeypatch.rb bad.rb bad.rb:1: syntax error, unexpected '-', expecting ';' or '\n' def lol_i-am-a-synt^xerror ``` Additionally we are still not able to handle the case where a program is streamed to ruby and does not exist on disk: ``` $ echo "def foo" | ruby ``` As the SyntaxError does not provide us with the contents of the script. ``` $ echo "def foo" | ruby -:1: syntax error, unexpected end-of-input def foo ```
1 parent c6cd679 commit 8646fd0

File tree

3 files changed

+108
-28
lines changed

3 files changed

+108
-28
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
## HEAD (unreleased)
22

3+
- Monkeypatch `SyntaxError#detailed_message` in Ruby 3.2+ instead of `require`, `load`, and `require_relative` (https://github.com/zombocom/dead_end/pull/139)
4+
35
## 3.1.2
46

57
- Fixed internal class AroundBlockScan, minor changes in outputs (https://github.com/zombocom/dead_end/pull/131)

lib/dead_end/core_ext.rb

Lines changed: 75 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,83 @@
11
# frozen_string_literal: true
22

3-
# Monkey patch kernel to ensure that all `require` calls call the same
4-
# method
5-
module Kernel
6-
module_function
7-
8-
alias_method :dead_end_original_require, :require
9-
alias_method :dead_end_original_require_relative, :require_relative
10-
alias_method :dead_end_original_load, :load
11-
12-
def load(file, wrap = false)
13-
dead_end_original_load(file)
14-
rescue SyntaxError => e
15-
DeadEnd.handle_error(e)
16-
end
3+
# Ruby 3.2+ has a cleaner way to hook into Ruby that doesn't use `require`
4+
if SyntaxError.new.respond_to?(:detailed_message)
5+
module DeadEnd
6+
class MiniStringIO
7+
def initialize(isatty: $stderr.isatty)
8+
@string = +""
9+
@isatty = isatty
10+
end
11+
12+
attr_reader :isatty
1713

18-
def require(file)
19-
dead_end_original_require(file)
20-
rescue SyntaxError => e
21-
DeadEnd.handle_error(e)
14+
def puts(value = $/, **)
15+
@string << value
16+
end
17+
18+
attr_reader :string
19+
end
2220
end
2321

24-
def require_relative(file)
25-
if Pathname.new(file).absolute?
26-
dead_end_original_require file
27-
else
28-
relative_from = caller_locations(1..1).first
29-
relative_from_path = relative_from.absolute_path || relative_from.path
30-
dead_end_original_require File.expand_path("../#{file}", relative_from_path)
22+
SyntaxError.prepend Module.new {
23+
def detailed_message(highlight: nil, **)
24+
message = super
25+
file = DeadEnd::PathnameFromMessage.new(message).call.name
26+
io = DeadEnd::MiniStringIO.new
27+
28+
if file
29+
DeadEnd.call(
30+
io: io,
31+
source: file.read,
32+
filename: file
33+
)
34+
annotation = io.string
35+
36+
annotation + message
37+
else
38+
message
39+
end
40+
rescue => e
41+
if ENV["DEBUG"]
42+
$stderr.warn(e.message)
43+
$stderr.warn(e.backtrace)
44+
end
45+
46+
raise e
47+
end
48+
}
49+
else
50+
# Monkey patch kernel to ensure that all `require` calls call the same
51+
# method
52+
module Kernel
53+
module_function
54+
55+
alias_method :dead_end_original_require, :require
56+
alias_method :dead_end_original_require_relative, :require_relative
57+
alias_method :dead_end_original_load, :load
58+
59+
def load(file, wrap = false)
60+
dead_end_original_load(file)
61+
rescue SyntaxError => e
62+
DeadEnd.handle_error(e)
63+
end
64+
65+
def require(file)
66+
dead_end_original_require(file)
67+
rescue SyntaxError => e
68+
DeadEnd.handle_error(e)
69+
end
70+
71+
def require_relative(file)
72+
if Pathname.new(file).absolute?
73+
dead_end_original_require file
74+
else
75+
relative_from = caller_locations(1..1).first
76+
relative_from_path = relative_from.absolute_path || relative_from.path
77+
dead_end_original_require File.expand_path("../#{file}", relative_from_path)
78+
end
79+
rescue SyntaxError => e
80+
DeadEnd.handle_error(e)
3181
end
32-
rescue SyntaxError => e
33-
DeadEnd.handle_error(e)
3482
end
3583
end

spec/integration/ruby_command_line_spec.rb

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@ module DeadEnd
3636
end
3737

3838
methods = (dead_end_methods_array - kernel_methods_array).sort
39-
expect(methods).to eq(["dead_end_original_load", "dead_end_original_require", "dead_end_original_require_relative"])
39+
if methods.any?
40+
expect(methods).to eq(["dead_end_original_load", "dead_end_original_require", "dead_end_original_require_relative"])
41+
end
4042

4143
methods = (api_only_methods_array - kernel_methods_array).sort
4244
expect(methods).to eq([])
@@ -71,5 +73,33 @@ module DeadEnd
7173
expect(out).to include('❯ 5 it "flerg"').once
7274
end
7375
end
76+
77+
it "annotates a syntax error in Ruby 3.2+ when require is not used" do
78+
pending("Support for SyntaxError#detailed_message monkeypatch needed https://gist.github.com/schneems/09f45cc23b9a8c46e9af6acbb6e6840d?permalink_comment_id=4172585#gistcomment-4172585")
79+
80+
skip if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("3.2")
81+
82+
Dir.mktmpdir do |dir|
83+
tmpdir = Pathname(dir)
84+
script = tmpdir.join("script.rb")
85+
script.write <<~EOM
86+
describe "things" do
87+
it "blerg" do
88+
end
89+
90+
it "flerg"
91+
end
92+
93+
it "zlerg" do
94+
end
95+
end
96+
EOM
97+
98+
out = `ruby -I#{lib_dir} -rdead_end #{script} 2>&1`
99+
100+
expect($?.success?).to be_falsey
101+
expect(out).to include('❯ 5 it "flerg"').once
102+
end
103+
end
74104
end
75105
end

0 commit comments

Comments
 (0)