Spec-Zone.ru › Ruby 3.4

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

## Строки 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

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

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

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

Spec-Zone.ru

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