Index: include/clang/Analysis/Analyses/ExprMutationAnalyzer.h =================================================================== --- include/clang/Analysis/Analyses/ExprMutationAnalyzer.h +++ include/clang/Analysis/Analyses/ExprMutationAnalyzer.h @@ -66,6 +66,9 @@ const Stmt *findReferenceMutation(const Expr *Exp); const Stmt *findFunctionArgMutation(const Expr *Exp); + const Stmt *findPointeeDirectMutation(const Expr *Exp); + const Stmt *findPointeeCastMutation(const Expr *Exp); + const Stmt &Stm; ASTContext &Context; llvm::DenseMapgetType()->getAs()) + return !PT->getPointeeType().isConstant(Context); + if (const auto *RT = Exp->getType()->getAs()) { + if (const CXXRecordDecl *RecordDecl = RT->getAsCXXRecordDecl()) { + bool ExpIsConst = Exp->getType().isConstant(Context); + return llvm::any_of( + RecordDecl->methods(), + [ExpIsConst, &Context](const CXXMethodDecl *MethodDecl) { + // Look for `operator*` with the same constness as `Exp`. + if (MethodDecl->getOverloadedOperator() != OO_Star || + MethodDecl->isConst() != ExpIsConst) + return false; + // Check whether the `operator*` returns non-const reference. + if (const auto *RT = + MethodDecl->getReturnType()->getAs()) + return !RT->getPointeeType().isConstant(Context); + return false; + }); + } + } + return false; +} + } // namespace const Stmt *ExprMutationAnalyzer::findMutation(const Expr *Exp) { @@ -100,7 +127,10 @@ } const Stmt *ExprMutationAnalyzer::findPointeeMutation(const Expr *Exp) { - return findMutationMemoized(Exp, {/*TODO*/}, PointeeResults); + return findMutationMemoized(Exp, + {&ExprMutationAnalyzer::findPointeeDirectMutation, + &ExprMutationAnalyzer::findPointeeCastMutation}, + PointeeResults); } const Stmt *ExprMutationAnalyzer::findPointeeMutation(const Decl *Dec) { @@ -192,6 +222,12 @@ } const Stmt *ExprMutationAnalyzer::findDirectMutation(const Expr *Exp) { + // `Exp` can be *directly* mutated if the type of `Exp` is not const, bail out + // early otherwise. + if (Exp->getType().isConstant(Context)) + return nullptr; + bool ExpIsPointer = Exp->getType()->getAs() != nullptr; + // LHS of any assignment operators. const auto AsAssignmentLhs = binaryOperator(isAssignmentOperator(), hasLHS(equalsNode(Exp))); @@ -204,14 +240,17 @@ // Invoking non-const member function. // A member function is assumed to be non-const when it is unresolved. const auto NonConstMethod = cxxMethodDecl(unless(isConst())); - const auto AsNonConstThis = - expr(anyOf(cxxMemberCallExpr(callee(NonConstMethod), on(equalsNode(Exp))), - cxxOperatorCallExpr(callee(NonConstMethod), - hasArgument(0, equalsNode(Exp))), - callExpr(callee(expr(anyOf( - unresolvedMemberExpr(hasObjectExpression(equalsNode(Exp))), - cxxDependentScopeMemberExpr( - hasObjectExpression(equalsNode(Exp))))))))); + const auto AsNonConstNonPointerThis = + ExpIsPointer + ? expr(unless(anything())) + : expr(anyOf( + cxxMemberCallExpr(callee(NonConstMethod), on(equalsNode(Exp))), + cxxOperatorCallExpr(callee(NonConstMethod), + hasArgument(0, equalsNode(Exp))), + callExpr(callee(expr(anyOf( + unresolvedMemberExpr(hasObjectExpression(equalsNode(Exp))), + cxxDependentScopeMemberExpr( + hasObjectExpression(equalsNode(Exp))))))))); // Taking address of 'Exp'. // We're assuming 'Exp' is mutated as soon as its address is taken, though in @@ -224,15 +263,19 @@ hasUnaryOperand(equalsNode(Exp))); const auto AsPointerFromArrayDecay = castExpr(hasCastKind(CK_ArrayToPointerDecay), + // A NoOp implicit cast is adding const. + unless(hasParent(implicitCastExpr(hasCastKind(CK_NoOp)))), unless(hasParent(arraySubscriptExpr())), has(equalsNode(Exp))); // Treat calling `operator->()` of move-only classes as taking address. // These are typically smart pointers with unique ownership so we treat // mutation of pointee as mutation of the smart pointer itself. const auto AsOperatorArrowThis = - cxxOperatorCallExpr(hasOverloadedOperatorName("->"), - callee(cxxMethodDecl(ofClass(isMoveOnly()), - returns(nonConstPointerType()))), - argumentCountIs(1), hasArgument(0, equalsNode(Exp))); + ExpIsPointer ? cxxOperatorCallExpr(unless(anything())) + : cxxOperatorCallExpr( + hasOverloadedOperatorName("->"), + callee(cxxMethodDecl(ofClass(isMoveOnly()), + returns(nonConstPointerType()))), + argumentCountIs(1), hasArgument(0, equalsNode(Exp))); // Used as non-const-ref argument when calling a function. // An argument is assumed to be non-const-ref when the function is unresolved. @@ -265,10 +308,11 @@ const auto AsNonConstRefReturn = returnStmt(hasReturnValue(equalsNode(Exp))); const auto Matches = - match(findAll(stmt(anyOf(AsAssignmentLhs, AsIncDecOperand, AsNonConstThis, - AsAmpersandOperand, AsPointerFromArrayDecay, - AsOperatorArrowThis, AsNonConstRefArg, - AsLambdaRefCaptureInit, AsNonConstRefReturn)) + match(findAll(stmt(anyOf(AsAssignmentLhs, AsIncDecOperand, + AsNonConstNonPointerThis, AsAmpersandOperand, + AsPointerFromArrayDecay, AsOperatorArrowThis, + AsNonConstRefArg, AsLambdaRefCaptureInit, + AsNonConstRefReturn)) .bind("stmt")), Stm, Context); return selectFirst("stmt", Matches); @@ -412,6 +456,62 @@ return nullptr; } +const Stmt *ExprMutationAnalyzer::findPointeeDirectMutation(const Expr *Exp) { + // Bail out early if pointee can't be *directly* mutated. + if (!isPointeeMutable(Exp, Context)) + return nullptr; + bool ExpIsPointer = Exp->getType()->getAs() != nullptr; + + // Invoking non-const member function. + // A member function is assumed to be non-const when it is unresolved. + // We don't handle pointer-like types here, because they need to be + // dereferenced first before any non-const member function of the pointee can + // be invoked, and once dereferenced they become real pointers. + const auto NonConstMethod = cxxMethodDecl(unless(isConst())); + const auto AsNonConstPointerThis = + ExpIsPointer + ? expr(anyOf( + cxxMemberCallExpr(callee(NonConstMethod), on(equalsNode(Exp))), + cxxOperatorCallExpr(callee(NonConstMethod), + hasArgument(0, equalsNode(Exp))), + callExpr(callee(expr(anyOf( + unresolvedMemberExpr(hasObjectExpression(equalsNode(Exp))), + cxxDependentScopeMemberExpr( + hasObjectExpression(equalsNode(Exp))))))))) + : expr(unless(anything())); + + // Used as an argument when calling a function. + const auto AsArg = + anyOf(callExpr(hasAnyArgument(equalsNode(Exp))), + cxxConstructExpr(hasAnyArgument(equalsNode(Exp))), + cxxUnresolvedConstructExpr(hasAnyArgument(equalsNode(Exp)))); + + // Captured by a lambda. + const auto AsLambdaCaptureInit = lambdaExpr(hasCaptureInit(Exp)); + + // Returned from the function. + const auto AsReturnValue = returnStmt(hasReturnValue(equalsNode(Exp))); + + const auto Matches = + match(findAll(stmt(anyOf(AsNonConstPointerThis, AsArg, + AsLambdaCaptureInit, AsReturnValue)) + .bind("stmt")), + Stm, Context); + return selectFirst("stmt", Matches); +} + +const Stmt *ExprMutationAnalyzer::findPointeeCastMutation(const Expr *Exp) { + // Only handling LValueToRValue for now for easier unit testing during + // implementation. + // TODO: properly handle all casts scenarios. + const auto Casts = + match(findAll(implicitCastExpr(hasSourceExpression(equalsNode(Exp)), + hasCastKind(CK_LValueToRValue)) + .bind(NodeID::value)), + Stm, Context); + return findExprPointeeMutation(Casts); +} + FunctionParmMutationAnalyzer::FunctionParmMutationAnalyzer( const FunctionDecl &Func, ASTContext &Context) : BodyAnalyzer(*Func.getBody(), Context) { Index: unittests/Analysis/ExprMutationAnalyzerTest.cpp =================================================================== --- unittests/Analysis/ExprMutationAnalyzerTest.cpp +++ unittests/Analysis/ExprMutationAnalyzerTest.cpp @@ -50,13 +50,23 @@ } bool isMutated(const SmallVectorImpl &Results, ASTUnit *AST) { - const auto *const S = selectFirst("stmt", Results); - const auto *const E = selectFirst("expr", Results); + const auto *S = selectFirst("stmt", Results); + const auto *E = selectFirst("expr", Results); + assert(S && E); return ExprMutationAnalyzer(*S, AST->getASTContext()).isMutated(E); } +bool isPointeeMutated(const SmallVectorImpl &Results, + ASTUnit *AST) { + const auto *S = selectFirst("stmt", Results); + const auto *E = selectFirst("expr", Results); + assert(S && E); + return ExprMutationAnalyzer(*S, AST->getASTContext()).isPointeeMutated(E); +} + SmallVector mutatedBy(const SmallVectorImpl &Results, ASTUnit *AST) { + EXPECT_TRUE(isMutated(Results, AST)); const auto *const S = selectFirst("stmt", Results); SmallVector Chain; ExprMutationAnalyzer Analyzer(*S, AST->getASTContext()); @@ -71,6 +81,19 @@ return Chain; } +std::string pointeeMutatedBy(const SmallVectorImpl &Results, + ASTUnit *AST) { + EXPECT_TRUE(isPointeeMutated(Results, AST)); + const auto *S = selectFirst("stmt", Results); + const auto *E = selectFirst("expr", Results); + ExprMutationAnalyzer Analyzer(*S, AST->getASTContext()); + std::string buffer; + llvm::raw_string_ostream stream(buffer); + const Stmt *By = Analyzer.findPointeeMutation(E); + By->printPretty(stream, nullptr, AST->getASTContext().getPrintingPolicy()); + return StringRef(stream.str()).trim().str(); +} + std::string removeSpace(std::string s) { s.erase(std::remove_if(s.begin(), s.end(), [](char c) { return std::isspace(c); }), @@ -100,10 +123,14 @@ } // namespace TEST(ExprMutationAnalyzerTest, Trivial) { - const auto AST = buildASTFromCode("void f() { int x; x; }"); - const auto Results = + auto AST = buildASTFromCode("void f() { int x; x; }"); + auto Results = match(withEnclosingCompound(declRefTo("x")), AST->getASTContext()); EXPECT_FALSE(isMutated(Results, AST.get())); + + AST = buildASTFromCode("void f() { const int x = 0; x; }"); + Results = match(withEnclosingCompound(declRefTo("x")), AST->getASTContext()); + EXPECT_FALSE(isMutated(Results, AST.get())); } class AssignmentTest : public ::testing::TestWithParam {}; @@ -134,41 +161,111 @@ Values("++x", "--x", "x++", "x--"), ); TEST(ExprMutationAnalyzerTest, NonConstMemberFunc) { - const auto AST = buildASTFromCode( + auto AST = buildASTFromCode( "void f() { struct Foo { void mf(); }; Foo x; x.mf(); }"); - const auto Results = + auto Results = match(withEnclosingCompound(declRefTo("x")), AST->getASTContext()); EXPECT_THAT(mutatedBy(Results, AST.get()), ElementsAre("x.mf()")); + EXPECT_FALSE(isPointeeMutated(Results, AST.get())); + + AST = buildASTFromCode( + "void f() { struct Foo { void mf(); }; Foo *p; p->mf(); }"); + Results = match(withEnclosingCompound(declRefTo("p")), AST->getASTContext()); + EXPECT_FALSE(isMutated(Results, AST.get())); + EXPECT_EQ(pointeeMutatedBy(Results, AST.get()), "p->mf()"); + + AST = buildASTFromCode( + "void f() { struct Foo { void mf(); }; Foo *x; Foo *&p = x; p->mf(); }"); + Results = match(withEnclosingCompound(declRefTo("p")), AST->getASTContext()); + EXPECT_FALSE(isMutated(Results, AST.get())); + EXPECT_EQ(pointeeMutatedBy(Results, AST.get()), "p->mf()"); } TEST(ExprMutationAnalyzerTest, AssumedNonConstMemberFunc) { auto AST = buildASTFromCodeWithArgs( "struct X { template void mf(); };" - "template void f() { X x; x.mf(); }", + "template void f() { X x; x.mf(); }" + "template void g() { X *p; p->mf(); }", {"-fno-delayed-template-parsing"}); auto Results = match(withEnclosingCompound(declRefTo("x")), AST->getASTContext()); EXPECT_THAT(mutatedBy(Results, AST.get()), ElementsAre("x.mf()")); + Results = match(withEnclosingCompound(declRefTo("p")), AST->getASTContext()); + EXPECT_FALSE(isMutated(Results, AST.get())); + EXPECT_EQ(pointeeMutatedBy(Results, AST.get()), "p->mf()"); - AST = buildASTFromCodeWithArgs("template void f() { T x; x.mf(); }", - {"-fno-delayed-template-parsing"}); + AST = + buildASTFromCodeWithArgs("template void f() { T x; x.mf(); }" + "template void g() { T *p; p->mf(); }", + {"-fno-delayed-template-parsing"}); Results = match(withEnclosingCompound(declRefTo("x")), AST->getASTContext()); EXPECT_THAT(mutatedBy(Results, AST.get()), ElementsAre("x.mf()")); + Results = match(withEnclosingCompound(declRefTo("p")), AST->getASTContext()); + EXPECT_FALSE(isMutated(Results, AST.get())); + EXPECT_EQ(pointeeMutatedBy(Results, AST.get()), "p->mf()"); AST = buildASTFromCodeWithArgs( "template struct X;" - "template void f() { X x; x.mf(); }", + "template void f() { X x; x.mf(); }" + "template void g() { X *p; p->mf(); }", {"-fno-delayed-template-parsing"}); Results = match(withEnclosingCompound(declRefTo("x")), AST->getASTContext()); EXPECT_THAT(mutatedBy(Results, AST.get()), ElementsAre("x.mf()")); + Results = match(withEnclosingCompound(declRefTo("p")), AST->getASTContext()); + EXPECT_FALSE(isMutated(Results, AST.get())); + EXPECT_EQ(pointeeMutatedBy(Results, AST.get()), "p->mf()"); + + AST = buildASTFromCodeWithArgs( + "struct X { template void mf() const; };" + "template void f() { const X x; x.mf(); }" + "template void g() { const X *p; p->mf(); }", + {"-fno-delayed-template-parsing"}); + Results = match(withEnclosingCompound(declRefTo("x")), AST->getASTContext()); + EXPECT_FALSE(isMutated(Results, AST.get())); + Results = match(withEnclosingCompound(declRefTo("p")), AST->getASTContext()); + EXPECT_FALSE(isPointeeMutated(Results, AST.get())); + + AST = buildASTFromCodeWithArgs( + "template void f() { const T x; x.mf(); }" + "template void g() { const T *p; p->mf(); }", + {"-fno-delayed-template-parsing"}); + Results = match(withEnclosingCompound(declRefTo("x")), AST->getASTContext()); + EXPECT_FALSE(isMutated(Results, AST.get())); + Results = match(withEnclosingCompound(declRefTo("p")), AST->getASTContext()); + EXPECT_FALSE(isPointeeMutated(Results, AST.get())); + + AST = buildASTFromCodeWithArgs( + "template struct X;" + "template void f() { const X x; x.mf(); }" + "template void g() { const X *p; p->mf(); }", + {"-fno-delayed-template-parsing"}); + Results = match(withEnclosingCompound(declRefTo("x")), AST->getASTContext()); + EXPECT_FALSE(isMutated(Results, AST.get())); + Results = match(withEnclosingCompound(declRefTo("p")), AST->getASTContext()); + EXPECT_FALSE(isPointeeMutated(Results, AST.get())); } TEST(ExprMutationAnalyzerTest, ConstMemberFunc) { - const auto AST = buildASTFromCode( + auto AST = buildASTFromCode( "void f() { struct Foo { void mf() const; }; Foo x; x.mf(); }"); - const auto Results = + auto Results = match(withEnclosingCompound(declRefTo("x")), AST->getASTContext()); EXPECT_FALSE(isMutated(Results, AST.get())); + + AST = buildASTFromCode( + "void f() { struct Foo { void mf() const; }; const Foo x; x.mf(); }"); + Results = match(withEnclosingCompound(declRefTo("x")), AST->getASTContext()); + EXPECT_FALSE(isMutated(Results, AST.get())); + + AST = buildASTFromCode( + "void f() { struct Foo { void mf() const; }; Foo *p; p->mf(); }"); + Results = match(withEnclosingCompound(declRefTo("p")), AST->getASTContext()); + EXPECT_FALSE(isPointeeMutated(Results, AST.get())); + + AST = buildASTFromCode( + "void f() { struct Foo { void mf() const; }; const Foo *p; p->mf(); }"); + Results = match(withEnclosingCompound(declRefTo("p")), AST->getASTContext()); + EXPECT_FALSE(isPointeeMutated(Results, AST.get())); } TEST(ExprMutationAnalyzerTest, NonConstOperator) { @@ -193,9 +290,10 @@ match(withEnclosingCompound(declRefTo("x")), AST->getASTContext()); EXPECT_FALSE(isMutated(Results, AST.get())); - AST = buildASTFromCode("void g(int*); void f() { int* x; g(x); }"); - Results = match(withEnclosingCompound(declRefTo("x")), AST->getASTContext()); + AST = buildASTFromCode("void g(int*); void f() { int* p; g(p); }"); + Results = match(withEnclosingCompound(declRefTo("p")), AST->getASTContext()); EXPECT_FALSE(isMutated(Results, AST.get())); + EXPECT_EQ(pointeeMutatedBy(Results, AST.get()), "g(p)"); AST = buildASTFromCode("typedef int* IntPtr;" "void g(IntPtr); void f() { int* x; g(x); }"); @@ -538,11 +636,20 @@ AST = buildASTFromCode("int* f() { int* x; return x; }"); Results = match(withEnclosingCompound(declRefTo("x")), AST->getASTContext()); EXPECT_FALSE(isMutated(Results, AST.get())); + EXPECT_EQ(pointeeMutatedBy(Results, AST.get()), "return x;"); + + AST = buildASTFromCode("const int* f() { int* x; return x; }"); + Results = match(withEnclosingCompound(declRefTo("x")), AST->getASTContext()); + EXPECT_FALSE(isMutated(Results, AST.get())); + EXPECT_FALSE(isPointeeMutated(Results, AST.get())); + // TODO: this should work when casts of pointers are handled. + // EXPECT_EQ(pointeeMutatedBy(Results, AST.get()), "return x;"); AST = buildASTFromCode("typedef int* IntPtr;" "IntPtr f() { int* x; return x; }"); Results = match(withEnclosingCompound(declRefTo("x")), AST->getASTContext()); EXPECT_FALSE(isMutated(Results, AST.get())); + EXPECT_EQ(pointeeMutatedBy(Results, AST.get()), "return x;"); } TEST(ExprMutationAnalyzerTest, ReturnAsNonConstRef) { @@ -577,18 +684,31 @@ } TEST(ExprMutationAnalyzerTest, TakeAddress) { - const auto AST = buildASTFromCode("void g(int*); void f() { int x; g(&x); }"); - const auto Results = + auto AST = buildASTFromCode("void g(int*); void f() { int x; g(&x); }"); + auto Results = match(withEnclosingCompound(declRefTo("x")), AST->getASTContext()); EXPECT_THAT(mutatedBy(Results, AST.get()), ElementsAre("&x")); + + AST = buildASTFromCode( + "void g(const int*); void f() { const int x = 10; g(&x); }"); + Results = match(withEnclosingCompound(declRefTo("x")), AST->getASTContext()); + EXPECT_FALSE(isMutated(Results, AST.get())); } TEST(ExprMutationAnalyzerTest, ArrayToPointerDecay) { - const auto AST = - buildASTFromCode("void g(int*); void f() { int x[2]; g(x); }"); - const auto Results = + auto AST = buildASTFromCode("void g(int*); void f() { int x[2]; g(x); }"); + auto Results = match(withEnclosingCompound(declRefTo("x")), AST->getASTContext()); EXPECT_THAT(mutatedBy(Results, AST.get()), ElementsAre("x")); + + AST = buildASTFromCode("void f() { int x[2]; *x = 10; }"); + Results = match(withEnclosingCompound(declRefTo("x")), AST->getASTContext()); + EXPECT_THAT(mutatedBy(Results, AST.get()), ElementsAre("x")); + + AST = buildASTFromCode( + "void g(const int*); void f() { const int x[2] = {}; g(x); }"); + Results = match(withEnclosingCompound(declRefTo("x")), AST->getASTContext()); + EXPECT_FALSE(isMutated(Results, AST.get())); } TEST(ExprMutationAnalyzerTest, TemplateWithArrayToPointerDecay) { @@ -880,33 +1000,53 @@ } TEST(ExprMutationAnalyzerTest, LambdaDefaultCaptureByValue) { - const auto AST = buildASTFromCode("void f() { int x; [=]() { x; }; }"); - const auto Results = + auto AST = buildASTFromCode("void f() { int x; [=]() { x; }; }"); + auto Results = match(withEnclosingCompound(declRefTo("x")), AST->getASTContext()); EXPECT_FALSE(isMutated(Results, AST.get())); + + AST = buildASTFromCode("void f() { int *p; [=]() { *p = 10; }; }"); + Results = match(withEnclosingCompound(declRefTo("p")), AST->getASTContext()); + EXPECT_FALSE(isMutated(Results, AST.get())); + EXPECT_THAT(pointeeMutatedBy(Results, AST.get()), + ResultOf(removeSpace, "[=](){*p=10;}")); } TEST(ExprMutationAnalyzerTest, LambdaExplicitCaptureByValue) { - const auto AST = buildASTFromCode("void f() { int x; [x]() { x; }; }"); - const auto Results = + auto AST = buildASTFromCode("void f() { int x; [x]() { x; }; }"); + auto Results = match(withEnclosingCompound(declRefTo("x")), AST->getASTContext()); EXPECT_FALSE(isMutated(Results, AST.get())); + + AST = buildASTFromCode("void f() { int *p; [p]() { *p = 10; }; }"); + Results = match(withEnclosingCompound(declRefTo("p")), AST->getASTContext()); + EXPECT_FALSE(isMutated(Results, AST.get())); + EXPECT_THAT(pointeeMutatedBy(Results, AST.get()), + ResultOf(removeSpace, "[p](){*p=10;}")); } TEST(ExprMutationAnalyzerTest, LambdaDefaultCaptureByRef) { - const auto AST = buildASTFromCode("void f() { int x; [&]() { x = 10; }; }"); - const auto Results = + auto AST = buildASTFromCode("void f() { int x; [&]() { x = 10; }; }"); + auto Results = match(withEnclosingCompound(declRefTo("x")), AST->getASTContext()); EXPECT_THAT(mutatedBy(Results, AST.get()), ElementsAre(ResultOf(removeSpace, "[&](){x=10;}"))); + + AST = buildASTFromCode("void f() { const int x = 10; [&]() { x; }; }"); + Results = match(withEnclosingCompound(declRefTo("x")), AST->getASTContext()); + EXPECT_FALSE(isMutated(Results, AST.get())); } TEST(ExprMutationAnalyzerTest, LambdaExplicitCaptureByRef) { - const auto AST = buildASTFromCode("void f() { int x; [&x]() { x = 10; }; }"); - const auto Results = + auto AST = buildASTFromCode("void f() { int x; [&x]() { x = 10; }; }"); + auto Results = match(withEnclosingCompound(declRefTo("x")), AST->getASTContext()); EXPECT_THAT(mutatedBy(Results, AST.get()), ElementsAre(ResultOf(removeSpace, "[&x](){x=10;}"))); + + AST = buildASTFromCode("void f() { const int x = 10; [&x]() { x; }; }"); + Results = match(withEnclosingCompound(declRefTo("x")), AST->getASTContext()); + EXPECT_FALSE(isMutated(Results, AST.get())); } TEST(ExprMutationAnalyzerTest, RangeForArrayByRefModified) { @@ -1106,4 +1246,12 @@ EXPECT_THAT(mutatedBy(Results, AST.get()), ElementsAre("x->mf()")); } +TEST(ExprMutationAnalyzerTest, Dereference) { + // TODO: add follow cases: + // - multiple levels of pointers `int ***`, `int * const *` + // (ensure that having a const pointer does no influence the pointee + // analysis `int * const p = &i; *p = 42;`) + // - a class with `operator*` + `operator->` +} + } // namespace clang