Spec-Zone.ru › Ruby 3.2

класс 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 — это способ определения многострочного текста. Они могут вызывать множество проблем. Если оставить их в одной строке, 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

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

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

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

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

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

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

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

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

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

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

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