class DeadEnd::CodeSearch

Searches code for a syntax error

The bulk of the heavy lifting is done in:

- CodeFrontier (Holds information for generating blocks and determining if we can stop searching)
- ParseBlocksFromLine (Creates blocks into the frontier)
- BlockExpand (Expands existing blocks to search more code

## Syntax error detection

When the frontier holds the syntax error, we can stop searching

search = CodeSearch.new(<<~EOM)
  def dog
    def lol
  end
EOM

search.call

search.invalid_blocks.map(&:to_s) # =>
# => ["def lol\n"]

Attributes

code_lines[R]
frontier[R]
invalid_blocks[R]
record_dir[R]

Public Class Methods

new(source, record_dir: ENV["DEAD_END_RECORD_DIR"] || ENV["DEBUG"] ? "tmp" : nil) click to toggle source
# File lib/dead_end/code_search.rb, line 31
def initialize(source, record_dir: ENV["DEAD_END_RECORD_DIR"] || ENV["DEBUG"] ? "tmp" : nil)
  @source = source
  if record_dir
    @time = Time.now.strftime('%Y-%m-%d-%H-%M-%s-%N')
    @record_dir = Pathname(record_dir).join(@time).tap {|p| p.mkpath }
    @write_count = 0
  end
  code_lines = source.lines.map.with_index do |line, i|
    CodeLine.new(line: line, index: i)
  end

  @code_lines = TrailingSlashJoin.new(code_lines: code_lines).call

  @frontier = CodeFrontier.new(code_lines: @code_lines)
  @invalid_blocks = []
  @name_tick = Hash.new {|hash, k| hash[k] = 0 }
  @tick = 0
  @block_expand = BlockExpand.new(code_lines: code_lines)
  @parse_blocks_from_indent_line = ParseBlocksFromIndentLine.new(code_lines: @code_lines)
end

Public Instance Methods

call() click to toggle source

Main search loop

# File lib/dead_end/code_search.rb, line 139
def call
  sweep_heredocs
  sweep_comments
  until frontier.holds_all_syntax_errors?
    @tick += 1

    if frontier.expand?
      expand_invalid_block
    else
      visit_new_blocks
    end
  end

  @invalid_blocks.concat(frontier.detect_invalid_blocks )
  @invalid_blocks.sort_by! {|block| block.starts_at }
  self
end
expand_invalid_block() click to toggle source

Given an already existing block in the frontier, expand it to see if it contains our invalid syntax

# File lib/dead_end/code_search.rb, line 111
def expand_invalid_block
  block = frontier.pop
  return unless block

  record(block: block, name: "pop")

  # block = block.expand_until_next_boundry
  block = @block_expand.call(block)
  push(block, name: "expand")
end
push(block, name: ) click to toggle source
# File lib/dead_end/code_search.rb, line 74
def push(block, name: )
  record(block: block, name: name)

  if block.valid?
    block.mark_invisible
    frontier << block
  else
    frontier << block
  end
end
record(block:, name: "record") click to toggle source

Used for debugging

# File lib/dead_end/code_search.rb, line 53
def record(block:, name: "record")
  return if !@record_dir
  @name_tick[name] += 1
  filename = "#{@write_count += 1}-#{name}-#{@name_tick[name]}.txt"
  if ENV["DEBUG"]
    puts "\n\n==== #{filename} ===="
    puts "\n```#{block.starts_at}:#{block.ends_at}"
    puts "#{block.to_s}"
    puts "```"
    puts "  block indent:     #{block.current_indent}"
  end
  @record_dir.join(filename).open(mode: "a") do |f|
    display = DisplayInvalidBlocks.new(
      blocks: block,
      terminal: false,
      code_lines: @code_lines,
    )
    f.write(display.indent display.code_with_lines)
  end
end
sweep(block:, name: ) click to toggle source

Removes the block without putting it back in the frontier

# File lib/dead_end/code_search.rb, line 86
def sweep(block:, name: )
  record(block: block, name: name)

  block.lines.each(&:mark_invisible)
  frontier.register_indent_block(block)
end
sweep_comments() click to toggle source
# File lib/dead_end/code_search.rb, line 131
def sweep_comments
  lines = @code_lines.select(&:is_comment?)
  return if lines.empty?
  block = CodeBlock.new(lines: lines)
  sweep(block: block, name: "comments")
end
sweep_heredocs() click to toggle source
# File lib/dead_end/code_search.rb, line 122
def sweep_heredocs
  HeredocBlockParse.new(
    source: @source,
    code_lines: @code_lines
  ).call.each do |block|
    push(block, name: "heredoc")
  end
end
visit_new_blocks() click to toggle source

Parses the most indented lines into blocks that are marked and added to the frontier

# File lib/dead_end/code_search.rb, line 95
def visit_new_blocks
  max_indent = frontier.next_indent_line&.indent

  while (line = frontier.next_indent_line) && (line.indent == max_indent)

    @parse_blocks_from_indent_line.each_neighbor_block(frontier.next_indent_line) do |block|
      record(block: block, name: "add")

      block.mark_invisible if block.valid?
      push(block, name: "add")
    end
  end
end