1 # frozen_string_literal: true
8 require_relative "capture/falling_indent_lines"
9 require_relative "capture/before_after_keyword_ends"
12 # Turns a "invalid block(s)" into useful context
14 # There are three main phases in the algorithm:
16 # 1. Sanitize/format input source
17 # 2. Search for invalid blocks
18 # 3. Format invalid blocks into something meaninful
20 # This class handles the third part.
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.
28 # In the case of known ambiguious cases, this class adds context
29 # back to the ambiguity so the programmer has full information.
31 # Beyond handling these ambiguities, it also captures surrounding
32 # code context information:
34 # puts block.to_s # => "def bark"
36 # context = CaptureCodeContext.new(
38 # code_lines: code_lines
41 # lines = context.call.map(&:original)
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
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)
70 @lines_to_output.select!(&:not_empty?)
71 @lines_to_output.uniq!
72 @lines_to_output.sort!
77 # Shows the context around code provided by "falling" indentation
91 def capture_falling_indent(block)
92 Capture::FallingIndentLines.new(
94 code_lines: @code_lines
96 @lines_to_output << line
100 # Shows surrounding kw/end pairs
102 # The purpose of showing these extra pairs is due to cases
103 # of ambiguity when only one visible line is matched.
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).
116 # When we detect the above problem it shows the issue
117 # as only being on line 2
121 # Showing "neighbor" keyword pairs gives extra context:
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,
135 around_lines -= block.lines
137 @lines_to_output.concat(around_lines)
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
152 # However due to https://github.com/ruby/syntax_suggest/issues/32
153 # the problem line will be identified as:
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
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)
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
181 # Return the first mis-matched keyword
182 # this would find line 2
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
195 return unless kw_line
196 @lines_to_output << kw_line
199 # The logical inverse of `capture_last_end_same_indent`
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
213 # the problem line will be identified as:
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
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
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
241 return unless orphan_end
242 @lines_to_output << orphan_end