Fix spelling
[ruby.git] / lib / syntax_suggest / code_search.rb
blob7628dcd1312f577eec9095cc647cc243c473e5d8
1 # frozen_string_literal: true
3 module SyntaxSuggest
4   # Searches code for a syntax error
5   #
6   # There are three main phases in the algorithm:
7   #
8   # 1. Sanitize/format input source
9   # 2. Search for invalid blocks
10   # 3. Format invalid blocks into something meaninful
11   #
12   # This class handles the part.
13   #
14   # The bulk of the heavy lifting is done in:
15   #
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)
19   #
20   # ## Syntax error detection
21   #
22   # When the frontier holds the syntax error, we can stop searching
23   #
24   #   search = CodeSearch.new(<<~EOM)
25   #     def dog
26   #       def lol
27   #     end
28   #   EOM
29   #
30   #   search.call
31   #
32   #   search.invalid_blocks.map(&:to_s) # =>
33   #   # => ["def lol\n"]
34   #
35   class CodeSearch
36     private
38     attr_reader :frontier
40     public
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
47       else
48         record_dir
49       end
51       if record_dir
52         @record_dir = SyntaxSuggest.record_dir(record_dir)
53         @write_count = 0
54       end
56       @tick = 0
57       @source = source
58       @name_tick = Hash.new { |hash, k| hash[k] = 0 }
59       @invalid_blocks = []
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)
66     end
68     # Used for debugging
69     def record(block:, name: "record")
70       return unless @record_dir
71       @name_tick[name] += 1
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}"
76         puts block
77         puts "```"
78         puts "  block indent:      #{block.current_indent}"
79       end
80       @record_dir.join(filename).open(mode: "a") do |f|
81         document = DisplayCodeWithLineNumbers.new(
82           lines: @code_lines.select(&:visible?),
83           terminal: false,
84           highlight_lines: block.lines
85         ).call
87         f.write("    Block lines: #{block.starts_at..block.ends_at} (#{name}) \n\n#{document}")
88       end
89     end
91     def push(block, name:)
92       record(block: block, name: name)
94       block.mark_invisible if block.valid?
95       frontier << block
96     end
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")
106         end
107       end
108     end
110     # Given an already existing block in the frontier, expand it to see
111     # if it contains our invalid syntax
112     def expand_existing
113       block = frontier.pop
114       return unless block
116       record(block: block, name: "before-expand")
118       block = @block_expand.call(block)
119       push(block, name: "expand")
120     end
122     # Main search loop
123     def call
124       until frontier.holds_all_syntax_errors?
125         @tick += 1
127         if frontier.expand?
128           expand_existing
129         else
130           create_blocks_from_untracked_lines
131         end
132       end
134       @invalid_blocks.concat(frontier.detect_invalid_blocks)
135       @invalid_blocks.sort_by! { |block| block.starts_at }
136       self
137     end
138   end