//===---- OverlappingReplacementsTest.cpp - clang-tidy --------------------===// // // The LLVM Compiler Infrastructure // // This file is distributed under the University of Illinois Open Source // License. See LICENSE.TXT for details. // //===----------------------------------------------------------------------===// #include "ClangTidyTest.h" #include "clang/AST/RecursiveASTVisitor.h" #include "gtest/gtest.h" namespace clang { namespace tidy { namespace test { namespace { const char BoundDecl[] = "decl"; const char BoundIf[] = "if"; // We define a reduced set of very small checks that allow to test different // overlapping situations (no overlapping, replacements partially overlap, etc), // as well as different kinds of diagnostics (one check produces several errors, // several replacement ranges in an error, etc). class UseCharCheck : public ClangTidyCheck { public: UseCharCheck(StringRef CheckName, ClangTidyContext *Context) : ClangTidyCheck(CheckName, Context) {} void registerMatchers(ast_matchers::MatchFinder *Finder) override { using namespace ast_matchers; Finder->addMatcher(varDecl(hasType(isInteger())).bind(BoundDecl), this); } void check(const ast_matchers::MatchFinder::MatchResult &Result) override { auto *VD = Result.Nodes.getNodeAs(BoundDecl); diag(VD->getLocStart(), "use char") << FixItHint::CreateReplacement( CharSourceRange::getTokenRange(VD->getLocStart(), VD->getLocStart()), "char"); } }; class IfFalseCheck : public ClangTidyCheck { public: IfFalseCheck(StringRef CheckName, ClangTidyContext *Context) : ClangTidyCheck(CheckName, Context) {} void registerMatchers(ast_matchers::MatchFinder *Finder) override { using namespace ast_matchers; Finder->addMatcher(ifStmt().bind(BoundIf), this); } void check(const ast_matchers::MatchFinder::MatchResult &Result) override { auto *If = Result.Nodes.getNodeAs(BoundIf); auto *Cond = If->getCond(); SourceRange Range = Cond->getSourceRange(); if (auto *D = If->getConditionVariable()) { Range = SourceRange(D->getLocStart(), D->getLocEnd()); } diag(Range.getBegin(), "the cake is a lie") << FixItHint::CreateReplacement( CharSourceRange::getTokenRange(Range), "false"); } }; class RefactorCheck : public ClangTidyCheck { public: RefactorCheck(StringRef CheckName, ClangTidyContext *Context) : ClangTidyCheck(CheckName, Context), NamePattern("::$") {} RefactorCheck(StringRef CheckName, ClangTidyContext *Context, StringRef NamePattern) : ClangTidyCheck(CheckName, Context), NamePattern(NamePattern) {} virtual std::string newName(StringRef OldName) = 0; void registerMatchers(ast_matchers::MatchFinder *Finder) final { using namespace ast_matchers; Finder->addMatcher(varDecl(matchesName(NamePattern)).bind(BoundDecl), this); } void check(const ast_matchers::MatchFinder::MatchResult &Result) final { auto *VD = Result.Nodes.getNodeAs(BoundDecl); std::string NewName = newName(VD->getName()); auto Diag = diag(VD->getLocation(), "refactor %0 into %1") << VD->getName() << NewName << FixItHint::CreateReplacement( CharSourceRange::getTokenRange(VD->getLocation(), VD->getLocation()), NewName); class UsageVisitor : public RecursiveASTVisitor { public: UsageVisitor(const ValueDecl *VD, StringRef NewName, DiagnosticBuilder &Diag) : VD(VD), NewName(NewName), Diag(Diag) {} bool VisitDeclRefExpr(DeclRefExpr *E) { if (const ValueDecl *D = E->getDecl()) { if (VD->getCanonicalDecl() == D->getCanonicalDecl()) { Diag << FixItHint::CreateReplacement( CharSourceRange::getTokenRange(E->getSourceRange()), NewName); } } return RecursiveASTVisitor::VisitDeclRefExpr(E); } private: const ValueDecl *VD; StringRef NewName; DiagnosticBuilder &Diag; }; UsageVisitor(VD, NewName, Diag) .TraverseDecl(Result.Context->getTranslationUnitDecl()); } protected: const std::string NamePattern; }; class StartsWithPotaCheck : public RefactorCheck { public: StartsWithPotaCheck(StringRef CheckName, ClangTidyContext *Context) : RefactorCheck(CheckName, Context, "::pota") {} std::string newName(StringRef OldName) override { return "toma" + OldName.substr(4).str(); } }; class EndsWithTatoCheck : public RefactorCheck { public: EndsWithTatoCheck(StringRef CheckName, ClangTidyContext *Context) : RefactorCheck(CheckName, Context, "tato$") {} std::string newName(StringRef OldName) override { return OldName.substr(0, OldName.size() - 4).str() + "melo"; } }; } // namespace TEST(OverlappingReplacementsTest, UseCharCheckTest) { const char Code[] = R"(void f() { int a = 0; if (int b = 0) { int c = a; } })"; const char CharFix[] = R"(void f() { char a = 0; if (char b = 0) { char c = a; } })"; EXPECT_EQ(CharFix, runCheckOnCode(Code)); } TEST(OverlappingReplacementsTest, IfFalseCheckTest) { const char Code[] = R"(void f() { int potato = 0; if (int b = 0) { int c = potato; } else if (true) { int d = 0; } })"; const char IfFix[] = R"(void f() { int potato = 0; if (false) { int c = potato; } else if (false) { int d = 0; } })"; EXPECT_EQ(IfFix, runCheckOnCode(Code)); } TEST(OverlappingReplacementsTest, StartsWithCheckTest) { const char Code[] = R"(void f() { int a = 0; int potato = 0; if (int b = 0) { int c = potato; } else if (true) { int d = 0; } })"; const char StartsFix[] = R"(void f() { int a = 0; int tomato = 0; if (int b = 0) { int c = tomato; } else if (true) { int d = 0; } })"; EXPECT_EQ(StartsFix, runCheckOnCode(Code)); } TEST(OverlappingReplacementsTest, EndsWithCheckTest) { const char Code[] = R"(void f() { int a = 0; int potato = 0; if (int b = 0) { int c = potato; } else if (true) { int d = 0; } })"; const char EndsFix[] = R"(void f() { int a = 0; int pomelo = 0; if (int b = 0) { int c = pomelo; } else if (true) { int d = 0; } })"; EXPECT_EQ(EndsFix, runCheckOnCode(Code)); } TEST(OverlappingReplacementTest, ReplacementsDoNotOverlap) { std::string Res; const char Code[] = R"(void f() { int potassium = 0; if (true) { int Potato = potassium; } })"; const char CharIfFix[] = R"(void f() { char potassium = 0; if (false) { char Potato = potassium; } })"; Res = runCheckOnCode(Code); EXPECT_EQ(CharIfFix, Res); const char StartsEndsFix[] = R"(void f() { int tomassium = 0; if (true) { int Pomelo = tomassium; } })"; Res = runCheckOnCode(Code); EXPECT_EQ(StartsEndsFix, Res); const char CharIfStartsEndsFix[] = R"(void f() { char tomassium = 0; if (false) { char Pomelo = tomassium; } })"; Res = runCheckOnCode(Code); EXPECT_EQ(CharIfStartsEndsFix, Res); } TEST(OverlappingReplacementsTest, ReplacementInsideOtherReplacement) { std::string Res; const char Code[] = R"(void f() { if (char potato = 0) { } else if (int a = 0) { char potato = 0; if (potato) potato; } })"; // Apply the UseCharCheck together with the IfFalseCheck. // // The 'If' fix contains the other, so that is the one that has to be applied. // } else if (int a = 0) { // ^^^ -> char // ~~~~~~~~~ -> false const char CharIfFix[] = R"(void f() { if (false) { } else if (false) { char potato = 0; if (false) potato; } })"; Res = runCheckOnCode(Code); EXPECT_EQ(CharIfFix, Res); Res = runCheckOnCode(Code); EXPECT_EQ(CharIfFix, Res); // Apply the IfFalseCheck with the StartsWithPotaCheck. // // The 'If' replacement is bigger here. // if (char potato = 0) { // ^^^^^^ -> tomato // ~~~~~~~~~~~~~~~ -> false // // But the refactoring is the one that contains the other here: // char potato = 0; // ^^^^^^ -> tomato // if (potato) potato; // ^^^^^^ ^^^^^^ -> tomato, tomato // ~~~~~~ -> false const char IfStartsFix[] = R"(void f() { if (false) { } else if (false) { char tomato = 0; if (tomato) tomato; } })"; Res = runCheckOnCode(Code); EXPECT_EQ(IfStartsFix, Res); Res = runCheckOnCode(Code); EXPECT_EQ(IfStartsFix, Res); } TEST(OverlappingReplacements, TwoReplacementsInsideOne) { std::string Res; const char Code[] = R"(void f() { if (int potato = 0) { int a = 0; } })"; // The two smallest replacements should not be applied. // if (int potato = 0) { // ^^^^^^ -> tomato // *** -> char // ~~~~~~~~~~~~~~ -> false // But other errors from the same checks should not be affected. // int a = 0; // *** -> char const char Fix[] = R"(void f() { if (false) { char a = 0; } })"; Res = runCheckOnCode(Code); EXPECT_EQ(Fix, Res); Res = runCheckOnCode(Code); EXPECT_EQ(Fix, Res); } TEST(OverlappingReplacementsTest, ApplyAtMostOneOfTheChangesWhenPartialOverlapping) { std::string Res; const char Code[] = R"(void f() { if (int potato = 0) { int a = potato; } })"; // These two replacements overlap, but none of them is completely contained // inside the other. // if (int potato = 0) { // ^^^^^^ -> tomato // ~~~~~~~~~~~~~~ -> false // int a = potato; // ^^^^^^ -> tomato // // The 'StartsWithPotaCheck' fix has endpoints inside the 'IfFalseCheck' fix, // so it is going to be set as inapplicable. The 'if' fix will be applied. const char IfFix[] = R"(void f() { if (false) { int a = potato; } })"; Res = runCheckOnCode(Code); EXPECT_EQ(IfFix, Res); } TEST(OverlappingReplacementsTest, TwoErrorsHavePerfectOverlapping) { std::string Res; const char Code[] = R"(void f() { int potato = 0; potato += potato * potato; if (char a = potato) potato; })"; // StartsWithPotaCheck will try to refactor 'potato' into 'tomato', and // EndsWithTatoCheck will try to use 'pomelo'. Both fixes have the same set of // ranges. This is a corner case of one error completely containing another: // the other completely contains the first one as well. Both errors are // discarded. Res = runCheckOnCode(Code); EXPECT_EQ(Code, Res); } } // namespace test } // namespace tidy } // namespace clang