| // 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)); |
| } |