//===- BugSuppression.cpp - Suppression interface -------------------------===//
//
// 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/StaticAnalyzer/Core/BugReporter/BugSuppression.h"
#include "clang/AST/DynamicRecursiveASTVisitor.h"
#include "clang/StaticAnalyzer/Core/BugReporter/BugReporter.h"
#include "llvm/Support/FormatVariadic.h"
#include "llvm/Support/TimeProfiler.h"

using namespace clang;
using namespace ento;

namespace {

using Ranges = llvm::SmallVectorImpl<SourceRange>;

inline bool hasSuppression(const Decl *D) {
  // FIXME: Implement diagnostic identifier arguments
  // (checker names, "hashtags").
  if (const auto *Suppression = D->getAttr<SuppressAttr>())
    return !Suppression->isGSL() &&
           (Suppression->diagnosticIdentifiers().empty());
  return false;
}
inline bool hasSuppression(const AttributedStmt *S) {
  // FIXME: Implement diagnostic identifier arguments
  // (checker names, "hashtags").
  return llvm::any_of(S->getAttrs(), [](const Attr *A) {
    const auto *Suppression = dyn_cast<SuppressAttr>(A);
    return Suppression && !Suppression->isGSL() &&
           (Suppression->diagnosticIdentifiers().empty());
  });
}

template <class NodeType> inline SourceRange getRange(const NodeType *Node) {
  return Node->getSourceRange();
}
template <> inline SourceRange getRange(const AttributedStmt *S) {
  // Begin location for attributed statement node seems to be ALWAYS invalid.
  //
  // It is unlikely that we ever report any warnings on suppression
  // attribute itself, but even if we do, we wouldn't want that warning
  // to be suppressed by that same attribute.
  //
  // Long story short, we can use inner statement and it's not going to break
  // anything.
  return getRange(S->getSubStmt());
}

inline bool isLessOrEqual(SourceLocation LHS, SourceLocation RHS,
                          const SourceManager &SM) {
  // SourceManager::isBeforeInTranslationUnit tests for strict
  // inequality, when we need a non-strict comparison (bug
  // can be reported directly on the annotated note).
  // For this reason, we use the following equivalence:
  //
  //   A <= B <==> !(B < A)
  //
  return !SM.isBeforeInTranslationUnit(RHS, LHS);
}

inline bool fullyContains(SourceRange Larger, SourceRange Smaller,
                          const SourceManager &SM) {
  // Essentially this means:
  //
  //   Larger.fullyContains(Smaller)
  //
  // However, that method has a very trivial implementation and couldn't
  // compare regular locations and locations from macro expansions.
  // We could've converted everything into regular locations as a solution,
  // but the following solution seems to be the most bulletproof.
  return isLessOrEqual(Larger.getBegin(), Smaller.getBegin(), SM) &&
         isLessOrEqual(Smaller.getEnd(), Larger.getEnd(), SM);
}

class CacheInitializer : public DynamicRecursiveASTVisitor {
public:
  static void initialize(const Decl *D, Ranges &ToInit) {
    CacheInitializer(ToInit).TraverseDecl(const_cast<Decl *>(D));
  }

  bool VisitDecl(Decl *D) override {
    // Bug location could be somewhere in the init value of
    // a freshly declared variable.  Even though it looks like the
    // user applied attribute to a statement, it will apply to a
    // variable declaration, and this is where we check for it.
    return VisitAttributedNode(D);
  }

  bool VisitAttributedStmt(AttributedStmt *AS) override {
    // When we apply attributes to statements, it actually creates
    // a wrapper statement that only contains attributes and the wrapped
    // statement.
    return VisitAttributedNode(AS);
  }

private:
  template <class NodeType> bool VisitAttributedNode(NodeType *Node) {
    if (hasSuppression(Node)) {
      // TODO: In the future, when we come up with good stable IDs for checkers
      //       we can return a list of kinds to ignore, or all if no arguments
      //       were provided.
      addRange(getRange(Node));
    }
    // We should keep traversing AST.
    return true;
  }

  void addRange(SourceRange R) {
    if (R.isValid()) {
      Result.push_back(R);
    }
  }

  CacheInitializer(Ranges &R) : Result(R) {}
  Ranges &Result;
};

std::string timeScopeName(const Decl *DeclWithIssue) {
  if (!llvm::timeTraceProfilerEnabled())
    return "";
  return llvm::formatv(
             "BugSuppression::isSuppressed init suppressions cache for {0}",
             DeclWithIssue->getDeclKindName())
      .str();
}

llvm::TimeTraceMetadata getDeclTimeTraceMetadata(const Decl *DeclWithIssue) {
  assert(DeclWithIssue);
  assert(llvm::timeTraceProfilerEnabled());
  std::string Name = "<noname>";
  if (const auto *ND = dyn_cast<NamedDecl>(DeclWithIssue)) {
    Name = ND->getNameAsString();
  }
  const auto &SM = DeclWithIssue->getASTContext().getSourceManager();
  auto Line = SM.getPresumedLineNumber(DeclWithIssue->getBeginLoc());
  auto Fname = SM.getFilename(DeclWithIssue->getBeginLoc());
  return llvm::TimeTraceMetadata{std::move(Name), Fname.str(),
                                 static_cast<int>(Line)};
}

} // end anonymous namespace

// TODO: Introduce stable IDs for checkers and check for those here
//       to be more specific.  Attribute without arguments should still
//       be considered as "suppress all".
//       It is already much finer granularity than what we have now
//       (i.e. removing the whole function from the analysis).
bool BugSuppression::isSuppressed(const BugReport &R) {
  PathDiagnosticLocation Location = R.getLocation();
  PathDiagnosticLocation UniqueingLocation = R.getUniqueingLocation();
  const Decl *DeclWithIssue = R.getDeclWithIssue();

  return isSuppressed(Location, DeclWithIssue, {}) ||
         isSuppressed(UniqueingLocation, DeclWithIssue, {});
}

bool BugSuppression::isSuppressed(const PathDiagnosticLocation &Location,
                                  const Decl *DeclWithIssue,
                                  DiagnosticIdentifierList Hashtags) {
  if (!Location.isValid())
    return false;

  if (!DeclWithIssue) {
    // FIXME: This defeats the purpose of passing DeclWithIssue to begin with.
    // If this branch is ever hit, we're re-doing all the work we've already
    // done as well as perform a lot of work we'll never need.
    // Gladly, none of our on-by-default checkers currently need it.
    DeclWithIssue = ACtx.getTranslationUnitDecl();
  } else {
    // This is the fast path. However, we should still consider the topmost
    // declaration that isn't TranslationUnitDecl, because we should respect
    // attributes on the entire declaration chain.
    while (true) {
      // Use the "lexical" parent. Eg., if the attribute is on a class, suppress
      // warnings in inline methods but not in out-of-line methods.
      const Decl *Parent =
          dyn_cast_or_null<Decl>(DeclWithIssue->getLexicalDeclContext());
      if (Parent == nullptr || isa<TranslationUnitDecl>(Parent))
        break;

      DeclWithIssue = Parent;
    }
  }

  // While some warnings are attached to AST nodes (mostly path-sensitive
  // checks), others are simply associated with a plain source location
  // or range.  Figuring out the node based on locations can be tricky,
  // so instead, we traverse the whole body of the declaration and gather
  // information on ALL suppressions.  After that we can simply check if
  // any of those suppressions affect the warning in question.
  //
  // Traversing AST of a function is not a heavy operation, but for
  // large functions with a lot of bugs it can make a dent in performance.
  // In order to avoid this scenario, we cache traversal results.
  auto InsertionResult = CachedSuppressionLocations.insert(
      std::make_pair(DeclWithIssue, CachedRanges{}));
  Ranges &SuppressionRanges = InsertionResult.first->second;
  if (InsertionResult.second) {
    llvm::TimeTraceScope TimeScope(
        timeScopeName(DeclWithIssue),
        [DeclWithIssue]() { return getDeclTimeTraceMetadata(DeclWithIssue); });
    // We haven't checked this declaration for suppressions yet!
    CacheInitializer::initialize(DeclWithIssue, SuppressionRanges);
  }

  SourceRange BugRange = Location.asRange();
  const SourceManager &SM = Location.getManager();

  return llvm::any_of(SuppressionRanges,
                      [BugRange, &SM](SourceRange Suppression) {
                        return fullyContains(Suppression, BugRange, SM);
                      });
}
