Skip to content
Merged
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
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)
- Requiring `dead_end/api` now loads code without monkeypatching core extensions (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)

## 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 `require 'dead_en/api'`. 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
164 changes: 2 additions & 162 deletions lib/dead_end.rb
Original file line number Diff line number Diff line change
@@ -1,164 +1,4 @@
# frozen_string_literal: true

require_relative "dead_end/version"

require "tmpdir"
require "stringio"
require "pathname"
require "ripper"
require "timeout"

module DeadEnd
# Used to indicate a default value that cannot
# be confused with another input
DEFAULT_VALUE = Object.new.freeze

class Error < StandardError; end
TIMEOUT_DEFAULT = ENV.fetch("DEAD_END_TIMEOUT", 1).to_i

def self.handle_error(e)
file = PathnameFromMessage.new(e.message).call.name
raise e unless file

$stderr.sync = true

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

raise e
end

def self.record_dir(dir)
time = Time.now.strftime("%Y-%m-%d-%H-%M-%s-%N")
dir = Pathname(dir)
symlink = dir.join("last").tap { |path| path.delete if path.exist? }
dir.join(time).tap { |path|
path.mkpath
FileUtils.symlink(path.basename, symlink)
}
end

def self.call(source:, filename: DEFAULT_VALUE, terminal: DEFAULT_VALUE, record_dir: nil, timeout: TIMEOUT_DEFAULT, io: $stderr)
search = nil
filename = nil if filename == DEFAULT_VALUE
Timeout.timeout(timeout) do
record_dir ||= ENV["DEBUG"] ? "tmp" : nil
search = CodeSearch.new(source, record_dir: record_dir).call
end

blocks = search.invalid_blocks
DisplayInvalidBlocks.new(
io: io,
blocks: blocks,
filename: filename,
terminal: terminal,
code_lines: search.code_lines
).call
rescue Timeout::Error => e
io.puts "Search timed out DEAD_END_TIMEOUT=#{timeout}, run with DEBUG=1 for more info"
io.puts e.backtrace.first(3).join($/)
end

# Used for counting spaces
module SpaceCount
def self.indent(string)
string.split(/\S/).first&.length || 0
end
end

# 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
# in our document yet.
#
# code_lines = [
# CodeLine.new(line: "def foo\n", index: 0)
# CodeLine.new(line: " def bar\n", index: 1)
# CodeLine.new(line: "end\n", index: 2)
# ]
#
# DeadEnd.valid_without?(
# without_lines: code_lines[1],
# code_lines: code_lines
# ) # => true
#
# DeadEnd.valid?(code_lines) # => false
def self.valid_without?(without_lines:, code_lines:)
lines = code_lines - Array(without_lines).flatten

if lines.empty?
true
else
valid?(lines)
end
end

def self.invalid?(source)
source = source.join if source.is_a?(Array)
source = source.to_s

Ripper.new(source).tap(&:parse).error?
end

# Returns truthy if a given input source is valid syntax
#
# DeadEnd.valid?(<<~EOM) # => true
# def foo
# end
# EOM
#
# DeadEnd.valid?(<<~EOM) # => false
# def foo
# def bar # Syntax error here
# end
# EOM
#
# You can also pass in an array of lines and they'll be
# joined before evaluating
#
# DeadEnd.valid?(
# [
# "def foo\n",
# "end\n"
# ]
# ) # => true
#
# DeadEnd.valid?(
# [
# "def foo\n",
# " def bar\n", # Syntax error here
# "end\n"
# ]
# ) # => false
#
# As an FYI the CodeLine class instances respond to `to_s`
# so passing a CodeLine in as an object or as an array
# will convert it to it's code representation.
def self.valid?(source)
!invalid?(source)
end
end

# Integration
require_relative "dead_end/cli"
require_relative "dead_end/auto"

# Core logic
require_relative "dead_end/code_search"
require_relative "dead_end/code_frontier"
require_relative "dead_end/explain_syntax"
require_relative "dead_end/clean_document"

# Helpers
require_relative "dead_end/lex_all"
require_relative "dead_end/code_line"
require_relative "dead_end/code_block"
require_relative "dead_end/block_expand"
require_relative "dead_end/ripper_errors"
require_relative "dead_end/insertion_sort"
require_relative "dead_end/around_block_scan"
require_relative "dead_end/pathname_from_message"
require_relative "dead_end/display_invalid_blocks"
require_relative "dead_end/parse_blocks_from_indent_line"
require_relative "dead_end/api"
require_relative "dead_end/core_ext"
196 changes: 196 additions & 0 deletions lib/dead_end/api.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
require_relative "version"

require "tmpdir"
require "stringio"
require "pathname"
require "ripper"
require "timeout"

module DeadEnd
# Used to indicate a default value that cannot
# be confused with another input.
DEFAULT_VALUE = Object.new.freeze

class Error < StandardError; end
TIMEOUT_DEFAULT = ENV.fetch("DEAD_END_TIMEOUT", 1).to_i

# DeadEnd.handle_error [Public]
#
# Takes a `SyntaxError`` exception, uses the
# 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)
unless e.is_a?(SyntaxError)
io.puts("DeadEnd: Must pass a SyntaxError, got: #{e.class}")
raise e
end

file = PathnameFromMessage.new(e.message, io: io).call.name
raise e unless file

io.sync = true

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

raise e if re_raise
end

# DeadEnd.call [Private]
#
# Main private interface
def self.call(source:, filename: DEFAULT_VALUE, terminal: DEFAULT_VALUE, record_dir: nil, timeout: TIMEOUT_DEFAULT, io: $stderr)
search = nil
filename = nil if filename == DEFAULT_VALUE
Timeout.timeout(timeout) do
record_dir ||= ENV["DEBUG"] ? "tmp" : nil
search = CodeSearch.new(source, record_dir: record_dir).call
end

blocks = search.invalid_blocks
DisplayInvalidBlocks.new(
io: io,
blocks: blocks,
filename: filename,
terminal: terminal,
code_lines: search.code_lines
).call
rescue Timeout::Error => e
io.puts "Search timed out DEAD_END_TIMEOUT=#{timeout}, run with DEBUG=1 for more info"
io.puts e.backtrace.first(3).join($/)
end

# DeadEnd.record_dir [Private]
#
# Used to generate a unique directory to record
# search steps for debugging
def self.record_dir(dir)
time = Time.now.strftime("%Y-%m-%d-%H-%M-%s-%N")
dir = Pathname(dir)
symlink = dir.join("last").tap { |path| path.delete if path.exist? }
dir.join(time).tap { |path|
path.mkpath
FileUtils.symlink(path.basename, symlink)
}
end

# DeadEnd.valid_without? [Private]
#
# 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
# in our document yet.
#
# code_lines = [
# CodeLine.new(line: "def foo\n", index: 0)
# CodeLine.new(line: " def bar\n", index: 1)
# CodeLine.new(line: "end\n", index: 2)
# ]
#
# DeadEnd.valid_without?(
# without_lines: code_lines[1],
# code_lines: code_lines
# ) # => true
#
# DeadEnd.valid?(code_lines) # => false
def self.valid_without?(without_lines:, code_lines:)
lines = code_lines - Array(without_lines).flatten

if lines.empty?
true
else
valid?(lines)
end
end

# DeadEnd.invalid? [Private]
#
# Opposite of `DeadEnd.valid?`
def self.invalid?(source)
source = source.join if source.is_a?(Array)
source = source.to_s

Ripper.new(source).tap(&:parse).error?
end

# DeadEnd.valid? [Private]
#
# Returns truthy if a given input source is valid syntax
#
# DeadEnd.valid?(<<~EOM) # => true
# def foo
# end
# EOM
#
# DeadEnd.valid?(<<~EOM) # => false
# def foo
# def bar # Syntax error here
# end
# EOM
#
# You can also pass in an array of lines and they'll be
# joined before evaluating
#
# DeadEnd.valid?(
# [
# "def foo\n",
# "end\n"
# ]
# ) # => true
#
# DeadEnd.valid?(
# [
# "def foo\n",
# " def bar\n", # Syntax error here
# "end\n"
# ]
# ) # => false
#
# As an FYI the CodeLine class instances respond to `to_s`
# so passing a CodeLine in as an object or as an array
# will convert it to it's code representation.
def self.valid?(source)
!invalid?(source)
end
end

# Integration
require_relative "cli"

# Core logic
require_relative "code_search"
require_relative "code_frontier"
require_relative "explain_syntax"
require_relative "clean_document"

# Helpers
require_relative "lex_all"
require_relative "code_line"
require_relative "code_block"
require_relative "block_expand"
require_relative "ripper_errors"
require_relative "insertion_sort"
require_relative "around_block_scan"
require_relative "pathname_from_message"
require_relative "display_invalid_blocks"
require_relative "parse_blocks_from_indent_line"
Loading