| // 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"); | |
| // 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< symbolic::X<int* _Nullable*> *_Nonnull >(...) | |
| // 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 int* _Nullable* 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); | |
| clang::DiagnosticOptions DiagOpts; | |
| TextDiagnostic(OS, LangOpts, DiagOpts) | |
| .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}, | |
| clang::tooling::ArgumentInsertPosition::END))); | |
| return F.Output.hadErrors() ? 1 : 0; | |
| } |