blob: 7050b4c504250944a9c5b969cd2e75813bc1e03f [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
// 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/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/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/ControlFlowContext.h"
#include "clang/Analysis/FlowSensitive/DataflowAnalysis.h"
#include "clang/Analysis/FlowSensitive/DataflowAnalysisContext.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/CompilationDatabase.h"
#include "clang/Tooling/StandaloneExecution.h"
#include "clang/Tooling/Tooling.h"
#include "llvm/ADT/ArrayRef.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/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")) {}
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,
llvm::ArrayRef<NullabilityKind> WantNulls,
llvm::ArrayRef<NullabilityKind> GotNulls) {
if (WantCanon != GotCanon) {
Diags.Report(Call, WrongTypeCanonical) << WantCanon << GotCanon;
} else if (WantNulls != GotNulls) {
Diags.Report(Call, WrongTypeNullability)
<< nullabilityToString(WantNulls) << nullabilityToString(GotNulls);
}
}
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;
};
// 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();
}
using AnalysisState =
const dataflow::DataflowAnalysisState<PointerNullabilityLattice>;
// Match any special assertions, check the condition, diagnose on failure.
void diagnoseCall(const CallExpr &CE, const ASTContext &Ctx, Diagnoser &Diags,
const AnalysisState &State) {
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 = getNullabilityAnnotationsFromType(*Want);
TypeNullability GotNulls = unspecifiedNullability(&Got);
if (const auto *GN = State.Lattice.getExprNullability(&Got)) GotNulls = *GN;
Diags.diagnoseType(CE.getBeginLoc(), Got.getSourceRange(), WantCanon,
GotCanon, WantNulls, GotNulls);
}
}
// 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, 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 CFCtx = require(dataflow::ControlFlowContext::build(Func));
PointerNullabilityAnalysis Analysis(Ctx);
require(
runDataflowAnalysis(CFCtx, Analysis, dataflow::Environment(DACtx, Func),
[&](const CFGElement &Elt, AnalysisState &State) {
if (auto CS = Elt.getAs<CFGStmt>())
if (auto *CE = dyn_cast<CallExpr>(CS->getStmt()))
diagnoseCall(*CE, Ctx, Diags, State);
}));
}
// Absorbs test start/end events and diagnostics.
// Produces stdout output, and also Bazel test.xml report.
class TestOutput : public DiagnosticConsumer {
public:
TestOutput()
: Out(llvm::outs()),
XMLStorage(openXML()),
XML(XMLStorage ? *XMLStorage : llvm::nulls()) {
XML << "<testsuites>\n";
}
~TestOutput() override { XML << "</testsuites>\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.getName();
CurrentCase->Start = std::chrono::steady_clock::now();
Out << "--- Test: " << CurrentCase->Name << " ---\n";
}
void endTest(llvm::StringRef LogPath) {
assert(CurrentCase.has_value());
Out << (CurrentCase->Failures.empty() ? "PASS" : "FAIL") << "\n";
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;
LangOptions LangOpts;
llvm::raw_ostream &Out; // Plain-text stdout 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 {
llvm::StringRef Name;
std::vector<std::pair<std::string, std::string>> Failures;
std::chrono::steady_clock::time_point Start;
};
std::optional<TestCase> CurrentCase;
};
// AST consumer that analyzes [[clang::annotate("test")]] functions as tests.
class Consumer : public ASTConsumer {
public:
Consumer(TestOutput &Output) : Output(Output) {}
private:
void Initialize(ASTContext &Context) override {
Diagnoser.emplace(Context.getDiagnostics());
}
void HandleTranslationUnit(ASTContext &Ctx) override {
assert(Diagnoser.has_value());
// 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>()) {
// This is a test we can run directly.
Output.startTest(*FD);
auto [LogPath, LogStream] = openTestLog(FD->getName());
runTest(*FD, *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)};
}
TestOutput &Output;
std::optional<Diagnoser> Diagnoser;
};
} // 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;
std::unique_ptr<clang::ASTConsumer> newASTConsumer() {
return std::make_unique<Consumer>(Output);
}
bool handleBeginSource(clang::CompilerInstance &CI) override {
const auto &SM = CI.getSourceManager();
Output.startSuite(llvm::sys::path::stem(llvm::sys::path::filename(
SM.getBufferName(SM.getLocForStartOfFile(SM.getMainFileID())))));
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 -- -Ipath/to/headers\n";
exit(1);
}
clang::tooling::StandaloneToolExecutor Executor{*CDB, Sources};
require(Executor.execute(clang::tooling::newFrontendActionFactory(&F, &F)));
return F.Output.hadErrors() ? 1 : 0;
}