1 # frozen_string_literal: true
4 # Searches code for a syntax error
6 # There are three main phases in the algorithm:
8 # 1. Sanitize/format input source
9 # 2. Search for invalid blocks
10 # 3. Format invalid blocks into something meaninful
12 # This class handles the part.
14 # The bulk of the heavy lifting is done in:
16 # - CodeFrontier (Holds information for generating blocks and determining if we can stop searching)
17 # - ParseBlocksFromLine (Creates blocks into the frontier)
18 # - BlockExpand (Expands existing blocks to search more code)
20 # ## Syntax error detection
22 # When the frontier holds the syntax error, we can stop searching
24 # search = CodeSearch.new(<<~EOM)
32 # search.invalid_blocks.map(&:to_s) # =>
42 attr_reader :invalid_blocks, :record_dir, :code_lines
44 def initialize(source, record_dir: DEFAULT_VALUE)
45 record_dir = if record_dir == DEFAULT_VALUE
46 (ENV["SYNTAX_SUGGEST_RECORD_DIR"] || ENV["SYNTAX_SUGGEST_DEBUG"]) ? "tmp" : nil
52 @record_dir = SyntaxSuggest.record_dir(record_dir)
58 @name_tick = Hash.new { |hash, k| hash[k] = 0 }
61 @code_lines = CleanDocument.new(source: source).call.lines
63 @frontier = CodeFrontier.new(code_lines: @code_lines)
64 @block_expand = BlockExpand.new(code_lines: @code_lines)
65 @parse_blocks_from_indent_line = ParseBlocksFromIndentLine.new(code_lines: @code_lines)
69 def record(block:, name: "record")
70 return unless @record_dir
72 filename = "#{@write_count += 1}-#{name}-#{@name_tick[name]}-(#{block.starts_at}__#{block.ends_at}).txt"
73 if ENV["SYNTAX_SUGGEST_DEBUG"]
74 puts "\n\n==== #{filename} ===="
75 puts "\n```#{block.starts_at}..#{block.ends_at}"
78 puts " block indent: #{block.current_indent}"
80 @record_dir.join(filename).open(mode: "a") do |f|
81 document = DisplayCodeWithLineNumbers.new(
82 lines: @code_lines.select(&:visible?),
84 highlight_lines: block.lines
87 f.write(" Block lines: #{block.starts_at..block.ends_at} (#{name}) \n\n#{document}")
91 def push(block, name:)
92 record(block: block, name: name)
94 block.mark_invisible if block.valid?
98 # Parses the most indented lines into blocks that are marked
99 # and added to the frontier
100 def create_blocks_from_untracked_lines
101 max_indent = frontier.next_indent_line&.indent
103 while (line = frontier.next_indent_line) && (line.indent == max_indent)
104 @parse_blocks_from_indent_line.each_neighbor_block(frontier.next_indent_line) do |block|
105 push(block, name: "add")
110 # Given an already existing block in the frontier, expand it to see
111 # if it contains our invalid syntax
116 record(block: block, name: "before-expand")
118 block = @block_expand.call(block)
119 push(block, name: "expand")
124 until frontier.holds_all_syntax_errors?
130 create_blocks_from_untracked_lines
134 @invalid_blocks.concat(frontier.detect_invalid_blocks)
135 @invalid_blocks.sort_by! { |block| block.starts_at }