| // 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 |
| |
| // Tests for propagating pointees through function calls. |
| // |
| // Not every test that contains a function call should go here -- just those |
| // that test some specific aspect of the logic that propagates pointees through |
| // function calls. |
| |
| #include "gmock/gmock.h" |
| #include "gtest/gtest.h" |
| #include "lifetime_analysis/test/lifetime_analysis_test.h" |
| |
| namespace clang { |
| namespace tidy { |
| namespace lifetimes { |
| namespace { |
| |
| TEST_F(LifetimeAnalysisTest, SimpleFnIdentity) { |
| EXPECT_THAT(GetLifetimes(R"( |
| int* f(int* a) { |
| return a; |
| } |
| int* target(int* a) { |
| return f(a); |
| } |
| )"), |
| LifetimesAre({{"f", "a -> a"}, {"target", "a -> a"}})); |
| } |
| |
| TEST_F(LifetimeAnalysisTest, SimpleFnStatic) { |
| EXPECT_THAT(GetLifetimes(R"( |
| int* f() { |
| static int i = 42; |
| return &i; |
| } |
| int* target() { |
| return f(); |
| } |
| )"), |
| LifetimesAre({{"f", "-> a"}, {"target", "-> a"}})); |
| } |
| |
| TEST_F(LifetimeAnalysisTest, SimpleFnStaticOutParam) { |
| EXPECT_THAT(GetLifetimes(R"( |
| void f(int** p) { |
| static int i = 42; |
| *p = &i; |
| } |
| int* target() { |
| int* p; |
| f(&p); |
| return p; |
| } |
| )"), |
| LifetimesAre({{"f", "(a, b)"}, {"target", "-> a"}})); |
| } |
| |
| TEST_F(LifetimeAnalysisTest, SimpleFnIdentityArg1) { |
| EXPECT_THAT(GetLifetimes(R"( |
| int* f(int* a, int* b) { |
| return a; |
| } |
| int* target(int* a, int* b) { |
| return f(b, a); |
| } |
| )"), |
| LifetimesAre({{"f", "a, b -> a"}, {"target", "a, b -> b"}})); |
| } |
| |
| TEST_F(LifetimeAnalysisTest, SimpleFnCall) { |
| EXPECT_THAT( |
| GetLifetimes(R"( |
| int* get_lesser_of(int* a, int* b) { |
| if (*a < *b) { |
| return a; |
| } |
| return b; |
| } |
| int* target(int* a, int* b) { |
| return get_lesser_of(a, b); |
| } |
| )"), |
| LifetimesAre({{"get_lesser_of", "a, a -> a"}, {"target", "a, a -> a"}})); |
| } |
| |
| TEST_F(LifetimeAnalysisTest, RefFnCall) { |
| EXPECT_THAT(GetLifetimes(R"( |
| int* get_as_ptr(int& a) { |
| return &a; |
| } |
| int* target(int& a) { |
| return get_as_ptr(a); |
| } |
| )"), |
| LifetimesAre({{"get_as_ptr", "a -> a"}, {"target", "a -> a"}})); |
| } |
| |
| TEST_F(LifetimeAnalysisTest, FnCall_NonPointerParameter) { |
| EXPECT_THAT(GetLifetimes(R"( |
| int id(int i) { |
| return i; |
| } |
| int target() { |
| return id(42); |
| } |
| )"), |
| LifetimesAre({{"id", "()"}, {"target", ""}})); |
| } |
| |
| TEST_F(LifetimeAnalysisTest, FnCall_DoublePointer) { |
| EXPECT_THAT(GetLifetimes(R"( |
| int** f(int** pp) { |
| return pp; |
| } |
| int** target(int** pp) { |
| return f(pp); |
| } |
| )"), |
| LifetimesAre( |
| {{"f", "(a, b) -> (a, b)"}, {"target", "(a, b) -> (a, b)"}})); |
| } |
| |
| TEST_F(LifetimeAnalysisTest, FnCall_DoublePointerDeref) { |
| EXPECT_THAT(GetLifetimes(R"( |
| int* f(int** pp) { |
| return *pp; |
| } |
| int* target(int** pp) { |
| return f(pp); |
| } |
| )"), |
| LifetimesAre({{"f", "(a, b) -> a"}, {"target", "(a, b) -> a"}})); |
| } |
| |
| TEST_F(LifetimeAnalysisTest, FnCall_MultipleDoublePointer) { |
| EXPECT_THAT(GetLifetimes(R"( |
| int** f(int** pp1, int** pp2) { |
| return pp1; |
| } |
| int* target(int* p1, int* p2) { |
| return *f(&p1, &p2); |
| } |
| )"), |
| LifetimesAre({{"f", "(a, b), (c, d) -> (a, b)"}, |
| {"target", "a, b -> a"}})); |
| } |
| |
| TEST_F(LifetimeAnalysisTest, FnCall_MultipleDoublePointerWithControlFlow) { |
| EXPECT_THAT(GetLifetimes(R"( |
| int** f(int** pp1, int** pp2) { |
| if (**pp1 < **pp2) { |
| *pp1 = *pp2; |
| } |
| return pp1; |
| } |
| int* target(int* p1, int* p2) { |
| return *f(&p1, &p2); |
| } |
| )"), |
| LifetimesAre({{"f", "(a, b), (a, c) -> (a, b)"}, |
| {"target", "a, a -> a"}})); |
| } |
| |
| TEST_F(LifetimeAnalysisTest, FnCall_MultipleDoublePointerWithOuterConst) { |
| EXPECT_THAT(GetLifetimes(R"( |
| void f(int** pp1, int** const pp2) { |
| *pp1 = *pp2; |
| } |
| int* target1(int* p1, int* p2) { |
| // Making this call can cause p1 to be overwritten with p2... |
| f(&p1, &p2); |
| return p1; |
| } |
| int* target2(int* p1, int* p2) { |
| // ...and it can also cause p2 to be overwritten with p1. |
| // |
| // The `const` only causes `pp2` itself to be const, but `*pp2` and |
| // `**pp2` are both non-const. In other words, from the lifetimes of `f()` |
| // alone, it would be entirely possible for it to do `*pp2 = *pp1`. |
| f(&p1, &p2); |
| return p2; |
| } |
| )"), |
| LifetimesAre({{"f", "(a, b), (a, c)"}, |
| {"target1", "a, a -> a"}, |
| {"target2", "a, a -> a"}})); |
| } |
| |
| TEST_F(LifetimeAnalysisTest, |
| DISABLED_FnCall_MultipleDoublePointerWithMiddleConst) { |
| // TODO(veluca): Here we correctly deduce in `target2` that *p1 <= *p2 and not |
| // the other way round, but the test fails because we fail to consider |
| // function calls to be a point where the lifetimes of a variable may change. |
| // A possible workaround may be to clone the lifetimes of parameters at |
| // function start. |
| EXPECT_THAT(GetLifetimes(R"( |
| void f(int** pp1, int* const * pp2) { |
| *pp1 = *pp2; |
| } |
| int* target1(int* p1, int* p2) { |
| // Making this call can cause p1 to be overwritten with p2... |
| f(&p1, &p2); |
| return p1; |
| } |
| int* target2(int* p1, int* p2) { |
| // ...but it can't cause p2 to be overwritten with p1. |
| f(&p1, &p2); |
| return p2; |
| } |
| )"), |
| LifetimesAre({{"f", "(a, b), (a, c)"}, |
| {"target1", "a, a -> a"}, |
| {"target2", "a, b -> b"}})); |
| } |
| |
| TEST_F(LifetimeAnalysisTest, FnCall_MultipleDoublePointerWithInnerConst) { |
| EXPECT_THAT(GetLifetimes(R"( |
| void f(const int** pp1, int** pp2) { |
| *pp1 = *pp2; |
| } |
| const int* target1(const int* p1, int* p2) { |
| // Making this call can cause p1 to be overwritten with p2. |
| f(&p1, &p2); |
| return p1; |
| } |
| const int* target2(const int* p1, int* p2) { |
| // The analysis concludes that p2 could also be overwritten by p1, |
| // despite the fact that a const int* cannot be converted to an int*. |
| // This is because, when determining what objects the callee might copy, |
| // the analysis looks only at lifetimes in the function signature but not |
| // at whether the objects that these lifetimes refer to can be converted |
| // into one another. |
| // As a result, the lifetimes we infer for target2() are stricter than |
| // they would need to be. |
| f(&p1, &p2); |
| return p2; |
| } |
| )"), |
| LifetimesAre({{"f", "(a, b), (a, c)"}, |
| {"target1", "a, a -> a"}, |
| {"target2", "a, a -> a"}})); |
| } |
| |
| TEST_F(LifetimeAnalysisTest, FnCall_TriplePointerWithConst_1) { |
| EXPECT_THAT(GetLifetimes(R"( |
| void f(int*** ppp1, int** const * ppp2) { |
| *ppp1 = *ppp2; |
| } |
| int** target(int* p1, int** pp2) { |
| // - `pp2` cannot be overwritten because of the `const` in the signature |
| // of `f()`. (Without this, we would infer a local lifetime for the |
| // return value.) |
| // - `*pp2` can be overwritten. |
| int** pp1 = &p1; |
| f(&pp1, &pp2); |
| return pp2; |
| } |
| )"), |
| LifetimesAre({{"f", "(a, b, c), (a, b, d)"}, |
| {"target", "a, (a, b) -> (a, b)"}})); |
| } |
| |
| TEST_F(LifetimeAnalysisTest, DISABLED_FnCall_TriplePointerWithConst_2) { |
| // TODO(veluca): see FnCall_MultipleDoublePointerWithMiddleConst. |
| EXPECT_THAT(GetLifetimes(R"( |
| void f(int*** ppp1, int* const ** ppp2) { |
| **ppp1 = **ppp2; |
| } |
| int* const * target(int* p1, int* const * pp2) { |
| // - `pp2` cannot be overwritten because of the lifetimes in the signature |
| // of `f()`. |
| // - `*pp2` cannot be overwritten because of the `const` in the signature |
| // of `f()`. |
| int** pp1 = &p1; |
| f(&pp1, &pp2); |
| return pp2; |
| } |
| )"), |
| LifetimesAre({{"f", "(a, b, c), (a, d, e)"}, |
| {"target", "a, (b, c) -> (b, c)"}})); |
| } |
| |
| TEST_F(LifetimeAnalysisTest, DISABLED_FnCall_TriplePointerWithConst_3) { |
| // TODO(veluca): see FnCall_MultipleDoublePointerWithMiddleConst. |
| EXPECT_THAT(GetLifetimes(R"( |
| void f(int* const ** ppp1, int* const ** ppp2) { |
| *ppp1 = *ppp2; |
| } |
| int* const * target(int* const p1, int* const * pp2) { |
| // - `pp2` can be overwritten (hence the return value has local lifetime) |
| // - `*pp2` can be overwritten (hence both `p1` and `pp2` have lifetime a) |
| int* const * pp1 = &p1; |
| f(&pp1, &pp2); |
| return pp2; |
| } |
| )"), |
| LifetimesAre({{"f", "(a, b, c), (a, b, d)"}, |
| {"target", |
| "ERROR: function returns reference to a local"}})); |
| } |
| |
| TEST_F(LifetimeAnalysisTest, FnCall_OutputParam) { |
| EXPECT_THAT(GetLifetimes(R"( |
| void f(int* in, int** out) { |
| *out = in; |
| } |
| int* target(int* p) { |
| int* result; |
| f(p, &result); |
| return result; |
| } |
| )"), |
| LifetimesAre({{"f", "a, (a, b)"}, {"target", "a -> a"}})); |
| } |
| |
| TEST_F(LifetimeAnalysisTest, FnCall_Operator) { |
| EXPECT_THAT(GetLifetimes(R"( |
| struct S {}; |
| bool operator<(const S& s1, const S& s2) { |
| return false; |
| } |
| bool target(const S& s) { |
| return s < s; |
| } |
| )"), |
| LifetimesAre({{"operator<", "a, b"}, {"target", "a"}})); |
| } |
| |
| TEST_F(LifetimeAnalysisTest, FnCall_PassLambda) { |
| // This test doesn't do anything interesting from a lifetimes point of view. |
| // It's just intended to test that we can instantiate a capture-less lambda |
| // and convert it to a function pointer. |
| EXPECT_THAT(GetLifetimes(R"( |
| void call_callback(void(*callback)()) { |
| // TODO(mboehme): Can't actually call the callback yet because we don't |
| // have support for indirect callees. |
| // callback(); |
| } |
| |
| void target() { |
| call_callback([] {}); |
| } |
| )"), |
| LifetimesContain({{"call_callback", "()"}, {"target", ""}})); |
| } |
| |
| TEST_F(LifetimeAnalysisTest, SimpleIndirectFnCall) { |
| EXPECT_THAT( |
| GetLifetimes(R"( |
| int* get_lesser_of(int* a, int* b) { |
| if (*a < *b) { |
| return a; |
| } |
| return b; |
| } |
| int* target(int* a, int* b) { |
| auto fp = get_lesser_of; |
| return fp(a, b); |
| } |
| )"), |
| LifetimesAre({{"get_lesser_of", "a, a -> a"}, {"target", "a, a -> a"}})); |
| } |
| |
| TEST_F(LifetimeAnalysisTest, SimpleIndirectFnCallFwdDecl) { |
| // Tests that the analysis correctly identifies dependencies due to non-call |
| // uses of a function. |
| EXPECT_THAT( |
| GetLifetimes(R"( |
| int* get_lesser_of(int* a, int* b); |
| int* target(int* a, int* b) { |
| auto fp = get_lesser_of; |
| return fp(a, b); |
| } |
| int* get_lesser_of(int* a, int* b) { |
| if (*a < *b) { |
| return a; |
| } |
| return b; |
| } |
| )"), |
| LifetimesAre({{"get_lesser_of", "a, a -> a"}, {"target", "a, a -> a"}})); |
| } |
| |
| TEST_F(LifetimeAnalysisTest, ConditionalIndirectFnCall) { |
| EXPECT_THAT(GetLifetimes(R"( |
| int* get_first(int* a, int* b) { |
| return a; |
| } |
| int* get_second(int* a, int* b) { |
| return b; |
| } |
| int* target(int* a, int* b) { |
| auto fp = *a < *b ? get_first : get_second; |
| return fp(a, b); |
| } |
| )"), |
| LifetimesAre({{"get_first", "a, b -> a"}, |
| {"get_second", "a, b -> b"}, |
| {"target", "a, a -> a"}})); |
| } |
| |
| // TODO(mboehme): Add a test where we're calling a function with lifetime |
| // signature `static -> a`. The analysis should realize that f could return |
| // its input pointee. Creating such a test is currently difficult because we |
| // don't have lifetime annotations and the inferred lifetime for the return |
| // value of f will always be static in this case. |
| |
| TEST_F(LifetimeAnalysisTest, ComplexFnCallGraph) { |
| EXPECT_THAT(GetLifetimes(R"( |
| int* f(int* a, int* b) { |
| if (*a < *b) { |
| return a; |
| } |
| return b; |
| } |
| int* g(int* a, int* b) { |
| return f(a, b); |
| } |
| int* h(int* a, int* b) { |
| return f(a, b); |
| } |
| int* target(int* a, int* b, int* c, int* d) { |
| return f(g(a, b), h(c, d)); |
| } |
| )"), |
| LifetimesAre({{"f", "a, a -> a"}, |
| {"g", "a, a -> a"}, |
| {"h", "a, a -> a"}, |
| {"target", "a, a, a, a -> a"}})); |
| } |
| |
| TEST_F(LifetimeAnalysisTest, ComplexFnCallGraphUnusedArgs) { |
| EXPECT_THAT(GetLifetimes(R"( |
| int* f(int* a, int* b) { |
| if (*a < *b) { |
| return a; |
| } |
| return b; |
| } |
| int* g(int* a, int* b) { |
| return f(a, b); |
| } |
| int* h(int* a, int* b) { |
| return f(a, b); |
| } |
| int* target(int* a, int* b, int* c, int* d) { |
| return f(g(a, b), h(a, b)); |
| } |
| )"), |
| LifetimesAre({{"f", "a, a -> a"}, |
| {"g", "a, a -> a"}, |
| {"h", "a, a -> a"}, |
| {"target", "a, a, b, c -> a"}})); |
| } |
| |
| TEST_F(LifetimeAnalysisTest, StructCall) { |
| // Tests that lifetimes of structs are properly propagated (in both |
| // directions) through function calls. |
| EXPECT_THAT(GetLifetimes(R"( |
| struct [[clang::annotate("lifetime_params", "a")]] S { |
| [[clang::annotate("member_lifetimes", "a")]] |
| int* a; |
| }; |
| void f(S* s, int* a) { |
| s->a = a; |
| } |
| int* target(S* s, int* a) { |
| f(s, a); |
| return s->a; |
| } |
| )"), |
| LifetimesAre({{"f", "(a, b), a"}, {"target", "(a, b), a -> a"}})); |
| } |
| |
| TEST_F(LifetimeAnalysisTest, StructDoubleCall) { |
| EXPECT_THAT(GetLifetimes(R"( |
| struct [[clang::annotate("lifetime_params", "a")]] S { |
| [[clang::annotate("member_lifetimes", "a")]] |
| int* a; |
| }; |
| void f(S* s, int* a) { |
| s->a = a; |
| } |
| int* g(S* s) { |
| return s->a; |
| } |
| int* target(S* s, int* a) { |
| f(s, a); |
| return g(s); |
| } |
| )"), |
| LifetimesAre({{"f", "(a, b), a"}, |
| {"g", "(a, b) -> a"}, |
| {"target", "(a, b), a -> a"}})); |
| } |
| |
| } // namespace |
| } // namespace lifetimes |
| } // namespace tidy |
| } // namespace clang |