класс 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 сама по себе, он посчитал бы строку 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
Вызывает все «очистители» документа и возвращает себя
Исходный код
# 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<Массив<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
Возвращает массив CodeLines в документе
Исходный код
# 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–2024 Yukihiro Matsumoto
Licensed under the Ruby License.
Ruby Standard Library © contributors
Licensed under their own licenses.