| // 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 |
| |
| // This is the test driver for a nullability_test. |
| // |
| // A test is a C++ source file that contains code to be nullability-analyzed. |
| // The code can include calls to special assertion functions like nullable(). |
| // These assert details of the analysis results (nullability of expressions). |
| // (These functions are declared in nullability_test.h, see details there). |
| // |
| // This tool's job is to parse the file, run the nullability analysis, |
| // check whether the assertions pass, and report the results. |
| // |
| // It can be invoked manually, and writes textual logs to stdout, but can also |
| // write Bazel structured test results. |
| // https://bazel.build/reference/test-encyclopedia |
| // |
| // The dataflow visualizer is useful in debugging test failures: |
| // When running under Bazel, pass --test_arg=-log |
| // When running manually, pass -dataflow-log=/some/scratch/dir |
| |
| #include <assert.h> |
| |
| #include <chrono> |
| #include <cstdlib> |
| #include <memory> |
| #include <optional> |
| #include <string> |
| #include <system_error> |
| #include <utility> |
| #include <vector> |
| |
| #include "absl/log/check.h" |
| #include "nullability/pointer_nullability.h" |
| #include "nullability/pointer_nullability_analysis.h" |
| #include "nullability/pointer_nullability_lattice.h" |
| #include "nullability/pragma.h" |
| #include "nullability/test/test_headers.h" |
| #include "nullability/type_nullability.h" |
| #include "clang/AST/ASTConsumer.h" |
| #include "clang/AST/ASTTypeTraits.h" |
| #include "clang/AST/CanonicalType.h" |
| #include "clang/AST/Decl.h" |
| #include "clang/AST/DeclTemplate.h" |
| #include "clang/AST/Expr.h" |
| #include "clang/AST/RecursiveASTVisitor.h" |
| #include "clang/AST/Type.h" |
| #include "clang/AST/TypeLoc.h" |
| #include "clang/Analysis/CFG.h" |
| #include "clang/Analysis/FlowSensitive/AdornedCFG.h" |
| #include "clang/Analysis/FlowSensitive/Arena.h" |
| #include "clang/Analysis/FlowSensitive/DataflowAnalysis.h" |
| #include "clang/Analysis/FlowSensitive/DataflowAnalysisContext.h" |
| #include "clang/Analysis/FlowSensitive/Value.h" |
| #include "clang/Analysis/FlowSensitive/WatchedLiteralsSolver.h" |
| #include "clang/Basic/Diagnostic.h" |
| #include "clang/Basic/LLVM.h" |
| #include "clang/Basic/SourceLocation.h" |
| #include "clang/Basic/Specifiers.h" |
| #include "clang/Frontend/CompilerInstance.h" |
| #include "clang/Frontend/TextDiagnostic.h" |
| #include "clang/Frontend/TextDiagnosticPrinter.h" |
| #include "clang/Tooling/ArgumentsAdjusters.h" |
| #include "clang/Tooling/CompilationDatabase.h" |
| #include "clang/Tooling/StandaloneExecution.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/StringExtras.h" |
| #include "llvm/ADT/StringRef.h" |
| #include "llvm/ADT/StringSwitch.h" |
| #include "llvm/Support/CommandLine.h" |
| #include "llvm/Support/Error.h" |
| #include "llvm/Support/FileSystem.h" |
| #include "llvm/Support/FormatVariadic.h" |
| #include "llvm/Support/GlobPattern.h" |
| #include "llvm/Support/Path.h" |
| #include "llvm/Support/raw_ostream.h" |
| |
| namespace clang::tidy::nullability { |
| namespace { |
| llvm::cl::list<std::string> Sources(llvm::cl::Positional, llvm::cl::OneOrMore); |
| llvm::cl::opt<bool> EmitTestLog("log"); |
| |
| test::EnableSmartPointers Enable; |
| |
| // Deal with unexpected llvm::Errors by exiting with failure status. |
| void require(llvm::Error E) { |
| if (E) { |
| llvm::errs() << toString(std::move(E)) << "\n"; |
| std::exit(1); |
| } |
| } |
| template <typename T> |
| T require(llvm::Expected<T> E) { |
| require(E.takeError()); |
| return std::move(*E); |
| } |
| |
| // Emit diagnostics for nullability assertion failures. |
| class Diagnoser { |
| public: |
| Diagnoser(DiagnosticsEngine &Diags) |
| : Diags(Diags), |
| WrongNullability(Diags.getCustomDiagID( |
| DiagnosticsEngine::Error, "expression is %1, expected %0")), |
| WrongTypeCanonical(Diags.getCustomDiagID( |
| DiagnosticsEngine::Error, |
| "argument with type %1 could never match assertion %0")), |
| WrongTypeNullability(Diags.getCustomDiagID( |
| DiagnosticsEngine::Error, "static nullability is %1, expected %0")), |
| WrongNodeKind(Diags.getCustomDiagID( |
| DiagnosticsEngine::Error, "TEST on %0 node is not supported")), |
| NoValue(Diags.getCustomDiagID(DiagnosticsEngine::Error, |
| "no value for boolean expression")), |
| NotProvable(Diags.getCustomDiagID(DiagnosticsEngine::Error, |
| "expression cannot be proved true")), |
| NotPossible(Diags.getCustomDiagID(DiagnosticsEngine::Error, |
| "expression is provably false")) {} |
| |
| void diagnoseNullability(SourceLocation Call, SourceRange Arg, |
| NullabilityKind Want, NullabilityKind Got) { |
| if (Want != Got) { |
| Diags.Report(Call, WrongNullability) |
| << Arg << getNullabilitySpelling(Want) << getNullabilitySpelling(Got); |
| } |
| } |
| |
| void diagnoseType(SourceLocation Call, SourceRange Arg, CanQualType WantCanon, |
| CanQualType GotCanon, const TypeNullability &WantNulls, |
| const TypeNullability &GotNulls) { |
| if (WantCanon != GotCanon) { |
| Diags.Report(Call, WrongTypeCanonical) << WantCanon << GotCanon; |
| } else if (WantNulls != GotNulls) { |
| Diags.Report(Call, WrongTypeNullability) |
| << nullabilityToString(WantNulls) << nullabilityToString(GotNulls); |
| } |
| } |
| |
| void diagnoseNoValue(const Expr &Arg) { |
| Diags.Report(Arg.getSourceRange().getBegin(), NoValue) |
| << Arg.getSourceRange(); |
| } |
| |
| void diagnoseNotProvable(const Expr &Arg) { |
| Diags.Report(Arg.getSourceRange().getBegin(), NotProvable) |
| << Arg.getSourceRange(); |
| } |
| |
| void diagnoseNotPossible(const Expr &Arg) { |
| Diags.Report(Arg.getSourceRange().getBegin(), NotPossible) |
| << Arg.getSourceRange(); |
| } |
| |
| void diagnoseBadTest(const DynTypedNode &N) { |
| Diags.Report(N.getSourceRange().getBegin(), WrongNodeKind) |
| << N.getNodeKind().asStringRef() << N.getSourceRange(); |
| } |
| |
| private: |
| DiagnosticsEngine &Diags; |
| unsigned WrongNullability; |
| unsigned WrongTypeCanonical; |
| unsigned WrongTypeNullability; |
| unsigned WrongNodeKind; |
| unsigned NoValue; |
| unsigned NotProvable; |
| unsigned NotPossible; |
| }; |
| |
| // Match a nullable()/nonnull()/unknown() call, return the nullability asserted. |
| std::optional<NullabilityKind> getAssertedNullability(const CallExpr &Call) { |
| auto *FD = Call.getDirectCallee(); |
| if (!FD || !FD->getDeclContext()->isTranslationUnit() || |
| !FD->getDeclName().isIdentifier()) |
| return std::nullopt; |
| return llvm::StringSwitch<std::optional<NullabilityKind>>(FD->getName()) |
| .Case("nullable", NullabilityKind::Nullable) |
| .Case("nonnull", NullabilityKind::NonNull) |
| .Case("unknown", NullabilityKind::Unspecified) |
| .Default(std::nullopt); |
| } |
| |
| // Match a type<...>() call, return the type asserted. |
| std::optional<QualType> getAssertedType(const CallExpr &Call) { |
| // must be a call to ::type |
| auto *FD = Call.getDirectCallee(); |
| if (!FD || !FD->getDeclContext()->isTranslationUnit() || |
| !FD->getDeclName().isIdentifier() || FD->getName() != "type") |
| return std::nullopt; |
| |
| // must have template arguments, first is an explicitly-written type |
| auto *DRE = dyn_cast<DeclRefExpr>(Call.getCallee()->IgnoreImplicit()); |
| if (!DRE || !DRE->hasExplicitTemplateArgs() || |
| DRE->getTemplateArgs()[0].getArgument().getKind() != |
| TemplateArgument::Type) |
| return std::nullopt; |
| |
| return DRE->getTemplateArgs()[0].getTypeSourceInfo()->getType(); |
| } |
| |
| // Match a provable()/possible() call, return the name of the called function. |
| llvm::StringRef getBoolAssertionName(const CallExpr &Call) { |
| if (Call.getNumArgs() != 1) return {}; |
| auto *FD = Call.getDirectCallee(); |
| if (!FD || !FD->getDeclContext()->isTranslationUnit() || |
| !FD->getDeclName().isIdentifier()) |
| return {}; |
| if (FD->getName() != "provable" && FD->getName() != "possible") return {}; |
| return FD->getName(); |
| } |
| |
| using AnalysisState = |
| const dataflow::DataflowAnalysisState<PointerNullabilityLattice>; |
| // Maps the IDs of symbolic nullability variables (like "X" for symbolic::X) |
| // onto the actual symbolic nullability variables used in the analysis. |
| using SymbolicMap = llvm::DenseMap<StringRef, PointerTypeNullability>; |
| |
| // If T is a symbolic nullability alias, return its ID. |
| // e.g. "X" if the alias is marked [[clang::annotate("symbolic_nullability:X")]] |
| std::optional<StringRef> getSymbolicID(TemplateDecl *TD) { |
| if (!TD || !isa<TypeAliasTemplateDecl>(TD)) return std::nullopt; |
| if (const auto *Annotate = TD->getTemplatedDecl()->getAttr<AnnotateAttr>()) { |
| StringRef Annotation = Annotate->getAnnotation(); |
| if (Annotation.consume_front("symbolic_nullability:")) return Annotation; |
| } |
| return std::nullopt; |
| } |
| |
| // We've seen a type<T>(expr) assertion, extract the nullability vector for T. |
| TypeNullability getAssertedTypeNullability(QualType T, SymbolicMap &Symbolic, |
| dataflow::Arena &A) { |
| // As a special case, we do not respect pragmas to interpret types within a |
| // type<...> assertion. Doing so would on balance make tests less clear. |
| TypeNullabilityDefaults EmptyDefaults; |
| return getTypeNullability( |
| T, FileID(), EmptyDefaults, |
| // Given type< Nonnull<symbolic::X<Nullable<int*>*>*> >(...) |
| // usual vector: [Nonnull, Unspecified, Nullable] |
| // we want: [Nonnull, Symbolic, Nullable]. |
| // We know symbolic::X<T>'s definition is T. When we see the substitution |
| // of Nullable<int*>* into T, we find its vector [Unspecified, Nullable] |
| // and replace the outer nullability for the symbolic one. |
| [&](const SubstTemplateTypeParmType *T) |
| -> std::optional<TypeNullability> { |
| if (auto ID = |
| getSymbolicID(dyn_cast<TemplateDecl>(T->getAssociatedDecl()))) { |
| auto Sym = Symbolic[*ID]; |
| if (!Sym.isSymbolic()) |
| // The test didn't bind anything to e.g. symbolic::X, but now wants |
| // to assert that some expression has this type! |
| // Create a variable now so we have something to assert against. |
| // That way the test will fail with a reasonable error message. |
| Sym = Symbolic[*ID] = PointerTypeNullability::createSymbolic(A); |
| auto Result = getAssertedTypeNullability(T->desugar(), Symbolic, A); |
| Result.front() = Sym; |
| return Result; |
| } |
| return std::nullopt; |
| }); |
| } |
| |
| // Match any special assertions, check the condition, diagnose on failure. |
| void diagnoseCall(const CallExpr &CE, const ASTContext &Ctx, Diagnoser &Diags, |
| const AnalysisState &State, SymbolicMap &Symbolic) { |
| if (auto Want = getAssertedNullability(CE); Want && CE.getNumArgs() == 1) { |
| auto &Arg = *CE.getArgs()[0]; |
| auto Got = getNullability(&Arg, State.Env); |
| Diags.diagnoseNullability(CE.getBeginLoc(), Arg.getSourceRange(), *Want, |
| Got); |
| } |
| if (auto Want = getAssertedType(CE); Want && CE.getNumArgs() == 1) { |
| auto &Got = *CE.getArgs()[0]; |
| auto WantCanon = Ctx.getCanonicalType(*Want); |
| auto GotCanon = Ctx.getCanonicalType(Got.getType()); |
| auto WantNulls = getAssertedTypeNullability( |
| *Want, Symbolic, State.Env.getDataflowAnalysisContext().arena()); |
| TypeNullability GotNulls = unspecifiedNullability(&Got); |
| if (const auto *GN = State.Lattice.getTypeNullability(&Got)) GotNulls = *GN; |
| Diags.diagnoseType(CE.getBeginLoc(), Got.getSourceRange(), WantCanon, |
| GotCanon, WantNulls, GotNulls); |
| } |
| if (llvm::StringRef Name = getBoolAssertionName(CE); !Name.empty()) { |
| auto &Arg = *CE.getArgs()[0]; |
| auto *Val = State.Env.get<dataflow::BoolValue>(Arg); |
| if (!Val) { |
| Diags.diagnoseNoValue(Arg); |
| return; |
| } |
| if (Name == "provable") { |
| if (!State.Env.proves(Val->formula())) Diags.diagnoseNotProvable(Arg); |
| } else { |
| if (!State.Env.allows(Val->formula())) Diags.diagnoseNotPossible(Arg); |
| } |
| } |
| } |
| |
| // Bind nullability variables for params marked symbolic::X<> etc. |
| // Returns the map from symbolic ID => nullability variables. |
| SymbolicMap bindSymbolicNullability(const FunctionDecl &Func, |
| PointerNullabilityAnalysis &Analysis, |
| dataflow::Arena &A) { |
| SymbolicMap Result; |
| for (const auto *Param : Func.parameters()) { |
| // For now, only support symbolic on the top level of parameter types. |
| if (auto *TST = Param->getType()->getAs<TemplateSpecializationType>()) |
| if (auto I = getSymbolicID(TST->getTemplateName().getAsTemplateDecl())) |
| Result.try_emplace(*I, Analysis.assignNullabilityVariable(Param, A)); |
| } |
| return Result; |
| } |
| |
| // To run a test, we simply run the nullability analysis, and then walk the |
| // CFG afterwards looking for calls to our assertions - nullable() etc. |
| // These each assert properties attached to the analysis state at that point. |
| void runTest(const FunctionDecl &Func, const NullabilityPragmas &Pragmas, |
| Diagnoser &Diags, std::unique_ptr<llvm::raw_ostream> LogStream) { |
| std::unique_ptr<dataflow::Logger> Logger; |
| if (LogStream) |
| Logger = dataflow::Logger::html([&] { |
| CHECK(LogStream) << "analyzing multiple functions?!"; |
| return std::move(LogStream); |
| }); |
| dataflow::DataflowAnalysisContext::Options Opts; |
| Opts.Log = Logger.get(); |
| dataflow::DataflowAnalysisContext DACtx( |
| std::make_unique<dataflow::WatchedLiteralsSolver>(), Opts); |
| auto &Ctx = Func.getDeclContext()->getParentASTContext(); |
| auto ACFG = require(dataflow::AdornedCFG::build(Func)); |
| dataflow::Environment Env(DACtx, Func); |
| PointerNullabilityAnalysis Analysis(Ctx, Env, Pragmas); |
| auto Symbolic = bindSymbolicNullability(Func, Analysis, DACtx.arena()); |
| dataflow::CFGEltCallbacks<PointerNullabilityAnalysis> PostAnalysisCallbacks; |
| PostAnalysisCallbacks.After = [&](const CFGElement &Elt, |
| AnalysisState &State) { |
| if (auto CS = Elt.getAs<CFGStmt>()) |
| if (auto *CE = dyn_cast<CallExpr>(CS->getStmt())) |
| diagnoseCall(*CE, Ctx, Diags, State, Symbolic); |
| }; |
| require(runDataflowAnalysis(ACFG, Analysis, std::move(Env), |
| PostAnalysisCallbacks)); |
| } |
| |
| // Absorbs test start/end events and diagnostics. |
| // Produces stdout output, and also Bazel test.xml report. |
| class TestOutput : public DiagnosticConsumer { |
| public: |
| TestOutput() |
| : Out(llvm::errs()), |
| XMLStorage(openXML()), |
| XML(XMLStorage ? *XMLStorage : llvm::nulls()) { |
| XML << "<testsuites>\n"; |
| } |
| ~TestOutput() override { |
| XML << "</testsuites>\n"; |
| Out << "Passed " << PassingTests << " test(s)\n"; |
| if (!FailingTests.empty()) { |
| Out << "Failed " << FailingTests.size() << " test(s):\n"; |
| for (const std::string &Name : FailingTests) Out << " " << Name << "\n"; |
| } |
| } |
| |
| void startSuite(llvm::StringRef Name) { |
| XML << llvm::formatv("<testsuite name='{0}'>\n", escape(Name)); |
| Out << "=== Suite: " << Name << " ===\n"; |
| } |
| void endSuite() { XML << "</testsuite>\n"; } |
| |
| void startTest(const FunctionDecl &F) { |
| CurrentCase.emplace(); |
| CurrentCase->Name = F.getDeclName().getAsString(); |
| CurrentCase->Start = std::chrono::steady_clock::now(); |
| Out << "--- Test: " << CurrentCase->Name << " ---\n"; |
| } |
| void endTest(llvm::StringRef LogPath) { |
| assert(CurrentCase.has_value()); |
| if (CurrentCase->Failures.empty()) { |
| Out << "PASS\n"; |
| ++PassingTests; |
| } else { |
| Out << "FAIL\n"; |
| FailingTests.emplace_back(CurrentCase->Name); |
| } |
| auto Millis = std::chrono::duration_cast<std::chrono::milliseconds>( |
| std::chrono::steady_clock::now() - CurrentCase->Start) |
| .count(); |
| XML << llvm::formatv("<testcase name='{0}' time='{1}'>\n", |
| escape(CurrentCase->Name), Millis); |
| for (const auto &Failure : CurrentCase->Failures) |
| XML << llvm::formatv("<failure message='{0}'>{1}</failure>\n", |
| escape(Failure.first), escape(Failure.second)); |
| if (!LogPath.empty()) { |
| XML << llvm::formatv( |
| "<properties><property name='test_output1' value='{0}'>" |
| "</property></properties>", |
| escape(LogPath)); |
| Out << "Log written to " << LogPath << "\n"; |
| } else if (!CurrentCase->Failures.empty()) { |
| XML << "<error message='Note: run with --test_arg=-log for detailed " |
| "analysis logs'></error>\n"; |
| } |
| XML << "</testcase>\n"; |
| CurrentCase.reset(); |
| } |
| bool hadErrors() const { return HadErrors; } |
| |
| void BeginSourceFile(const LangOptions &LangOpts, |
| const Preprocessor *PP) override { |
| this->LangOpts = LangOpts; |
| } |
| void HandleDiagnostic(DiagnosticsEngine::Level Level, |
| const Diagnostic &Info) override { |
| llvm::SmallString<256> Message; |
| Info.FormatDiagnostic(Message); |
| |
| // TODO: in the printed diagnostic, the absolute path to the file is shown. |
| // This is hard to read and breaks linkification in log viewers. |
| // This happens because the tooling makes input file paths absolute. |
| // We should find a way to avoid this. |
| std::string Rendered; |
| llvm::raw_string_ostream OS(Rendered); |
| TextDiagnostic(OS, LangOpts, new DiagnosticOptions()) |
| .emitDiagnostic( |
| FullSourceLoc(Info.getLocation(), Info.getSourceManager()), Level, |
| Message, Info.getRanges(), Info.getFixItHints()); |
| |
| Out << Rendered; |
| if (Level >= DiagnosticsEngine::Error) { |
| if (CurrentCase) CurrentCase->Failures.emplace_back(Message, Rendered); |
| HadErrors = true; |
| } |
| } |
| |
| private: |
| // Create test.xml file in the right place, if running under Bazel. |
| static std::unique_ptr<llvm::raw_ostream> openXML() { |
| if (const char *Filename = std::getenv("XML_OUTPUT_FILE")) { |
| std::error_code EC; |
| auto OS = std::make_unique<llvm::raw_fd_ostream>(Filename, EC); |
| if (EC) { |
| llvm::errs() << "Failed to open XML output " << Filename << ": " |
| << EC.message() << "\n"; |
| } else { |
| return OS; |
| } |
| } |
| return nullptr; |
| } |
| |
| static std::string escape(llvm::StringRef Raw) { |
| std::string S; |
| llvm::raw_string_ostream OS(S); |
| llvm::printHTMLEscaped(Raw, OS); |
| return S; |
| } |
| |
| bool HadErrors = false; |
| int PassingTests = 0; |
| // Names of failing tests. These need to be strings, not `StringRef`s, because |
| // this class may outlive the AST. |
| llvm::SmallVector<std::string> FailingTests; |
| LangOptions LangOpts; |
| |
| llvm::raw_ostream &Out; // Plain-text output stream. |
| std::unique_ptr<llvm::raw_ostream> XMLStorage; |
| llvm::raw_ostream &XML; // test.xml output stream (or null stream if no XML). |
| |
| // Gather info about the currently running test case. |
| // The <testcase> element can only be written once it's finished. |
| struct TestCase { |
| std::string Name; |
| std::vector<std::pair<std::string, std::string>> Failures; |
| std::chrono::steady_clock::time_point Start; |
| }; |
| std::optional<TestCase> CurrentCase; |
| }; |
| |
| // Provides a filter for running only specific tests. |
| struct TestFilter { |
| llvm::SmallVector<llvm::GlobPattern> PositivePatterns, NegativePatterns; |
| |
| bool shouldRun(llvm::StringRef Name) const { |
| auto PatternMatchesName = [&](const llvm::GlobPattern &Pattern) { |
| return Pattern.match(Name); |
| }; |
| if (!PositivePatterns.empty() && |
| !llvm::any_of(PositivePatterns, PatternMatchesName)) |
| return false; |
| return !llvm::any_of(NegativePatterns, PatternMatchesName); |
| } |
| }; |
| |
| llvm::Expected<llvm::SmallVector<llvm::GlobPattern>> globsFromPatternString( |
| llvm::StringRef PatternStr) { |
| llvm::SmallVector<llvm::GlobPattern> Globs; |
| llvm::SmallVector<llvm::StringRef> Patterns; |
| PatternStr.split(Patterns, ':'); |
| for (auto Pattern : Patterns) { |
| if (Pattern.empty()) continue; |
| llvm::GlobPattern Glob; |
| if (llvm::Error Err = llvm::GlobPattern::create(Pattern).moveInto(Glob)) |
| return Err; |
| Globs.push_back(std::move(Glob)); |
| } |
| return Globs; |
| } |
| |
| llvm::Expected<TestFilter> getTestFilter() { |
| // Test filter syntax is the same as that of GoogleTest. |
| llvm::StringRef TestFilterStr = getenv("TESTBRIDGE_TEST_ONLY"); |
| auto [PositivePatternsStr, NegativePatternsStr] = TestFilterStr.split('-'); |
| |
| TestFilter Filter; |
| if (llvm::Error Err = globsFromPatternString(PositivePatternsStr) |
| .moveInto(Filter.PositivePatterns)) |
| return Err; |
| if (llvm::Error Err = globsFromPatternString(NegativePatternsStr) |
| .moveInto(Filter.NegativePatterns)) |
| return Err; |
| |
| return Filter; |
| } |
| |
| // AST consumer that analyzes [[clang::annotate("test")]] functions as tests. |
| class Consumer : public ASTConsumer { |
| public: |
| Consumer(TestOutput &Output, const TestFilter &Filter, |
| const NullabilityPragmas &Pragmas) |
| : Pragmas(Pragmas), Output(Output), Filter(Filter) {} |
| |
| private: |
| void Initialize(ASTContext &Context) override { |
| Diagnoser.emplace(Context.getDiagnostics()); |
| } |
| |
| void HandleTranslationUnit(ASTContext &Ctx) override { |
| assert(Diagnoser.has_value()); |
| if (Ctx.getDiagnostics().hasUncompilableErrorOccurred()) return; |
| // Walk the AST, calling runTestAt on every TEST annotation. |
| struct TestVisitor : public RecursiveASTVisitor<TestVisitor> { |
| Consumer &Outer; |
| ASTContext &Ctx; |
| |
| bool VisitAnnotateAttr(AnnotateAttr *A) { |
| if (A->getAnnotation() == "test") |
| Outer.runTestAt(DynTypedNode::create(*A), Ctx); |
| return true; |
| } |
| }; |
| TestVisitor{{}, *this, Ctx}.TraverseAST(Ctx); |
| } |
| // Starting at a TEST annotation, find the associated test and run it. |
| void runTestAt(const DynTypedNode &Test, ASTContext &Ctx) { |
| if (const auto *FD = Test.get<FunctionDecl>()) { |
| if (FD->getIdentifier() != nullptr && !Filter.shouldRun(FD->getName())) |
| return; |
| |
| // This is a test we can run directly. |
| Output.startTest(*FD); |
| auto [LogPath, LogStream] = openTestLog(FD->getDeclName().getAsString()); |
| runTest(*FD, Pragmas, *Diagnoser, std::move(LogStream)); |
| Output.endTest(LogPath); |
| } else if (Test.get<Attr>() || Test.get<TypeLoc>()) { |
| // Walk up to find out what decl etc this marker is attached to. |
| auto Parents = Ctx.getParents(Test); |
| CHECK(!Parents.empty()); |
| for (const auto &Parent : Parents) runTestAt(Parent, Ctx); |
| } else { |
| // Uh-oh, TEST marker was in the wrong place! |
| Diagnoser->diagnoseBadTest(Test); |
| } |
| } |
| // Decide whether to write a per-test detailed log file that Bazel can find. |
| // We do this if the "-log" flag is set (--test_arg=-log). |
| // If we are writing one, create it and return its path and an open stream. |
| std::pair<std::string, std::unique_ptr<llvm::raw_ostream>> openTestLog( |
| llvm::StringRef Name) { |
| const char *RootDir = ::getenv("TEST_UNDECLARED_OUTPUTS_DIR"); |
| if (!EmitTestLog || !RootDir) return {"", nullptr}; |
| llvm::SmallString<256> PathModel(RootDir), Path; |
| llvm::sys::path::append(PathModel, Name + "-%%%%%%%%.html"); |
| int FD; |
| if (auto EC = llvm::sys::fs::createUniqueFile(PathModel, FD, Path)) |
| return {"", nullptr}; |
| llvm::StringRef RelativePath = Path; |
| RelativePath.consume_front(RootDir); |
| while (!RelativePath.empty() && |
| llvm::sys::path::is_separator(RelativePath.front())) |
| RelativePath = RelativePath.drop_front(); |
| return {RelativePath.str(), |
| std::make_unique<llvm::raw_fd_ostream>(FD, /*ShouldClose=*/true)}; |
| } |
| |
| const NullabilityPragmas &Pragmas; |
| TestOutput &Output; |
| std::optional<Diagnoser> Diagnoser; |
| const TestFilter &Filter; |
| }; |
| |
| } // namespace |
| } // namespace clang::tidy::nullability |
| |
| int main(int argc, const char **argv) { |
| using namespace clang::tidy::nullability; |
| struct Factory : public clang::tooling::SourceFileCallbacks { |
| TestOutput Output; |
| TestFilter Filter; |
| NullabilityPragmas Pragmas; |
| |
| std::unique_ptr<clang::ASTConsumer> newASTConsumer() { |
| return std::make_unique<Consumer>(Output, Filter, Pragmas); |
| } |
| bool handleBeginSource(clang::CompilerInstance &CI) override { |
| registerPragmaHandler(CI.getPreprocessor(), Pragmas); |
| const auto &SM = CI.getSourceManager(); |
| llvm::StringRef FilenameStem = |
| llvm::sys::path::stem(llvm::sys::path::filename( |
| SM.getBufferName(SM.getLocForStartOfFile(SM.getMainFileID())))); |
| llvm::StringRef LangStd = |
| clang::LangStandard::getLangStandardForKind(CI.getLangOpts().LangStd) |
| .getName(); |
| std::string SuiteName = |
| (llvm::Twine(FilenameStem) + " (" + LangStd + ")").str(); |
| Output.startSuite(SuiteName); |
| CI.getDiagnostics().setClient(&Output, /*Owns=*/false); |
| return true; |
| } |
| void handleEndSource() override { Output.endSuite(); } |
| } F; |
| std::string Err; |
| auto CDB = clang::tooling::FixedCompilationDatabase::loadFromCommandLine( |
| argc, argv, Err); |
| llvm::cl::ParseCommandLineOptions(argc, argv); |
| if (!CDB) { |
| llvm::errs() << "Usage: nullability_test source.cc\n" << Err << "\n"; |
| exit(1); |
| } |
| if (llvm::Error E = getTestFilter().moveInto(F.Filter)) { |
| llvm::errs() << "Invalid test filter: " << E << "\n"; |
| exit(1); |
| } |
| clang::tooling::StandaloneToolExecutor Executor{*CDB, Sources}; |
| for (const auto &Entry : |
| llvm::ArrayRef(test_headers_create(), test_headers_size())) |
| Executor.mapVirtualFile(Entry.name, Entry.data); |
| // Run in C++17 and C++20 mode to cover differences in the AST between modes |
| // (e.g. C++20 can contain `CXXRewrittenBinaryOperator`). |
| for (const char *CxxMode : {"-std=c++17", "-std=c++20"}) |
| require(Executor.execute( |
| clang::tooling::newFrontendActionFactory(&F, &F), |
| clang::tooling::getInsertArgumentAdjuster( |
| // Ensure test_headers are on the include path. |
| {"-isystem.", CxxMode, |
| // TODO: b/357760487 -- use the flag until the issue is resolved or |
| // we find a workaround. |
| "-Xclang", "-fretain-subst-template-type-parm-type-ast-nodes"}, |
| clang::tooling::ArgumentInsertPosition::END))); |
| return F.Output.hadErrors() ? 1 : 0; |
| } |