类 SyntaxSuggest::CodeFrontier

算法主要分为三个阶段

  1. 清理/格式化输入源

  2. 搜索无效代码块

  3. 将无效代码块格式化为有意义的内容

代码边界是第二步的关键部分

## 了解我们已经去过的地方

生成代码块后,它会被添加到边界。然后它将按缩进排序,边界可以被过滤。完全包含较小代码块的大代码块会导致较小代码块被驱逐。

CodeFrontier#<<(block) # Adds block to frontier
CodeFrontier#pop # Removes block from frontier

## 了解我们可以去哪里

在内部,边界跟踪“未访问”行,这些行通过调用 `next_indent_line` 方法暴露出来,该方法返回缩进级别最高的代码行。

返回的代码行可以用来构建一个 CodeBlock,然后该代码块被添加回边界。然后,这些行从“未访问”中删除,这样我们就不会重复创建相同的代码块。

CodeFrontier#next_indent_line # Shows next line
CodeFrontier#register_indent_block(block) # Removes lines from unvisited

## 了解何时停止

边界知道如何检查整个文档是否存在语法错误。当代码块被添加到边界时,它们会被从文档中删除。当所有包含语法错误的代码都被添加到边界时,文档将可解析且没有语法错误,搜索可以停止。

CodeFrontier#holds_all_syntax_errors? # Returns true when frontier holds all syntax errors

## 过滤误报

搜索完成后,边界可能包含多个不包含语法错误的代码块。为了将结果限制在“无效代码块”的最小子集,请调用

CodeFrontier#detect_invalid_blocks

公共类方法

combination(array) 点击切换源代码

示例

combination([:a, :b, :c, :d])
# => [[:a], [:b], [:c], [:d], [:a, :b], [:a, :c], [:a, :d], [:b, :c], [:b, :d], [:c, :d], [:a, :b, :c], [:a, :b, :d], [:a, :c, :d], [:b, :c, :d], [:a, :b, :c, :d]]
# File lib/syntax_suggest/code_frontier.rb, line 162
def self.combination(array)
  guesses = []
  1.upto(array.length).each do |size|
    guesses.concat(array.combination(size).to_a)
  end
  guesses
end
new(code_lines:, unvisited: UnvisitedLines.new(code_lines: code_lines)) 点击切换源代码
# File lib/syntax_suggest/code_frontier.rb, line 53
def initialize(code_lines:, unvisited: UnvisitedLines.new(code_lines: code_lines))
  @code_lines = code_lines
  @unvisited = unvisited
  @queue = PriorityEngulfQueue.new

  @check_next = true
end

公共实例方法

<<(block) 点击切换源代码

将代码块添加到边界

此方法确保边界始终保持排序(按缩进顺序),并且每个代码块的行都从缩进哈希中删除,这样我们就不会多次评估同一行。

# File lib/syntax_suggest/code_frontier.rb, line 148
def <<(block)
  @unvisited.visit_block(block)

  @queue.push(block)

  @check_next = true if block.invalid?

  self
end
count() 点击切换源代码
# File lib/syntax_suggest/code_frontier.rb, line 61
def count
  @queue.length
end
detect_invalid_blocks() 点击切换源代码

鉴于我们知道语法错误存在于我们的边界范围内,我们希望找到包含所有语法错误的最小可能的代码块集。

# File lib/syntax_suggest/code_frontier.rb, line 172
def detect_invalid_blocks
  self.class.combination(@queue.to_a.select(&:invalid?)).detect do |block_array|
    holds_all_syntax_errors?(block_array, can_cache: false)
  end || []
end
expand?() 点击查看源代码
# File lib/syntax_suggest/code_frontier.rb, line 111
def expand?
  return false if @queue.empty?
  return true if @unvisited.empty?

  frontier_indent = @queue.peek.current_indent
  unvisited_indent = next_indent_line.indent

  if ENV["SYNTAX_SUGGEST_DEBUG"]
    puts "```"
    puts @queue.peek
    puts "```"
    puts "  @frontier indent:  #{frontier_indent}"
    puts "  @unvisited indent: #{unvisited_indent}"
  end

  # Expand all blocks before moving to unvisited lines
  frontier_indent >= unvisited_indent
end
holds_all_syntax_errors?(block_array = @queue, can_cache: true) 点击查看源代码

如果文档在删除所有行后仍然有效,则返回 true。默认情况下,它检查边界数组中存在的所有代码块,但也可以用于任意代码块数组。

# File lib/syntax_suggest/code_frontier.rb, line 89
def holds_all_syntax_errors?(block_array = @queue, can_cache: true)
  return false if can_cache && can_skip_check?

  without_lines = block_array.to_a.flat_map do |block|
    block.lines
  end

  SyntaxSuggest.valid_without?(
    without_lines: without_lines,
    code_lines: @code_lines
  )
end
next_indent_line() 点击查看源代码
# File lib/syntax_suggest/code_frontier.rb, line 107
def next_indent_line
  @unvisited.peek
end
pop() 点击查看源代码

返回具有最大缩进的代码块。

# File lib/syntax_suggest/code_frontier.rb, line 103
def pop
  @queue.pop
end
register_engulf_block(block) 点击查看源代码

当一个元素完全包含另一个元素时,我们从边界中移除较小的代码块。这可以防止双重扩展和各种奇怪的行为。但是,这种保证维护起来相当昂贵。

# File lib/syntax_suggest/code_frontier.rb, line 140
def register_engulf_block(block)
end
register_indent_block(block) 点击查看源代码

跟踪哪些行已添加到代码块中,哪些行尚未访问。

# File lib/syntax_suggest/code_frontier.rb, line 132
def register_indent_block(block)
  @unvisited.visit_block(block)
  self
end

私有实例方法

can_skip_check?() 点击查看源代码

性能优化

使用 ripper 进行解析非常昂贵。如果我们知道没有任何代码块包含无效语法,那么我们就知道还没有找到错误的语法。

当将无效代码块添加到边界时,检查文档状态。

# File lib/syntax_suggest/code_frontier.rb, line 74
        def can_skip_check?
  check_next = @check_next
  @check_next = false

  if check_next
    false
  else
    true
  end
end