diff --git a/clang-tools-extra/clangd/IncludeCleaner.cpp b/clang-tools-extra/clangd/IncludeCleaner.cpp --- a/clang-tools-extra/clangd/IncludeCleaner.cpp +++ b/clang-tools-extra/clangd/IncludeCleaner.cpp @@ -51,10 +51,12 @@ #include "llvm/Support/FormatVariadic.h" #include "llvm/Support/Path.h" #include "llvm/Support/Regex.h" +#include #include #include #include #include +#include #include #include @@ -413,6 +415,69 @@ return {std::move(UnusedIncludes), std::move(MissingIncludes)}; } +Fix removeAllUnusedIncludes(llvm::ArrayRef UnusedIncludes) { + Fix RemoveAll; + RemoveAll.Message = "remove all unused #includes"; + for (const auto *Inc : UnusedIncludes) { + RemoveAll.Edits.emplace_back(); + RemoveAll.Edits.back().range.start.line = Inc->HashLine; + RemoveAll.Edits.back().range.end.line = Inc->HashLine + 1; + } + return RemoveAll; +} +Fix addAllMissingIncludes(llvm::ArrayRef MissingIncludeDiags) { + Fix AddAllMissing; + AddAllMissing.Message = "add all missing #includes"; + + for (const auto &Diag : MissingIncludeDiags) { + assert(Diag.Fixes.size() == 1 && "Expected exactly one fix."); + AddAllMissing.Edits.insert(AddAllMissing.Edits.end(), + Diag.Fixes.front().Edits.begin(), + Diag.Fixes.front().Edits.end()); + } + llvm::sort(AddAllMissing.Edits, [](const TextEdit &L, const TextEdit &R) { + return std::tie(L.range.start, L.range.end, L.newText) < + std::tie(R.range.start, R.range.end, R.newText); + }); + AddAllMissing.Edits.erase( + std::unique(AddAllMissing.Edits.begin(), AddAllMissing.Edits.end()), + AddAllMissing.Edits.end()); + return AddAllMissing; +} + +std::vector generateIncludeCleanerDiagnostic( + ParsedAST &AST, const IncludeCleanerFindings &Findings, + llvm::StringRef Code) { + Fix RemoveAllUnused = removeAllUnusedIncludes(Findings.UnusedIncludes); + + std::vector MissingIncludeDiags = generateMissingIncludeDiagnostics( + AST, Findings.MissingIncludes, Code); + Fix AddAllMissing = addAllMissingIncludes(MissingIncludeDiags); + + Fix FixAll; + FixAll.Message = "add all missing #includes and remove all unused ones"; + FixAll.Edits = RemoveAllUnused.Edits; + // Append the missing include edits. + FixAll.Edits.insert(FixAll.Edits.end(), AddAllMissing.Edits.begin(), + AddAllMissing.Edits.end()); + + for (auto &Diag : MissingIncludeDiags) { + Diag.Fixes.push_back(AddAllMissing); + Diag.Fixes.push_back(FixAll); + } + std::vector UnusedIncludes = generateUnusedIncludeDiagnostics( + AST.tuPath(), Findings.UnusedIncludes, Code); + for (auto &Diag : UnusedIncludes) { + Diag.Fixes.push_back(RemoveAllUnused); + Diag.Fixes.push_back(FixAll); + } + + auto Result = std::move(MissingIncludeDiags); + llvm::move(UnusedIncludes, + std::back_inserter(Result)); + return Result; +} + std::vector issueIncludeCleanerDiagnostics(ParsedAST &AST, llvm::StringRef Code) { // Interaction is only polished for C/CPP. @@ -428,13 +493,7 @@ // will need include-cleaner results, call it once Findings = computeIncludeCleanerFindings(AST); } - - std::vector Result = generateUnusedIncludeDiagnostics( - AST.tuPath(), Findings.UnusedIncludes, Code); - llvm::move( - generateMissingIncludeDiagnostics(AST, Findings.MissingIncludes, Code), - std::back_inserter(Result)); - return Result; + return generateIncludeCleanerDiagnostic(AST, Findings, Code); } } // namespace clangd diff --git a/clang-tools-extra/clangd/unittests/DiagnosticsTests.cpp b/clang-tools-extra/clangd/unittests/DiagnosticsTests.cpp --- a/clang-tools-extra/clangd/unittests/DiagnosticsTests.cpp +++ b/clang-tools-extra/clangd/unittests/DiagnosticsTests.cpp @@ -54,6 +54,11 @@ ::testing::Matcher FixMatcher2) { return Field(&Diag::Fixes, UnorderedElementsAre(FixMatcher1, FixMatcher2)); } +::testing::Matcher withFix(::testing::Matcher FixMatcher1, + ::testing::Matcher FixMatcher2, + ::testing::Matcher FixMatcher3) { + return Field(&Diag::Fixes, UnorderedElementsAre(FixMatcher1, FixMatcher2, FixMatcher3)); +} ::testing::Matcher withID(unsigned ID) { return Field(&Diag::ID, ID); @@ -1908,11 +1913,14 @@ auto AST = TU.build(); EXPECT_THAT( *AST.getDiagnostics(), - UnorderedElementsAre(AllOf( - Diag(Test.range("diag"), - "included header unused.h is not used directly"), - withTag(DiagnosticTag::Unnecessary), diagSource(Diag::Clangd), - withFix(Fix(Test.range("fix"), "", "remove #include directive"))))); + UnorderedElementsAre( + AllOf(Diag(Test.range("diag"), + "included header unused.h is not used directly"), + withTag(DiagnosticTag::Unnecessary), diagSource(Diag::Clangd), + withFix(Fix(Test.range("fix"), "", "remove #include directive"), + fixMessage("remove all unused #includes"), + fixMessage("add all missing #includes and remove all " + "unused ones"))))); auto &Diag = AST.getDiagnostics()->front(); EXPECT_EQ(getDiagnosticDocURI(Diag.Source, Diag.ID, Diag.Name), std::string("https://clangd.llvm.org/guides/include-cleaner")); diff --git a/clang-tools-extra/clangd/unittests/IncludeCleanerTests.cpp b/clang-tools-extra/clangd/unittests/IncludeCleanerTests.cpp --- a/clang-tools-extra/clangd/unittests/IncludeCleanerTests.cpp +++ b/clang-tools-extra/clangd/unittests/IncludeCleanerTests.cpp @@ -44,8 +44,8 @@ using ::testing::Pointee; using ::testing::UnorderedElementsAre; -Matcher withFix(::testing::Matcher FixMatcher) { - return Field(&Diag::Fixes, ElementsAre(FixMatcher)); +Matcher withFix(std::vector<::testing::Matcher> FixMatcheres) { + return Field(&Diag::Fixes, testing::UnorderedElementsAreArray(FixMatcheres)); } MATCHER_P2(Diag, Range, Message, @@ -59,6 +59,8 @@ return arg.Message == Message && arg.Edits.size() == 1 && arg.Edits[0].range == Range && arg.Edits[0].newText == Replacement; } +MATCHER_P(FixMessage, Message, "") { return arg.Message == Message; } + std::string guard(llvm::StringRef Code) { return "#pragma once\n" + Code.str(); @@ -254,42 +256,69 @@ UnorderedElementsAre( AllOf(Diag(MainFile.range("b"), "No header providing \"b\" is directly included"), - withFix(Fix(MainFile.range("insert_b"), "#include \"b.h\"\n", - "#include \"b.h\""))), + withFix({Fix(MainFile.range("insert_b"), "#include \"b.h\"\n", + "#include \"b.h\""), + FixMessage("add all missing #includes"), + FixMessage("add all missing #includes and remove all " + "unused ones")})), AllOf(Diag(MainFile.range("bar"), "No header providing \"ns::Bar\" is directly included"), - withFix(Fix(MainFile.range("insert_d"), - "#include \"dir/d.h\"\n", "#include \"dir/d.h\""))), + withFix({Fix(MainFile.range("insert_d"), + "#include \"dir/d.h\"\n", "#include \"dir/d.h\""), + FixMessage("add all missing #includes"), + FixMessage("add all missing #includes and remove all " + "unused ones")})), AllOf(Diag(MainFile.range("f"), "No header providing \"f\" is directly included"), - withFix(Fix(MainFile.range("insert_f"), "#include \n", - "#include "))), + withFix({Fix(MainFile.range("insert_f"), "#include \n", + "#include "), + FixMessage("add all missing #includes"), + FixMessage("add all missing #includes and remove all " + "unused ones")})), AllOf( Diag(MainFile.range("foobar"), "No header providing \"foobar\" is directly included"), - withFix(Fix(MainFile.range("insert_foobar"), - "#include \"public.h\"\n", "#include \"public.h\""))), + withFix({Fix(MainFile.range("insert_foobar"), + "#include \"public.h\"\n", "#include \"public.h\""), + FixMessage("add all missing #includes"), + FixMessage("add all missing #includes and remove all " + "unused ones")})), AllOf( Diag(MainFile.range("vector"), "No header providing \"std::vector\" is directly included"), - withFix(Fix(MainFile.range("insert_vector"), - "#include \n", "#include "))), + withFix({Fix(MainFile.range("insert_vector"), + "#include \n", "#include "), + FixMessage("add all missing #includes"), + FixMessage("add all missing #includes and remove all " + "unused ones")})), AllOf(Diag(MainFile.range("FOO"), "No header providing \"FOO\" is directly included"), - withFix(Fix(MainFile.range("insert_foo"), - "#include \"foo.h\"\n", "#include \"foo.h\""))), + withFix({Fix(MainFile.range("insert_foo"), + "#include \"foo.h\"\n", "#include \"foo.h\""), + FixMessage("add all missing #includes"), + FixMessage("add all missing #includes and remove all " + "unused ones")})), AllOf(Diag(MainFile.range("DEF"), "No header providing \"Foo\" is directly included"), - withFix(Fix(MainFile.range("insert_foo"), - "#include \"foo.h\"\n", "#include \"foo.h\""))), + withFix({Fix(MainFile.range("insert_foo"), + "#include \"foo.h\"\n", "#include \"foo.h\""), + FixMessage("add all missing #includes"), + FixMessage("add all missing #includes and remove all " + "unused ones")})), AllOf(Diag(MainFile.range("BAR"), "No header providing \"BAR\" is directly included"), - withFix(Fix(MainFile.range("insert_foo"), - "#include \"foo.h\"\n", "#include \"foo.h\""))), + withFix({Fix(MainFile.range("insert_foo"), + "#include \"foo.h\"\n", "#include \"foo.h\""), + FixMessage("add all missing #includes"), + FixMessage("add all missing #includes and remove all " + "unused ones")})), AllOf(Diag(MainFile.range("Foo"), "No header providing \"Foo\" is directly included"), - withFix(Fix(MainFile.range("insert_foo"), - "#include \"foo.h\"\n", "#include \"foo.h\""))))); + withFix({Fix(MainFile.range("insert_foo"), + "#include \"foo.h\"\n", "#include \"foo.h\""), + FixMessage("add all missing #includes"), + FixMessage("add all missing #includes and remove all " + "unused ones")})))); } TEST(IncludeCleaner, IWYUPragmas) {