chromium/tools/clang/trace_annotator/TraceAnnotator.cpp

// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//
// This implements a Clang tool to annotate methods with tracing. It should be
// run using the tools/clang/scripts/run_tool.py helper as described in
// README.md

#include <string>
#include <vector>
#include "clang/ASTMatchers/ASTMatchFinder.h"
#include "clang/ASTMatchers/ASTMatchers.h"
#include "clang/Basic/SourceManager.h"
#include "clang/Frontend/FrontendActions.h"
#include "clang/Tooling/CommonOptionsParser.h"
#include "clang/Tooling/Refactoring.h"
#include "clang/Tooling/Tooling.h"
#include "llvm/Support/CommandLine.h"
#include "llvm/Support/FormatVariadic.h"

using namespace clang::ast_matchers;
using clang::tooling::CommonOptionsParser;
using clang::tooling::Replacement;
using clang::tooling::Replacements;

namespace {

class FunctionDefCallback : public MatchFinder::MatchCallback {
 public:
  explicit FunctionDefCallback(std::vector<Replacement>* replacements)
      : replacements_(replacements) {}

  void run(const MatchFinder::MatchResult& result) override;

 private:
  std::vector<Replacement>* const replacements_;
};

class TraceAnnotator {
 public:
  explicit TraceAnnotator(std::vector<Replacement>* replacements)
      : function_def_callback_(replacements) {}

  void SetupMatchers(MatchFinder* match_finder);

 private:
  FunctionDefCallback function_def_callback_;
};

// Given:
//   template <typename T, typename T2> void foo(T t, T2 t2) {};  // N1 and N4
//   template <typename T2> void foo<int, T2>(int t, T2 t) {};    // N2
//   template <> void foo<int, char>(int t, char t2) {};          // N3
//   void foo() {
//     // This creates implicit template specialization (N4) out of the
//     // explicit template definition (N1).
//     foo<bool, double>(true, 1.23);
//   }
// with the following AST nodes:
//   FunctionTemplateDecl foo
//   |-FunctionDecl 0x191da68 foo 'void (T, T2)'         // N1
//   `-FunctionDecl 0x194bf08 foo 'void (bool, double)'  // N4
//   FunctionTemplateDecl foo
//   `-FunctionDecl foo 'void (int, T2)'                 // N2
//   FunctionDecl foo 'void (int, char)'                 // N3
//
// Matches AST node N4, but not AST nodes N1, N2 nor N3.
AST_MATCHER(clang::FunctionDecl, isImplicitFunctionTemplateSpecialization) {
  switch (Node.getTemplateSpecializationKind()) {
    case clang::TSK_ImplicitInstantiation:
      return true;
    case clang::TSK_Undeclared:
    case clang::TSK_ExplicitSpecialization:
    case clang::TSK_ExplicitInstantiationDeclaration:
    case clang::TSK_ExplicitInstantiationDefinition:
      return false;
  }
}

AST_POLYMORPHIC_MATCHER(isInMacroLocation,
                        AST_POLYMORPHIC_SUPPORTED_TYPES(clang::Decl,
                                                        clang::Stmt,
                                                        clang::TypeLoc)) {
  return Node.getBeginLoc().isMacroID();
}

void TraceAnnotator::SetupMatchers(MatchFinder* match_finder) {
  const clang::ast_matchers::DeclarationMatcher function_call =
      functionDecl(
          has(compoundStmt().bind("function body")),
          /* Avoid matching the following cases: */
          unless(anyOf(
              /* Do not match implicit function template specializations to
                 avoid conflicting edits. */
              isImplicitFunctionTemplateSpecialization(),
              /* Do not match constexpr functions. */
              isConstexpr(), isDefaulted(),
              /* Do not match ctor/dtor. */
              cxxConstructorDecl(), cxxDestructorDecl(),
              /* Tracing macros can be tricky (e.g., QuicUint128Impl comparison
                 operators). */
              isInMacroLocation(), has(compoundStmt(isInMacroLocation())),
              /* Do not trace lambdas (no name, possbly tracking more parameters
                 than intended because of [&]). */
              hasParent(cxxRecordDecl(isLambda())))))
          .bind("function");
  match_finder->addMatcher(function_call, &function_def_callback_);
}

// Returns a string containing the qualified name of the function. Does not
// output template parameters of the function or in case of methods of the
// associated class (as opposed to |function->getQualifiedNameAsString|).
std::string getFunctionName(const clang::FunctionDecl* function) {
  std::string qualified_name;
  // Add namespace(s) to the name.
  if (auto* name_space = llvm::dyn_cast<clang::NamespaceDecl>(
          function->getEnclosingNamespaceContext())) {
    qualified_name += name_space->getQualifiedNameAsString();
    qualified_name += "::";
  }
  // If the function is a method, add class name (without templates).
  if (auto* method = llvm::dyn_cast<clang::CXXMethodDecl>(function)) {
    qualified_name += method->getParent()->getNameAsString();
    qualified_name += "::";
  }
  // Add function name (without templates).
  qualified_name += function->getNameAsString();
  return qualified_name;
}

void FunctionDefCallback::run(const MatchFinder::MatchResult& result) {
  const clang::FunctionDecl* function =
      result.Nodes.getNodeAs<clang::FunctionDecl>("function");
  // Using this instead of |function->getBody| prevents conflicts with parameter
  // names in headers and implementations.
  const clang::CompoundStmt* function_body =
      result.Nodes.getNodeAs<clang::CompoundStmt>("function body");
  clang::CharSourceRange range =
      clang::CharSourceRange::getTokenRange(function_body->getBeginLoc());

  const char kReplacementTextTemplate[] = R"( TRACE_EVENT0("test", "{0}"); )";
  std::string function_name = getFunctionName(function);
  std::string replacement_text =
      llvm::formatv(kReplacementTextTemplate, function_name).str();

  const char kAnnotationTemplate[] = " { {0}";
  std::string annotation =
      llvm::formatv(kAnnotationTemplate, replacement_text).str();

  replacements_->push_back(
      Replacement(*result.SourceManager, range, annotation));
}

}  // namespace

static llvm::cl::extrahelp common_help(CommonOptionsParser::HelpMessage);

int main(int argc, const char* argv[]) {
  llvm::cl::OptionCategory category("TraceAnnotator Tool");
  llvm::Expected<CommonOptionsParser> options =
      CommonOptionsParser::create(argc, argv, category);
  if (!options) {
    llvm::outs() << llvm::toString(options.takeError());
    return 1;
  }
  clang::tooling::ClangTool tool(options->getCompilations(),
                                 options->getSourcePathList());

  std::vector<Replacement> replacements;
  TraceAnnotator converter(&replacements);
  MatchFinder match_finder;
  converter.SetupMatchers(&match_finder);

  std::unique_ptr<clang::tooling::FrontendActionFactory> frontend_factory =
      clang::tooling::newFrontendActionFactory(&match_finder);
  int result = tool.run(frontend_factory.get());
  if (result != 0)
    return result;

  if (replacements.empty())
    return 0;

  // Each replacement line should have the following format:
  // r:<file path>:<offset>:<length>:<replacement text>
  // Only the <replacement text> field can contain embedded ":" characters.
  // TODO(dcheng): Use a more clever serialization. Ideally we'd use the YAML
  // serialization and then use clang-apply-replacements, but that would require
  // copying and pasting a larger amount of boilerplate for all Chrome clang
  // tools.

  // Keep a set of files where we have already added base_tracing include.
  std::set<std::string> include_added_to;

  llvm::outs() << "==== BEGIN EDITS ====\n";
  for (const auto& r : replacements) {
    // Add base_tracing import if necessary.
    if (include_added_to.find(r.getFilePath().str()) ==
        include_added_to.end()) {
      include_added_to.insert(r.getFilePath().str());
      // Add also copyright so that |test-expected.cc| passes presubmit.
      llvm::outs() << "include-user-header:::" << r.getFilePath()
                   << ":::-1:::-1:::base/trace_event/base_tracing.h"
                   << "\n";
    }
    // Add the actual replacement.
    llvm::outs() << "r:::" << r.getFilePath() << ":::" << r.getOffset()
                 << ":::" << r.getLength() << ":::" << r.getReplacementText()
                 << "\n";
  }
  llvm::outs() << "==== END EDITS ====\n";

  return 0;
}