Clang-Tidy Для Автоматического Рефакторинга Кода

Инструментов для анализа кода много: они могут искать ошибки, узкие места, плохую архитектуру и предлагать оптимизацию.

Но много ли среди них инструментов, способных не только найти, но и самостоятельно исправить код? Представьте, что у вас есть большой проект на C или C++ (или даже C#), который разрабатывался в течение многих лет и многими людьми.

В результате разные части проекта выглядят по-разному — не существует единого стиля именования переменных, функций или типов данных.

То есть в разных частях проекта использовались разные стили кодирования: где-то имена в верхнем регистре, где-то CamelCase, где-то с префиксами, где-то — без.

Некрасиво, в общем.



Clang-Tidy для автоматического рефакторинга кода

А теперь вы собираетесь навести порядок, начав, например, с имен переменных.

Мы решили, что все имена должны быть в верхнем регистре и с префиксом типа данных (так называемый Венгерская нотация ).

Вот пример переименования:

Clang-Tidy для автоматического рефакторинга кода

Некоторые, наверное, подумали: «Ну и в чем проблема? Автозамена поможет. В крайнем случае, напишите на коленях скрипт Python." Хорошо, давайте разберемся: простая автозамена заменит все и везде, независимо от области действия.

Если у вас есть переменные с одинаковыми именами как в глобальной области видимости, так и в локальной, то все будет переименовано единообразно.

Но мы можем захотеть дать разные имена, в зависимости от контекста.

Это значит, что автозамена не подходит. Мы пишем сценарий на коленях.

Разобраться в масштабах не сложно.

Но угадайте, переменные, функции и даже типы данных иногда могут иметь одинаковые имена.

То есть на самом деле такая конструкция вполне легальна( однако только в GNU C ):

  
  
  
  
  
  
  
  
  
  
  
  
   

typedef int Something; int main() { int Something(Something Something) { return Something + Something; } printf("This is Something %d!\n", Something(10)); return 0; }



C:\HelloWorld.exe This is Something 20!

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

Вам ничего не напоминает? Мы собираемся написать компилятор! Вернее, та его часть, которая строит абстрактное синтаксическое дерево.

Абстрактное синтаксическое дерево (Абстрактное синтаксическое дерево, AST) — это, по сути, ваш исходный код, разобранный на мельчайшие атомы, то есть переменные, константы, функции, типы данных и т. д., которые выложены в ориентированный граф.



typedef int SomeNumber; int SomeFunction(SomeNumber num) { return num + num; } int main() { printf("This is some number = %d!\n", SomeFunction(10)); return 0; }



Clang-Tidy для автоматического рефакторинга кода



Clang-Tidy для автоматического рефакторинга кода

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

Никакой регистрации и смс!

Где растут деревья?

Поскольку мы рассматриваем проект на языке C (или C++), первое, что приходит на ум, — это великая и мощная коллекция компиляторов GNU, также известная как GCC. Фронтенд в GCC генерирует AST-дерево для собственных нужд и позволяет экспортировать его для дальнейшего использования.

Это, в принципе, удобно для ручного анализа кода «изнутри», но если мы задались целью внести автоматические исправления в сам код, то все остальное для этого в этом случае придется писать самостоятельно.

Не менее мощный LLVM/clang также может экспортировать AST, но этот проект пошел еще дальше и предложил готовый инструмент для парсинга и анализа дерева — Clang-Tidy. Это инструмент 3-в-1 — он генерирует дерево, анализирует его, выполняет проверки и автоматически вносит исправления в код там, где это необходимо.

Для нас также важно то, что это Open Source проект с доступным исходным кодом, в который легко добавить свои новые функции, если вдруг чего-то не хватает.

Лес рубят - щепки летят

Чтобы изучить дерево AST нашего проекта, нам понадобится Clang. Если у вас его еще нет, скачать готовую сборку можно со страницы проекта LLVM: https://clang.llvm.org/ Для начала нужно понять, что построение дерева — это часть обычного процесса компиляции.

Поэтому вы не можете просто взять отдельный исходный файл и построить по нему дерево.

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

Второй момент: чтобы получить «правильное» дерево, нужно скомпилировать с той же конфигурацией, которая используется в вашем проекте.

Если какая-либо единица перевода в вашем проекте скомпилирована, например, с ключом -DDEBUG, то при построении дерева нужно использовать тот же ключ.

В противном случае ваш код может либо вообще не скомпилироваться (тогда вы сразу поймете, что что-то идет не так), либо построить не то вам дерево (и вы потратите неделю, пытаясь во всем разобраться).

Первый шаг — убедиться, что ваш проект успешно компилируется в Clang. Если вдруг нет, не отчаивайтесь.

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

Правда, документацию еще читать придется.

Эта команда отобразит все дерево AST сразу:

clang -c -Xclang -ast-dump
Вы можете быть удивлены, насколько он огромен.

Для моего примера «Hello World» выше из 7 строк кода дерево получилось 6259 строк.

Это потому, что он также будет содержать все, что было включено из стандартных заголовочных файлов: типы данных, функции и т. д. Поиск того, что вам нужно в таком огромном объеме информации, может оказаться утомительным, поэтому удобнее использовать команду clang-query для извлечения только необходимой вам информации.

Запросы пишутся с использованием специального синтаксиса AST Matchers, который описан здесь.

здесь Например, следующий запрос предоставит нам весь внутренний мир функции под названием «SomeFunction»:

clang-query> set output dump clang-query> match functionDecl(hasName("SomeFunction")) Match #1: Binding for "root": FunctionDecl 0x195581994f0 <C:\HelloWorld.c:5:1, line:8:1> line:5:5 used SomeFunction 'int (SomeNumber)' |-ParmVarDecl 0x19558199420 <col:18, col:29> col:29 used num 'SomeNumber':'int' `-CompoundStmt 0x19558199638 <line:6:1, line:8:1> `-ReturnStmt 0x19558199628 <line:7:2, col:15> `-BinaryOperator 0x19558199608 <col:9, col:15> 'int' '+' |-ImplicitCastExpr 0x195581995d8 <col:9> 'SomeNumber':'int' <LValueToRValue> | `-DeclRefExpr 0x19558199598 <col:9> 'SomeNumber':'int' lvalue ParmVar 0x19558199420 'num' 'SomeNumber':'int' `-ImplicitCastExpr 0x195581995f0 <col:15> 'SomeNumber':'int' <LValueToRValue> `-DeclRefExpr 0x195581995b8 <col:15> 'SomeNumber':'int' lvalue ParmVar 0x19558199420 'num' 'SomeNumber':'int' 1 match.

Что ж, попробуем запустить сам Clang-Tidy, просто ради интереса:

C:\clang-tidy HelloWorld.c -checks=* -- C:\HelloWorld.c:12:53: warning: 10 is a magic number; consider replacing it with a named constant [cppcoreguidelines-avoid-magic-numbers] printf("This is some number = %d!\n", SomeFunction(10)); ^ C:\HelloWorld.c:12:53: warning: 10 is a magic number; consider replacing it with a named constant [readability-magic-numbers]

Работает! И даже встроенные шашки подают признаки жизни.



Пристегнитесь, начнем программировать!

О дереве мы поговорили, пора перейти к нашей задаче по переименованию.

Прежде всего нам нужны исходники LLVM. Да, вот весь проект LLVM, огромный, отсюда: репозиторий Еще вам понадобится cmake, вот он Здесь Удобство проектов, написанных с помощью cmake, заключается в том, что вы можете автоматически генерировать проект для нескольких разных сред разработки.

Например, у меня Visual Studio 2019 для Windows, поэтому мой алгоритм получения рабочего проекта выглядит так:

git-клон https://github.com/llvm/llvm-project.git компакт-диск llvm-проекта сборка mkdir сборка компакт-диска cmake -DLLVM_ENABLE_PROJECTS='clang;clang-tools-extra' -G 'Visual Studio 16 2019' -A x64 -Thost=x64 .

/llvm

После этих действий будет сгенерирован LLVM.sln, который вы сможете открыть в Visual Studio и собрать необходимые компоненты.

Минимальный набор: сам clang-tidy и clang-apply-замены.

Если вам совсем не жалко времени, то можно собрать всю LLVM, но в целом для нашей задачи это не требуется.

Исходники, которые нас интересуют, находятся в папке llvm\clang-tools-extra\clang-tidy. Здесь вы можете посмотреть примеры других чекеров, то есть модулей в Clang-Tidy для выполнения различных проверок.

Они сгруппированы в несколько категорий, таких как читаемость, портативность, производительность и т. д. Их назначение, в принципе, понятно из названия.

Еще здесь есть скрипт, который поможет нам сгенерировать шаблон для нашего чекера:

python add_new_check.py разное ультра-крутое переименование переменных
Здесь «разное» — это категория, в которой мы определили нашу программу проверки, а «ультра-крутая-переименователь-переменной» — это имя нашей проверки.

Генератор скриптов создаст несколько новых файлов, в том числе для документации и тестов.

Но на этом этапе нас интересуют только два, в папке разное: UltraCoolVariableRenamer.h и UltraCoolVariableRenamer.cpp. Важный момент: поскольку наш проект для Visual Studio создан с помощью cmake, новые файлы сами по себе в проект не попадут. Для этого вам нужно снова перезапустить cmake, и он автоматически обновит проект. Собираем и запускаем Clang-Tidy. Видим, что появился наш чекер и показывает сообщения от сгенерирован шашка-заготовка, радуемся этому:

C:\clang-tidy HelloWorld.c -header-filter=.

* -checks=-*,misc-ultra-cool-variable-renamer – 354 warnings generated. C:\HelloWorld.c:5:5: warning: function 'SomeFunction' is insufficiently awesome [misc-ultra-cool-variable-renamer] int SomeFunction(SomeNumber num) ^ C:\HelloWorld.c:5:5: note: insert 'awesome' int SomeFunction(SomeNumber num) ^ awesome_ C:\HelloWorld.c:10:5: warning: function 'main' is insufficiently awesome [misc-ultra-cool-variable-renamer] int main() ^ C:\HelloWorld.c:10:5: note: insert 'awesome' int main() ^ awesome_

Генератор создал для нас не только скелет нового чекера, но и включил пример кода, предлагающий немного улучшить имена функций.

Мы видим эти сообщения здесь.

Если мы посмотрим на код самого чекера, то увидим, что он состоит всего из двух методов: RegisterMatchers(…) и check(…).



void UltraCoolVariableRenamerCheck::registerMatchers(MatchFinder* Finder) { // FIXME: Add matchers. Finder->addMatcher(functionDecl().

bind("x"), this); } void UltraCoolVariableRenamerCheck::check(const MatchFinder::MatchResult& Result) { // FIXME: Add callback implementation. const auto* MatchedDecl = Result.Nodes.getNodeAs<FunctionDecl>("x"); if (MatchedDecl->getName().

startswith("awesome_")) return; diag(MatchedDecl->getLocation(), "function %0 is insufficiently awesome") << MatchedDecl; diag(MatchedDecl->getLocation(), "insert 'awesome'", DiagnosticIDs::Note) << FixItHint::CreateInsertion(MatchedDecl->getLocation(), "awesome_"); }

Метод RegisterMatchers(.

) вызывается один раз, при старте Clang-Tidy, и нужен для того, чтобы добавить правила «отлова» нужных нам мест в AST-дереве.

В то же время здесь используется тот же синтаксис сопоставителей AST, который мы использовали ранее в clang-запросе.

Затем для каждого триггера зарегистрированного правила будет вызываться метод check(…).

Сгенерированный шаблон проверки регистрирует только одно правило для поиска объявлений функций.

Для каждого такого объявления будет вызываться метод check(), содержащий всю логику проверки кода.

Для нашей задачи нам нужно проверять переменные, поэтому изменим этот код на:

auto VariableDeclaration = varDecl(); Finder->addMatcher(VariableDeclaration.bind("variable_declaration"), this); auto VariableReference = declRefExpr(to(varDecl())); Finder->addMatcher(VariableReference.bind("variable_reference"), this);

Поскольку мы хотим переименовывать переменные не только там, где они объявлены, но и везде, где они используются, нам нужно прописать два правила.

Обратите внимание, что для регистрации правила необходимо использовать тот тип объекта из AST-дерева, который мы хотим обработать.

То есть varDecl для объявлений переменных и declRefExpr для ссылок на некоторый объявленный объект. Поскольку объявленный объект может быть чем-то большим, чем просто переменная, здесь мы используем дополнительный критерий (varDecl()), чтобы отфильтровать только ссылки на переменные.

Все, что нам нужно сделать, это написать содержимое метода check(.

), который проверит имя переменной и выдаст предупреждение, если оно не соответствует используемому нами стандарту:

void UltraCoolVariableRenamerCheck::check(const MatchFinder::MatchResult& Result) { const DeclRefExpr* VariableRef = Result.Nodes.getNodeAs<DeclRefExpr>("variable_reference"); const VarDecl* VariableDecl = Result.Nodes.getNodeAs<VarDecl>("variable_declaration"); SourceLocation location; StringRef name; StringRef type; if (VariableDecl) { location = VariableDecl->getLocation(); name = VariableDecl->getName(); type = StringRef(VariableDecl->getType().

getAsString()); } else if (VariableRef) { location = VariableRef->getLocation(); name = VariableRef->getDecl()->getName(); type = StringRef(VariableRef->getDecl()->getType().

getAsString()); } else { return; } if (!checkVarName(name, type)) { diag(location, "variable '%0' does not comply with the coding style") << name; } }

Что здесь происходит: метод check(.

) можно вызвать как для объявления переменной, так и для ее использования.

Мы обрабатываем оба этих варианта.

Далее функция checkVarName(.

) (оставим ее содержимое за кадром) проверяет, соответствует ли имя переменной принятому нами стилю кодирования, и если соответствия нет, выводим предупреждение.

Давайте убедимся, что мы видим это предупреждение во всех трех местах нашего кода, где встречается переменная:

C:\HelloWorld.c:5:29: warning: variable 'num' does not comply with the coding style [misc-ultra-cool-variable-renamer] int SomeFunction(SomeNumber num) ^ C:\HelloWorld.c:7:9: warning: variable 'num' does not comply with the coding style [misc-ultra-cool-variable-renamer] return num + num; ^ C:\HelloWorld.c:7:15: warning: variable 'num' does not comply with the coding style [misc-ultra-cool-variable-renamer] return num + num; ^



Еще трюки – добавление исправления

На самом деле мы уже почти все сделали.

Чтобы Clang-Tidy не только показывал предупреждения, но и сам вносил исправления, нам достаточно добавить пару строк, чтобы точно описать, как и что нужно исправить.

Напишем функцию генерироватьVarName(StringRef oldVarName, StringRef varType), которая будет преобразовывать старое имя переменной в верхний регистр и в зависимости от типа данных добавлять к ней префикс (примитивное содержимое этой функции мы также оставим за скобками ).

Все, что нам осталось, это добавить в вывод диаграммы правило замены, указывающее, где и чем заменять:

if (!checkVarName(name, type)) { diag(location, "variable '%0' does not comply with the coding style") << name << type; diag(location, "replace to '%0'", DiagnosticIDs::Note) << generateVarName(name, type) << FixItHint::CreateReplacement(location, generateVarName(name, type)); }

Теперь, если мы снова запустим Clang-Tidy, мы увидим не только сообщения о неправильных именах, но и предлагаемые исправления.

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

C:\HelloWorld.c:5:29: warning: variable 'num' does not comply with the coding style [misc-ultra-cool-variable-renamer] int SomeFunction(SomeNumber num) ^ C:\HelloWorld.c:5:29: note: replace to 'snNUM' int SomeFunction(SomeNumber num) ^~~ 'snNUM' C:\HelloWorld.c:7:9: warning: variable 'num' does not comply with the coding style [misc-ultra-cool-variable-renamer] return num + num; ^ C:\HelloWorld.c:7:9: note: replace to 'snNUM' return num + num; ^~~ 'snNUM' C:\HelloWorld.c:7:15: warning: variable 'num' does not comply with the coding style [misc-ultra-cool-variable-renamer] return num + num; ^ C:\HelloWorld.c:7:15: note: replace to 'snNUM' return num + num; ^~~ 'snNUM'

Итак, мы сделали автоматическую коррекцию всех переменных в нашем проекте, написав менее 100 строк кода.

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

И тем не менее это вполне рабочий прототип, поверх которого можно добавить любую логику, какую захотите.



Видим бочку с медом, где деготь?

Ну метод хороший, а как же без ограничений? Основное ограничение исходит из самой идеи использования синтаксического дерева: в поле зрения и обработки попадет только то, что есть в дереве.

То, что не попало в дерево, останется неизменным.

Почему вдруг это может быть плохо?

  • Все, что отключено условной компиляцией, не будет включено в дерево.

    Помните, что мы строим дерево, используя реальные параметры вашего проекта? Итак, если в вашем проекте есть части кода, которые выглядят следующим образом, например:

    #if ARCH==ARM do_something(); #elif ARCH==MIPS do_something_else(); #endif

    .

    тогда у меня плохие новости - в дерево попадет только то, что не будет вырезано препроцессором на основе констант вашего проекта.

    Что тогда делать с «неактивным» кодом? Единого рецепта здесь нет: можно запустить Tidy несколько раз, используя другой набор констант, или экспортировать список переименованных символов и затем попробовать обновить оставшиеся части кода с помощью автозамены.

    В общем, вам придется проявить интеллект и фантазию.

  • Все, что не является исходным кодом, также не будет включено в дерево.

    То есть комментарии в исходном коде останутся нетронутыми.

    Если в ваших комментариях упоминаются ваши переменные или функции, вам придется обновить их вручную.

    Вы также можете попытаться автоматизировать это извне, используя переименование информации, которую Clang-Tidy может экспортировать.

  • С макросами не все так просто.

    Они обрабатываются препроцессором до начала самой компиляции.

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

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



Насколько глубока кроличья нора?

С простой задачей мы разобрались, а как насчет сложной? К этому моменту у вас наверняка возникло много вопросов: насколько умными могут быть шашки, что они умеют и где они могут черпать идеи? Прежде всего, с помощью Clang-Tidy можно производить практически любой анализ и любые манипуляции с исходным кодом.

В примере выше мы рассмотрели работу с переменными как простейший случай.

Однако функции, константы, классы, типы данных и все остальное, что составляет код, также разбирается на атомы в дереве AST и также доступно в Clang-Tidy. В качестве отправной точки для изучения того, какие проблемы можно решить с помощью Clang-Tidy и как это можно сделать, советую посмотреть исходный код других чекеров, который находится в одной папке с исходниками Clang-Tidy. Это отличное дополнение к официальной документации LLVM, которая, честно говоря, не очень многословна.

Особенно интересно посмотреть, как сделаны шашки Google. Напоследок хотелось бы пожелать вам никогда не сдаваться, проявить изобретательность и фантазию и… конечно, удачи! Теги: #программирование #C++ #проектирование и рефакторинг #ast #llvm #компиляторы #Clang #clang-tidy

Вместе с данным постом часто просматривают: