//===----------------------------------------------------------------------===//
//
// 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 "StdNamespaceModificationCheck.h"
#include "clang/ASTMatchers/ASTMatchFinder.h"
#include "clang/ASTMatchers/ASTMatchersInternal.h"

using namespace clang;
using namespace clang::ast_matchers;

namespace {

AST_POLYMORPHIC_MATCHER_P(
    hasAnyTemplateArgumentIncludingPack,
    AST_POLYMORPHIC_SUPPORTED_TYPES(ClassTemplateSpecializationDecl,
                                    TemplateSpecializationType, FunctionDecl),
    clang::ast_matchers::internal::Matcher<TemplateArgument>, InnerMatcher) {
  const ArrayRef<TemplateArgument> Args =
      clang::ast_matchers::internal::getTemplateSpecializationArgs(Node);
  for (const auto &Arg : Args) {
    if (Arg.getKind() != TemplateArgument::Pack)
      continue;
    const ArrayRef<TemplateArgument> PackArgs = Arg.getPackAsArray();
    if (matchesFirstInRange(InnerMatcher, PackArgs.begin(), PackArgs.end(),
                            Finder, Builder) != PackArgs.end())
      return true;
  }
  return matchesFirstInRange(InnerMatcher, Args.begin(), Args.end(), Finder,
                             Builder) != Args.end();
}

} // namespace

namespace clang::tidy::bugprone {

void StdNamespaceModificationCheck::registerMatchers(MatchFinder *Finder) {
  auto HasStdParent =
      hasDeclContext(namespaceDecl(hasAnyName("std", "posix"),
                                   unless(hasParent(namespaceDecl())))
                         .bind("nmspc"));
  auto UserDefinedType = qualType(
      hasUnqualifiedDesugaredType(tagType(unless(hasDeclaration(tagDecl(
          hasAncestor(namespaceDecl(hasAnyName("std", "posix"),
                                    unless(hasParent(namespaceDecl()))))))))));
  auto HasNoProgramDefinedTemplateArgument = unless(
      hasAnyTemplateArgumentIncludingPack(refersToType(UserDefinedType)));
  auto InsideStdClassOrClassTemplateSpecialization = hasDeclContext(
      anyOf(cxxRecordDecl(HasStdParent),
            classTemplateSpecializationDecl(
                HasStdParent, HasNoProgramDefinedTemplateArgument)));

  // Try to follow exactly CERT rule DCL58-CPP (this text is taken from C++
  // standard into the CERT rule):
  // "
  // 1 The behavior of a C++ program is undefined if it adds declarations or
  // definitions to namespace std or to a namespace within namespace std unless
  // otherwise specified. A program may add a template specialization for any
  // standard library template to namespace std only if the declaration depends
  // on a user-defined type and the specialization meets the standard library
  // requirements for the original template and is not explicitly prohibited. 2
  // The behavior of a C++ program is undefined if it declares — an explicit
  // specialization of any member function of a standard library class template,
  // or — an explicit specialization of any member function template of a
  // standard library class or class template, or — an explicit or partial
  // specialization of any member class template of a standard library class or
  // class template.
  // "
  // The "standard library requirements" and explicit prohibition are not
  // checked.

  auto BadNonTemplateSpecializationDecl =
      decl(unless(anyOf(functionDecl(isExplicitTemplateSpecialization()),
                        varDecl(isExplicitTemplateSpecialization()),
                        cxxRecordDecl(isExplicitTemplateSpecialization()))),
           HasStdParent);
  auto BadClassTemplateSpec = classTemplateSpecializationDecl(
      HasNoProgramDefinedTemplateArgument, HasStdParent);
  auto BadInnerClassTemplateSpec = classTemplateSpecializationDecl(
      InsideStdClassOrClassTemplateSpecialization);
  auto BadFunctionTemplateSpec =
      functionDecl(unless(cxxMethodDecl()), isExplicitTemplateSpecialization(),
                   HasNoProgramDefinedTemplateArgument, HasStdParent);
  auto BadMemberFunctionSpec =
      cxxMethodDecl(isExplicitTemplateSpecialization(),
                    InsideStdClassOrClassTemplateSpecialization);

  Finder->addMatcher(decl(anyOf(BadNonTemplateSpecializationDecl,
                                BadClassTemplateSpec, BadInnerClassTemplateSpec,
                                BadFunctionTemplateSpec, BadMemberFunctionSpec),
                          unless(isExpansionInSystemHeader()))
                         .bind("decl"),
                     this);
}
} // namespace clang::tidy::bugprone

static const NamespaceDecl *getTopLevelLexicalNamespaceDecl(const Decl *D) {
  const NamespaceDecl *LastNS = nullptr;
  while (D) {
    if (const auto *NS = dyn_cast<NamespaceDecl>(D))
      LastNS = NS;
    D = dyn_cast_or_null<Decl>(D->getLexicalDeclContext());
  }
  return LastNS;
}

void clang::tidy::bugprone::StdNamespaceModificationCheck::check(
    const MatchFinder::MatchResult &Result) {
  const auto *D = Result.Nodes.getNodeAs<Decl>("decl");
  const auto *NS = Result.Nodes.getNodeAs<NamespaceDecl>("nmspc");
  if (!D || !NS)
    return;

  diag(D->getLocation(),
       "modification of %0 namespace can result in undefined behavior")
      << NS;
  // 'NS' is not always the namespace declaration that lexically contains 'D',
  // try to find such a namespace.
  if (const NamespaceDecl *LexNS = getTopLevelLexicalNamespaceDecl(D)) {
    assert(NS->getCanonicalDecl() == LexNS->getCanonicalDecl() &&
           "Mismatch in found namespace");
    diag(LexNS->getLocation(), "%0 namespace opened here", DiagnosticIDs::Note)
        << LexNS;
  }
}
