From 4b9ac2f3584adccd775e64ac58b662159710a798 Mon Sep 17 00:00:00 2001 From: schneems Date: Wed, 18 May 2022 12:06:06 -0700 Subject: [PATCH] Annotate syntax error without require MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently dead_end works by monkey patching require. This causes confusion and problems as other tools are not expecting this. For example https://github.com/zombocom/derailed_benchmarks/issues/204 and https://github.com/zombocom/dead_end/issues/124. This PR utilizes the new SyntaxError#detailed_message as introduced in https://github.com/ruby/ruby/pull/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 ``` --- CHANGELOG.md | 2 + lib/dead_end/core_ext.rb | 102 +++++++++++++++------ spec/integration/ruby_command_line_spec.rb | 32 ++++++- 3 files changed, 108 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a27b3ca..35e59a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ ## HEAD (unreleased) +- Support Ruby 3.2 integration with SyntaxError (https://github.com/zombocom/dead_end/pull/139) + ## 3.1.2 - Fixed internal class AroundBlockScan, minor changes in outputs (https://github.com/zombocom/dead_end/pull/131) diff --git a/lib/dead_end/core_ext.rb b/lib/dead_end/core_ext.rb index 2886785..7a1fe6d 100644 --- a/lib/dead_end/core_ext.rb +++ b/lib/dead_end/core_ext.rb @@ -1,35 +1,83 @@ # frozen_string_literal: true -# Monkey patch kernel to ensure that all `require` calls call the same -# method -module Kernel - module_function - - alias_method :dead_end_original_require, :require - alias_method :dead_end_original_require_relative, :require_relative - alias_method :dead_end_original_load, :load - - def load(file, wrap = false) - dead_end_original_load(file) - rescue SyntaxError => e - DeadEnd.handle_error(e) - end +# Ruby 3.2+ has a cleaner way to hook into Ruby that doesn't use `require` +if SyntaxError.new.respond_to?(:detailed_message) + module DeadEnd + class MiniStringIO + def initialize(isatty: $stderr.isatty) + @string = +"" + @isatty = isatty + end + + attr_reader :isatty - def require(file) - dead_end_original_require(file) - rescue SyntaxError => e - DeadEnd.handle_error(e) + def puts(value = $/, **) + @string << value + end + + attr_reader :string + end end - def require_relative(file) - if Pathname.new(file).absolute? - dead_end_original_require file - else - relative_from = caller_locations(1..1).first - relative_from_path = relative_from.absolute_path || relative_from.path - dead_end_original_require File.expand_path("../#{file}", relative_from_path) + SyntaxError.prepend Module.new { + def detailed_message(highlight: nil, **) + message = super + file = DeadEnd::PathnameFromMessage.new(message).call.name + io = DeadEnd::MiniStringIO.new + + if file + DeadEnd.call( + io: io, + source: file.read, + filename: file + ) + annotation = io.string + + annotation + message + else + message + end + rescue => e + if ENV["DEBUG"] + $stderr.warn(e.message) + $stderr.warn(e.backtrace) + end + + raise e + end + } +else + # Monkey patch kernel to ensure that all `require` calls call the same + # method + module Kernel + module_function + + alias_method :dead_end_original_require, :require + alias_method :dead_end_original_require_relative, :require_relative + alias_method :dead_end_original_load, :load + + def load(file, wrap = false) + dead_end_original_load(file) + rescue SyntaxError => e + DeadEnd.handle_error(e) + end + + def require(file) + dead_end_original_require(file) + rescue SyntaxError => e + DeadEnd.handle_error(e) + end + + def require_relative(file) + if Pathname.new(file).absolute? + dead_end_original_require file + else + relative_from = caller_locations(1..1).first + relative_from_path = relative_from.absolute_path || relative_from.path + dead_end_original_require File.expand_path("../#{file}", relative_from_path) + end + rescue SyntaxError => e + DeadEnd.handle_error(e) end - rescue SyntaxError => e - DeadEnd.handle_error(e) end end diff --git a/spec/integration/ruby_command_line_spec.rb b/spec/integration/ruby_command_line_spec.rb index e124287..cbe6deb 100644 --- a/spec/integration/ruby_command_line_spec.rb +++ b/spec/integration/ruby_command_line_spec.rb @@ -36,7 +36,9 @@ module DeadEnd end methods = (dead_end_methods_array - kernel_methods_array).sort - expect(methods).to eq(["dead_end_original_load", "dead_end_original_require", "dead_end_original_require_relative"]) + if methods.any? + expect(methods).to eq(["dead_end_original_load", "dead_end_original_require", "dead_end_original_require_relative"]) + end methods = (api_only_methods_array - kernel_methods_array).sort expect(methods).to eq([]) @@ -71,5 +73,33 @@ module DeadEnd expect(out).to include('❯ 5 it "flerg"').once end end + + it "annotates a syntax error in Ruby 3.2+ when require is not used" do + pending("Support for SyntaxError#detailed_message monkeypatch needed https://gist.github.com/schneems/09f45cc23b9a8c46e9af6acbb6e6840d?permalink_comment_id=4172585#gistcomment-4172585") + + skip if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("3.2") + + Dir.mktmpdir do |dir| + tmpdir = Pathname(dir) + script = tmpdir.join("script.rb") + script.write <<~EOM + describe "things" do + it "blerg" do + end + + it "flerg" + end + + it "zlerg" do + end + end + EOM + + out = `ruby -I#{lib_dir} -rdead_end #{script} 2>&1` + + expect($?.success?).to be_falsey + expect(out).to include('❯ 5 it "flerg"').once + end + end end end