/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
   file LICENSE.rst or https://cmake.org/licensing for details.  */
#include "UseCmsysFstreamCheck.h"

#include <clang/ASTMatchers/ASTMatchFinder.h>

namespace clang {
namespace tidy {
namespace cmake {
using namespace ast_matchers;

UseCmsysFstreamCheck::UseCmsysFstreamCheck(StringRef Name,
                                           ClangTidyContext* Context)
  : ClangTidyCheck(Name, Context)
{
}

void UseCmsysFstreamCheck::registerMatchers(MatchFinder* Finder)
{
  this->createMatcher("::std::basic_ifstream", "::cmsys::ifstream", Finder,
                      "ifstream");
  this->createMatcher("::std::basic_ofstream", "::cmsys::ofstream", Finder,
                      "ofstream");
  this->createMatcher("::std::basic_fstream", "::cmsys::fstream", Finder,
                      "fstream");
}

void UseCmsysFstreamCheck::check(MatchFinder::MatchResult const& Result)
{
  TypeLoc const* ParentTypeNode =
    Result.Nodes.getNodeAs<TypeLoc>("parentType");
  NestedNameSpecifierLoc const* ParentNameNode =
    Result.Nodes.getNodeAs<NestedNameSpecifierLoc>("parentName");
  TypeLoc const* RootNode = nullptr;
  StringRef BindName;
  StringRef Warning;

  if ((RootNode = Result.Nodes.getNodeAs<TypeLoc>("ifstream")) != nullptr) {
    BindName = "cmsys::ifstream";
    Warning = "use cmsys::ifstream";
  } else if ((RootNode = Result.Nodes.getNodeAs<TypeLoc>("ofstream")) !=
             nullptr) {
    BindName = "cmsys::ofstream";
    Warning = "use cmsys::ofstream";
  } else if ((RootNode = Result.Nodes.getNodeAs<TypeLoc>("fstream")) !=
             nullptr) {
    BindName = "cmsys::fstream";
    Warning = "use cmsys::fstream";
  }

  if (ParentTypeNode != nullptr) {
    if (ParentTypeNode->getBeginLoc().isValid()) {
      this->diag(ParentTypeNode->getBeginLoc(), Warning)
        << FixItHint::CreateReplacement(ParentTypeNode->getSourceRange(),
                                        BindName);
    }
  } else if (ParentNameNode != nullptr) {
    if (ParentNameNode->getBeginLoc().isValid()) {
      this->diag(ParentNameNode->getBeginLoc(), Warning)
        << FixItHint::CreateReplacement(
             SourceRange(ParentNameNode->getBeginLoc(), RootNode->getEndLoc()),
             BindName);
    }
  } else if (RootNode != nullptr) {
    if (RootNode->getBeginLoc().isValid()) {
      this->diag(RootNode->getBeginLoc(), Warning)
        << FixItHint::CreateReplacement(RootNode->getSourceRange(), BindName);
    }
  }
}

void UseCmsysFstreamCheck::createMatcher(StringRef StdName,
                                         StringRef CmsysName,
                                         ast_matchers::MatchFinder* Finder,
                                         StringRef Bind)
{
  TypeLocMatcher IsStd = loc(qualType(hasUnqualifiedDesugaredType(
    recordType(hasDeclaration(classTemplateSpecializationDecl(
      hasName(StdName),
      hasTemplateArgument(
        0, templateArgument(refersToType(asString("char"))))))))));

  // TODO This only checks to see if the type directly refers to
  // cmsys::fstream. There are some corner cases involving template parameters
  // that refer to cmsys::fstream that are missed by this matcher, resulting in
  // a false positive. Figure out how to find these indirect references to
  // cmsys::fstream and filter them out. In the meantime, such false positives
  // can be silenced with NOLINT(cmake-use-cmsys-fstream).
  TypeLocMatcher IsCmsys =
    loc(usingType(throughUsingDecl(namedDecl(hasName(CmsysName)))));

  Finder->addMatcher(
    typeLoc(IsStd, unless(IsCmsys),
            optionally(hasParent(nestedNameSpecifierLoc().bind("parentName"))))
      .bind(Bind),
    this);
}
}
}
}
