класс 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 может привести к синтаксической ошибке. Чтобы исправить это, все строки объединяются в одну.
## 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
Если бы мы не объединили эти строки, наш алгоритм посчитал бы, что строка 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 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–2022 Yukihiro Matsumoto
Licensed under the Ruby License.
Ruby Standard Library © contributors
Licensed under their own licenses.