| // 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_verification/pointer_nullability_diagnosis.h" |
| |
| #include <optional> |
| #include <string> |
| |
| #include "nullability_verification/pointer_nullability.h" |
| #include "nullability_verification/pointer_nullability_matchers.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 { |
| namespace tidy { |
| namespace 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:"; |
| 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) { |
| assert(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(ArrayRef<QualType> ParamTypes, |
| ArrayRef<const Expr*> Args, |
| const Environment& Env, ASTContext& Ctx) { |
| assert(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. |
| std::vector<NullabilityKind> 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); |
| std::optional<ArrayRef<NullabilityKind>> MaybeComputed = |
| State.Lattice.getExprNullability(GivenExpr); |
| if (!MaybeComputed.has_value()) { |
| llvm::dbgs() |
| << "Could not evaluate __assert_nullability. Could not find the " |
| "nullability of the argument expression: "; |
| CE->dump(); |
| return false; |
| } |
| if (MaybeComputed->vec() == 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; |
| } |
| |
| // TODO(b/233582219): Handle call expressions whose callee is not a decl (e.g. |
| // a function returned from another function), or when the callee cannot be |
| // interpreted as a function type (e.g. a pointer to a function pointer). |
| std::optional<CFGElement> diagnoseCallExpr( |
| const CallExpr* CE, const MatchFinder::MatchResult& Result, |
| const TransferStateForDiagnostics<PointerNullabilityLattice>& State) { |
| auto* Callee = CE->getCalleeDecl(); |
| if (!Callee) return std::nullopt; |
| |
| auto* CalleeType = Callee->getFunctionType(); |
| if (!CalleeType) return std::nullopt; |
| |
| if (auto* FD = Callee->getAsFunction()) { |
| 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 ParamTypes = CalleeType->getAs<FunctionProtoType>()->getParamTypes(); |
| ArrayRef<const Expr*> Args(CE->getArgs(), CE->getNumArgs()); |
| if (isa<CXXOperatorCallExpr>(CE)) { |
| // The first argument of an operator call expression is the operand which |
| // does not appear in the list of parameter types. |
| Args = Args.drop_front(); |
| } |
| |
| return isIncompatibleArgumentList(ParamTypes, 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 ConstructorParamTypes = CE->getConstructor() |
| ->getType() |
| ->getAs<FunctionProtoType>() |
| ->getParamTypes(); |
| ArrayRef<const Expr*> ConstructorArgs(CE->getArgs(), CE->getNumArgs()); |
| return isIncompatibleArgumentList(ConstructorParamTypes, 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(); |
| assert(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) { |
| assert(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 nullability |
| } // namespace tidy |
| } // namespace clang |