blob: bbc96198e3271da68ffe96be79d612b67930be1e [file] [log] [blame]
// Part of the Crubit project, under the Apache License v2.0 with LLVM
// Exceptions. See /LICENSE for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
// infer_tu_main infers nullability within a single translation unit.
//
// By default (-diagnostics=1) it shows findings as diagnostics.
// It can optionally (-protos=1) print the Inference proto.
//
// This is not the intended way to fully analyze a real codebase.
// e.g. it can't jointly inspect all callsites of a function (in different TUs).
#include <memory>
#include <string>
#include <utility>
#include "absl/base/nullability.h"
#include "absl/log/check.h"
#include "absl/strings/str_cat.h"
#include "nullability/inference/ctn_replacement_macros.h"
#include "nullability/inference/infer_tu.h"
#include "nullability/inference/inference.proto.h"
#include "nullability/inference/replace_macros.h"
#include "nullability/pragma.h"
#include "nullability/type_nullability.h"
#include "clang/AST/ASTConsumer.h"
#include "clang/AST/Decl.h"
#include "clang/AST/DeclarationName.h"
#include "clang/AST/RecursiveASTVisitor.h"
#include "clang/Basic/LLVM.h"
#include "clang/Basic/SourceLocation.h"
#include "clang/Frontend/CompilerInstance.h"
#include "clang/Frontend/FrontendAction.h"
#include "clang/Frontend/FrontendActions.h"
#include "clang/Index/USRGeneration.h"
#include "clang/Tooling/ArgumentsAdjusters.h"
#include "clang/Tooling/Execution.h"
#include "clang/Tooling/Tooling.h"
#include "llvm/ADT/ArrayRef.h"
#include "llvm/ADT/DenseMap.h"
#include "llvm/ADT/STLExtras.h"
#include "llvm/ADT/SmallString.h"
#include "llvm/ADT/StringRef.h"
#include "llvm/ADT/Twine.h"
#include "llvm/Support/Casting.h"
#include "llvm/Support/CommandLine.h"
#include "llvm/Support/Error.h"
#include "llvm/Support/Format.h"
#include "llvm/Support/Regex.h"
#include "llvm/Support/raw_ostream.h"
using ::clang::tidy::nullability::ReplacementMacrosHeaderFileName;
llvm::cl::OptionCategory Opts("infer_tu_main options");
llvm::cl::opt<bool> PrintProtos{
"protos",
llvm::cl::desc("Print the Inference protos"),
llvm::cl::init(false),
};
llvm::cl::opt<bool> Diagnostics{
"diagnostics",
llvm::cl::desc("Print inference results as diagnostics"),
llvm::cl::init(true),
};
llvm::cl::opt<bool> PrintEvidence{
"evidence",
llvm::cl::desc("Print sample evidence as notes (requires -diagnostics)"),
llvm::cl::init(true),
};
llvm::cl::opt<bool> PrintMetrics{
"metrics",
llvm::cl::desc("Print inference metrics"),
llvm::cl::init(true),
};
llvm::cl::opt<bool> IncludeTrivial{
"trivial",
llvm::cl::desc("Include trivial inferences (annotated, no conflicts)"),
llvm::cl::init(false),
};
llvm::cl::opt<std::string> FileFilter{
"file-filter",
llvm::cl::desc("Regular expression filenames must match to be analyzed. "
"May be negated with - prefix."),
};
llvm::cl::opt<std::string> NameFilter{
"name-filter",
llvm::cl::desc("Regular expression decl names must match to be analyzed. "
"May be negated with - prefix."),
};
llvm::cl::opt<unsigned> Iterations{
"iterations",
llvm::cl::desc("Number of inference iterations"),
llvm::cl::init(1),
};
namespace clang::tidy::nullability {
namespace {
// Walks the AST looking for declarations of symbols we inferred.
// When it finds them, prints the inference as diagnostics.
class DiagnosticPrinter : public RecursiveASTVisitor<DiagnosticPrinter> {
llvm::DenseMap<llvm::StringRef, absl::Nonnull<const Inference *>>
InferenceByUSR;
DiagnosticsEngine &Diags;
unsigned DiagInferHere;
unsigned DiagSample;
void render(const Inference &I, const Decl &D) {
for (const auto &Slot : I.slot_inference()) {
Diags.Report(D.getLocation(), DiagInferHere)
<< slotName(Slot.slot(), D) << Nullability_Name(Slot.nullability());
if (PrintEvidence) {
for (const auto &Sample : Slot.sample_evidence()) {
if (SourceLocation Loc = parseLoc(Sample.location()); Loc.isValid())
Diags.Report(Loc, DiagSample) << Evidence::Kind_Name(Sample.kind());
}
}
}
}
std::string slotName(unsigned S, const Decl &D) {
if (const auto *Field = dyn_cast<FieldDecl>(&D))
return Field->getName().str();
if (const auto *Var = dyn_cast<VarDecl>(&D)) return Var->getName().str();
if (S == SLOT_RETURN_TYPE) return "return type";
unsigned ParamIdx = S - SLOT_PARAM;
llvm::StringRef ParamName;
if (const auto *Func = dyn_cast<FunctionDecl>(&D)) {
const ParmVarDecl *Param = Func->getParamDecl(ParamIdx);
if (Param->getDeclName().isIdentifier()) ParamName = Param->getName();
}
llvm::Twine Name = "parameter " + llvm::Twine(ParamIdx);
if (ParamName.empty()) return Name.str();
return (Name + " ('" + ParamName + "')").str();
}
// Terrible hack: parse "foo.cc:4:2" back into a SourceLocation.
SourceLocation parseLoc(llvm::StringRef LocStr) {
auto &SM = Diags.getSourceManager();
auto &FM = SM.getFileManager();
auto [Rest, ColStr] = llvm::StringRef(LocStr).rsplit(':');
auto [Name, LineStr] = Rest.rsplit(':');
auto File = FM.getOptionalFileRef(Name);
unsigned Line, Col;
if (!File || LineStr.getAsInteger(10, Line) || ColStr.getAsInteger(10, Col))
return SourceLocation();
return SM.translateFileLineCol(&File->getFileEntry(), Line, Col);
}
public:
DiagnosticPrinter(llvm::ArrayRef<Inference> All, DiagnosticsEngine &Diags)
: Diags(Diags) {
for (const auto &I : All) InferenceByUSR.try_emplace(I.symbol().usr(), &I);
DiagInferHere = Diags.getCustomDiagID(DiagnosticsEngine::Remark,
"would mark %0 as %1 here");
DiagSample = Diags.getCustomDiagID(DiagnosticsEngine::Note, "%0 here");
}
bool VisitDecl(absl::Nonnull<const Decl *> FD) {
llvm::SmallString<128> USR;
if (!index::generateUSRForDecl(FD, USR))
if (auto *I = InferenceByUSR.lookup(USR)) render(*I, *FD);
return true;
}
};
// Selects which declarations to analyze based on filter flags.
struct DeclFilter {
bool operator()(const Decl &D) const {
auto &SM = D.getDeclContext()->getParentASTContext().getSourceManager();
if (!checkLocation(D.getLocation(), SM)) return false;
if (auto *ND = llvm::dyn_cast<NamedDecl>(&D))
if (!checkName(*ND)) return false;
return true;
}
bool checkLocation(SourceLocation Loc, const SourceManager &SM) const {
if (!FileFilter.getNumOccurrences()) return true;
auto ID = SM.getFileID(SM.getFileLoc(Loc));
auto [It, Inserted] = FileCache.try_emplace(ID);
if (Inserted) {
static auto &Pattern = *new RegexFlagFilter(FileFilter);
auto FID = SM.getFileEntryRefForID(ID);
It->second = !FID.has_value() || Pattern(FID->getName());
}
return It->second;
}
bool checkName(const NamedDecl &ND) const {
if (!NameFilter.getNumOccurrences()) return true;
static auto &Pattern = *new RegexFlagFilter(NameFilter);
return Pattern(ND.getQualifiedNameAsString());
}
mutable llvm::DenseMap<FileID, bool> FileCache;
struct RegexFlagFilter {
RegexFlagFilter(llvm::StringRef Regex)
: Negative(Regex.consume_front("-")), Pattern(Regex) {
std::string Err;
CHECK(Pattern.isValid(Err)) << Regex.str() << ": " << Err;
}
bool operator()(llvm::StringRef Text) {
bool Match = Pattern.match(Text);
return Negative ? !Match : Match;
}
bool Negative;
llvm::Regex Pattern;
};
};
class Action : public SyntaxOnlyAction {
NullabilityPragmas Pragmas;
absl::Nonnull<std::unique_ptr<ASTConsumer>> CreateASTConsumer(
CompilerInstance &, llvm::StringRef) override {
class Consumer : public ASTConsumer {
public:
NullabilityPragmas &Pragmas;
Consumer(NullabilityPragmas &Pragmas) : Pragmas(Pragmas) {}
private:
void HandleTranslationUnit(ASTContext &Ctx) override {
llvm::errs() << "Running inference...\n";
auto Results = inferTU(Ctx, Pragmas, Iterations, DeclFilter());
if (!IncludeTrivial)
llvm::erase_if(Results, [](Inference &I) {
llvm::erase_if(
*I.mutable_slot_inference(),
[](const Inference::SlotInference &S) { return S.trivial(); });
return I.slot_inference_size() == 0;
});
if (PrintProtos)
for (const auto &I : Results) llvm::outs() << absl::StrCat(I) << "\n";
if (PrintMetrics) {
unsigned Nonnull = 0;
unsigned Nullable = 0;
unsigned Unknown = 0;
unsigned Conflict = 0;
for (const auto &I : Results) {
for (const auto &Slot : I.slot_inference()) {
if (Slot.conflict()) {
++Conflict;
continue;
}
switch (Slot.nullability()) {
case Nullability::NULLABLE:
++Nullable;
break;
case Nullability::NONNULL:
++Nonnull;
break;
case Nullability::UNKNOWN:
++Unknown;
break;
}
}
}
llvm::outs() << "Inferred " << Nonnull + Nullable + Unknown + Conflict
<< " symbols\n";
llvm::outs() << "Nonnull: " << Nonnull << "\n";
llvm::outs() << "Nullable: " << Nullable << "\n";
llvm::outs() << "Unknown: " << Unknown << "\n";
llvm::outs() << "Conflicts: " << Conflict << "\n";
llvm::outs() << "Percent not Unknown and not Conflict: "
<< llvm::format("%0.2f", 100.0 * (Nonnull + Nullable) /
(Nonnull + Nullable +
Unknown + Conflict))
<< "%\n";
}
if (Diagnostics)
DiagnosticPrinter(Results, Ctx.getDiagnostics()).TraverseAST(Ctx);
}
};
return std::make_unique<Consumer>(Pragmas);
}
bool BeginSourceFileAction(clang::CompilerInstance &CI) override {
if (!ASTFrontendAction::BeginSourceFileAction(CI) ||
!CI.getLangOpts().CPlusPlus)
return false;
registerPragmaHandler(CI.getPreprocessor(), Pragmas);
CI.getPreprocessor().addPPCallbacks(
std::make_unique<ReplaceMacrosCallbacks>(CI.getPreprocessor()));
return true;
}
};
} // namespace
} // namespace clang::tidy::nullability
int main(int argc, absl::Nonnull<const char **> argv) {
using namespace clang::tooling;
auto Exec = createExecutorFromCommandLineArgs(argc, argv, Opts);
QCHECK(Exec) << toString(Exec.takeError());
CHECK_EQ(ctn_replacement_macros_size(), 1);
llvm::StringRef MacroReplacementText =
ctn_replacement_macros_create()[0].data;
(*Exec)->mapVirtualFile(ReplacementMacrosHeaderFileName,
MacroReplacementText);
clang::tidy::nullability::enableSmartPointers(true);
auto Err = (*Exec)->execute(
newFrontendActionFactory<clang::tidy::nullability::Action>(),
getInsertArgumentAdjuster(
{// Disable warnings, test cases are full of unused expressions etc.
"-w",
// Include the file containing macro replacements that enable
// additional inference.
"-include", std::string(ReplacementMacrosHeaderFileName)},
ArgumentInsertPosition::BEGIN));
QCHECK(!Err) << toString(std::move(Err));
}