Skip to content

Commit ecc431d

Browse files
author
schneems
committed
[close #64] WIP
1 parent 1d6b368 commit ecc431d

28 files changed

+1083
-450
lines changed

lib/dead_end/around_block_scan.rb

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ module DeadEnd
99
#
1010
# Example:
1111
#
12-
# def dog
13-
# puts "bark"
14-
# puts "bark"
15-
# end
12+
# def dog # 1
13+
# puts "bark" # 2
14+
# puts "bark" # 3
15+
# end # 4
1616
#
1717
# scan = AroundBlockScan.new(
1818
# code_lines: code_lines
@@ -22,7 +22,7 @@ module DeadEnd
2222
# scan.scan_while { true }
2323
#
2424
# puts scan.before_index # => 0
25-
# puts scan.after_index # => 3
25+
# puts scan.after_index # => 3
2626
#
2727
# Contents can also be filtered using AroundBlockScan#skip
2828
#
@@ -109,8 +109,6 @@ def capture_neighbor_context
109109
kw_count = 0
110110
end_count = 0
111111
after_lines.each do |line|
112-
# puts "line: #{line.number} #{line.original_line}, indent: #{line.indent}, #{line.empty?} #{line.indent == @orig_indent}"
113-
114112
next if line.empty?
115113
break if line.indent < @orig_indent
116114
next if line.indent != @orig_indent
@@ -124,7 +122,6 @@ def capture_neighbor_context
124122

125123
lines << line
126124
end
127-
lines.select! { |line| !line.is_comment? }
128125

129126
lines
130127
end

lib/dead_end/capture_code_context.rb

Lines changed: 123 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,27 @@
11
# frozen_string_literal: true
22

33
module 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
126233
end

0 commit comments

Comments
 (0)