| // 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 |
| |
| #include "nullability/test/check_diagnostics.h" |
| |
| #include <algorithm> |
| #include <array> |
| #include <iterator> |
| #include <memory> |
| #include <set> |
| #include <string> |
| #include <vector> |
| |
| #include "nullability/pointer_nullability_diagnosis.h" |
| #include "nullability/pragma.h" |
| #include "nullability/test/test_headers.h" |
| #include "clang/AST/ASTConsumer.h" |
| #include "clang/AST/ASTContext.h" |
| #include "clang/AST/Decl.h" |
| #include "clang/ASTMatchers/ASTMatchers.h" |
| #include "clang/Analysis/CFG.h" |
| #include "clang/Basic/LLVM.h" |
| #include "clang/Frontend/FrontendActions.h" |
| #include "clang/Testing/CommandLineArgs.h" |
| #include "clang/Testing/TestAST.h" |
| #include "clang/Tooling/Tooling.h" |
| #include "third_party/llvm/llvm-project/clang/unittests/Analysis/FlowSensitive/TestingSupport.h" |
| #include "llvm/ADT/ArrayRef.h" |
| #include "llvm/ADT/DenseMap.h" |
| #include "llvm/ADT/SmallVector.h" |
| #include "llvm/ADT/StringRef.h" |
| #include "llvm/Support/raw_ostream.h" |
| #include "llvm/Testing/Annotations/Annotations.h" |
| #include "external/llvm-project/third-party/unittest/googletest/include/gtest/gtest.h" |
| |
| namespace clang::tidy::nullability { |
| |
| bool checkDiagnostics(ASTContext &AST, llvm::Annotations AnnotatedCode, |
| const NullabilityPragmas &Pragmas, bool AllowUntracked) { |
| using ast_matchers::BoundNodes; |
| using ast_matchers::hasAnyName; |
| using ast_matchers::match; |
| using ast_matchers::stmt; |
| using ast_matchers::valueDecl; |
| |
| SmallVector<BoundNodes, 1> MatchResult = |
| match(valueDecl(hasAnyName("target", "~target")).bind("target"), AST); |
| if (MatchResult.empty()) { |
| ADD_FAILURE() << "didn't find target declaration"; |
| return false; |
| } |
| |
| bool Success = true; |
| // We already checked MatchResult is not empty above, but we skip |
| // isTemplated() functions. Make sure we didn't skip every MatchResult. |
| bool FoundMatch = false; |
| for (const ast_matchers::BoundNodes &BN : MatchResult) { |
| const auto *Target = BN.getNodeAs<ValueDecl>("target"); |
| // Skip templates and only analyze instantiations |
| // (where isTemplated is false) |
| if (Target->isTemplated()) continue; |
| FoundMatch = true; |
| |
| llvm::DenseMap<unsigned, std::string> Annotations = |
| dataflow::test::buildLineToAnnotationMapping( |
| AST.getSourceManager(), AST.getLangOpts(), Target->getSourceRange(), |
| AnnotatedCode); |
| |
| SmallVector<PointerNullabilityDiagnostic> Diagnostics; |
| if (llvm::Error Err = |
| diagnosePointerNullability(Target, Pragmas).moveInto(Diagnostics)) { |
| ADD_FAILURE() << Err; |
| return false; |
| } |
| |
| // Note: use sorted sets for expected and actual lines to improve |
| // readability of the error output in case the test fails. |
| std::set<unsigned> ExpectedLines, ActualLines; |
| for (const auto &[Line, _] : Annotations) { |
| ExpectedLines.insert(Line); |
| } |
| for (const auto &Diag : Diagnostics) { |
| // Untracked errors are not reported in production by default, so only |
| // consider those if explicitly requested by AllowUntracked. |
| if (AllowUntracked || |
| Diag.Code != PointerNullabilityDiagnostic::ErrorCode::Untracked) { |
| ActualLines.insert(AST.getSourceManager().getPresumedLineNumber( |
| Diag.Range.getBegin())); |
| } |
| } |
| if (ActualLines != ExpectedLines) { |
| Success = false; |
| |
| // Clang's line numbers are one-based, so add an extra empty line at the |
| // beginning. |
| SmallVector<llvm::StringRef> Lines = {""}; |
| AnnotatedCode.code().split(Lines, '\n'); |
| |
| std::vector<unsigned> ExpectedButNotFound; |
| std::set_difference(ExpectedLines.begin(), ExpectedLines.end(), |
| ActualLines.begin(), ActualLines.end(), |
| std::back_inserter(ExpectedButNotFound)); |
| std::vector<unsigned> FoundButNotExpected; |
| std::set_difference(ActualLines.begin(), ActualLines.end(), |
| ExpectedLines.begin(), ExpectedLines.end(), |
| std::back_inserter(FoundButNotExpected)); |
| |
| std::string ErrorMessage; |
| llvm::raw_string_ostream OS(ErrorMessage); |
| |
| if (!ExpectedButNotFound.empty()) { |
| OS << "Expected diagnostics but didn't find them:\n"; |
| for (unsigned Line : ExpectedButNotFound) |
| OS << Line << ": " << Lines[Line] << "\n"; |
| } |
| if (!FoundButNotExpected.empty()) { |
| OS << "Found diagnostics but didn't expect them:\n"; |
| for (unsigned Line : FoundButNotExpected) |
| OS << Line << ": " << Lines[Line] << "\n"; |
| } |
| |
| ADD_FAILURE() << ErrorMessage; |
| } |
| } |
| if (!FoundMatch) { |
| ADD_FAILURE() << "didn't find target declaration (after skipping templated " |
| "functions) -- add an instantiation?"; |
| return false; |
| } |
| |
| return Success; |
| } |
| |
| static bool checkDiagnostics(llvm::StringRef SourceCode, TestLanguage Lang, |
| bool AllowUntracked) { |
| llvm::Annotations AnnotatedCode(SourceCode); |
| clang::TestInputs Inputs(AnnotatedCode.code()); |
| Inputs.Language = Lang; |
| Inputs.ExtraArgs = { |
| "-fsyntax-only", |
| "-Wno-unused-value", |
| "-Wno-nonnull", |
| "-include", |
| "check_diagnostics_preamble.h", |
| "-I.", |
| }; |
| for (const auto &Entry : |
| llvm::ArrayRef(test_headers_create(), test_headers_size())) |
| Inputs.ExtraFiles.try_emplace(Entry.name, Entry.data); |
| NullabilityPragmas Pragmas; |
| Inputs.MakeAction = [&] { |
| struct Action : public SyntaxOnlyAction { |
| NullabilityPragmas &Pragmas; |
| Action(NullabilityPragmas &Pragmas) : Pragmas(Pragmas) {} |
| |
| std::unique_ptr<ASTConsumer> CreateASTConsumer( |
| CompilerInstance &CI, llvm::StringRef File) override { |
| registerPragmaHandler(CI.getPreprocessor(), Pragmas); |
| return SyntaxOnlyAction::CreateASTConsumer(CI, File); |
| } |
| }; |
| return std::make_unique<Action>(Pragmas); |
| }; |
| clang::TestAST AST(Inputs); |
| return checkDiagnostics(AST.context(), AnnotatedCode, Pragmas, |
| AllowUntracked); |
| } |
| |
| // Run in C++17 and C++20 mode to cover differences in the AST between modes |
| // (e.g. C++20 can contain `CXXRewrittenBinaryOperator`). |
| static constexpr std::array<TestLanguage, 2> CXXLanguagesToTest = { |
| TestLanguage::Lang_CXX17, TestLanguage::Lang_CXX20}; |
| |
| bool checkDiagnostics(llvm::StringRef SourceCode) { |
| for (TestLanguage Lang : CXXLanguagesToTest) |
| if (!checkDiagnostics(SourceCode, Lang, /*AllowUntracked=*/false)) |
| return false; |
| return true; |
| } |
| |
| bool checkDiagnosticsHasUntracked(llvm::StringRef SourceCode) { |
| for (TestLanguage Lang : CXXLanguagesToTest) |
| if (!checkDiagnostics(SourceCode, Lang, /*AllowUntracked=*/true)) |
| return false; |
| return true; |
| } |
| |
| bool checkDiagnosticsWithMin(llvm::StringRef SourceCode, TestLanguage Min) { |
| for (TestLanguage Lang : CXXLanguagesToTest) { |
| if (Lang < Min) continue; |
| if (!checkDiagnostics(SourceCode, Lang, /*AllowUntracked=*/false)) |
| return false; |
| } |
| return true; |
| } |
| |
| } // namespace clang::tidy::nullability |