класс 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 — это способ определения многострочного текста. Они могут вызывать множество проблем. Если оставить их в одной строке, Ripper попытается разобрать содержимое как код 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:)
source.lines.map do |line|
if line.match?(/^\s*(#[^{].*)?$/) # https://rubular.com/r/LLE10D8HKMkJvs
$/
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 223
def join_consecutive!
consecutive_groups = @document.select(&:ignore_newline_not_beg?).map do |code_line|
take_while_including(code_line.index..-1) 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 264
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..-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 179
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 244
def join_trailing_slash!
trailing_groups = @document.select(&:trailing_slash?).map do |code_line|
take_while_including(code_line.index..-1) { |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 294
def take_while_including(range = 0..-1)
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.