类 SyntaxSuggest::CleanDocument

解析和清理源代码,使其成为一个词法感知的文档

在内部,文档由一个数组表示,每个索引包含一个 CodeLine,对应于源代码中的一行。

算法主要分为三个阶段

  1. 清理/格式化输入源代码

  2. 搜索无效代码块

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

此类处理第一部分。

此类存在的目的是为了格式化输入源代码,以便更好地/更容易/更干净地进行探索。

CodeSearch 类在行级别上操作,因此我们必须小心,不要引入看起来有效的行,但删除这些行会导致语法错误或奇怪的行为。

## 合并尾部斜杠

带有尾部斜杠的代码在逻辑上被视为单行

1 it "code can be split" \
2    "across multiple lines" do

在这种情况下,删除第 2 行会导致语法错误。我们可以通过将这两行内部合并为一个“行”对象来解决这个问题

## 逻辑上连续的行

可以跨多行断开的代码,例如方法调用,位于不同的行上

1 User.
2   where(name: "schneems").
3   first

删除第 2 行会导致语法错误。为了解决这个问题,所有行都被合并为一行。

## heredoc

heredoc 是一种定义多行字符串的方式。它们会导致很多问题。如果保留为单行,解析器会尝试将内容解析为 ruby 代码,而不是字符串。即使没有这个问题,我们仍然会遇到缩进问题

1 foo = <<~HEREDOC
2  "Be yourself; everyone else is already taken.""
3    ― Oscar Wilde
4      puts "I look like ruby code" # but i'm still a heredoc
5 HEREDOC

如果我们不合并这些行,那么我们的算法会认为第 4 行与其他行是分开的,具有更高的缩进,然后首先查看它并删除它。

如果代码单独评估第 5 行,它会认为第 5 行是一个常量,将其删除,并引入语法错误。

所有这些问题都可以通过将整个 heredoc 连接成一行来解决。

## 注释和空白

注释可能会干扰词法分析器判断行逻辑上属于下一行的判断。这在 Ruby 中是有效的,但会导致与之前不同的词法分析输出。

1 User.
2   where(name: "schneems").
3   # Comment here
4   first

为了解决这个问题,我们可以用空行替换注释行,然后重新对源代码进行词法分析。这种删除和重新词法分析保留了行索引和文档大小,但生成了更容易处理的文档。

公共类方法

new(source:) 点击切换源代码
# File lib/syntax_suggest/clean_document.rb, line 87
def initialize(source:)
  lines = clean_sweep(source: source)
  @document = CodeLine.from_source(lines.join, lines: lines)
end

公共实例方法

call() 点击切换源代码

调用所有文档“清理器”并返回 self

# File lib/syntax_suggest/clean_document.rb, line 94
def call
  join_trailing_slash!
  join_consecutive!
  join_heredoc!

  self
end
clean_sweep(source:) 点击切换源代码

删除注释

用空行替换

source = <<~'EOM'
  # Comment 1
  puts "hello"
  # Comment 2
  puts "world"
EOM

lines = CleanDocument.new(source: source).lines
expect(lines[0].to_s).to eq("\n")
expect(lines[1].to_s).to eq("puts "hello")
expect(lines[2].to_s).to eq("\n")
expect(lines[3].to_s).to eq("puts "world")

重要:这必须在词法分析之前完成。

完成此更改后,我们对文档进行词法分析,因为删除注释可能会改变文档的解析方式。

例如

values = LexAll.new(source: <<~EOM))
  User.
    # comment
    where(name: 'schneems')
EOM
expect(
  values.count {|v| v.type == :on_ignored_nl}
).to eq(1)

删除注释后

 values = LexAll.new(source: <<~EOM))
   User.

     where(name: 'schneems')
 EOM
 expect(
  values.count {|v| v.type == :on_ignored_nl}
).to eq(2)
# File lib/syntax_suggest/clean_document.rb, line 157
def clean_sweep(source:)
  # Match comments, but not HEREDOC strings with #{variable} interpolation
  # https://rubular.com/r/HPwtW9OYxKUHXQ
  source.lines.map do |line|
    if line.match?(/^\s*#([^{].*|)$/)
      $/
    else
      line
    end
  end
end
join_consecutive!() 点击切换源代码

将逻辑上“连续”的行合并在一起

source = <<~'EOM'
  User.
    where(name: 'schneems').
    first
EOM

lines = CleanDocument.new(source: source).join_consecutive!.lines
expect(lines[0].to_s).to eq(source)
expect(lines[1].to_s).to eq("")

已知的一个无法处理的情况是

Ripper.lex <<~EOM
  a &&
   b ||
   c
EOM

出于某种原因,这引入了“on_ignore_newline”,但类型为 BEG

# File lib/syntax_suggest/clean_document.rb, line 225
def join_consecutive!
  consecutive_groups = @document.select(&:ignore_newline_not_beg?).map do |code_line|
    take_while_including(code_line.index..) do |line|
      line.ignore_newline_not_beg?
    end
  end

  join_groups(consecutive_groups)
  self
end
join_groups(groups) 点击切换源代码

用于合并行“组”的辅助方法

预期输入类型为 Array<Array<CodeLine>>

外部数组保存各种“组”,而内部数组保存代码行。

所有代码行都“合并”到其组中的第一行。

为了保留文档大小,空行将放置在“合并”行的位置。

# File lib/syntax_suggest/clean_document.rb, line 266
def join_groups(groups)
  groups.each do |lines|
    line = lines.first

    # Handle the case of multiple groups in a a row
    # if one is already replaced, move on
    next if @document[line.index].empty?

    # Join group into the first line
    @document[line.index] = CodeLine.new(
      lex: lines.map(&:lex).flatten,
      line: lines.join,
      index: line.index
    )

    # Hide the rest of the lines
    lines[1..].each do |line|
      # The above lines already have newlines in them, if add more
      # then there will be double newline, use an empty line instead
      @document[line.index] = CodeLine.new(line: "", index: line.index, lex: [])
    end
  end
  self
end
join_heredoc!() 点击切换源代码

将所有 heredoc 行合并成一行

source = <<~'EOM'
  foo = <<~HEREDOC
     lol
     hehehe
  HEREDOC
EOM

lines = CleanDocument.new(source: source).join_heredoc!.lines
expect(lines[0].to_s).to eq(source)
expect(lines[1].to_s).to eq("")
# File lib/syntax_suggest/clean_document.rb, line 181
def join_heredoc!
  start_index_stack = []
  heredoc_beg_end_index = []
  lines.each do |line|
    line.lex.each do |lex_value|
      case lex_value.type
      when :on_heredoc_beg
        start_index_stack << line.index
      when :on_heredoc_end
        start_index = start_index_stack.pop
        end_index = line.index
        heredoc_beg_end_index << [start_index, end_index]
      end
    end
  end

  heredoc_groups = heredoc_beg_end_index.map { |start_index, end_index| @document[start_index..end_index] }

  join_groups(heredoc_groups)
  self
end
join_trailing_slash!() 点击切换源代码

将行与尾部斜杠连接

source = <<~'EOM'
  it "code can be split" \
     "across multiple lines" do
EOM

lines = CleanDocument.new(source: source).join_consecutive!.lines
expect(lines[0].to_s).to eq(source)
expect(lines[1].to_s).to eq("")
# File lib/syntax_suggest/clean_document.rb, line 246
def join_trailing_slash!
  trailing_groups = @document.select(&:trailing_slash?).map do |code_line|
    take_while_including(code_line.index..) { |x| x.trailing_slash? }
  end
  join_groups(trailing_groups)
  self
end
lines() 点击切换源代码

返回文档中的 CodeLines 数组

# File lib/syntax_suggest/clean_document.rb, line 104
def lines
  @document
end
take_while_including(range = 0..) { |line)| ... } 点击切换源代码

用于从文档中获取元素的辅助方法

类似于 `take_while`,但当它停止迭代时,它也会返回导致它停止的行

# File lib/syntax_suggest/clean_document.rb, line 296
def take_while_including(range = 0..)
  take_next_and_stop = false
  @document[range].take_while do |line|
    next if take_next_and_stop

    take_next_and_stop = !(yield line)
    true
  end
end
to_s() 点击切换源代码

将文档渲染回字符串

# File lib/syntax_suggest/clean_document.rb, line 109
def to_s
  @document.join
end