Skip to content

Commit

Permalink
[close #64] WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
schneems committed Oct 11, 2021
1 parent 1d6b368 commit ecc431d
Show file tree
Hide file tree
Showing 28 changed files with 1,083 additions and 450 deletions.
13 changes: 5 additions & 8 deletions lib/dead_end/around_block_scan.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ module DeadEnd
#
# Example:
#
# def dog
# puts "bark"
# puts "bark"
# end
# def dog # 1
# puts "bark" # 2
# puts "bark" # 3
# end # 4
#
# scan = AroundBlockScan.new(
# code_lines: code_lines
Expand All @@ -22,7 +22,7 @@ module DeadEnd
# scan.scan_while { true }
#
# puts scan.before_index # => 0
# puts scan.after_index # => 3
# puts scan.after_index # => 3
#
# Contents can also be filtered using AroundBlockScan#skip
#
Expand Down Expand Up @@ -109,8 +109,6 @@ def capture_neighbor_context
kw_count = 0
end_count = 0
after_lines.each do |line|
# puts "line: #{line.number} #{line.original_line}, indent: #{line.indent}, #{line.empty?} #{line.indent == @orig_indent}"

next if line.empty?
break if line.indent < @orig_indent
next if line.indent != @orig_indent
Expand All @@ -124,7 +122,6 @@ def capture_neighbor_context

lines << line
end
lines.select! { |line| !line.is_comment? }

lines
end
Expand Down
139 changes: 123 additions & 16 deletions lib/dead_end/capture_code_context.rb
Original file line number Diff line number Diff line change
@@ -1,13 +1,27 @@
# frozen_string_literal: true

module DeadEnd
# Given a block, this method will capture surrounding
# code to give the user more context for the location of
# the problem.
# Turns a "invalid block(s)" into useful context
#
# Return is an array of CodeLines to be rendered.
# There are three main phases in the algorithm:
#
# Surrounding code is captured regardless of visible state
# 1. Sanitize/format input source
# 2. Search for invalid blocks
# 3. Format invalid blocks into something meaninful
#
# This class handles the third part.
#
# The algorithm is very good at capturing all of a syntax
# error in a single block in number 2, however the results
# can contain ambiguities. Humans are good at pattern matching
# and filtering and can mentally remove extraneous data, but
# they can't add extra data that's not present.
#
# In the case of known ambiguious cases, this class adds context
# back to the ambiguitiy so the programmer has full information.
#
# Beyond handling these ambiguities, it also captures surrounding
# code context information:
#
# puts block.to_s # => "def bark"
#
Expand All @@ -16,7 +30,8 @@ module DeadEnd
# code_lines: code_lines
# )
#
# puts context.call.join
# lines = context.call.map(&:original)
# puts lines.join
# # =>
# class Dog
# def bark
Expand All @@ -34,19 +49,34 @@ def initialize(blocks:, code_lines:)

def call
@blocks.each do |block|
capture_first_kw_end_same_indent(block)
capture_last_end_same_indent(block)
capture_before_after_kws(block)
capture_falling_indent(block)
end

@lines_to_output.select!(&:not_empty?)
@lines_to_output.select!(&:not_comment?)
@lines_to_output.uniq!
@lines_to_output.sort!

@lines_to_output
end

# Shows the context around code provided by "falling" indentation
#
# Converts:
#
# it "foo" do
#
# into:
#
# class OH
# def hello
# it "foo" do
# end
# end
#
#
def capture_falling_indent(block)
AroundBlockScan.new(
block: block,
Expand All @@ -56,7 +86,36 @@ def capture_falling_indent(block)
end
end

# Shows surrounding kw/end pairs
#
# The purpose of showing these extra pairs is due to cases
# of ambiguity when only one visible line is matched.
#
# For example:
#
# 1 class Dog
# 2 def bark
# 4 def eat
# 5 end
# 6 end
#
# In this case either line 2 could be missing an `end` or
# line 4 was an extra line added by mistake (it happens).
#
# When we detect the above problem it shows the issue
# as only being on line 2
#
# 2 def bark
#
# Showing "neighbor" keyword pairs gives extra context:
#
# 2 def bark
# 4 def eat
# 5 end
#
def capture_before_after_kws(block)
return unless block.visible_lines.count == 1

around_lines = AroundBlockScan.new(code_lines: @code_lines, block: block)
.start_at_next_line
.capture_neighbor_context
Expand All @@ -66,9 +125,10 @@ def capture_before_after_kws(block)
@lines_to_output.concat(around_lines)
end

# When there is an invalid with a keyword
# right before an end, it's unclear where
# the correct code should be.
# When there is an invalid block with a keyword
# missing an end right before another end,
# it is unclear where which keyword is missing the
# end
#
# Take this example:
#
Expand All @@ -87,20 +147,21 @@ def capture_before_after_kws(block)
# line 4. Also work backwards and if there's a mis-matched keyword, show it
# too
def capture_last_end_same_indent(block)
start_index = block.visible_lines.first.index
lines = @code_lines[start_index..block.lines.last.index]
return if block.visible_lines.length != 1
return unless block.visible_lines.first.is_kw?

visible_line = block.visible_lines.first
lines = @code_lines[visible_line.index..block.lines.last.index]

# Find first end with same indent
# (this would return line 4)
#
# end # 4
matching_end = lines.find { |line| line.indent == block.current_indent && line.is_end? }
matching_end = lines.detect { |line| line.indent == block.current_indent && line.is_end? }
return unless matching_end

@lines_to_output << matching_end

lines = @code_lines[start_index..matching_end.index]

# Work backwards from the end to
# see if there are mis-matched
# keyword/end pairs
Expand All @@ -113,7 +174,7 @@ def capture_last_end_same_indent(block)
# end # 4
end_count = 0
kw_count = 0
kw_line = lines.reverse.detect do |line|
kw_line = @code_lines[visible_line.index..matching_end.index].reverse.detect do |line|
end_count += 1 if line.is_end?
kw_count += 1 if line.is_kw?

Expand All @@ -122,5 +183,51 @@ def capture_last_end_same_indent(block)
return unless kw_line
@lines_to_output << kw_line
end

# The logical inverse of `capture_last_end_same_indent`
#
# When there is an invalid block with an `end`
# missing a keyword right after another `end`,
# it is unclear where which end is missing the
# keyword.
#
# Take this example:
#
# class Dog # 1
# puts "woof" # 2
# end # 3
# end # 4
#
# the problem line will be identified as:
#
# ❯ end # 4
#
# This happens because lines 1, 2, and 3 are technically valid code and are expanded
# first, deemed valid, and hidden. We need to un-hide the matching keyword on
# line 1. Also work backwards and if there's a mis-matched end, show it
# too
def capture_first_kw_end_same_indent(block)
return if block.visible_lines.length != 1
return unless block.visible_lines.first.is_end?

visible_line = block.visible_lines.first
lines = @code_lines[block.lines.first.index..visible_line.index]
matching_kw = lines.reverse.detect { |line| line.indent == block.current_indent && line.is_kw? }
return unless matching_kw

@lines_to_output << matching_kw

kw_count = 0
end_count = 0
orphan_end = @code_lines[matching_kw.index..visible_line.index].detect do |line|
kw_count += 1 if line.is_kw?
end_count += 1 if line.is_end?

end_count >= kw_count
end

return unless orphan_end
@lines_to_output << orphan_end
end
end
end
Loading

0 comments on commit ecc431d

Please sign in to comment.