11# frozen_string_literal: true
22
33module DeadEnd
4- # Given a block, this method will capture surrounding
5- # code to give the user more context for the location of
6- # the problem.
4+ # Turns a "invalid block(s)" into useful context
75 #
8- # Return is an array of CodeLines to be rendered.
6+ # There are three main phases in the algorithm:
97 #
10- # Surrounding code is captured regardless of visible state
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 third part.
13+ #
14+ # The algorithm is very good at capturing all of a syntax
15+ # error in a single block in number 2, however the results
16+ # can contain ambiguities. Humans are good at pattern matching
17+ # and filtering and can mentally remove extraneous data, but
18+ # they can't add extra data that's not present.
19+ #
20+ # In the case of known ambiguious cases, this class adds context
21+ # back to the ambiguitiy so the programmer has full information.
22+ #
23+ # Beyond handling these ambiguities, it also captures surrounding
24+ # code context information:
1125 #
1226 # puts block.to_s # => "def bark"
1327 #
@@ -16,7 +30,8 @@ module DeadEnd
1630 # code_lines: code_lines
1731 # )
1832 #
19- # puts context.call.join
33+ # lines = context.call.map(&:original)
34+ # puts lines.join
2035 # # =>
2136 # class Dog
2237 # def bark
@@ -34,19 +49,34 @@ def initialize(blocks:, code_lines:)
3449
3550 def call
3651 @blocks . each do |block |
52+ capture_first_kw_end_same_indent ( block )
3753 capture_last_end_same_indent ( block )
3854 capture_before_after_kws ( block )
3955 capture_falling_indent ( block )
4056 end
4157
4258 @lines_to_output . select! ( &:not_empty? )
43- @lines_to_output . select! ( &:not_comment? )
4459 @lines_to_output . uniq!
4560 @lines_to_output . sort!
4661
4762 @lines_to_output
4863 end
4964
65+ # Shows the context around code provided by "falling" indentation
66+ #
67+ # Converts:
68+ #
69+ # it "foo" do
70+ #
71+ # into:
72+ #
73+ # class OH
74+ # def hello
75+ # it "foo" do
76+ # end
77+ # end
78+ #
79+ #
5080 def capture_falling_indent ( block )
5181 AroundBlockScan . new (
5282 block : block ,
@@ -56,7 +86,36 @@ def capture_falling_indent(block)
5686 end
5787 end
5888
89+ # Shows surrounding kw/end pairs
90+ #
91+ # The purpose of showing these extra pairs is due to cases
92+ # of ambiguity when only one visible line is matched.
93+ #
94+ # For example:
95+ #
96+ # 1 class Dog
97+ # 2 def bark
98+ # 4 def eat
99+ # 5 end
100+ # 6 end
101+ #
102+ # In this case either line 2 could be missing an `end` or
103+ # line 4 was an extra line added by mistake (it happens).
104+ #
105+ # When we detect the above problem it shows the issue
106+ # as only being on line 2
107+ #
108+ # 2 def bark
109+ #
110+ # Showing "neighbor" keyword pairs gives extra context:
111+ #
112+ # 2 def bark
113+ # 4 def eat
114+ # 5 end
115+ #
59116 def capture_before_after_kws ( block )
117+ return unless block . visible_lines . count == 1
118+
60119 around_lines = AroundBlockScan . new ( code_lines : @code_lines , block : block )
61120 . start_at_next_line
62121 . capture_neighbor_context
@@ -66,9 +125,10 @@ def capture_before_after_kws(block)
66125 @lines_to_output . concat ( around_lines )
67126 end
68127
69- # When there is an invalid with a keyword
70- # right before an end, it's unclear where
71- # the correct code should be.
128+ # When there is an invalid block with a keyword
129+ # missing an end right before another end,
130+ # it is unclear where which keyword is missing the
131+ # end
72132 #
73133 # Take this example:
74134 #
@@ -87,20 +147,21 @@ def capture_before_after_kws(block)
87147 # line 4. Also work backwards and if there's a mis-matched keyword, show it
88148 # too
89149 def capture_last_end_same_indent ( block )
90- start_index = block . visible_lines . first . index
91- lines = @code_lines [ start_index ..block . lines . last . index ]
150+ return if block . visible_lines . length != 1
151+ return unless block . visible_lines . first . is_kw?
152+
153+ visible_line = block . visible_lines . first
154+ lines = @code_lines [ visible_line . index ..block . lines . last . index ]
92155
93156 # Find first end with same indent
94157 # (this would return line 4)
95158 #
96159 # end # 4
97- matching_end = lines . find { |line | line . indent == block . current_indent && line . is_end? }
160+ matching_end = lines . detect { |line | line . indent == block . current_indent && line . is_end? }
98161 return unless matching_end
99162
100163 @lines_to_output << matching_end
101164
102- lines = @code_lines [ start_index ..matching_end . index ]
103-
104165 # Work backwards from the end to
105166 # see if there are mis-matched
106167 # keyword/end pairs
@@ -113,7 +174,7 @@ def capture_last_end_same_indent(block)
113174 # end # 4
114175 end_count = 0
115176 kw_count = 0
116- kw_line = lines . reverse . detect do |line |
177+ kw_line = @code_lines [ visible_line . index .. matching_end . index ] . reverse . detect do |line |
117178 end_count += 1 if line . is_end?
118179 kw_count += 1 if line . is_kw?
119180
@@ -122,5 +183,51 @@ def capture_last_end_same_indent(block)
122183 return unless kw_line
123184 @lines_to_output << kw_line
124185 end
186+
187+ # The logical inverse of `capture_last_end_same_indent`
188+ #
189+ # When there is an invalid block with an `end`
190+ # missing a keyword right after another `end`,
191+ # it is unclear where which end is missing the
192+ # keyword.
193+ #
194+ # Take this example:
195+ #
196+ # class Dog # 1
197+ # puts "woof" # 2
198+ # end # 3
199+ # end # 4
200+ #
201+ # the problem line will be identified as:
202+ #
203+ # ❯ end # 4
204+ #
205+ # This happens because lines 1, 2, and 3 are technically valid code and are expanded
206+ # first, deemed valid, and hidden. We need to un-hide the matching keyword on
207+ # line 1. Also work backwards and if there's a mis-matched end, show it
208+ # too
209+ def capture_first_kw_end_same_indent ( block )
210+ return if block . visible_lines . length != 1
211+ return unless block . visible_lines . first . is_end?
212+
213+ visible_line = block . visible_lines . first
214+ lines = @code_lines [ block . lines . first . index ..visible_line . index ]
215+ matching_kw = lines . reverse . detect { |line | line . indent == block . current_indent && line . is_kw? }
216+ return unless matching_kw
217+
218+ @lines_to_output << matching_kw
219+
220+ kw_count = 0
221+ end_count = 0
222+ orphan_end = @code_lines [ matching_kw . index ..visible_line . index ] . detect do |line |
223+ kw_count += 1 if line . is_kw?
224+ end_count += 1 if line . is_end?
225+
226+ end_count >= kw_count
227+ end
228+
229+ return unless orphan_end
230+ @lines_to_output << orphan_end
231+ end
125232 end
126233end
0 commit comments