blob: 6456910b8d759640f4e8a0e3e06710d5a3696fc5 [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
#include "nullability/pointer_nullability_diagnosis.h"
#include <optional>
#include <string>
#include "nullability/pointer_nullability.h"
#include "nullability/pointer_nullability_matchers.h"
#include "nullability/type_nullability.h"
#include "clang/AST/ASTContext.h"
#include "clang/AST/DeclCXX.h"
#include "clang/AST/Expr.h"
#include "clang/AST/ExprCXX.h"
#include "clang/AST/Stmt.h"
#include "clang/ASTMatchers/ASTMatchFinder.h"
#include "clang/Analysis/FlowSensitive/CFGMatchSwitch.h"
#include "clang/Analysis/FlowSensitive/DataflowEnvironment.h"
#include "clang/Basic/Specifiers.h"
namespace clang::tidy::nullability {
using ast_matchers::MatchFinder;
using dataflow::CFGMatchSwitchBuilder;
using dataflow::Environment;
using dataflow::TransferStateForDiagnostics;
namespace {
// Returns true if `Expr` is uninterpreted or known to be nullable.
bool isNullableOrUntracked(const Expr *E, const Environment &Env) {
auto *ActualVal = getPointerValueFromExpr(E, Env);
if (ActualVal == nullptr) {
llvm::dbgs()
<< "The dataflow analysis framework does not model a PointerValue for "
"the following Expr, and thus its dereference is marked as "
"unsafe:\n";
E->dump();
}
return !ActualVal || isNullable(*ActualVal, Env);
}
// Returns true if an uninterpreted or nullable `Expr` was assigned to a
// construct with a non-null `DeclaredType`.
bool isIncompatibleAssignment(QualType DeclaredType, const Expr *E,
const Environment &Env, ASTContext &Ctx) {
CHECK(DeclaredType->isAnyPointerType());
return getNullabilityKind(DeclaredType, Ctx) == NullabilityKind::NonNull &&
isNullableOrUntracked(E, Env);
}
std::optional<CFGElement> diagnoseDereference(
const UnaryOperator *UnaryOp, const MatchFinder::MatchResult &,
const TransferStateForDiagnostics<PointerNullabilityLattice> &State) {
if (isNullableOrUntracked(UnaryOp->getSubExpr(), State.Env)) {
return std::optional<CFGElement>(CFGStmt(UnaryOp));
}
return std::nullopt;
}
std::optional<CFGElement> diagnoseArrow(
const MemberExpr *MemberExpr, const MatchFinder::MatchResult &Result,
const TransferStateForDiagnostics<PointerNullabilityLattice> &State) {
if (isNullableOrUntracked(MemberExpr->getBase(), State.Env)) {
return std::optional<CFGElement>(CFGStmt(MemberExpr));
}
return std::nullopt;
}
bool isIncompatibleArgumentList(const FunctionProtoType &CalleeFPT,
ArrayRef<const Expr *> Args,
const Environment &Env, ASTContext &Ctx) {
auto ParamTypes = CalleeFPT.getParamTypes();
// C-style varargs cannot be annotated and therefore are unchecked.
if (CalleeFPT.isVariadic()) {
CHECK_GE(Args.size(), ParamTypes.size());
Args = Args.take_front(ParamTypes.size());
}
CHECK_EQ(ParamTypes.size(), Args.size());
for (unsigned int I = 0; I < Args.size(); ++I) {
auto ParamType = ParamTypes[I].getNonReferenceType();
if (!ParamType->isAnyPointerType()) {
continue;
}
if (isIncompatibleAssignment(ParamType, Args[I], Env, Ctx)) {
return true;
}
}
return false;
}
NullabilityKind parseNullabilityKind(StringRef EnumName) {
return llvm::StringSwitch<NullabilityKind>(EnumName)
.Case("NK_nonnull", NullabilityKind::NonNull)
.Case("NK_nullable", NullabilityKind::Nullable)
.Case("NK_unspecified", NullabilityKind::Unspecified)
.Default(NullabilityKind::Unspecified);
}
/// Evaluates the `__assert_nullability` call by comparing the expected
/// nullability to the nullability computed by the dataflow analysis.
///
/// If the function being diagnosed is called `__assert_nullability`, we assume
/// it is a call of the shape __assert_nullability<a, b, c, ...>(p), where `p`
/// is an expression that contains pointers and a, b, c ... represent each of
/// the NullabilityKinds in `p`'s expected nullability. An expression's
/// nullability can be expressed as a vector of NullabilityKinds, where each
/// vector element corresponds to one of the pointers contained in the
/// expression.
///
/// For example:
/// \code
/// enum NullabilityKind {
/// NK_nonnull,
/// NK_nullable,
/// NK_unspecified,
/// };
///
/// template<NullabilityKind ...NK, typename T>
/// void __assert_nullability(T&);
///
/// template<typename T0, typename T1>
/// struct Struct2Arg {
/// T0 arg0;
/// T1 arg1;
/// };
///
/// void target(Struct2Arg<int *, int * _Nullable> p) {
/// __assert_nullability<NK_unspecified, NK_nullable>(p);
/// }
/// \endcode
bool diagnoseAssertNullabilityCall(
const CallExpr *CE,
const TransferStateForDiagnostics<PointerNullabilityLattice> &State,
ASTContext &Ctx) {
auto *DRE = cast<DeclRefExpr>(CE->getCallee()->IgnoreImpCasts());
// Extract the expected nullability from the template parameter pack.
TypeNullability Expected;
for (auto P : DRE->template_arguments()) {
if (P.getArgument().getKind() == TemplateArgument::Expression) {
if (auto *EnumDRE = dyn_cast<DeclRefExpr>(P.getSourceExpression())) {
Expected.push_back(parseNullabilityKind(EnumDRE->getDecl()->getName()));
}
}
}
// Compare the nullability computed by nullability analysis with the
// expected one.
const Expr *GivenExpr = CE->getArg(0);
const TypeNullability *MaybeComputed =
State.Lattice.getExprNullability(GivenExpr);
if (MaybeComputed == nullptr) {
llvm::dbgs()
<< "Could not evaluate __assert_nullability. Could not find the "
"nullability of the argument expression: ";
CE->dump();
return false;
}
if (*MaybeComputed == Expected) return true;
// The computed and expected nullabilities differ. Print both to aid
// debugging.
llvm::dbgs() << "__assert_nullability failed at location: ";
CE->getExprLoc().print(llvm::dbgs(), Ctx.getSourceManager());
llvm::dbgs() << "\nExpression:\n";
GivenExpr->dump();
llvm::dbgs() << "Expected nullability: ";
llvm::dbgs() << nullabilityToString(Expected) << "\n";
llvm::dbgs() << "Computed nullability: ";
llvm::dbgs() << nullabilityToString(*MaybeComputed) << "\n";
return false;
}
std::optional<CFGElement> diagnoseCallExpr(
const CallExpr *CE, const MatchFinder::MatchResult &Result,
const TransferStateForDiagnostics<PointerNullabilityLattice> &State) {
// Check whether the callee is null.
// - Skip direct callees to avoid handling builtin functions, which don't
// decay to pointer.
// - Skip member callees, as they are not pointers at all (rather "bound
// member function type").
// Note that in `(obj.*nullable_pmf)()` the deref is *before* the call.
if (!CE->getDirectCallee() && !isa<CXXMemberCallExpr>(CE) &&
isNullableOrUntracked(CE->getCallee(), State.Env)) {
return std::optional<CFGElement>(CFGStmt(CE->getCallee()));
}
if (auto *FD = CE->getDirectCallee()) {
if (FD->getDeclName().isIdentifier() &&
FD->getName() == "__assert_nullability" &&
!diagnoseAssertNullabilityCall(CE, State, *Result.Context)) {
// TODO: Handle __assert_nullability failures differently from regular
// diagnostic ([[unsafe]]) failures.
return std::optional<CFGElement>(CFGStmt(CE));
}
}
auto *Callee = CE->getCalleeDecl();
// TODO(mboehme): Retrieve the nullability directly from the callee using
// `getNullabilityForChild(CE->getCallee())`, as what we have here now
// doesn't work for callees that don't have a decl.
if (!Callee) return std::nullopt;
auto *CalleeType = Callee->getFunctionType();
if (!CalleeType) return std::nullopt;
// TODO(mboehme): We're only looking at the nullability spelled on the
// `FunctionProtoType`, but there could be extra information in the callee.
// An example (due to sammccall@):
//
// template <typename T> struct Sink {
// static void eat(T) { ... }
// }
// void target(Sink<Nonnull<int*>> &S) {
// S<Nonnull<int*>>::eat(nullptr); // no warning
// // callee is instantiated Sink<int*>::eat(int*)
// // however nullability vector of DRE S::eat should be [Nonnull]
// // (not sure if it is today)
// }
auto *CalleeFPT = CalleeType->getAs<FunctionProtoType>();
if (!CalleeFPT) return std::nullopt;
ArrayRef<const Expr *> Args(CE->getArgs(), CE->getNumArgs());
// The first argument of an member operator call expression is the implicit
// object argument, which does not appear in the list of parameter types.
// Note that operator calls always have a direct callee.
if (isa<CXXOperatorCallExpr>(CE) &&
isa<CXXMethodDecl>(CE->getDirectCallee())) {
Args = Args.drop_front();
}
return isIncompatibleArgumentList(*CalleeFPT, Args, State.Env,
*Result.Context)
? std::optional<CFGElement>(CFGStmt(CE))
: std::nullopt;
}
std::optional<CFGElement> diagnoseConstructExpr(
const CXXConstructExpr *CE, const MatchFinder::MatchResult &Result,
const TransferStateForDiagnostics<PointerNullabilityLattice> &State) {
auto *CalleeFPT = CE->getConstructor()->getType()->getAs<FunctionProtoType>();
if (!CalleeFPT) return std::nullopt;
ArrayRef<const Expr *> ConstructorArgs(CE->getArgs(), CE->getNumArgs());
return isIncompatibleArgumentList(*CalleeFPT, ConstructorArgs, State.Env,
*Result.Context)
? std::optional<CFGElement>(CFGStmt(CE))
: std::nullopt;
}
std::optional<CFGElement> diagnoseReturn(
const ReturnStmt *RS, const MatchFinder::MatchResult &Result,
const TransferStateForDiagnostics<PointerNullabilityLattice> &State) {
auto ReturnType = cast<FunctionDecl>(State.Env.getDeclCtx())->getReturnType();
// TODO: Handle non-pointer return types.
if (!ReturnType->isPointerType()) {
return std::nullopt;
}
auto *ReturnExpr = RS->getRetValue();
CHECK(ReturnExpr->getType()->isPointerType());
return isIncompatibleAssignment(ReturnType, ReturnExpr, State.Env,
*Result.Context)
? std::optional<CFGElement>(CFGStmt(RS))
: std::nullopt;
}
std::optional<CFGElement> diagnoseMemberInitializer(
const CXXCtorInitializer *CI, const MatchFinder::MatchResult &Result,
const TransferStateForDiagnostics<PointerNullabilityLattice> &State) {
CHECK(CI->isAnyMemberInitializer());
auto MemberType = CI->getAnyMember()->getType();
if (!MemberType->isAnyPointerType()) {
return std::nullopt;
}
auto MemberInitExpr = CI->getInit();
return isIncompatibleAssignment(MemberType, MemberInitExpr, State.Env,
*Result.Context)
? std::optional<CFGElement>(CFGInitializer(CI))
: std::nullopt;
}
auto buildDiagnoser() {
return CFGMatchSwitchBuilder<const dataflow::TransferStateForDiagnostics<
PointerNullabilityLattice>,
std::optional<CFGElement>>()
// (*)
.CaseOfCFGStmt<UnaryOperator>(isPointerDereference(), diagnoseDereference)
// (->)
.CaseOfCFGStmt<MemberExpr>(isPointerArrow(), diagnoseArrow)
// Check compatibility of parameter assignments
.CaseOfCFGStmt<CallExpr>(isCallExpr(), diagnoseCallExpr)
.CaseOfCFGStmt<ReturnStmt>(isPointerReturn(), diagnoseReturn)
.CaseOfCFGStmt<CXXConstructExpr>(isConstructExpr(), diagnoseConstructExpr)
.CaseOfCFGInit<CXXCtorInitializer>(isCtorMemberInitializer(),
diagnoseMemberInitializer)
.Build();
}
} // namespace
PointerNullabilityDiagnoser::PointerNullabilityDiagnoser()
: Diagnoser(buildDiagnoser()) {}
} // namespace clang::tidy::nullability