[Перевод] Анализ AST и рефакторинг кода в Clang
В продолжение темы кастомизации компилятора С++ публикую перевод еще одной интересной статьи от Eli Bendersky AST matchers and Clang refactoring tools.
Инструментарий Clang вызывает большой интерес и внимание к разработке в последние годы. Наконец-то у нас есть удобный, точный, с открытым исходным кодом и хорошо поддерживаемый фреймворк для программного анализа и рефакторинга кода C++ и это я нахожу это очень захватывающим!
Прекрасным результатом этого быстрого темпа разработки является то, что постоянно появляются новые API и инструменты.
Например, некоторое время назад разработчики инструментария Clang выяснили, что людям, выполняющим обходы AST, приходится писать много повторяющегося кода, чтобы найти нужные им узлы AST, поэтому они придумали отличный новый API под названием AST matchers.
Посетители против сопоставителей (visitors vs. matchers)
Вот мотивирующий пример. Предположим, мы ищем переменные типа указателя, используемые в сравнениях if
. Чтобы сделать это более конкретным, предположим, что мы ищем случаи, когда переменная типа указателя находится в левой части сравнения равенства (==). Чтобы найти такие узлы в рекурсивном посетителе, нам пришлось бы написать что-то вроде этого:
bool VisitIfStmt(IfStmt *s) {
if (const BinaryOperator *BinOP =
llvm::dyn_cast(s->getCond())) {
if (BinOP->getOpcode() == BO_EQ) {
const Expr *LHS = BinOP->getLHS();
if (const ImplicitCastExpr *Cast =
llvm::dyn_cast(LHS)) {
LHS = Cast->getSubExpr();
}
if (const DeclRefExpr *DeclRef = llvm::dyn_cast(LHS)) {
if (const VarDecl *Var =
llvm::dyn_cast(DeclRef->getDecl())) {
if (Var->getType()->isPointerType()) {
Var->dump(); // УРА, нашел!!!!!!!!!
}
}
}
}
}
return true;
}
Это довольно много кода, но ничего необычного, если вы работали с Clang AST некоторое время. Возможно, его можно сжать до более короткой формы, но главная проблема в том, что для написания этого кода нужно просмотреть довольно много документации и заголовочных файлов, чтобы выяснить, какие методы нужно вызывать и какие типы объектов они возвращают.
А вот эквивалентный код для AST matcher`a (далее буду использовать термин сопоставитель):
Finder.addMatcher(
ifStmt(hasCondition(binaryOperator(
hasOperatorName("=="),
hasLHS(ignoringParenImpCasts(declRefExpr(
to(varDecl(hasType(pointsTo(AnyType))).bind("lhs"))
)))
))),
&HandlerForIf);
Есть некоторая разница, не так ли? Декларативная природа определений для AST сопоставителей делает их код очень естественным для чтения и сопоставления с фактической проблемой. HandlerForIf
— это объект MatchCallback
, который имеет прямой доступ к связанным узлам сопоставителя:
struct IfStmtHandler : public MatchFinder::MatchCallback {
virtual void run(const MatchFinder::MatchResult &Result) {
const VarDecl *lhs = Result.Nodes.getNodeAs("lhs");
lhs->dump(); // УРА, нашел!!!!!!!!!
}
};
На самом деле, на официальном сайте Clang доступно довольно много документации о сопоставителях AST. Для полного примера, который может быть построен вне дерева LLVM, я переделал пример инструментария из предыдущей статьи, теперь с сопоставителями AST (все доступно в репозитории llvm-clang-samples). *
*) Так как проект llvm и clang, как его часть, развиваются очень стремительно, а статья была написана далеко не вчера, то ссылка автора ведет на уже не поддерживамый репозиторий. Тем не менее код в статье и сам принцип анализа AST являются актуальными.
Использование clang-query для проверки сопоставителей и исследования AST
Для упрощения использования сопоставителей при анализе AST в проекте Clang был разработан новый инструмент — clang-query
. Это интерактивный вычислитель для сопоставителей AST, который можно использовать как для их быстрого тестирования без компиляции строчки кода, так и для изучения AST.
Допустим, мы хотим разработать сопоставитель AST для сравнения указателей, похожий на показанный выше. Вот пример входного файла, над которым мы будем работать:
int foo(int* p, int v) {
if (p == 0) {
return v + 1;
} else {
return v - 1;
}
}
Давайте запустим clang-query
и посмотрим, что он может сделать:
$ clang-query /tmp/iflhsptr.c --
clang-query> set output diag
clang-query> match functionDecl()
Match #1:
/tmp/iflhsptr.c:1:1: note: "root" binds here
int foo(int* p, int v) {
^~~~~~~~~~~~~~~~~~~~~~~~
1 match.
Это базовый smoke тест (простой тест, который подтверждает корректную работу ключевых функций), чтобы увидеть, как он найдет объявление функции. Режим вывода установлен при вызове программы в первой команде. Также можно настроить инструмент для выгрузки или печати AST, но для нашей цели удобен диагностический вывод.
Вот как мы можем сопоставить более глубокие узлы и связать их:
clang-query> match ifStmt(hasCondition(binaryOperator(hasOperatorName("==")).bind("op")))
Match #1:
/tmp/iflhsptr.c:2:7: note: "op" binds here
if (p == 0) {
^~~~~~
/tmp/iflhsptr.c:2:3: note: "root" binds here
if (p == 0) {
^~~~~~~~~~~~~
1 match.
Если мы намерены предоставить собственные привязки, корневую привязку можно отключить:
clang-query> set bind-root false
Давайте посмотрим несколько совпадений:
clang-query> match varDecl().bind("var")
Match #1:
/tmp/iflhsptr.c:1:9: note: "var" binds here
int foo(int* p, int v) {
^~~~~~
Match #2:
/tmp/iflhsptr.c:1:17: note: "var" binds here
int foo(int* p, int v) {
^~~~~
2 matches.
На этом я остановлюсь, потому что длинные сопоставители не очень удобно форматировать в блоге, но я уверен, что вы поняли идею. Совершенно очевидно, насколько будет полезен этот инструмент для разработки сопоставителей. Он все еще новый и имеет некоторые грубые края, но уже весьма полезен.
Инструменты рефакторинга и замены
С ростом использования libTooling
неудивительно, что его разработчики продолжают придумывать более высокие уровни абстракции, которые помогают писать новые инструменты с меньшими усилиями. Представленная выше структура сопоставлений AST является одним из примеров. Другим примером является RefactoringTool
, подкласс ClangTool
, который позволяет создавать новые инструменты с очень небольшим количеством кода. Скоро я покажу пример, но сначала несколько слов о заменах.
Инструменты, которые я демонстрировал до сих пор, использовали Rewriter
для изменения базового исходного кода в ответ на обнаружение интересных вещей в AST. Это хороший подход, но у него есть проблема масштабирования для больших проектов.
Представьте себе запуск инструмента в большом проекте с множеством исходных файлов и множеством заголовочных файлов. Некоторые переписывания, возможно, придется выполнять в заголовочных файлах, но как это сделать, учитывая, что одни и те же заголовки включаются в несколько единиц трансляции? Некоторые правки могут оказаться дублированными или даже конфликтующими, и это проблема.
Решением являются замены (Replacements
). Задача преобразования источника делится на два отдельных этапа:
Пользовательские инструменты проходят через исходную базу, находят шаблоны рефакторинга для применения и генерируют сериализованные замены в файлы. Думайте о заменах как о чем-то вроде файлов исправлений (точных указаний о том, как изменить исходный файл), но в несколько более удобном формате.
clang-apply-replacements
может запуститься с доступом ко всем заменам, выполнить необходимую дедупликацию и разрешение конфликтов, а затем фактически применить изменения к источнику.
Такой подход также позволяет эффективно распараллеливать рефакторинг на огромных базах кода, хотя в мире не так много проектов и компаний с исходным кодом настолько большим, чтобы это стало реальной проблемой.
Тогда вернемся к примеру. Я взял простой пример инструмента из предыдущей статьи (просто нашел интересные узлы if
и добавил в них несколько комментариев) и переписал его еще раз, используя RefactoringTool
и Replacements
. Полный код доступен в проекте примеров, но он настолько короткий, что я могу показать большую его часть здесь.
Вот полная функция main. Для простоты она только выводит замены в stdout
вместо их сериализации или применения:
int main(int argc, const char **argv) {
CommonOptionsParser op(argc, argv, ToolingSampleCategory);
RefactoringTool Tool(op.getCompilations(), op.getSourcePathList());
// Настройка обратных вызовов сопоставления AST.
IfStmtHandler HandlerForIf(&Tool.getReplacements());
MatchFinder Finder;
Finder.addMatcher(ifStmt().bind("ifStmt"), &HandlerForIf);
// Запустить инструмент и собрать список замен. Мы могли бы вызвать
// runAndSave, который деструктивно перезапишет файлы с помощью
// их новое содержимое. Однако, для демонстрационных целей это
// интересно показать замены.
if (int Result = Tool.run(newFrontendActionFactory(&Finder).get())) {
return Result;
}
llvm::outs() << "Replacements collected by the tool:\n";
for (auto &r : Tool.getReplacements()) {
llvm::outs() << r.toString() << "\n";
}
return 0;
}
IfStmtHandler
— это просто MatchCallback
, который срабатывает в операторах if
:
class IfStmtHandler : public MatchFinder::MatchCallback {
public:
IfStmtHandler(Replacements *Replace) : Replace(Replace) {}
virtual void run(const MatchFinder::MatchResult &Result) {
// Соответствующий оператор 'if' был привязан к 'ifStmt'
if (const IfStmt *IfS =
Result.Nodes.getNodeAs("ifStmt")) {
const Stmt *Then = IfS->getThen();
Replacement Rep(*(Result.SourceManager), Then->getLocStart(), 0,
"// the 'if' part\n");
Replace->insert(Rep);
if (const Stmt *Else = IfS->getElse()) {
Replacement Rep(*(Result.SourceManager), Else->getLocStart(), 0,
"// the 'else' part\n");
Replace->insert(Rep);
}
}
}
private:
Replacements *Replace;
};
Обратите внимание, как мало повторяющихся участков содержит этот код. Инструмент настраивается всего в несколько строк кода и большая его часть связана с реальным рефакторингом. Это определенно делает написание инструментов для анализа кода более быстрым и простым, чем когда-либо прежде.