Class: DeadEnd::CodeSearch

Inherits:
Object
  • Object
show all
Defined in:
lib/dead_end/code_search.rb

Overview

Searches code for a syntax error

There are three main phases in the algorithm:

  1. Sanitize/format input source

  2. Search for invalid blocks

  3. Format invalid blocks into something meaninful

This class handles the part.

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"]

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(source, record_dir: DEFAULT_VALUE) ⇒ CodeSearch

Returns a new instance of CodeSearch.



44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# File 'lib/dead_end/code_search.rb', line 44

def initialize(source, record_dir: DEFAULT_VALUE)
  record_dir = if record_dir == DEFAULT_VALUE
    ENV["DEAD_END_RECORD_DIR"] || ENV["DEBUG"] ? "tmp" : nil
  else
    record_dir
  end

  if record_dir
    @record_dir = DeadEnd.record_dir(record_dir)
    @write_count = 0
  end

  @tick = 0
  @source = source
  @name_tick = Hash.new { |hash, k| hash[k] = 0 }
  @invalid_blocks = []

  @code_lines = CleanDocument.new(source: source).call.lines

  @frontier = CodeFrontier.new(code_lines: @code_lines)
  @block_expand = BlockExpand.new(code_lines: @code_lines)
  @parse_blocks_from_indent_line = ParseBlocksFromIndentLine.new(code_lines: @code_lines)
end

Instance Attribute Details

#code_linesObject (readonly)

Returns the value of attribute code_lines.



42
43
44
# File 'lib/dead_end/code_search.rb', line 42

def code_lines
  @code_lines
end

#invalid_blocksObject (readonly)

Returns the value of attribute invalid_blocks.



42
43
44
# File 'lib/dead_end/code_search.rb', line 42

def invalid_blocks
  @invalid_blocks
end

#record_dirObject (readonly)

Returns the value of attribute record_dir.



42
43
44
# File 'lib/dead_end/code_search.rb', line 42

def record_dir
  @record_dir
end

Instance Method Details

#callObject

Main search loop



123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
# File 'lib/dead_end/code_search.rb', line 123

def call
  until frontier.holds_all_syntax_errors?
    @tick += 1

    if frontier.expand?
      expand_existing
    else
      create_blocks_from_untracked_lines
    end
  end

  @invalid_blocks.concat(frontier.detect_invalid_blocks)
  @invalid_blocks.sort_by! { |block| block.starts_at }
  self
end

#create_blocks_from_untracked_linesObject

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



100
101
102
103
104
105
106
107
108
# File 'lib/dead_end/code_search.rb', line 100

def create_blocks_from_untracked_lines
  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|
      push(block, name: "add")
    end
  end
end

#expand_existingObject

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



112
113
114
115
116
117
118
119
120
# File 'lib/dead_end/code_search.rb', line 112

def expand_existing
  block = frontier.pop
  return unless block

  record(block: block, name: "before-expand")

  block = @block_expand.call(block)
  push(block, name: "expand")
end

#push(block, name:) ⇒ Object



91
92
93
94
95
96
# File 'lib/dead_end/code_search.rb', line 91

def push(block, name:)
  record(block: block, name: name)

  block.mark_invisible if block.valid?
  frontier << block
end

#record(block:, name: "record") ⇒ Object

Used for debugging



69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/dead_end/code_search.rb', line 69

def record(block:, name: "record")
  return unless @record_dir
  @name_tick[name] += 1
  filename = "#{@write_count += 1}-#{name}-#{@name_tick[name]}-(#{block.starts_at}__#{block.ends_at}).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|
    document = DisplayCodeWithLineNumbers.new(
      lines: @code_lines.select(&:visible?),
      terminal: false,
      highlight_lines: block.lines
    ).call

    f.write("    Block lines: #{block.starts_at..block.ends_at} (#{name}) \n\n#{document}")
  end
end