// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "BlinkDataMemberTypeChecker.h"

#include "Util.h"
#include "clang/AST/Attr.h"
#include "clang/AST/Decl.h"
#include "clang/AST/DeclTemplate.h"
#include "clang/AST/Type.h"
#include "clang/Basic/Diagnostic.h"

using namespace clang;

namespace chrome_checker {

BlinkDataMemberTypeChecker::BlinkDataMemberTypeChecker(
    CompilerInstance& instance)
    : instance_(instance),
      diagnostic_(instance.getDiagnostics()),
      discouraged_types_({
          {"GURL", "KURL"},
          {"std::deque", "blink::Deque"},
          {"std::map", "blink::HashMap or blink::LinkedHashSet"},
          {"std::multimap",
           "blink::HashMap<K, blink::Vector<V>> or blink::HashCountedSet<T>"},
          {"std::multiset", "blink::HashCountedSet<T>"},
          {"std::set", "blink::HashSet or blink::LinkedHashSet"},
          {"std::unordered_set", "blink::HashSet"},
          {"std::unordered_map", "blink::HashMap"},
          {"std::vector", "blink::Vector"},
      }),
      included_filenames_regex_("/third_party/blink/renderer/"),
      excluded_filenames_regex_(
          "_(unit|perf)?test\\."
          "|_fuzzer\\."
          "|/testing/"
          "|/tests/"
          "|_test_helpers") {
  auto error_level = diagnostic_.getWarningsAsErrors()
                         ? DiagnosticsEngine::Error
                         : DiagnosticsEngine::Warning;

  diag_disallowed_blink_data_member_type_ = diagnostic_.getCustomDiagID(
      error_level,
      "[blink-style] '%0' is discouraged for data members in blink renderer. "
      "Use %1 if possible. If the usage is necessary, add "
      "ALLOW_DISCOURAGED_TYPE(reason) to the data member or the type alias to "
      "suppress this message.");
}

void BlinkDataMemberTypeChecker::CheckClass(SourceLocation location,
                                            const CXXRecordDecl* record) {
  std::string filename = GetFilename(instance_.getSourceManager(), location,
                                     FilenameLocationType::kSpellingLoc);
  if (!included_filenames_regex_.match(filename))
    return;
  if (excluded_filenames_regex_.match(filename))
    return;

  for (auto* field : record->fields())
    CheckField(field);
}

bool BlinkDataMemberTypeChecker::AllowsDiscouragedType(const Decl* decl) {
  for (auto* attr : decl->attrs()) {
    if (auto* annotate = dyn_cast<AnnotateAttr>(attr)) {
      if (annotate->getAnnotation() == "allow_discouraged_type")
        return true;
    }
  }
  return false;
}

void BlinkDataMemberTypeChecker::CheckField(const FieldDecl* field) {
  if (AllowsDiscouragedType(field))
    return;

  const Type* type = field->getType().getTypePtr();
  while (type) {
    if (auto* array = dyn_cast<ArrayType>(type)) {
      // Find the element type of the array type.
      type = array->getElementType().getTypePtr();
      continue;
    }

    const NamedDecl* decl = nullptr;
    if (auto* type_def = dyn_cast<TypedefType>(type)) {
      decl = type_def->getDecl();
      // We will either break the loop below if the type name is not under blink
      // namespace, or continue to check the underlying type of typedef/using.
      type = type_def->desugar().getTypePtr();
    } else if (auto* spec = dyn_cast<TemplateSpecializationType>(type)) {
      // Check cases like "std::vector<T> v;" in a template.
      decl = spec->getTemplateName().getAsTemplateDecl();
      // We may continue if the type still have an underlying type, like the
      // typedef case.
      type = spec->isSugared() ? spec->desugar().getTypePtr() : nullptr;
    } else {
      // For other kinds of types, get the root underlying type directly.
      decl = type->getAsCXXRecordDecl();
      // This will break the loop as we have found the root underlying type.
      type = nullptr;
    }

    if (!decl || AllowsDiscouragedType(decl))
      return;

    std::string type_name = decl->getQualifiedNameAsString();
    auto it = discouraged_types_.find(type_name);
    if (it != discouraged_types_.end()) {
      diagnostic_.Report(field->getLocation(),
                         diag_disallowed_blink_data_member_type_)
          << type_name << it->second;
      return;
    }

    // Skip the following conditions if we will break the loop anyway.
    if (!type)
      return;

    // Stop if the underlying type is not under blink namespace, instead of
    // finding the root underlying type. This is to allow the following case:
    // namespace cc {
    //   using LayerList = std::vector<Layer*>;
    // }
    // namespace blink {
    //   class LayerBuilder {
    //     ...
    //     // This is allowed as long as cc::LayerList is allowed in
    //     // audit_non_blink_usages.py.
    //     cc::LayerList layer_list_;
    //   };
    // Finding the root underlying type would disallow the above usage.
    if (type_name.find("blink::") != 0)
      return;

    // Similarly, stop finding the root underlying type if the intermediate
    // type is defined in a file that should not be checked, e.g. in a file
    // under third_party/blink/public/common.
    std::string filename =
        GetFilename(instance_.getSourceManager(), decl->getLocation(),
                    FilenameLocationType::kSpellingLoc);
    if (!included_filenames_regex_.match(filename))
      return;
  }
}

}  // namespace chrome_checker
