類別 SyntaxSuggest::CleanDocument
分析並清除原始碼,形成具有詞法意識的文件
文件在內部以陣列表示,每個索引包含一個 CodeLine
,與原始碼中的一行對應。
演算法中有三個主要階段
-
清除/格式化輸入原始碼
-
搜尋無效區塊
-
將無效區塊格式化成有意義的內容
此類別處理第一部分。
此類別存在的理由是,格式化輸入原始碼,以利於更佳/更輕鬆/更簡潔的探索。
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
為了解決這個問題,我們可以用空行取代註解行,然後重新對來源進行詞法分析。這種移除和重新詞法分析會保留行索引和文件大小,但會產生一個更容易處理的文件。
公用類別方法
# 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
公用實例方法
呼叫所有文件「清除器」,並傳回自我
# File lib/syntax_suggest/clean_document.rb, line 94 def call join_trailing_slash! join_consecutive! join_heredoc! self end
移除註解
以空的新行取代
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
壓縮邏輯上「連續」的行
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
用於合併「群組」行的輔助方法
輸入預期為類型 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
將所有 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
合併有尾斜線的行
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
傳回文件中 CodeLines 的陣列
# File lib/syntax_suggest/clean_document.rb, line 104 def lines @document end
用於從文件中擷取元素的輔助方法
類似於「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
將文件轉譯回字串
# File lib/syntax_suggest/clean_document.rb, line 109 def to_s @document.join end