image


Еще одна статья в продолжение темы анализа сходных текстов на С/С++ с помощью Clang. Предыдущие публикации:



Это не перевод довольно подробной публикации Emitting Diagnostics in Clang от Peter Goldsborough про различные нюансы диагностических инструментов у Clang, а преимущественно адаптация старого кода под текущую версию компилятора.


И основная идея, которая меня заинтересовала в исходной публикации, это использование инструмента FixIt из набора диагностики clang для внесения исправлений в исходные файлы.


Чтобы было понятно о чем идет речь.


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


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


Статью я переводить не стал, так как материала много и он технически сложный, а заодно и передаю привет ППА Хабра.


Рабочий код примера из статьи
// Clang includes
#include "clang/AST/ASTConsumer.h"
#include "clang/AST/ASTContext.h"
#include "clang/AST/Expr.h"
#include "clang/ASTMatchers/ASTMatchFinder.h"
#include "clang/ASTMatchers/ASTMatchers.h"
#include "clang/Basic/Diagnostic.h"
#include "clang/Basic/SourceLocation.h"
#include "clang/Frontend/FrontendAction.h"
#include "clang/Rewrite/Frontend/FixItRewriter.h"
#include "clang/Tooling/CommonOptionsParser.h"
#include "clang/Tooling/Tooling.h"

// LLVM includes
#include "llvm/ADT/ArrayRef.h"
#include "llvm/ADT/StringRef.h"
#include "llvm/Support/CommandLine.h"
#include "llvm/Support/raw_ostream.h"

// Standard includes
#include <cassert>
#include <memory>
#include <string>
#include <type_traits>
#include <iostream>

namespace MinusTool {

    class FixItRewriterOptions : public clang::FixItOptions {
    public:
        using super = clang::FixItOptions;

        /// Constructor.
        ///
        /// The \p RewriteSuffix is the option from the command line.

        explicit FixItRewriterOptions(const std::string& RewriteSuffix)
        : RewriteSuffix(RewriteSuffix) {
            super::InPlace = false;
        }

        /// For a file to be rewritten, returns the (possibly) new filename.
        ///
        /// If the \c RewriteSuffix is empty, returns the \p Filename, causing
        /// in-place rewriting. If it is not empty, the \p Filename with that suffix
        /// is returned.

        std::string RewriteFilename(const std::string& Filename, int& fd) override {
            fd = -1;

            llvm::errs() << "Rewriting FixIts ";

            if (RewriteSuffix.empty()) {
                llvm::errs() << "in-place\n";
                return Filename;
            }

            const auto NewFilename = Filename + RewriteSuffix;
            llvm::errs() << "from " << Filename << " to " << NewFilename << "\n";

            return NewFilename;
        }

    private:
        /// The suffix appended to rewritten files.
        std::string RewriteSuffix;
    };

    class MatchHandler : public clang::ast_matchers::MatchFinder::MatchCallback {
    public:
        using MatchResult = clang::ast_matchers::MatchFinder::MatchResult;
        using RewriterPointer = std::unique_ptr<clang::FixItRewriter>;

        /// Constructor.
        ///
        /// \p DoRewrite and \p RewriteSuffix are the command line options passed
        /// to the tool.

        MatchHandler(bool DoRewrite, const std::string& RewriteSuffix)
        : FixItOptions(RewriteSuffix), DoRewrite(DoRewrite) {
        }

        /// Runs the MatchHandler's action.
        ///
        /// Emits a diagnostic for each matched expression, optionally rewriting the
        /// file in-place or to another file, depending on the command line options.

        void run(const MatchResult& Result) {
            auto& Context = *Result.Context;

            const auto& Op = Result.Nodes.getNodeAs<clang::BinaryOperator>("op");
            assert(Op != nullptr);

            const auto StartLocation = Op->getOperatorLoc();
            const auto EndLocation = StartLocation.getLocWithOffset(+1);
            const clang::SourceRange SourceRange(StartLocation, EndLocation);
            const auto FixIt = clang::FixItHint::CreateReplacement(SourceRange, "-");

            auto& DiagnosticsEngine = Context.getDiagnostics();

            // The FixItRewriter is quite a heavy object, so let's
            // not create it unless we really have to.
            RewriterPointer Rewriter;
            if (DoRewrite) {
                Rewriter = createRewriter(DiagnosticsEngine, Context);
            }

            const auto ID =
                    DiagnosticsEngine.getCustomDiagID(clang::DiagnosticsEngine::Warning,
                    "This should probably be a minus");

            DiagnosticsEngine.Report(StartLocation, ID).AddFixItHint(FixIt);

            if (DoRewrite) {
                assert(Rewriter != nullptr);
                Rewriter->WriteFixedFiles();
            }
        }

    private:
        /// Allocates a \c FixItRewriter and sets it as the client of the given \p
        /// DiagnosticsEngine.
        ///
        /// The \p Context is forwarded to the constructor of the \c FixItRewriter.

        RewriterPointer createRewriter(clang::DiagnosticsEngine& DiagnosticsEngine,
                clang::ASTContext& Context) {
            auto Rewriter =
                    std::make_unique<clang::FixItRewriter>(DiagnosticsEngine,
                    Context.getSourceManager(),
                    Context.getLangOpts(),
                    &FixItOptions);

            // Note: it would make more sense to just create a raw pointer and have the
            // DiagnosticEngine own it. However, the FixItRewriter stores a pointer to
            // the client of the DiagnosticsEngine when it gets constructed with it.
            // If we then set the rewriter to be the client of the engine, the old
            // client gets destroyed, leading to happy segfaults when the rewriter
            // handles a diagnostic.
            DiagnosticsEngine.setClient(Rewriter.get(), /*ShouldOwnClient=*/false);

            return Rewriter;
        }

        FixItRewriterOptions FixItOptions;
        bool DoRewrite;
    };

    /// Consumes an AST and attempts to match for the
    /// kinds of nodes we are looking for.

    class Consumer : public clang::ASTConsumer {
    public:
        /// Constructor.
        ///
        /// All arguments are forwarded to the \c MatchHandler.

        template <typename... Args>
        explicit Consumer(Args&&... args) : Handler(std::forward<Args>(args)...) {
            using namespace clang::ast_matchers;

            // Want to match:
            // int x = 4   +   2;
            //     ^   ^   ^   ^
            //   var  lhs op  rhs

            // clang-format off
            const auto Matcher = varDecl(
                    hasType(isInteger()),
                    hasInitializer(binaryOperator(
                    hasOperatorName("+"),
                    hasLHS(integerLiteral().bind("lhs")),
                    hasRHS(integerLiteral().bind("rhs"))).bind("op"))).bind("var");
            // clang-format on

            MatchFinder.addMatcher(Matcher, &Handler);
        }

        /// Attempts to match the match expression defined in the constructor.

        void HandleTranslationUnit(clang::ASTContext& Context) override {
            MatchFinder.matchAST(Context);
        }

    private:
        /// Our callback for matches.
        MatchHandler Handler;

        /// The MatchFinder we use for matching on the AST.
        clang::ast_matchers::MatchFinder MatchFinder;
    };

    class Action : public clang::ASTFrontendAction {
    public:
        using ASTConsumerPointer = std::unique_ptr<clang::ASTConsumer>;

        /// Constructor, taking the \p RewriteOption and \p RewriteSuffixOption.

        Action(bool DoRewrite, const std::string& RewriteSuffix)
        : DoRewrite(DoRewrite), RewriteSuffix(RewriteSuffix) {
        }

        /// Creates the Consumer instance, forwarding the command line options.

        ASTConsumerPointer CreateASTConsumer(clang::CompilerInstance& Compiler,
                llvm::StringRef Filename) override {
            return std::make_unique<Consumer>(DoRewrite, RewriteSuffix);
        }

    private:
        /// Whether we want to rewrite files. Forwarded to the consumer.
        bool DoRewrite;

        /// The suffix for rewritten files. Forwarded to the consumer.
        std::string RewriteSuffix;
    };
} // namespace MinusTool

namespace {
    llvm::cl::OptionCategory MinusToolCategory("minus-tool options");

    llvm::cl::extrahelp MinusToolCategoryHelp(R"(
This tool turns all your plusses into minuses, because why not.
Given a binary plus operation with two integer operands:

int x = 4 + 2;

This tool will rewrite the code to change the plus into a minus:

int x = 4 - 2;

You're welcome.
)");

    llvm::cl::opt<bool>
    RewriteOption("rewrite",
            llvm::cl::init(false),
            llvm::cl::desc("If set, emits rewritten source code"),
            llvm::cl::cat(MinusToolCategory));

    llvm::cl::opt<std::string> RewriteSuffixOption(
            "rewrite-suffix",
            llvm::cl::desc("If -rewrite is set, changes will be rewritten to a file "
            "with the same name, but this suffix"),
            llvm::cl::cat(MinusToolCategory));

    llvm::cl::extrahelp
    CommonHelp(clang::tooling::CommonOptionsParser::HelpMessage);
} // namespace

/// A custom \c FrontendActionFactory so that we can pass the options
/// to the constructor of the tool.

struct ToolFactory : public clang::tooling::FrontendActionFactory {

    std::unique_ptr<clang::FrontendAction> create() override {
        return std::unique_ptr<clang::FrontendAction>(new MinusTool::Action(RewriteOption, RewriteSuffixOption));
    }
};

auto main(int argc, const char* argv[]) -> int {
    using namespace clang::tooling;

    auto OptionsParser = CommonOptionsParser::create(argc, argv, MinusToolCategory);
    if (!OptionsParser) {
        llvm::outs() << OptionsParser.takeError();
        return -1;
    }
    ClangTool Tool(OptionsParser->getCompilations(), OptionsParser->getSourcePathList());

    return Tool.run(new ToolFactory());
}

Сслка на репозиторий https://github.com/rsashka/clang_fixit

Комментарии (2)


  1. simplepersonru
    09.01.2025 12:37

    КМК для решения этой задачи

    ... автоматизация поиска и замены различных синтаксических конструкций в исходных файлах С++. Причем не только в виде вывода пользователю в консоль или на экран в среду разработки, но и напрямую в файл.

    Стандартное решение в экосистеме clang\llvm - это свой модуль для clang tidy (если не нашлось подходящего среди сотен существующих опубликованных). Многие из них с поддержкой авто-замены (колонка Offers fixes).

    Есть и оффициальные гайды по вкатыванию в свои модули, с примерами и скриптами-помогаторами

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

    Статью я переводить не стал, так как материала много и он технически сложный, а заодно и передаю привет ППА Хабра.

    А в чем собственно суть статьи? По содержанию на пост больше тянет имхо (и скорее в собственном блоге)


    1. rsashka Автор
      09.01.2025 12:37

      Стандартное решение в экосистеме clang\llvm - это свой модуль для clang tidy (если не нашлось подходящего среди сотен существующих опубликованных).

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

      clang-tidy check is a good choice for linter-style checks, checks that are related to a certain coding style, checks that address code readability, etc.

      Но если нужно решать вопросы выходящие за рамки стиля кодирования или читаемости кода, то тут без программирования никак не обойтись. А разрабатывать модуль для clang tidy или сразу с помощью LibTooling (который в clang tidy и используется), это уже дело вкуса.