class 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 может привести к синтаксической ошибке. Чтобы этого избежать, все строки объединяются в одну.
## Документы 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 отдельно, код будет воспринят как константа, строка будет удалена, и возникнет синтаксическая ошибка.
Все эти проблемы решаются объединением всего heredoc в одну строку.
## Комментарии и пробелы
Комментарии могут мешать лексеру определить, что строка логически связана со следующей. Этот код на 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
Вызывает все «очистители» документа и возвращает self
# 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) # 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
По какой-то причине это добавляет `on_ignore_newline`, но с типом BEG
# 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>>
Внешний массив содержит различные «группы», а внутренний массив — строки кода.
Все строки кода «объединяются» в первую строку своей группы.
Чтобы сохранить размер документа, на месте объединённых строк размещаются пустые строки.
# 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("")
# 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("")
# File lib/syntax_suggest/clean_document.rb, line 104 def lines @document end
Возвращает массив CodeLine документа
# 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`, но при прекращении перебора он также возвращает строку, на которой перебор остановился.
# File lib/syntax_suggest/clean_document.rb, line 109 def to_s @document.join end
Преобразует документ обратно в строку
Ruby Core © 1993–2025 Yukihiro Matsumoto
Licensed under the Ruby License.
Ruby Standard Library © contributors
Licensed under their own licenses.