| // 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 "absl/base/nullability.h" |
| #include "absl/log/check.h" |
| #include "nullability/pointer_nullability.h" |
| #include "nullability/pointer_nullability_matchers.h" |
| #include "nullability/type_nullability.h" |
| #include "clang/AST/ASTContext.h" |
| #include "clang/AST/Decl.h" |
| #include "clang/AST/DeclCXX.h" |
| #include "clang/AST/Expr.h" |
| #include "clang/AST/ExprCXX.h" |
| #include "clang/AST/Stmt.h" |
| #include "clang/AST/Type.h" |
| #include "clang/ASTMatchers/ASTMatchFinder.h" |
| #include "clang/Analysis/CFG.h" |
| #include "clang/Analysis/FlowSensitive/CFGMatchSwitch.h" |
| #include "clang/Analysis/FlowSensitive/DataflowEnvironment.h" |
| #include "clang/Analysis/FlowSensitive/Value.h" |
| #include "clang/Basic/LLVM.h" |
| #include "clang/Basic/SourceLocation.h" |
| #include "clang/Basic/Specifiers.h" |
| #include "llvm/ADT/SmallVector.h" |
| #include "llvm/Support/Debug.h" |
| |
| #define DEBUG_TYPE "nullability-diagnostic" |
| |
| namespace clang::tidy::nullability { |
| |
| using ast_matchers::MatchFinder; |
| using dataflow::CFGMatchSwitchBuilder; |
| using dataflow::Environment; |
| using dataflow::PointerValue; |
| using dataflow::TransferStateForDiagnostics; |
| using ::llvm::SmallVector; |
| |
| namespace { |
| |
| // Diagnoses whether `E` violates the expectation that it is nonnull. |
| SmallVector<PointerNullabilityDiagnostic> diagnoseNonnullExpected( |
| absl::Nonnull<const Expr *> E, const Environment &Env, |
| PointerNullabilityDiagnostic::Context DiagCtx, |
| std::optional<std::string> ParamName = std::nullopt) { |
| PointerValue *ActualVal = nullptr; |
| if (isSupportedRawPointerType(E->getType())) |
| ActualVal = getPointerValueFromExpr(E, Env); |
| else |
| ActualVal = getPointerValueFromSmartPointerExpr(E, Env); |
| if (ActualVal != nullptr) { |
| if (isNullable(*ActualVal, Env)) |
| return {{PointerNullabilityDiagnostic::ErrorCode::ExpectedNonnull, |
| DiagCtx, CharSourceRange::getTokenRange(E->getSourceRange()), |
| std::move(ParamName)}}; |
| return {}; |
| } |
| |
| LLVM_DEBUG({ |
| 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 {{PointerNullabilityDiagnostic::ErrorCode::Untracked, DiagCtx, |
| CharSourceRange::getTokenRange(E->getSourceRange())}}; |
| } |
| |
| // Diagnoses whether the nullability of `E` is incompatible with the expectation |
| // set by `DeclaredType`. |
| SmallVector<PointerNullabilityDiagnostic> diagnoseTypeExprCompatibility( |
| QualType DeclaredType, absl::Nonnull<const Expr *> E, |
| const Environment &Env, ASTContext &Ctx, |
| PointerNullabilityDiagnostic::Context DiagCtx, |
| std::optional<std::string> ParamName = std::nullopt) { |
| CHECK(isSupportedPointerType(DeclaredType)); |
| return getNullabilityAnnotationsFromType(DeclaredType).front().concrete() == |
| NullabilityKind::NonNull |
| ? diagnoseNonnullExpected(E, Env, DiagCtx, ParamName) |
| : SmallVector<PointerNullabilityDiagnostic>{}; |
| } |
| |
| SmallVector<PointerNullabilityDiagnostic> diagnoseDereference( |
| absl::Nonnull<const UnaryOperator *> UnaryOp, |
| const MatchFinder::MatchResult &, |
| const TransferStateForDiagnostics<PointerNullabilityLattice> &State) { |
| return diagnoseNonnullExpected( |
| UnaryOp->getSubExpr(), State.Env, |
| PointerNullabilityDiagnostic::Context::NullableDereference); |
| } |
| |
| SmallVector<PointerNullabilityDiagnostic> diagnoseSmartPointerDereference( |
| absl::Nonnull<const CXXOperatorCallExpr *> Op, |
| const MatchFinder::MatchResult &, |
| const TransferStateForDiagnostics<PointerNullabilityLattice> &State) { |
| return diagnoseNonnullExpected( |
| Op->getArg(0), State.Env, |
| PointerNullabilityDiagnostic::Context::NullableDereference); |
| } |
| |
| SmallVector<PointerNullabilityDiagnostic> diagnoseSubscript( |
| absl::Nonnull<const ArraySubscriptExpr *> Subscript, |
| const MatchFinder::MatchResult &, |
| const TransferStateForDiagnostics<PointerNullabilityLattice> &State) { |
| return diagnoseNonnullExpected( |
| Subscript->getBase(), State.Env, |
| PointerNullabilityDiagnostic::Context::NullableDereference); |
| } |
| |
| SmallVector<PointerNullabilityDiagnostic> diagnoseArrow( |
| absl::Nonnull<const MemberExpr *> MemberExpr, |
| const MatchFinder::MatchResult &Result, |
| const TransferStateForDiagnostics<PointerNullabilityLattice> &State) { |
| return diagnoseNonnullExpected( |
| MemberExpr->getBase(), State.Env, |
| PointerNullabilityDiagnostic::Context::NullableDereference); |
| } |
| |
| // Diagnoses whether any of the arguments are incompatible with the |
| // corresponding type in the function prototype. |
| SmallVector<PointerNullabilityDiagnostic> diagnoseArgumentCompatibility( |
| const FunctionProtoType &CalleeFPT, ArrayRef<const Expr *> Args, |
| ArrayRef<const ParmVarDecl *> ParmDecls, 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()); |
| SmallVector<PointerNullabilityDiagnostic> Diagnostics; |
| for (unsigned int I = 0; I < Args.size(); ++I) { |
| auto ParamType = ParamTypes[I].getNonReferenceType(); |
| if (isSupportedPointerType(ParamType)) { |
| std::string ParamName = (I < ParmDecls.size()) |
| ? ParmDecls[I]->getDeclName().getAsString() |
| : ""; |
| Diagnostics.append(diagnoseTypeExprCompatibility( |
| ParamType, Args[I], Env, Ctx, |
| PointerNullabilityDiagnostic::Context::FunctionArgument, |
| std::move(ParamName))); |
| } |
| } |
| return Diagnostics; |
| } |
| |
| 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 |
| SmallVector<PointerNullabilityDiagnostic> diagnoseAssertNullabilityCall( |
| absl::Nonnull<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) |
| return {{PointerNullabilityDiagnostic::ErrorCode::Untracked, |
| PointerNullabilityDiagnostic::Context::Other, |
| CharSourceRange::getTokenRange(CE->getSourceRange())}}; |
| |
| if (*MaybeComputed == Expected) return {}; |
| |
| LLVM_DEBUG({ |
| // 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 {{PointerNullabilityDiagnostic::ErrorCode::AssertFailed, |
| PointerNullabilityDiagnostic::Context::Other, |
| CharSourceRange::getTokenRange(CE->getSourceRange())}}; |
| } |
| |
| SmallVector<PointerNullabilityDiagnostic> diagnoseCallExpr( |
| absl::Nonnull<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() && |
| !CE->getCallee()->hasPlaceholderType(BuiltinType::BoundMember)) { |
| auto D = |
| diagnoseNonnullExpected(CE->getCallee(), State.Env, |
| PointerNullabilityDiagnostic::Context::Other); |
| if (!D.empty()) return D; |
| } |
| |
| if (auto *FD = CE->getDirectCallee()) { |
| if (FD->getDeclName().isIdentifier() && |
| FD->getName() == "__assert_nullability") { |
| return diagnoseAssertNullabilityCall(CE, State, *Result.Context); |
| } |
| } |
| |
| 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 {}; |
| |
| auto *CalleeType = Callee->getFunctionType(); |
| if (!CalleeType) return {}; |
| |
| // 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 {}; |
| |
| 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(); |
| } |
| ArrayRef<const ParmVarDecl *> ParmDecls = {}; |
| if (Callee->getAsFunction()) |
| ParmDecls = Callee->getAsFunction()->parameters(); |
| return diagnoseArgumentCompatibility(*CalleeFPT, Args, ParmDecls, State.Env, |
| *Result.Context); |
| } |
| |
| SmallVector<PointerNullabilityDiagnostic> diagnoseConstructExpr( |
| absl::Nonnull<const CXXConstructExpr *> CE, |
| const MatchFinder::MatchResult &Result, |
| const TransferStateForDiagnostics<PointerNullabilityLattice> &State) { |
| auto *CalleeFPT = CE->getConstructor()->getType()->getAs<FunctionProtoType>(); |
| if (!CalleeFPT) return {}; |
| ArrayRef<const Expr *> ConstructorArgs(CE->getArgs(), CE->getNumArgs()); |
| |
| return diagnoseArgumentCompatibility( |
| *CalleeFPT, ConstructorArgs, |
| CE->getConstructor()->getAsFunction()->parameters(), State.Env, |
| *Result.Context); |
| } |
| |
| SmallVector<PointerNullabilityDiagnostic> diagnoseReturn( |
| absl::Nonnull<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 (!isSupportedPointerType(ReturnType)) { |
| return {}; |
| } |
| |
| auto *ReturnExpr = RS->getRetValue(); |
| CHECK(isSupportedPointerType(ReturnExpr->getType())); |
| |
| return diagnoseTypeExprCompatibility( |
| ReturnType, ReturnExpr, State.Env, *Result.Context, |
| PointerNullabilityDiagnostic::Context::ReturnValue); |
| } |
| |
| SmallVector<PointerNullabilityDiagnostic> diagnoseMemberInitializer( |
| absl::Nonnull<const CXXCtorInitializer *> CI, |
| const MatchFinder::MatchResult &Result, |
| const TransferStateForDiagnostics<PointerNullabilityLattice> &State) { |
| CHECK(CI->isAnyMemberInitializer()); |
| auto MemberType = CI->getAnyMember()->getType(); |
| if (!isSupportedPointerType(MemberType)) return {}; |
| |
| auto *MemberInitExpr = CI->getInit(); |
| return diagnoseTypeExprCompatibility( |
| MemberType, MemberInitExpr, State.Env, *Result.Context, |
| PointerNullabilityDiagnostic::Context::Initializer); |
| } |
| |
| } // namespace |
| |
| PointerNullabilityDiagnoser pointerNullabilityDiagnoser() { |
| return CFGMatchSwitchBuilder<const dataflow::TransferStateForDiagnostics< |
| PointerNullabilityLattice>, |
| SmallVector<PointerNullabilityDiagnostic>>() |
| // (*) |
| .CaseOfCFGStmt<UnaryOperator>(isPointerDereference(), diagnoseDereference) |
| .CaseOfCFGStmt<CXXOperatorCallExpr>(isSmartPointerOperatorCall("*"), |
| diagnoseSmartPointerDereference) |
| // ([]) |
| .CaseOfCFGStmt<ArraySubscriptExpr>(isPointerSubscript(), |
| diagnoseSubscript) |
| .CaseOfCFGStmt<CXXOperatorCallExpr>(isSmartPointerOperatorCall("[]"), |
| diagnoseSmartPointerDereference) |
| // (->) |
| .CaseOfCFGStmt<MemberExpr>(isPointerArrow(), diagnoseArrow) |
| .CaseOfCFGStmt<CXXOperatorCallExpr>(isSmartPointerOperatorCall("->"), |
| diagnoseSmartPointerDereference) |
| // Check compatibility of parameter assignments and return values. |
| .CaseOfCFGStmt<CallExpr>(isCallExpr(), diagnoseCallExpr) |
| .CaseOfCFGStmt<CXXConstructExpr>(isConstructExpr(), diagnoseConstructExpr) |
| .CaseOfCFGStmt<ReturnStmt>(isPointerReturn(), diagnoseReturn) |
| // Check compatibility of member initializers. |
| .CaseOfCFGInit<CXXCtorInitializer>(isCtorMemberInitializer(), |
| diagnoseMemberInitializer) |
| .Build(); |
| } |
| |
| } // namespace clang::tidy::nullability |