Optimized instruction for Hash#freeze
[ruby.git] / lib / syntax_suggest / capture_code_context.rb
blob1f232cfae3e4f54f02733d559d2b630b22b34d47
1 # frozen_string_literal: true
3 module SyntaxSuggest
4   module Capture
5   end
6 end
8 require_relative "capture/falling_indent_lines"
9 require_relative "capture/before_after_keyword_ends"
11 module SyntaxSuggest
12   # Turns a "invalid block(s)" into useful context
13   #
14   # There are three main phases in the algorithm:
15   #
16   # 1. Sanitize/format input source
17   # 2. Search for invalid blocks
18   # 3. Format invalid blocks into something meaninful
19   #
20   # This class handles the third part.
21   #
22   # The algorithm is very good at capturing all of a syntax
23   # error in a single block in number 2, however the results
24   # can contain ambiguities. Humans are good at pattern matching
25   # and filtering and can mentally remove extraneous data, but
26   # they can't add extra data that's not present.
27   #
28   # In the case of known ambiguious cases, this class adds context
29   # back to the ambiguity so the programmer has full information.
30   #
31   # Beyond handling these ambiguities, it also captures surrounding
32   # code context information:
33   #
34   #   puts block.to_s # => "def bark"
35   #
36   #   context = CaptureCodeContext.new(
37   #     blocks: block,
38   #     code_lines: code_lines
39   #   )
40   #
41   #   lines = context.call.map(&:original)
42   #   puts lines.join
43   #   # =>
44   #     class Dog
45   #       def bark
46   #     end
47   #
48   class CaptureCodeContext
49     attr_reader :code_lines
51     def initialize(blocks:, code_lines:)
52       @blocks = Array(blocks)
53       @code_lines = code_lines
54       @visible_lines = @blocks.map(&:visible_lines).flatten
55       @lines_to_output = @visible_lines.dup
56     end
58     def call
59       @blocks.each do |block|
60         capture_first_kw_end_same_indent(block)
61         capture_last_end_same_indent(block)
62         capture_before_after_kws(block)
63         capture_falling_indent(block)
64       end
66       sorted_lines
67     end
69     def sorted_lines
70       @lines_to_output.select!(&:not_empty?)
71       @lines_to_output.uniq!
72       @lines_to_output.sort!
74       @lines_to_output
75     end
77     # Shows the context around code provided by "falling" indentation
78     #
79     # Converts:
80     #
81     #       it "foo" do
82     #
83     # into:
84     #
85     #   class OH
86     #     def hello
87     #       it "foo" do
88     #     end
89     #   end
90     #
91     def capture_falling_indent(block)
92       Capture::FallingIndentLines.new(
93         block: block,
94         code_lines: @code_lines
95       ).call do |line|
96         @lines_to_output << line
97       end
98     end
100     # Shows surrounding kw/end pairs
101     #
102     # The purpose of showing these extra pairs is due to cases
103     # of ambiguity when only one visible line is matched.
104     #
105     # For example:
106     #
107     #     1  class Dog
108     #     2    def bark
109     #     4    def eat
110     #     5    end
111     #     6  end
112     #
113     # In this case either line 2 could be missing an `end` or
114     # line 4 was an extra line added by mistake (it happens).
115     #
116     # When we detect the above problem it shows the issue
117     # as only being on line 2
118     #
119     #     2    def bark
120     #
121     # Showing "neighbor" keyword pairs gives extra context:
122     #
123     #     2    def bark
124     #     4    def eat
125     #     5    end
126     #
127     def capture_before_after_kws(block)
128       return unless block.visible_lines.count == 1
130       around_lines = Capture::BeforeAfterKeywordEnds.new(
131         code_lines: @code_lines,
132         block: block
133       ).call
135       around_lines -= block.lines
137       @lines_to_output.concat(around_lines)
138     end
140     # When there is an invalid block with a keyword
141     # missing an end right before another end,
142     # it is unclear where which keyword is missing the
143     # end
144     #
145     # Take this example:
146     #
147     #   class Dog       # 1
148     #     def bark      # 2
149     #       puts "woof" # 3
150     #   end             # 4
151     #
152     # However due to https://github.com/ruby/syntax_suggest/issues/32
153     # the problem line will be identified as:
154     #
155     #  > class Dog       # 1
156     #
157     # Because lines 2, 3, and 4 are technically valid code and are expanded
158     # first, deemed valid, and hidden. We need to un-hide the matching end
159     # line 4. Also work backwards and if there's a mis-matched keyword, show it
160     # too
161     def capture_last_end_same_indent(block)
162       return if block.visible_lines.length != 1
163       return unless block.visible_lines.first.is_kw?
165       visible_line = block.visible_lines.first
166       lines = @code_lines[visible_line.index..block.lines.last.index]
168       # Find first end with same indent
169       # (this would return line 4)
170       #
171       #   end             # 4
172       matching_end = lines.detect { |line| line.indent == block.current_indent && line.is_end? }
173       return unless matching_end
175       @lines_to_output << matching_end
177       # Work backwards from the end to
178       # see if there are mis-matched
179       # keyword/end pairs
180       #
181       # Return the first mis-matched keyword
182       # this would find line 2
183       #
184       #     def bark      # 2
185       #       puts "woof" # 3
186       #   end             # 4
187       end_count = 0
188       kw_count = 0
189       kw_line = @code_lines[visible_line.index..matching_end.index].reverse.detect do |line|
190         end_count += 1 if line.is_end?
191         kw_count += 1 if line.is_kw?
193         !kw_count.zero? && kw_count >= end_count
194       end
195       return unless kw_line
196       @lines_to_output << kw_line
197     end
199     # The logical inverse of `capture_last_end_same_indent`
200     #
201     # When there is an invalid block with an `end`
202     # missing a keyword right after another `end`,
203     # it is unclear where which end is missing the
204     # keyword.
205     #
206     # Take this example:
207     #
208     #   class Dog       # 1
209     #       puts "woof" # 2
210     #     end           # 3
211     #   end             # 4
212     #
213     # the problem line will be identified as:
214     #
215     #  > end            # 4
216     #
217     # This happens because lines 1, 2, and 3 are technically valid code and are expanded
218     # first, deemed valid, and hidden. We need to un-hide the matching keyword on
219     # line 1. Also work backwards and if there's a mis-matched end, show it
220     # too
221     def capture_first_kw_end_same_indent(block)
222       return if block.visible_lines.length != 1
223       return unless block.visible_lines.first.is_end?
225       visible_line = block.visible_lines.first
226       lines = @code_lines[block.lines.first.index..visible_line.index]
227       matching_kw = lines.reverse.detect { |line| line.indent == block.current_indent && line.is_kw? }
228       return unless matching_kw
230       @lines_to_output << matching_kw
232       kw_count = 0
233       end_count = 0
234       orphan_end = @code_lines[matching_kw.index..visible_line.index].detect do |line|
235         kw_count += 1 if line.is_kw?
236         end_count += 1 if line.is_end?
238         end_count >= kw_count
239       end
241       return unless orphan_end
242       @lines_to_output << orphan_end
243     end
244   end