Spec-Zone.ru › Ruby 3.4
  1. SyntaxSuggest::
  2. CleanDocument

class SyntaxSuggest::CleanDocument

Родительский класс:
Object

Разбирает и очищает исходный код, создавая документ с учётом лексической структуры

Внутренне документ представлен массивом, каждый элемент которого содержит 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 может привести к синтаксической ошибке. Чтобы этого избежать, все строки объединяются в одну.

## Документы 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

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

Общедоступные методы класса

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

Вызывает все «очистители» документа и возвращает self

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

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

Все строки кода «объединяются» в первую строку своей группы.

Чтобы сохранить размер документа, на месте объединённых строк размещаются пустые строки.

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

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

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–2025 Yukihiro Matsumoto
Licensed under the Ruby License.
Ruby Standard Library © contributors
Licensed under their own licenses.

Spec-Zone.ru

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