class SyntaxSuggest::CleanDocument
解析并清理源代码,使其成为具有词法意识的文档
在内部,文档由一个数组表示,每个索引包含一个 CodeLine,对应于源代码中的一行。
算法有三个主要阶段
-
净化/格式化输入源码
-
搜索无效代码块
-
将无效代码块格式化为有意义的内容
此类处理第一部分。
此类存在的原因是为了格式化输入源,以便更好地/更容易/更清晰地进行探索。
类 CodeSearch 在行级别运行,因此我们必须小心,不要引入单独看起来有效但删除后会触发语法错误或奇怪行为的行。
## 连接尾随斜杠
带尾随斜杠的代码在逻辑上被视为单行
1 it "code can be split" \ 2 "across multiple lines" do
在这种情况下,删除第二行会产生语法错误。我们通过在内部将两行连接成一个“行”对象来解决这个问题。
## 逻辑上连续的行
可以跨越多行的代码,例如方法调用,位于不同的行上。
1 User. 2 where(name: "schneems"). 3 first
删除第二行会产生语法错误。为了解决这个问题,所有行都连接成一行。
## Heredocs
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
如果我们不连接这些行,我们的算法将认为第四行与其余部分分开,具有更高的缩进,然后先查看并删除它。
如果代码单独评估第五行,它会认为第五行是一个常量,删除它,并引入语法错误。
通过将整个 heredoc 连接成一行来解决所有这些问题。
## 注释和空白
注释会干扰词法分析器告诉我们该行在逻辑上属于下一行的方式。这是有效的 Ruby,但结果的词法输出与之前不同。
1 User. 2 where(name: "schneems"). 3 # Comment here 4 first
为了解决这个问题,我们可以用空行替换注释行,然后重新词法分析源代码。这种删除和重新词法分析保留了行索引和文档大小,但生成了一个更容易处理的文档。
Public Class Methods
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
Public Instance Methods
Source
# File lib/syntax_suggest/clean_document.rb, line 94 def call join_trailing_slash! join_consecutive! join_heredoc! self end
调用所有文档“清理器”并返回 self
Source
# 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'
# 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)
Source
# 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
合并逻辑上“连续”的行
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
出于某种原因,这会引入带有 BEG 类型的 'on_ignore_newline'
Source
# 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 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
用于连接行“组”的辅助方法
输入预计为类型 Array<Array<CodeLine>>
外层数组包含各种“组”,而内层数组包含代码行。
所有代码行都“合并”到其组中的第一行。
为了保持文档大小,在被“合并”的行的位置放置空行。
Source
# 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
将所有 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("")
Source
# 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
连接带尾随斜杠的行
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("")
Source
# File lib/syntax_suggest/clean_document.rb, line 104 def lines @document end
返回文档中的 CodeLines 数组
Source
# 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
用于从文档中获取元素的辅助方法
类似于 'take_while',但当它停止迭代时,它还会返回导致它停止的行。
Source
# File lib/syntax_suggest/clean_document.rb, line 109 def to_s @document.join end
将文档渲染回字符串