//===- unittests/Analysis/FlowSensitive/CachedConstAccessorsLatticeTest.cpp ==//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//

#include "clang/Analysis/FlowSensitive/CachedConstAccessorsLattice.h"

#include <cassert>
#include <memory>

#include "clang/AST/Decl.h"
#include "clang/AST/DeclBase.h"
#include "clang/AST/DeclCXX.h"
#include "clang/AST/Expr.h"
#include "clang/AST/Type.h"
#include "clang/ASTMatchers/ASTMatchFinder.h"
#include "clang/ASTMatchers/ASTMatchers.h"
#include "clang/Analysis/FlowSensitive/DataflowAnalysisContext.h"
#include "clang/Analysis/FlowSensitive/DataflowLattice.h"
#include "clang/Analysis/FlowSensitive/NoopLattice.h"
#include "clang/Analysis/FlowSensitive/StorageLocation.h"
#include "clang/Analysis/FlowSensitive/Value.h"
#include "clang/Analysis/FlowSensitive/WatchedLiteralsSolver.h"
#include "clang/Basic/LLVM.h"
#include "clang/Testing/TestAST.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"

namespace clang::dataflow {
namespace {

using ast_matchers::BoundNodes;
using ast_matchers::callee;
using ast_matchers::cxxMemberCallExpr;
using ast_matchers::functionDecl;
using ast_matchers::hasName;
using ast_matchers::match;
using ast_matchers::selectFirst;

using dataflow::DataflowAnalysisContext;
using dataflow::Environment;
using dataflow::LatticeJoinEffect;
using dataflow::RecordStorageLocation;
using dataflow::Value;
using dataflow::WatchedLiteralsSolver;

using testing::SizeIs;

NamedDecl *lookup(StringRef Name, const DeclContext &DC) {
  auto Result = DC.lookup(&DC.getParentASTContext().Idents.get(Name));
  EXPECT_TRUE(Result.isSingleResult()) << Name;
  return Result.front();
}

class CachedConstAccessorsLatticeTest : public ::testing::Test {
protected:
  using LatticeT = CachedConstAccessorsLattice<NoopLattice>;

  DataflowAnalysisContext DACtx{std::make_unique<WatchedLiteralsSolver>()};
  Environment Env{DACtx};
};

// Basic test AST with two const methods (return a value, and return a ref).
struct CommonTestInputs {
  CommonTestInputs()
      : AST(R"cpp(
    struct S {
      int *valProperty() const;
      int &refProperty() const;
    };
    void target() {
      S s;
      s.valProperty();
      S s2;
      s2.refProperty();
    }
  )cpp") {
    auto *SDecl = cast<CXXRecordDecl>(
        lookup("S", *AST.context().getTranslationUnitDecl()));
    SType = AST.context().getRecordType(SDecl);
    CallVal = selectFirst<CallExpr>(
        "call",
        match(cxxMemberCallExpr(callee(functionDecl(hasName("valProperty"))))
                  .bind("call"),
              AST.context()));
    assert(CallVal != nullptr);

    CallRef = selectFirst<CallExpr>(
        "call",
        match(cxxMemberCallExpr(callee(functionDecl(hasName("refProperty"))))
                  .bind("call"),
              AST.context()));
    assert(CallRef != nullptr);
  }

  TestAST AST;
  QualType SType;
  const CallExpr *CallVal;
  const CallExpr *CallRef;
};

TEST_F(CachedConstAccessorsLatticeTest,
       SamePrimitiveValBeforeClearOrDiffAfterClear) {
  CommonTestInputs Inputs;
  auto *CE = Inputs.CallVal;
  RecordStorageLocation Loc(Inputs.SType, RecordStorageLocation::FieldToLoc(),
                            {});

  LatticeT Lattice;
  Value *Val1 = Lattice.getOrCreateConstMethodReturnValue(Loc, CE, Env);
  Value *Val2 = Lattice.getOrCreateConstMethodReturnValue(Loc, CE, Env);

  EXPECT_EQ(Val1, Val2);

  Lattice.clearConstMethodReturnValues(Loc);
  Value *Val3 = Lattice.getOrCreateConstMethodReturnValue(Loc, CE, Env);

  EXPECT_NE(Val3, Val1);
  EXPECT_NE(Val3, Val2);
}

TEST_F(CachedConstAccessorsLatticeTest, SameLocBeforeClearOrDiffAfterClear) {
  CommonTestInputs Inputs;
  auto *CE = Inputs.CallRef;
  RecordStorageLocation Loc(Inputs.SType, RecordStorageLocation::FieldToLoc(),
                            {});

  LatticeT Lattice;
  auto NopInit = [](StorageLocation &) {};
  StorageLocation *Loc1 = Lattice.getOrCreateConstMethodReturnStorageLocation(
      Loc, CE, Env, NopInit);
  auto NotCalled = [](StorageLocation &) {
    ASSERT_TRUE(false) << "Not reached";
  };
  StorageLocation *Loc2 = Lattice.getOrCreateConstMethodReturnStorageLocation(
      Loc, CE, Env, NotCalled);

  EXPECT_EQ(Loc1, Loc2);

  Lattice.clearConstMethodReturnStorageLocations(Loc);
  StorageLocation *Loc3 = Lattice.getOrCreateConstMethodReturnStorageLocation(
      Loc, CE, Env, NopInit);

  EXPECT_NE(Loc3, Loc1);
  EXPECT_NE(Loc3, Loc2);
}

TEST_F(CachedConstAccessorsLatticeTest,
       SameStructValBeforeClearOrDiffAfterClear) {
  TestAST AST(R"cpp(
    struct S {
      S structValProperty() const;
    };
    void target() {
      S s;
      s.structValProperty();
    }
  )cpp");
  auto *SDecl =
      cast<CXXRecordDecl>(lookup("S", *AST.context().getTranslationUnitDecl()));
  QualType SType = AST.context().getRecordType(SDecl);
  const CallExpr *CE = selectFirst<CallExpr>(
      "call", match(cxxMemberCallExpr(
                        callee(functionDecl(hasName("structValProperty"))))
                        .bind("call"),
                    AST.context()));
  ASSERT_NE(CE, nullptr);

  RecordStorageLocation Loc(SType, RecordStorageLocation::FieldToLoc(), {});

  LatticeT Lattice;
  // Accessors that return a record by value are modeled by a record storage
  // location (instead of a Value).
  auto NopInit = [](StorageLocation &) {};
  StorageLocation *Loc1 = Lattice.getOrCreateConstMethodReturnStorageLocation(
      Loc, CE, Env, NopInit);
  auto NotCalled = [](StorageLocation &) {
    ASSERT_TRUE(false) << "Not reached";
  };
  StorageLocation *Loc2 = Lattice.getOrCreateConstMethodReturnStorageLocation(
      Loc, CE, Env, NotCalled);

  EXPECT_EQ(Loc1, Loc2);

  Lattice.clearConstMethodReturnStorageLocations(Loc);
  StorageLocation *Loc3 = Lattice.getOrCreateConstMethodReturnStorageLocation(
      Loc, CE, Env, NopInit);

  EXPECT_NE(Loc3, Loc1);
  EXPECT_NE(Loc3, Loc1);
}

TEST_F(CachedConstAccessorsLatticeTest, ClearDifferentLocs) {
  CommonTestInputs Inputs;
  auto *CE = Inputs.CallRef;
  RecordStorageLocation LocS1(Inputs.SType, RecordStorageLocation::FieldToLoc(),
                              {});
  RecordStorageLocation LocS2(Inputs.SType, RecordStorageLocation::FieldToLoc(),
                              {});

  LatticeT Lattice;
  auto NopInit = [](StorageLocation &) {};
  StorageLocation *RetLoc1 =
      Lattice.getOrCreateConstMethodReturnStorageLocation(LocS1, CE, Env,
                                                          NopInit);
  Lattice.clearConstMethodReturnStorageLocations(LocS2);
  auto NotCalled = [](StorageLocation &) {
    ASSERT_TRUE(false) << "Not reached";
  };
  StorageLocation *RetLoc2 =
      Lattice.getOrCreateConstMethodReturnStorageLocation(LocS1, CE, Env,
                                                          NotCalled);

  EXPECT_EQ(RetLoc1, RetLoc2);
}

TEST_F(CachedConstAccessorsLatticeTest, DifferentValsFromDifferentLocs) {
  TestAST AST(R"cpp(
    struct S {
      int *valProperty() const;
    };
    void target() {
      S s1;
      s1.valProperty();
      S s2;
      s2.valProperty();
    }
  )cpp");
  auto *SDecl =
      cast<CXXRecordDecl>(lookup("S", *AST.context().getTranslationUnitDecl()));
  QualType SType = AST.context().getRecordType(SDecl);
  SmallVector<BoundNodes, 1> valPropertyCalls =
      match(cxxMemberCallExpr(callee(functionDecl(hasName("valProperty"))))
                .bind("call"),
            AST.context());
  ASSERT_THAT(valPropertyCalls, SizeIs(2));

  const CallExpr *CE1 = selectFirst<CallExpr>("call", valPropertyCalls);
  ASSERT_NE(CE1, nullptr);

  valPropertyCalls.erase(valPropertyCalls.begin());
  const CallExpr *CE2 = selectFirst<CallExpr>("call", valPropertyCalls);
  ASSERT_NE(CE2, nullptr);
  ASSERT_NE(CE1, CE2);

  RecordStorageLocation LocS1(SType, RecordStorageLocation::FieldToLoc(), {});
  RecordStorageLocation LocS2(SType, RecordStorageLocation::FieldToLoc(), {});

  LatticeT Lattice;
  Value *Val1 = Lattice.getOrCreateConstMethodReturnValue(LocS1, CE1, Env);
  Value *Val2 = Lattice.getOrCreateConstMethodReturnValue(LocS2, CE2, Env);

  EXPECT_NE(Val1, Val2);
}

TEST_F(CachedConstAccessorsLatticeTest, JoinSameNoop) {
  CommonTestInputs Inputs;
  auto *CE = Inputs.CallVal;
  RecordStorageLocation Loc(Inputs.SType, RecordStorageLocation::FieldToLoc(),
                            {});

  LatticeT EmptyLattice;
  LatticeT EmptyLattice2;
  EXPECT_EQ(EmptyLattice.join(EmptyLattice2), LatticeJoinEffect::Unchanged);

  LatticeT Lattice1;
  Lattice1.getOrCreateConstMethodReturnValue(Loc, CE, Env);
  EXPECT_EQ(Lattice1.join(Lattice1), LatticeJoinEffect::Unchanged);
}

TEST_F(CachedConstAccessorsLatticeTest, ProducesNewValueAfterJoinDistinct) {
  CommonTestInputs Inputs;
  auto *CE = Inputs.CallVal;
  RecordStorageLocation Loc(Inputs.SType, RecordStorageLocation::FieldToLoc(),
                            {});

  // L1 w/ v vs L2 empty
  LatticeT Lattice1;
  Value *Val1 = Lattice1.getOrCreateConstMethodReturnValue(Loc, CE, Env);

  LatticeT EmptyLattice;

  EXPECT_EQ(Lattice1.join(EmptyLattice), LatticeJoinEffect::Changed);
  Value *ValAfterJoin =
      Lattice1.getOrCreateConstMethodReturnValue(Loc, CE, Env);

  EXPECT_NE(ValAfterJoin, Val1);

  // L1 w/ v1 vs L3 w/ v2
  LatticeT Lattice3;
  Value *Val3 = Lattice3.getOrCreateConstMethodReturnValue(Loc, CE, Env);

  EXPECT_EQ(Lattice1.join(Lattice3), LatticeJoinEffect::Changed);
  Value *ValAfterJoin2 =
      Lattice1.getOrCreateConstMethodReturnValue(Loc, CE, Env);

  EXPECT_NE(ValAfterJoin2, ValAfterJoin);
  EXPECT_NE(ValAfterJoin2, Val3);
}

} // namespace
} // namespace clang::dataflow
