類別 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 行可能會引入語法錯誤。為了解決此問題,所有行都連接成一行。

## Here 文件

Here 文件是一種定義多行字串的方法。它們可能會造成許多問題。如果保留為單一行,剖析器會嘗試將內容剖析為 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 行是一個常數,將其移除,並引入一個語法錯誤。

所有這些問題都可以透過將整個 Here 文件連接成單一行來解決。

## 註解和空白

註解會讓詞法分析器無法正確判斷該行在邏輯上應與下一行連接。這是一個有效的 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() 按一下以切換來源

呼叫所有文件「清除器」,並傳回自我

# 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