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?(<<~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.

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