Index: unittests/clang-tidy/CMakeLists.txt =================================================================== --- unittests/clang-tidy/CMakeLists.txt +++ unittests/clang-tidy/CMakeLists.txt @@ -13,6 +13,7 @@ GoogleModuleTest.cpp LLVMModuleTest.cpp MiscModuleTest.cpp + OverlappingReplacementsTest.cpp ReadabilityModuleTest.cpp) target_link_libraries(ClangTidyTests Index: unittests/clang-tidy/ClangTidyTest.h =================================================================== --- unittests/clang-tidy/ClangTidyTest.h +++ unittests/clang-tidy/ClangTidyTest.h @@ -18,6 +18,7 @@ #include "clang/Tooling/Refactoring.h" #include "clang/Tooling/Tooling.h" #include +#include namespace clang { namespace tidy { @@ -25,9 +26,10 @@ class TestClangTidyAction : public ASTFrontendAction { public: - TestClangTidyAction(ClangTidyCheck &Check, ast_matchers::MatchFinder &Finder, + TestClangTidyAction(SmallVectorImpl> &Checks, + ast_matchers::MatchFinder &Finder, ClangTidyContext &Context) - : Check(Check), Finder(Finder), Context(Context) {} + : Checks(Checks), Finder(Finder), Context(Context) {} private: std::unique_ptr CreateASTConsumer(CompilerInstance &Compiler, @@ -36,17 +38,37 @@ Context.setCurrentFile(File); Context.setASTContext(&Compiler.getASTContext()); - Check.registerMatchers(&Finder); - Check.registerPPCallbacks(Compiler); + for (auto &Check : Checks) { + Check->registerMatchers(&Finder); + Check->registerPPCallbacks(Compiler); + } return Finder.newASTConsumer(); } - ClangTidyCheck &Check; + SmallVectorImpl> &Checks; ast_matchers::MatchFinder &Finder; ClangTidyContext &Context; }; -template +template struct CheckFactory { + static void + createChecks(ClangTidyContext *Context, + SmallVectorImpl> &Result) { + CheckFactory::createChecks(Context, Result); + CheckFactory::createChecks(Context, Result); + } +}; + +template struct CheckFactory { + static void + createChecks(ClangTidyContext *Context, + SmallVectorImpl> &Result) { + Result.emplace_back(llvm::make_unique( + "test-check-" + std::to_string(Result.size()), Context)); + } +}; + +template std::string runCheckOnCode(StringRef Code, std::vector *Errors = nullptr, const Twine &Filename = "input.cc", @@ -59,7 +81,6 @@ ClangTidyContext Context(llvm::make_unique( ClangTidyGlobalOptions(), Options)); ClangTidyDiagnosticConsumer DiagConsumer(Context); - T Check("test-check", &Context); std::vector ArgCXX11(1, "clang-tidy"); ArgCXX11.push_back("-fsyntax-only"); @@ -71,8 +92,11 @@ ast_matchers::MatchFinder Finder; llvm::IntrusiveRefCntPtr Files( new FileManager(FileSystemOptions())); + + SmallVector, 1> Checks; + CheckFactory::createChecks(&Context, Checks); tooling::ToolInvocation Invocation( - ArgCXX11, new TestClangTidyAction(Check, Finder, Context), Files.get()); + ArgCXX11, new TestClangTidyAction(Checks, Finder, Context), Files.get()); Invocation.mapVirtualFile(Filename.str(), Code); for (const auto &FileContent : PathsToContent) { Invocation.mapVirtualFile(Twine("include/" + FileContent.first).str(), Index: unittests/clang-tidy/OverlappingReplacementsTest.cpp =================================================================== --- /dev/null +++ unittests/clang-tidy/OverlappingReplacementsTest.cpp @@ -0,0 +1,379 @@ +//===---- 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") + << 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 is bigger, 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); + // FIXME: 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 bigger 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); + // FIXME: EXPECT_EQ(IfStartsFix, Res); + + // Silence warnings. + (void)CharIfFix; + (void)IfStartsFix; +} + +TEST(OverlappingReplacementsTest, ApplyFullErrorOrNothingWhenOverlapping) { + std::string Res; + const char Code[] = + R"(void f() { + int potato = 0; + potato += potato * potato; + if (char this_name_make_this_if_really_long = potato) potato; +})"; + + // StartsWithPotaCheck will try to refactor 'potato' into 'tomato', + // and EndsWithTatoCheck will try to use 'pomelo'. We have to apply + // either all conversions from one check, or all from the other. + const char StartsFix[] = + R"(void f() { + int tomato = 0; + tomato += tomato * tomato; + if (char this_name_make_this_if_really_long = tomato) tomato; +})"; + const char EndsFix[] = + R"(void f() { + int pomelo = 0; + pomelo += pomelo * pomelo; + if (char this_name_make_this_if_really_long = pomelo) pomelo; +})"; + // In case of overlapping, we will prioritize the biggest fix. However, these + // two fixes have the same size and position, so we don't know yet which one + // will have preference. + Res = runCheckOnCode(Code); + // FIXME: EXPECT_TRUE(Res == StartsFix || Res == EndsFix); + + // StartsWithPotaCheck will try to refactor 'potato' into 'tomato', but + // replacing the 'if' condition is a bigger change than all the refactoring + // changes together (48 vs 36), so this is the one that is going to be + // applied. + const char IfFix[] = + R"(void f() { + int potato = 0; + potato += potato * potato; + if (true) potato; +})"; + Res = runCheckOnCode(Code); + // FIXME: EXPECT_EQ(IfFix, Res); + + // Silence warnings. + (void)StartsFix; + (void)EndsFix; + (void)IfFix; +} + +} // namespace test +} // namespace tidy +} // namespace clang Index: unittests/clang-tidy/ReadabilityModuleTest.cpp =================================================================== --- unittests/clang-tidy/ReadabilityModuleTest.cpp +++ unittests/clang-tidy/ReadabilityModuleTest.cpp @@ -237,7 +237,7 @@ TEST(BracesAroundStatementsCheck, IfElseWithShortStatements) { ClangTidyOptions Options; - Options.CheckOptions["test-check.ShortStatementLines"] = "1"; + Options.CheckOptions["test-check-0.ShortStatementLines"] = "1"; EXPECT_EQ("int main() {\n" " if (true) return 1;\n"