Хабрахабр

Что делать, если для вашего любимого языка нет статического анализатора?

д., то это в другой хаб. Ну, если под любимым языком подразумевается русский, английский и т. На первый взгляд, это очень сложно, но, к счастью, существуют готовые многоязыковые инструменты, в которые относительно легко добавить поддержку нового языка. А если язык программирования или разметки, то конечно писать анализатор самим! Сегодня я покажу, как можно с достаточно незначительными затратами времени добавить поддержку языка Modelica в анализатор PMD.

Тот факт, что сторонние программисты копировали в свои патчи куски существующего кода проекта вместо грамотного абстрагирования. Кстати, знаете, что может ухудшить качество кодовой базы, полученной из последовательности идеальных pull request-ов? Согласитесь, в какой-то мере такую банальность отловить ещё сложнее, чем некачественный код — он же качественный и даже уже тщательно отлаженный, поэтому тут недостаточно локальной проверки, нужно держать в голове всю кодовую базу, а человеку это непросто… Так вот: если на добавление полной поддержки Modelica (без создания конкретных правил) до состояния «может запускать примитивные проверки» у меня ушло около недели, то поддержку только copy-paste detector часто можно вообще добавить за день!

Какая ещё Modelica?

На самом деле, не только физических: можно описывать химические процессы, количественное поведение популяций животных и т. Modelica — это, как можно догадаться из названия, язык для написания моделей физических систем. — то, что описывается системами дифференциальных уравнений вида der(X) = f(X), где X — вектор неизвестных. д. Уравнения в частных производных в явном виде не поддерживаются, но можно разбить исследуемую область на кусочки (как мы бы, наверное, и делали на каком-нибудь языке общего назначения), а потом записать уравнения для каждого элемента, сведя задачу к предыдущей. Императивные куски кода тоже поддерживаются. д. Прикол же Моделики в том, что решение этого самого der(X) = f(X) ложится на компилятор: вы можете просто поменять солвер в настройках, уравнение не обязано быть линейным и т. Введение в Моделику — тема отдельной статьи (которые уже на Хабре несколько раз появлялись), а то и целого цикла, сегодня же она меня интересует как открытый и имеющий несколько реализаций, но, увы, всё ещё сыроватый стандарт. Короче говоря, есть как свои плюсы (выписал формулу из учебника — и оно заработало), так и минусы (с большей абстракцией мы получаем меньший контроль).

Наконец, в отличие от какого-нибудь C++, для которого существует туча статических анализаторов и компиляторов с прекрасной, а главное развёрнутой, см. К тому же, Моделика, с одной стороны, имеет статическую типизацию (что нам поможет быстрее написать какой-нибудь осмысленный анализ), с другой, инстанциируя модель, компилятор не обязан полностью проверять всю библиотеку (поэтому статический анализатор весьма полезен для отлова «спящих» багов). C++ templates диагностикой ошибок, компиляторы Моделики периодически всё же выдают Internal compiler error, а значит, есть где помочь пользователю даже с достаточно простым анализатором.

А что за PMD?

Когда-то я хотел сделать какой-то небольшой pull request в среду разработки для OpenModelica. Я отвечу песней байкой. Не поняв, с какими именно внутренностями редактора он взаимодействует, но осознав, что с точки зрения этого кусочка кода моя задача полностью идентична, я просто вынес его в функцию, чтобы переиспользовать у себя и не сломать. Увидев, как в другой части кода обрабатывается сохранение модели, я заметил маленький и не очень понятный внутри кусок кода строчек из четырёх, который поддерживал какой-то инвариант. Погуглив, я нашёл Copy-paste Detector (CPD) — часть статического анализатора PMD — который поддерживает ещё больше языков, чем сам анализатор. Ментейнер сказал, мол, замечательно, только тогда уж замени этот код на вызов функции и в остальных двадцати местах… Я решил пока не связываться, и просто сделал ещё одну копию, отметив, что потом как-нибудь нужно будет сразу всё причесать, не смешивая с текущим патчем. Их-то я как раз не увидел (каждый из них просто по количеству токенов не превысил порог), зато увидел, например, повторение чуть ли не на полсотни строк C++ кода. Натравив его на кодовую базу OMEdit, я ожидал увидеть те два десятка кусочков из четырёх строк. А вот пропустить такое в PR он мог запросто — ведь код по определению уже соответствовал всем стандартам проекта! Как я уже говорил, вряд ли ментейнер так запросто скопипастил гигантский кусок из другого файла. Когда я поделился наблюдением с ментейнером, он согласился, что нужно будет прибраться в рамках отдельной задачи.

Может, множества значений, которые может принимать переменная, он и не вычисляет (хотя кто его знает...), но для добавления в него правил даже не нужно знать Java и вообще как-то менять его код! Соответственно, Program Mistake Detector (PMD) — это легко расширяемый статический анализатор. А на что похоже дерево разбора исходника? Дело в том, что первым делом он, что и неудивительно, строит AST файлов с исходниками. Значит, можно описывать правила просто как XPath запросы — на что совпало, на то и выдаём предупреждение. На дерево разбора XML! Более сложные правила, конечно, можно писать прямо на Java в виде visitor-ов для AST. У них даже графический отладчик для правил есть!

Следствие: PMD можно использовать не только для суровых и универсальных правил, которые суровые Java-программисты закоммитили в код анализатора, но и для местных coding style — хоть в каждый репозиторий свой собственный местный ruleset.xml загоняй!

Уровень 1: находим копипаст автоматизированно

Дословно пересказывать документацию «как делать» смысла не вижу — она очень понятная, структурированная и пошаговая. В принципе, добавить поддержку нового языка в CPD часто очень просто. Лучше опишу, что вас при этом ждёт (TLDR: ничего страшного): Такую пересказывать — только в испорченный телефон играть.

  • Разработка анализатора (и PMD, и CPD) ведётся на Гитхабе в репозитории pmd/pmd
  • Визуальный отладчик правил вынесен в отдельный репозиторий pmd/pmd-designer. Обратите внимание, что готовый jar-ник автоматически подкладывается в PMD binary distribution, который для вас соберёт Gradle в предыдущем репозитории, специально клонировать pmd-designer для этого не требуется.
  • Проект имеет Developer Documentation. Та, которую я читал, была весьма подробна. Правда, чуточку устаревшая, но это лечится вторым pull request-ом 🙂

На Windows оно тоже должно работать отлично — и в смысле качества, и в смысле незначительно отличающегося способа запуска тулов. Сразу предупрежу, что разработку я веду на Ubuntu.

Итак, чтобы добавить новый язык в CPD, нужно всего лишь...

  • ВНИМАНИЕ: если вы хотите полную поддерку PMD до релиза PMD 7, то лучше сразу перейдите к уровню 2, поскольку нормальная поддержка простого пути через готовую Antlr-грамматику появится, по слухам, в той самой версии 7, а пока вы просто потратите время (хотя и чуть чуть...)
  • Форкнуть репозиторий pmd/pmd.
  • Найти в antlr/grammars-v4 уже готовую грамматику для вашего языка — конечно, если язык внутренний, её придётся написать самостоятельно, но для Моделики, например, она нашлась. Тут, конечно, нужно соблюсти формальности с лицензиями — я не юрист, но, как минимум, нужно указать источник, откуда скопировали.
  • После этого нужно создать модуль pmd-<your language name>, добавить его в Gradle и положить туда файл грамматики. Далее, прочитав две странички ненапряжной документации, переделать из модуля для Go сборочный скрипт, парочку классов для загрузки модуля через рефлексию ну и там по мелочи...
  • Поправить эталонный вывод в одном из тестов, ведь теперь CPD поддерживает ещё один язык! Как вы найдёте этот тест? Очень легко: он захочет сломать билд.
  • PROFIT! Это реально просто при условии, что есть готовая грамматика

В принципе, можно выполнить ../mvnw clean verify изнутри вашего нового модуля, что радикально ускорит сборку, но потом придётся правильно подложить собранный jar-ник в распакованный binary distribution (например, собранный единожды после регистрации нового модуля). Теперь, находясь в корне репозитория pmd, вы можете набрать ./mvnw clean verify, при этом в pmd-dist/target вы получите среди прочего binary distribution в виде zip-архива, который нужно распаковать и запустить с помощью ./bin/run.sh cpd --minimum-tokens 100 --files /path/to/source/dir --language <your language name> из распакованного каталога.

Уровень 2: находим ошибки и нарушения style guide

Если же вам, как и мне, не хочется ждать у моря релиза, то придётся откуда-то получить описание грамматики языка в формате JJTree. Как я уже говорил, полную поддержку Antlr обещают в PMD 7. Естественно, если грамматика получилась переработкой существующей, опять же, укажите источник, проверьте соблюдение лицензий и. Может, можно и самим накостылить поддержку произвольного парсера — в документации говорят, что это возможно, но не рассказывают, как именно… Я же просто взял за основу modelica.g4 из всё того же репозитория с грамматиками для Anltr, и вручную переделал в JJTree. д. т.

Я же до этого «всерьёз» пользовался разве что собственноручно написанными регулярками и parser combinator-ами на Scala. Кстати, для человека, хорошо разбирающегося во всевозможных генераторах парсеров, это вряд ли станет сюрпризом. Поэтому очевидная, в сущности, вещь поначалу меня опечалила: AST я, конечно, по modelica.g4 получу, но выглядит оно не очень понятно и «юзабельно»: в нём будут тучи лишних узлов, а если не смотреть на токены, а только на узлы, то не всегда понятно, где, например, кончается ветка then, и начинается else.

Лучше оставлю небольшие подсказки, выясненные по ходу дела: Опять же, не буду пересказывать документацию на JJTree и неплохой tutorial — в этот раз, правда, не потому, что оригинал блещет подробностью и понятностью, а потому, что я и сам до конца не разобрался, а неправильно, но с уверенностью пересказанная документация, очевидно, хуже отсутствия пересказа.

  • Во-первых, код описания парсера на JavaCC предполагает наличие вставок на Java, которые будут вписаны в сгенерированный парсер
  • Пусть вас не смущает, что при построении AST синтаксис вроде [ Expression() ] означает необязательность, а в контексте описания токенов — выбор символа, как в регулярном выражении. Насколько я понял объяснение разработчиков PMD, это похожие конструкции, которые имеют такой вот разный смысл — legacy, сэр...
  • Для корневого узла (в моём случае — StoredDefinition) нужно указать вместо void его тип (то есть ASTStoredDefiniton)
  • С помощью синтаксиса #void после имени узла можно скрыть его из распарсенного дерева (то есть он будет влиять только на то, что является корректным исходником, а что нет, и как будут вкладываться остальные узлы)
  • C помощью конструкции вида void SimpleExpression() #SimpleExpression(>1) можно сказать, что узел нужно показывать в результирующем AST, если у него больше одного потомка. Это очень удобно при описании выражений с многими операторами с разными приоритетами: то есть, с точки зрения парсера одинокая константа 1 будет чем-нибудь вроде LogicExpression(AdditiveExpression(MultiplicativeExperssion(Constant(1))))вписать все n уровней приоритетов операций — но код анализатора получит просто Constant(1)
  • У узла имеется стандартная переменная image (см. методы getImage, setImage), в которую обычно проставляется «самая суть» этого узла: например, для узла, соответствующего имени локальной переменной, в image логично скопировать сопоставившийся токен с идентификатором (по умолчанию все токены из дерева будут выкинуты, поэтому стоит скопировать содержащийся в них смысл, во всяком случае, если это что-то переменное, а не просто ключевые слова)
  • LOOKAHEAD — ну, это отдельная песня, ей даже посвящена отдельная глава в документации
    • грубо говоря, в JavaCC если зашёл в узел, откинуть его и попробовать распарсить по-другому уже нельзя, но можно заранее подглядеть вперёд и решить, заходим или нет
    • в простейшем случае, увидев предупреждение JavaCC, вы просто говорите в заголовке LOOKAHEAD = n и получаете загадочные ошибки парсинга, потому что в общем случае оно, вроде, не может решить все проблемы (ну, разве что, выставив несколько миллиардов токенов, вы, фактически, получите предпросмотр всего, но не факт, что оно работает именно так...)
    • перед именем вложенного узла вы можете явно указать, на основании какого числа токенов здесь точно-точно можно принять окончательное решение
    • если в общем случае не существует такого фиксированного количества токенов, вы можете сказать «заходим сюда, если предварительно, начиная с этой точки, удалось сопоставить такой префикс — и далее обычное описание поддерева»
    • будьте осторожны: в общем случае JavaCC не может проверить корректность расстановки директив LOOKAHEAD — он доверяет вам, поэтому хотя бы прикиньте математическое доказательство, почему такого lookahead достаточно...

Большинство из них имеют вид «создайте класс похожий на реализацию для java или vm, но адаптированный». Теперь, когда у вас есть описание грамматики языка в формате JJTree, эти простые 14 шагов помогут вам добавить поддержку языка. Отмечу лишь типичные особенности, часть из них появятся в основной документации, если примут мой pull request для документации:

  • закомментировав удаление всех сгененерированных файлов в скрипте сборки alljavacc.xml (лежащем в вашем новом модуле), вы сможете перенести их в дерево исходников из target/generated-sources. Но лучше не надо. Вероятно, у нас будет изменена лишь небольшая часть, поэтому лучше озаботиться удалением лишь некоторых: увидели необходимость поменять реализацию по умолчанию, скопировали в дерево исходников, добавили в список удаляемых файлов, пересобрали — и теперь вы управляете файлом — конкретно этим файлом. В противном случае будет сложно разобраться, что конкретно изменено, да и поддержку едва ли можно будет назвать приятной
  • теперь, имея реализацию «основного» режима PMD, вы сможете легко навесить на свой JJTree-парсер обвязку ещё и для CPD по аналогии с Java или ещё какой-нибудь имеющейся реализацией
  • не забудьте реализовать метод, возвращающий имя узла для XPath запросов. При реализации по умолчанию там то ли бесконечная рекурсия получается (имя узла через toString и наоборот), то ли ещё что, в общем из-за этого ещё и дерево в PMD Designer не посмотреть, а без этого отлаживать грамматику совсем грустно
  • часть регистраций компонентов делается через добавление в META-INF/services текстовых файлов с fully qualified class name точки входа
  • то, что в правилах можно описать декларативно (например, развёрнутое описание проверки и примеры ошибок), описывается не в коде, а в category/<language name>/<ruleset>.xml — вам в любом случае придётся там регистрировать свои правила
  • … но при реализации тестов, по видимому, активно используется некий, возможно, доморощенный, механизм auto discovery, поэтому
    • если вам говорят «добавьте тривиальный тест на каждую версию языка» — лучше не спорьте, мол «мне это не нужно, это и так работает» — возможно, это механизм auto discovery
    • если вы видите тест на конкретное правило с телом класса, содержащим лишь комментарий // no additional unit tests, то это не тесты отсутствуют, это они просто лежат в ресурсах в виде XML-описания входных данных и ожидаемых реакций анализатора, сразу пачкой: несколько корректных и несколько некорректных примеров.

Маленький, но важный квест: допили PMD Designer

Но зачем? Возможно, у вас получится всё отладить без визуализатора. Во-вторых, это очень сильно поможет вашим пользователям, не знакомым с Java: они легко и просто (если это вообще применимо к XPath), ну или хотя бы без перекомпиляции PMD смогут описывать простые паттерны того, что им не нравится (в простейшем случае — style guide вроде «имя моделиковского пакета всегда начинается со строчной p»). Во-первых, допилить его очень просто.

Но вот вы выбираете свой язык, загружаете тестовый файл, и видите AST. В отличие от остальных ошибок, которые сразу видны, проблемы с PMD Designer ведут себя достаточно коварно: казалось бы, вы уже поняли, что та надпись Java в правой части меню — это не кнопка, а выпадающий список выбора языка O_o, в котором уже появилась Modelica, потому что в classpath появился новый модуль с регистрациями точек входа. Оно цветное!.. И вроде бы, победа, но какое-то оно чёрно-белое, да и выделенное поддерево можно было бы в тексте подсветить — хотя нет, подсветка есть, но обновляется криво — а ещё, как же они не догадались подсвечивать найденные совпадения с XPath… Уже прикидывая объём работ, вы задумываетесь об очередном pull request-е, но тут случайно решаете переключить язык на Java и загрузить какой-нибудь исходник самого PMD… Ой! Эээ… а оно, оказывается, нормально подсвечивает найденные совпадения и выписывает куски текста в окошко справа от запроса… Такое ощущение, что когда в JavaFX-коде во время отрисовки интерфейса происходит exception, он прерывает отрисовку, но не печатается в консоль... И подсветка поддерева работает!

В моём случае это был net.sourceforge.pmd.util.fxdesigner.util.codearea.syntaxhighlighting. В общем, нужно всего лишь добавить ма-а-аленький класс для подсветки синтаксиса на основе регулярных выражений. Обратите внимание, что оба этих изменения происходят в репозитории pmd-designer, артефакт от сборки которого нужно подложить в свой binary distribution. ModelicaSyntaxHighlighter, который нужно зарегистрировать в классе AvailableSyntaxHighlighters.

В итоге это выглядит примерно так (гифка взята из README в репозитории PMD Designer):

PMD Designer в работе

Промежуточный итог

Если вы прошли все указанные уровни, то у вас теперь есть:

  • детектор копипаста
  • движок для выполнения правил
  • визуализатор для отладки AST и приведения его в удобный для анализа вид (как мы уже видели, не все грамматики одного языка одинаково полезны!)
  • тот же визуализатор для отладки XPath-правил, которые ваши пользователи могут писать без перекомпиляции PMD и вообще знание Java (XPath, конечно, тоже не BASIC, но это хотя бы стандарт, а не местный язык запросов)

Надеюсь, у вас также есть понимание того факта, что грамматика теперь является стабильным API вашей реализации поддержки языка — не меняйте её (а точнее описываемую ею функцию преобразования исходника в AST) без крайней необходимости, а уж если поменяли, оповещайте как о breaking change, а то пользователи расстроятся: скорее всего, не все напишут тесты на свои правила, а это очень грустно, когда правила проверяли код, а потом без предупреждения перестали — почти как бекап, совершенно внезапно сломавшийся, причём год назад...

На этом история не заканчивается: предстоит как минимум написать какие-нибудь полезные правила.

С каждым узлом AST ассоциирован scope: тело класса, функции, цикла… Весь файл, на худой конец! Но и это ещё не всё: PMD штатно поддерживает scopes и declarations. Как и в других случаях, предлагается реализовывать по аналогии с другими языками, например, Моделикой (но на момент написания, логика в моём pull request-е, прямо скажем, сыровата). А в каждом скоупе есть список определений (declarations), которые он непосредственно содержит. Запускается всё это через ещё один метод класса, описывающего нашу поддержку языка. Код вычисления scopes и declarations представляет из себя visitor, называемый как-нибудь вроде ScopeAndDeclarationFinder, который бегает по дереву и развешивает по нему требуемые ярлычки — в целом, как и обычное правило, только правила, я всё-таки думаю, обычно read-only по отношению к AST.

public class ModelicaHandler extends AbstractLanguageVersionHandler }; }
}

Вывод

Скорее всего, он будет проигрывать специализированным «монстрам» вроде Clang Static Analyzer для конкретных языков, но он очень полезен для реализации статического анализа для нового языка. Статический анализатор PMD является универсальным и легко расширяемым. Но даже если анализаторы уже имеются, поддержку языка можно добавить в CPD (а это ещё проще), и получить поиск скопированных кусков кода в пределах проекта.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть