Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[clangd] Make clangd run format::cleanupAroundReplacements() for all code actions just as clang-tidy does #118569

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
22 changes: 4 additions & 18 deletions clang-tools-extra/clang-tidy/ClangTidy.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -149,26 +149,12 @@ class ErrorReporter {
Repl.getLength(), Repl.getReplacementText());
auto &Entry = FileReplacements[R.getFilePath()];
Replacements &Replacements = Entry.Replaces;
llvm::Error Err = Replacements.add(R);
llvm::Error Err = Replacements.addOrMerge(R);
if (Err) {
chomosuke marked this conversation as resolved.
Show resolved Hide resolved
// FIXME: Implement better conflict handling.
llvm::errs() << "Trying to resolve conflict: "
<< llvm::toString(std::move(Err)) << "\n";
unsigned NewOffset =
Replacements.getShiftedCodePosition(R.getOffset());
unsigned NewLength = Replacements.getShiftedCodePosition(
R.getOffset() + R.getLength()) -
NewOffset;
if (NewLength == R.getLength()) {
R = Replacement(R.getFilePath(), NewOffset, NewLength,
R.getReplacementText());
Replacements = Replacements.merge(tooling::Replacements(R));
CanBeApplied = true;
++AppliedFixes;
} else {
llvm::errs()
<< "Can't resolve conflict, skipping the replacement.\n";
}
llvm::errs()
<< "Can't resolve conflict, skipping the replacement: "
<< llvm::toString(std::move(Err)) << '\n';
} else {
CanBeApplied = true;
++AppliedFixes;
Expand Down
35 changes: 33 additions & 2 deletions clang-tools-extra/clangd/Diagnostics.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
#include "clang/Basic/SourceLocation.h"
#include "clang/Basic/SourceManager.h"
#include "clang/Basic/TokenKinds.h"
#include "clang/Format/Format.h"
#include "clang/Lex/Lexer.h"
#include "clang/Lex/Token.h"
#include "llvm/ADT/ArrayRef.h"
Expand Down Expand Up @@ -741,7 +742,7 @@ void StoreDiags::HandleDiagnostic(DiagnosticsEngine::Level DiagLevel,
return false;
// Copy as we may modify the ranges.
auto FixIts = Info.getFixItHints().vec();
llvm::SmallVector<TextEdit, 1> Edits;
auto Replacements = std::make_optional<tooling::Replacements>();
for (auto &FixIt : FixIts) {
// Allow fixits within a single macro-arg expansion to be applied.
// This can be incorrect if the argument is expanded multiple times in
Expand All @@ -761,7 +762,37 @@ void StoreDiags::HandleDiagnostic(DiagnosticsEngine::Level DiagLevel,
return false;
if (!isInsideMainFile(FixIt.RemoveRange.getBegin(), SM))
return false;
Edits.push_back(toTextEdit(FixIt, SM, *LangOpts));

auto R = tooling::Replacement(SM, FixIt.RemoveRange, FixIt.CodeToInsert,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we deliberately do not apply formatting here due to latency concerns. there can be many fix-its available in a file and eagerly formatting all of them is likely to affect diagnostics latencies (as clangd potentially re-evaluates them at every keystroke).

can we rather perform this when the user asks for a code-action to be applied?

*LangOpts);
auto Err = Replacements->addOrMerge(R);
if (Err) {
chomosuke marked this conversation as resolved.
Show resolved Hide resolved
log("Skipping formatting the replacement due to conflict: {0}",
llvm::toString(std::move(Err)));
Replacements = std::nullopt;
break;
}
}

llvm::SmallVector<TextEdit, 1> Edits;

if (Replacements) {
StringRef Code = SM.getBufferData(SM.getMainFileID());
auto Repl = format::cleanupAroundReplacements(Code, *Replacements,
format::getNoStyle());
chomosuke marked this conversation as resolved.
Show resolved Hide resolved
if (!Repl) {
log("Skipping formatting the replacement due to conflict: {0}",
llvm::toString(std::move(Repl.takeError())));
Replacements = std::nullopt;
} else {
auto Es = replacementsToEdits(Code, *Repl);
chomosuke marked this conversation as resolved.
Show resolved Hide resolved
Edits.append(Es.begin(), Es.end());
}
}
if (!Replacements) {
for (auto &FixIt : FixIts) {
Edits.push_back(toTextEdit(FixIt, SM, *LangOpts));
}
chomosuke marked this conversation as resolved.
Show resolved Hide resolved
}

llvm::SmallString<64> Message;
Expand Down
51 changes: 43 additions & 8 deletions clang-tools-extra/clangd/unittests/DiagnosticsTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -876,17 +876,17 @@ TEST(DiagnosticTest, ClangTidySelfContainedDiags) {

clangd::Fix ExpectedCFix;
ExpectedCFix.Message = "variable 'C' is not initialized";
ExpectedCFix.Edits.push_back(TextEdit{Main.range("CFix"), " = NAN"});
ExpectedCFix.Edits.push_back(
TextEdit{Main.range("MathHeader"), "#include <math.h>\n\n"});
ExpectedCFix.Edits.push_back(TextEdit{Main.range("CFix"), " = NAN"});

// Again in clang-tidy only the include directive would be emitted for the
// first warning. However we need the include attaching for both warnings.
clangd::Fix ExpectedDFix;
ExpectedDFix.Message = "variable 'D' is not initialized";
ExpectedDFix.Edits.push_back(TextEdit{Main.range("DFix"), " = NAN"});
ExpectedDFix.Edits.push_back(
TextEdit{Main.range("MathHeader"), "#include <math.h>\n\n"});
ExpectedDFix.Edits.push_back(TextEdit{Main.range("DFix"), " = NAN"});
EXPECT_THAT(
TU.build().getDiagnostics(),
ifTidyChecks(UnorderedElementsAre(
Expand Down Expand Up @@ -921,14 +921,14 @@ TEST(DiagnosticTest, ClangTidySelfContainedDiagsFormatting) {
clangd::Fix const ExpectedFix1{
"prefer using 'override' or (rarely) 'final' "
"instead of 'virtual'",
{TextEdit{Main.range("override1"), " override"},
TextEdit{Main.range("virtual1"), ""}},
{TextEdit{Main.range("virtual1"), ""},
TextEdit{Main.range("override1"), " override"}},
{}};
clangd::Fix const ExpectedFix2{
"prefer using 'override' or (rarely) 'final' "
"instead of 'virtual'",
{TextEdit{Main.range("override2"), " override"},
TextEdit{Main.range("virtual2"), ""}},
{TextEdit{Main.range("virtual2"), ""},
TextEdit{Main.range("override2"), " override"}},
{}};
// Note that in the Fix we expect the "virtual" keyword and the following
// whitespace to be deleted
Expand Down Expand Up @@ -1930,10 +1930,10 @@ TEST(ParsedASTTest, ModuleSawDiag) {
TestTU TU;

auto AST = TU.build();
#if 0
#if 0
EXPECT_THAT(AST.getDiagnostics(),
testing::Contains(Diag(Code.range(), KDiagMsg.str())));
#endif
#endif
}

TEST(Preamble, EndsOnNonEmptyLine) {
Expand Down Expand Up @@ -2136,6 +2136,41 @@ TEST(DiagnosticsTest, UnusedInHeader) {
EXPECT_THAT(TU.build().getDiagnostics(), IsEmpty());
}

TEST(DiagnosticsTest, CleanupAroundReplacements) {
Annotations Test(R"cpp(
struct PositiveValueChar {
PositiveValueChar() : $c0fix[[c0()]]$comma[[,]] $c1fix[[c1()]] {}
const char $c0[[c0]];
wchar_t $c1[[c1]];
};
)cpp");
auto TU = TestTU::withCode(Test.code());
TU.ClangTidyProvider = addTidyChecks("modernize-use-default-member-init");

clangd::Fix C0Fix;
C0Fix.Message = "use default member initializer for 'c0'";
C0Fix.Edits.push_back(
TextEdit{{Test.range("c0fix").start, Test.range("comma").end}, ""});
C0Fix.Edits.push_back(
TextEdit{{Test.range("c0").end, Test.range("c0").end}, "{}"});

clangd::Fix C1Fix;
C1Fix.Message = "use default member initializer for 'c1'";
C1Fix.Edits.push_back(TextEdit{Test.range("comma"), ""});
C1Fix.Edits.push_back(TextEdit{Test.range("c1fix"), ""});
C1Fix.Edits.push_back(
TextEdit{{Test.range("c1").end, Test.range("c1").end}, "{}"});

EXPECT_THAT(TU.build().getDiagnostics(),
ifTidyChecks(UnorderedElementsAre(
AllOf(Diag(Test.range("c0"),
"use default member initializer for 'c0'"),
withFix(equalToFix(C0Fix))),
AllOf(Diag(Test.range("c1"),
"use default member initializer for 'c1'"),
withFix(equalToFix(C1Fix))))));
}

} // namespace
} // namespace clangd
} // namespace clang
2 changes: 2 additions & 0 deletions clang-tools-extra/docs/ReleaseNotes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ Code actions
- Improved the extract-to-function code action to allow extracting statements
with overloaded operators like ``<<`` of ``std::ostream``.

- Improved diagnostics code actions to better remove leftover commas and colons.

Signature help
^^^^^^^^^^^^^^

Expand Down
4 changes: 4 additions & 0 deletions clang/include/clang/Tooling/Core/Replacement.h
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,10 @@ class Replacements {
/// category of replacements.
llvm::Error add(const Replacement &R);

/// Adds a replacement `R` into `Replaces` or merges it into `Replaces` by
/// applying all existing Replaces first if there is conflict.
llvm::Error addOrMerge(const Replacement &R);

/// Merges \p Replaces into the current replacements. \p Replaces
/// refers to code after applying the current replacements.
[[nodiscard]] Replacements merge(const Replacements &Replaces) const;
Expand Down
16 changes: 15 additions & 1 deletion clang/lib/Tooling/Core/Replacement.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,20 @@ llvm::Error Replacements::add(const Replacement &R) {
return llvm::Error::success();
}

llvm::Error Replacements::addOrMerge(const Replacement &R) {
auto Err = add(R);
if (Err) {
chomosuke marked this conversation as resolved.
Show resolved Hide resolved
llvm::consumeError(std::move(Err));
auto Replace = getReplacementInChangedCode(R);
if (Replace.getLength() != R.getLength()) {
return llvm::make_error<ReplacementError>(
replacement_error::overlap_conflict, R);
}
*this = merge(tooling::Replacements(Replace));
}
return llvm::Error::success();
}

namespace {

// Represents a merged replacement, i.e. a replacement consisting of multiple
Expand Down Expand Up @@ -578,7 +592,7 @@ bool applyAllReplacements(const Replacements &Replaces, Rewriter &Rewrite) {
}

llvm::Expected<std::string> applyAllReplacements(StringRef Code,
const Replacements &Replaces) {
const Replacements &Replaces) {
if (Replaces.empty())
return Code.str();

Expand Down
Loading