| //===---- 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<VarDecl>(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<IfStmt>(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<VarDecl>(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<UsageVisitor> { |
| 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<UsageVisitor>::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<UseCharCheck>(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<IfFalseCheck>(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<StartsWithPotaCheck>(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<EndsWithTatoCheck>(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<UseCharCheck, IfFalseCheck>(Code); |
| EXPECT_EQ(CharIfFix, Res); |
| |
| const char StartsEndsFix[] = |
| R"(void f() { |
| int tomassium = 0; |
| if (true) { |
| int Pomelo = tomassium; |
| } |
| })"; |
| Res = runCheckOnCode<StartsWithPotaCheck, EndsWithTatoCheck>(Code); |
| EXPECT_EQ(StartsEndsFix, Res); |
| |
| const char CharIfStartsEndsFix[] = |
| R"(void f() { |
| char tomassium = 0; |
| if (false) { |
| char Pomelo = tomassium; |
| } |
| })"; |
| Res = runCheckOnCode<UseCharCheck, IfFalseCheck, StartsWithPotaCheck, |
| EndsWithTatoCheck>(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<UseCharCheck, IfFalseCheck>(Code); |
| EXPECT_EQ(CharIfFix, Res); |
| Res = runCheckOnCode<IfFalseCheck, UseCharCheck>(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<IfFalseCheck, StartsWithPotaCheck>(Code); |
| EXPECT_EQ(IfStartsFix, Res); |
| Res = runCheckOnCode<StartsWithPotaCheck, IfFalseCheck>(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<UseCharCheck, IfFalseCheck, StartsWithPotaCheck>(Code); |
| EXPECT_EQ(Fix, Res); |
| Res = runCheckOnCode<StartsWithPotaCheck, IfFalseCheck, UseCharCheck>(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<IfFalseCheck, StartsWithPotaCheck>(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<StartsWithPotaCheck, EndsWithTatoCheck>(Code); |
| EXPECT_EQ(Code, Res); |
| } |
| |
| } // namespace test |
| } // namespace tidy |
| } // namespace clang |