Skip to content

Commit

Permalink
Add interface to require dead_end without core_ext
Browse files Browse the repository at this point in the history
Currently `dead_end` works by monkey patching `require` and friends.

If someone wants to programmatically execute dead_end manually via `DeadEnd.handle_error` without monkey patches they can now do that by setting the environment variable `DISABLE_DEAD_END_CORE_EXT=1`.
  • Loading branch information
schneems committed Nov 18, 2021
1 parent 1095e98 commit 1f2541a
Show file tree
Hide file tree
Showing 7 changed files with 125 additions and 35 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
## HEAD (unreleased)

- Requiring `dead_end/auto` is now deprecated please require `dead_end` instead (https://github.com/zombocom/dead_end/pull/119)
- The interface `DeadEnd.handle_error` is declared public and stable (https://github.com/zombocom/dead_end/pull/119)
- Requiring the gem with the environment variable `DISABLE_DEAD_END_CORE_EXT=1` now disables monkeypatching core extensions (https://github.com/zombocom/dead_end/pull/119)

## 3.0.3

- Expand explanations coming from additional Ripper errors (https://github.com/zombocom/dead_end/pull/117)
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,16 @@ Here's an example:

![](assets/syntax_search.gif)

## Use internals

To use the `dead_end` gem without monkeypatching you can set the environment variable `DISABLE_DEAD_END_CORE_EXT=1` before requiring the gem. This will allow you to load `dead_end` and use its internals without mutating `require`.

Stable internal interface(s):

- `DeadEnd.handle_error(e)`

Any other entrypoints are subject to change without warning. If you want to use an internal interface from `dead_end` not on this list, open an issue to explain your use case.

## Development

After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
Expand Down
35 changes: 31 additions & 4 deletions lib/dead_end.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,41 @@ module DeadEnd
class Error < StandardError; end
TIMEOUT_DEFAULT = ENV.fetch("DEAD_END_TIMEOUT", 1).to_i

def self.handle_error(e)
# DeadEnd.handle_error [Public interface]
#
# Takes an exception from a syntax error, uses that
# error message to locate the file. Then the file
# will be analyzed to find the location of the syntax
# error and emit that location to stderr.
#
# Example:
#
# begin
# require 'bad_file'
# rescue => e
# DeadEnd.handle_error(e)
# end
#
# By default it will re_raise the exception unless
# `re_raise: false`. The message output location
# can be configured using the `io: $stderr` input.
#
# If a valid filename cannot be determined, the original
# exception will be re-raised (even with
# `re_raise: false`).
def self.handle_error(e, re_raise: true, io: $stderr)
file = PathnameFromMessage.new(e.message).call.name
raise e unless file

$stderr.sync = true
io.sync = true

call(
io: io,
source: file.read,
filename: file
)

raise e
raise e if re_raise
end

def self.record_dir(dir)
Expand Down Expand Up @@ -68,6 +91,8 @@ def self.indent(string)
end
end

# DeadEnd.valid_without? [Private interface]
#
# This will tell you if the `code_lines` would be valid
# if you removed the `without_lines`. In short it's a
# way to detect if we've found the lines with syntax errors
Expand Down Expand Up @@ -102,6 +127,8 @@ def self.invalid?(source)
Ripper.new(source).tap(&:parse).error?
end

# DeadEnd.valid? [Private interface]
#
# Returns truthy if a given input source is valid syntax
#
# DeadEnd.valid?(<<~EOM) # => true
Expand Down Expand Up @@ -143,7 +170,7 @@ def self.valid?(source)

# Integration
require_relative "dead_end/cli"
require_relative "dead_end/auto"
require_relative "dead_end/core_ext" unless ENV["DISABLE_DEAD_END_CORE_EXT"]

# Core logic
require_relative "dead_end/code_search"
Expand Down
33 changes: 2 additions & 31 deletions lib/dead_end/auto.rb
Original file line number Diff line number Diff line change
@@ -1,35 +1,6 @@
# frozen_string_literal: true

require_relative "../dead_end"
require_relative "core_ext"

# 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
dead_end_original_require File.expand_path("../#{file}", Kernel.caller_locations(1, 1)[0].absolute_path)
end
rescue SyntaxError => e
DeadEnd.handle_error(e)
end
end
warn "Calling `require 'dead_end/auto'` is deprecated, please `require 'dead_end'` instead."
33 changes: 33 additions & 0 deletions lib/dead_end/core_ext.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# 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

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
dead_end_original_require File.expand_path("../#{file}", Kernel.caller_locations(1, 1)[0].absolute_path)
end
rescue SyntaxError => e
DeadEnd.handle_error(e)
end
end
16 changes: 16 additions & 0 deletions spec/integration/dead_end_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,22 @@

module DeadEnd
RSpec.describe "Integration tests that don't spawn a process (like using the cli)" do
it "has a `handle_error` interface" do
fake_error = Object.new
def fake_error.message
"#{__FILE__}:216: unterminated string meets end of file "
end

io = StringIO.new
DeadEnd.handle_error(
fake_error,
re_raise: false,
io: io
)

expect(io.string.strip).to eq("Syntax OK")
end

it "returns good results on routes.rb" do
source = fixtures_dir.join("routes.rb.txt").read

Expand Down
29 changes: 29 additions & 0 deletions spec/integration/ruby_command_line_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -55,5 +55,34 @@ module DeadEnd
expect(out).to include('❯ 5 it "flerg"').once
end
end

it "doesn't monkeypatch when env var is set" do
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

require_rb = tmpdir.join("require.rb")
require_rb.write <<~EOM
load "#{script.expand_path}"
EOM

out = `DISABLE_DEAD_END_CORE_EXT=1 ruby -I#{lib_dir} -rdead_end #{require_rb} 2>&1`

expect($?.success?).to be_falsey
expect(out).to_not include('❯ 5 it "flerg"').once
end
end
end
end

0 comments on commit 1f2541a

Please sign in to comment.