Извлечение исходных комментариев из исходного файла Scala

#scala #javadoc #scaladoc #doclet

Вопрос:

Я хотел бы программно извлечь комментарии к коду из исходного файла Scala.

У меня есть доступ как к исходному файлу, так и к объектам классов, чьи комментарии меня интересуют. Я также открыт для написания комментариев в исходном файле Scala в определенной форме, чтобы облегчить извлечение (хотя все еще придерживаюсь соглашений Scaladoc).

В частности, я не ищу HTML или аналогичные выходные данные.

Тем не менее, объект json, который я затем могу просмотреть, чтобы получить комментарии для каждого поля, был бы совершенно нормальным (хотя json не является обязательным требованием). В идеале я хотел бы получить комментарий класса или члена класса с указанием его «полного» имени или объекта класса.

Как мне лучше всего это сделать? Я надеюсь на решение, которое можно поддерживать (без особых усилий) от Scala 2.11 до Scala 3.

Ценю любую помощь!

Ответ №1:

У меня есть доступ к обоим исходным файлам

Исходя из этого, я предполагаю, что у вас есть путь к файлу, который я представлю в своем коде как:

 val pathToFile: String = ???
 

TL;DR

 import scala.io.Source

def comments(pathToFile: String): List[String] = {
  def lines: Iterator[(String, Int)] = Source.fromFile(pathToFile).getLines().zipWithIndex

  val singleLineJavaDocStartAndEnds = lines.filter {
    case (line, lineNumber) => line.contains("/*") amp;amp; line.contains("*/")
  }.map { case (line, _) => line }

  val javaDocComments = lines.filter {
    case (line, lineNumber) =>
      (line.contains("/*") amp;amp; !line.contains("*/")) ||
      (!line.contains("/*") amp;amp; line.contains("*/"))
  }
  .grouped(2).map {
    case Seq((_, firstLineNumber), (_, secondLineNumber)) =>
      lines
        .map { case (line, _) => line }
        .slice(firstLineNumber, secondLineNumber 1)
        .mkString("n")
  }

  val slashSlashComments = lines
    .filter { case (line, _) => line.contains("//") }
    .map { case (line, _) => line }

  (singleLineJavaDocStartAndEnds    javaDocComments    slashSlashComments).toList
}
 

Полное объяснение

Первое, что нужно сделать, это прочитать содержимое файла:

 import scala.io.Source

def lines: Iterator[String]  = Source.fromFile(pathToFile).getLines()

// here we preserve new lines, for Windows you may need to replace "n" with "rn
val content: String = lines.mkString("n")
// where `content` is the whole file as a `String`
 

Я сделал lines def это, чтобы предотвратить непреднамеренные результаты при lines многократном вызове. Это связано с типом возвращаемого Source.fromFile значения и тем, как он обрабатывает итерацию по файлу. Этот комментарий здесь добавляет объяснение. Поскольку вы читаете файлы исходного кода, я думаю, что перечитывание файла является безопасной операцией и не приведет к проблемам с памятью или производительностью.

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


Редактировать:

Как справедливо отметил @jwvh, где я использовал .trim.startsWith игнорируемые комментарии, такие как:

 val x = 1 //mid-code-comments

/*fullLineComment*/
 

Чтобы решить эту проблему, я заменил .trim.startsWith ее на .contains .


Для однострочных комментариев это просто:

 val slashComments: Iterator[String] = lines.filter(line => line.contains("//"))
 

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


Теперь мы подадим многострочные комментарии или JavaDoc; например (содержимое не важно):

 /**
 * Class String is special cased within the Serialization Stream Protocol.
 *
 * A String instance is written into an ObjectOutputStream according to
 * .....
 * .....
 */
 

Самое безопасное, что можно сделать, — это четко обозначить линии /* */ , на которых отображаются и, и включить все промежуточные линии:

 def lines: Iterator[(String, Int)] = Source.fromFile(pathToFile).getLines().zipWithIndex

val javaDocStartAndEnds: Iterator[(String, Int)] = lines.filter { 
  case (line, lineNumber) => line.contains("/*") || line.contains("*/")
}
 

.zipWithIndex дает нам увеличивающееся число рядом с каждой строкой. Мы можем использовать их для представления номеров строк исходного файла. На данный момент это даст нам список строк, содержащих /* и */ . Нам нужно group разделить их на группы по 2, так как все эти типы комментариев будут иметь совпадающую пару /* и */ . Как только у нас появятся эти группы, мы сможем выбрать , используя slice все lines , начиная с первого индекса и до последнего. Мы хотим включить последнюю строку, поэтому мы делаем 1 с ней а.

 val javaDocComments = javaDocStartAndEnds.grouped(2).map {
  case Seq((_, firstLineNumber), (_, secondLineNumber)) =>
    lines // re-calling `def lines: Iterator[(String, Int)]`
      .map { case (line, _) => line } // here we only care about the `line`, not the `lineNumber`
      .slice(firstLineNumber, secondLineNumber 1)
      .mkString("n")
  }
 

Наконец-то мы можем объединить slashComments и javaDocComments :

 val comments: List[String] = (slashComments    javaDocComments).toList
 

Независимо от порядка, в котором мы к ним присоединяемся, они не будут отображаться в упорядоченном списке. Улучшение, которое можно было бы сделать здесь, состояло бы в том, чтобы сохранить lineNumber и упорядочить это в конце.

Я включу версию «слишком долго; не читал» (TL;DR) вверху, чтобы любой мог просто скопировать код полностью без пошагового объяснения.


Как мне лучше всего это сделать? Я надеюсь на решение, которое можно поддерживать (без особых усилий) от Scala 2.11 до Scala 3.

Надеюсь, я ответил на ваш вопрос и предложил полезное решение. Вы упомянули файл JSON в качестве вывода. То, что я предоставил, находится List[String] в памяти, которую вы можете обработать. Если требуется вывод в JSON, я могу обновить свой ответ этим.

Комментарии:

1. Все val x = 1 //mid-code-comments они пропущены, и это /*fullLineComment*/ сбросит ваш многострочный расчет.

2. Привет @jwvh. Это совершенно верно! Я обновлю свой ответ, чтобы заменить .trim.startsWith его на contains .

3. @skm Пожалуйста, дайте мне знать, как вы пробуете это решение 🙂

4. Вам действительно следует протестировать свой код перед публикацией. Текущая итерация завершается неудачно для нескольких комбинаций кода/комментария. Кроме того, совсем не ясно, что (теперь молчащая) операция хочет простой синтаксический анализатор текста. Это дело о «данном … объект класса» в лучшем случае сбивает с толку.

5. Спасибо @jvwh. Вы, безусловно, правы. Я вижу /*fullLineComment*/ , что в последней версии это не было обработано. Не могли бы вы привести примеры любых других «комбинаций кода/комментариев» , и я обновлю их. Я намерен сделать пример как можно более простым и поэтому намерен рассматривать только самые распространенные случаи. Некоторые вещи все еще проскальзывают через сеть, например URL-адреса , содержащие a // , и будут появляться.