Spec-Zone.ru › Ruby 3.3

класс SyntaxSuggest::CleanDocument

Родитель:
Объект

Парсит и очищает исходный код в лексически осознанный документ

Внутренне документ представлен массивом, где каждый индекс содержит CodeLine, соответствующий строке из исходного кода.

В алгоритме есть три основных этапа:

  1. Очистка/форматирование входного исходного кода

  2. Поиск недопустимых блоков

  3. Форматирование недопустимых блоков в нечто осмысленное

Этот класс обрабатывает первую часть.

Этот класс существует для того, чтобы отформатировать входной исходный код для лучшего/более простого/более чистого изучения.

Класс 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

Для решения этой проблемы мы можем заменить строки комментариев пустыми строками, а затем пересчитать исходный код. Это удаление и пересчёт сохраняет индекс строки и размер документа, но генерирует более удобный для работы документ.

Публичные методы класса

new(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

Публичные методы экземпляра

call() Показать исходный код
# File lib/syntax_suggest/clean_document.rb, line 94
def call
  join_trailing_slash!
  join_consecutive!
  join_heredoc!

  self
end

Вызывает все “очистители” документа и возвращает себя

clean_sweep(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)
join_consecutive!() Показать исходный код
# 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

join_groups(groups) Показать исходный код
# 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>>

Внешний массив содержит различные “группы”, а внутренний массив содержит строки кода.

Все строки кода “объединяются” с первой строкой в их группе.

Для сохранения размера документа пустые строки помещаются на место строк, которые были “объединены”.

join_heredoc!() Показать исходный код
# 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("")
join_trailing_slash!() Показать исходный код
# 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("")
lines() Показать исходный код
# File lib/syntax_suggest/clean_document.rb, line 104
def lines
  @document
end

Возвращает массив CodeLines в документе

take_while_including(range = 0..) { |line)| ... } Показать исходный код
# 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», но когда он прекращает итерацию, также возвращает строку, которая заставила его остановиться

to_s() Показать исходный код
# 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.

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API