Module: DeadEnd

Defined in:
lib/dead_end/api.rb,
lib/dead_end/cli.rb,
lib/dead_end/lex_all.rb,
lib/dead_end/code_line.rb,
lib/dead_end/lex_value.rb,
lib/dead_end/code_block.rb,
lib/dead_end/code_search.rb,
lib/dead_end/block_expand.rb,
lib/dead_end/code_frontier.rb,
lib/dead_end/ripper_errors.rb,
lib/dead_end/clean_document.rb,
lib/dead_end/explain_syntax.rb,
lib/dead_end/priority_queue.rb,
lib/dead_end/unvisited_lines.rb,
lib/dead_end/around_block_scan.rb,
lib/dead_end/capture_code_context.rb,
lib/dead_end/left_right_lex_count.rb,
lib/dead_end/pathname_from_message.rb,
lib/dead_end/priority_engulf_queue.rb,
lib/dead_end/display_invalid_blocks.rb,
lib/dead_end/parse_blocks_from_indent_line.rb,
lib/dead_end/display_code_with_line_numbers.rb

Defined Under Namespace

Classes: AroundBlockScan, BlockExpand, CaptureCodeContext, CleanDocument, Cli, CodeBlock, CodeFrontier, CodeLine, CodeSearch, DisplayCodeWithLineNumbers, DisplayInvalidBlocks, Error, ExplainSyntax, LeftRightLexCount, LexAll, LexValue, ParseBlocksFromIndentLine, PathnameFromMessage, PriorityEngulfQueue, PriorityQueue, RipperErrors, UnvisitedLines

Constant Summary collapse

VERSION =
UnloadedDeadEnd::VERSION
DEFAULT_VALUE =

Used to indicate a default value that cannot be confused with another input.

Object.new.freeze
TIMEOUT_DEFAULT =
ENV.fetch("DEAD_END_TIMEOUT", 1).to_i

Class Method Summary collapse

Class Method Details

.call(source:, filename: DEFAULT_VALUE, terminal: DEFAULT_VALUE, record_dir: DEFAULT_VALUE, timeout: TIMEOUT_DEFAULT, io: $stderr) ⇒ Object

DeadEnd.call [Private]

Main private interface



66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/dead_end/api.rb', line 66

def self.call(source:, filename: DEFAULT_VALUE, terminal: DEFAULT_VALUE, record_dir: DEFAULT_VALUE, 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

.handle_error(e, re_raise: true, io: $stderr) ⇒ Object

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).



43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/dead_end/api.rb', line 43

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

.invalid?(source) ⇒ Boolean

DeadEnd.invalid? [Private]

Opposite of DeadEnd.valid?

Returns:

  • (Boolean)


133
134
135
136
137
138
# File 'lib/dead_end/api.rb', line 133

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

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

.record_dir(dir) ⇒ Object

DeadEnd.record_dir [Private]

Used to generate a unique directory to record search steps for debugging



91
92
93
94
95
96
97
98
99
# File 'lib/dead_end/api.rb', line 91

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

.valid?(source) ⇒ Boolean

DeadEnd.valid? [Private]

Returns truthy if a given input source is valid syntax

DeadEnd.valid?("def foo\nend\n") # => true

DeadEnd.valid?("def foo\n  def bar # Syntax error here\nend\n") # => false

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.

Returns:

  • (Boolean)


176
177
178
# File 'lib/dead_end/api.rb', line 176

def self.valid?(source)
  !invalid?(source)
end

.valid_without?(without_lines:, code_lines:) ⇒ Boolean

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

Returns:

  • (Boolean)


120
121
122
123
124
125
126
127
128
# File 'lib/dead_end/api.rb', line 120

def self.valid_without?(without_lines:, code_lines:)
  lines = code_lines - Array(without_lines).flatten

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