зеркало из https://github.com/github/ruby.git
234 строки
6.5 KiB
Ruby
234 строки
6.5 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
module SyntaxSuggest
|
|
# Turns a "invalid block(s)" into useful context
|
|
#
|
|
# There are three main phases in the algorithm:
|
|
#
|
|
# 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"
|
|
#
|
|
# context = CaptureCodeContext.new(
|
|
# blocks: block,
|
|
# code_lines: code_lines
|
|
# )
|
|
#
|
|
# lines = context.call.map(&:original)
|
|
# puts lines.join
|
|
# # =>
|
|
# class Dog
|
|
# def bark
|
|
# end
|
|
#
|
|
class CaptureCodeContext
|
|
attr_reader :code_lines
|
|
|
|
def initialize(blocks:, code_lines:)
|
|
@blocks = Array(blocks)
|
|
@code_lines = code_lines
|
|
@visible_lines = @blocks.map(&:visible_lines).flatten
|
|
@lines_to_output = @visible_lines.dup
|
|
end
|
|
|
|
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.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,
|
|
code_lines: @code_lines
|
|
).on_falling_indent do |line|
|
|
@lines_to_output << line
|
|
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
|
|
|
|
around_lines -= block.lines
|
|
|
|
@lines_to_output.concat(around_lines)
|
|
end
|
|
|
|
# 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:
|
|
#
|
|
# class Dog # 1
|
|
# def bark # 2
|
|
# puts "woof" # 3
|
|
# end # 4
|
|
#
|
|
# However due to https://github.com/ruby/syntax_suggest/issues/32
|
|
# the problem line will be identified as:
|
|
#
|
|
# > class Dog # 1
|
|
#
|
|
# Because lines 2, 3, and 4 are technically valid code and are expanded
|
|
# first, deemed valid, and hidden. We need to un-hide the matching end
|
|
# line 4. Also work backwards and if there's a mis-matched keyword, show it
|
|
# too
|
|
def capture_last_end_same_indent(block)
|
|
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.detect { |line| line.indent == block.current_indent && line.is_end? }
|
|
return unless matching_end
|
|
|
|
@lines_to_output << matching_end
|
|
|
|
# Work backwards from the end to
|
|
# see if there are mis-matched
|
|
# keyword/end pairs
|
|
#
|
|
# Return the first mis-matched keyword
|
|
# this would find line 2
|
|
#
|
|
# def bark # 2
|
|
# puts "woof" # 3
|
|
# end # 4
|
|
end_count = 0
|
|
kw_count = 0
|
|
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?
|
|
|
|
!kw_count.zero? && kw_count >= end_count
|
|
end
|
|
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
|