Skip to content

Commit 0255b21

Browse files
authored
[clangd] Add tweak for turning an unscoped into a scoped enum (#69481)
1 parent 2f8e3d5 commit 0255b21

File tree

5 files changed

+283
-0
lines changed

5 files changed

+283
-0
lines changed

clang-tools-extra/clangd/refactor/tweaks/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ add_clang_library(clangDaemonTweaks OBJECT
2727
PopulateSwitch.cpp
2828
RawStringLiteral.cpp
2929
RemoveUsingNamespace.cpp
30+
ScopifyEnum.cpp
3031
SpecialMembers.cpp
3132
SwapIfBranches.cpp
3233

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
//===--- ScopifyEnum.cpp --------------------------------------- -*- C++-*-===//
2+
//
3+
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4+
// See https://llvm.org/LICENSE.txt for license information.
5+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6+
//
7+
//===----------------------------------------------------------------------===//
8+
9+
#include "ParsedAST.h"
10+
#include "XRefs.h"
11+
#include "refactor/Tweak.h"
12+
#include "clang/Basic/SourceLocation.h"
13+
#include "clang/Basic/SourceManager.h"
14+
#include "clang/Tooling/Core/Replacement.h"
15+
#include "llvm/ADT/StringRef.h"
16+
#include "llvm/Support/Error.h"
17+
18+
#include <functional>
19+
20+
namespace clang::clangd {
21+
namespace {
22+
23+
/// Turns an unscoped into a scoped enum type.
24+
/// Before:
25+
/// enum E { EV1, EV2 };
26+
/// ^
27+
/// void f() { E e1 = EV1; }
28+
///
29+
/// After:
30+
/// enum class E { EV1, EV2 };
31+
/// void f() { E e1 = E::EV1; }
32+
///
33+
/// Note that the respective project code might not compile anymore
34+
/// if it made use of the now-gone implicit conversion to int.
35+
/// This is out of scope for this tweak.
36+
///
37+
/// TODO: In the above example, we could detect that the values
38+
/// start with the enum name, and remove that prefix.
39+
40+
class ScopifyEnum : public Tweak {
41+
const char *id() const final;
42+
std::string title() const override { return "Convert to scoped enum"; }
43+
llvm::StringLiteral kind() const override {
44+
return CodeAction::REFACTOR_KIND;
45+
}
46+
bool prepare(const Selection &Inputs) override;
47+
Expected<Tweak::Effect> apply(const Selection &Inputs) override;
48+
49+
using MakeReplacement =
50+
std::function<tooling::Replacement(StringRef, StringRef, unsigned)>;
51+
llvm::Error addClassKeywordToDeclarations();
52+
llvm::Error scopifyEnumValues();
53+
llvm::Error scopifyEnumValue(const EnumConstantDecl &CD, StringRef Prefix);
54+
llvm::Expected<StringRef> getContentForFile(StringRef FilePath);
55+
unsigned getOffsetFromPosition(const Position &Pos, StringRef Content) const;
56+
llvm::Error addReplacementForReference(const ReferencesResult::Reference &Ref,
57+
const MakeReplacement &GetReplacement);
58+
llvm::Error addReplacement(StringRef FilePath, StringRef Content,
59+
const tooling::Replacement &Replacement);
60+
Position getPosition(const Decl &D) const;
61+
62+
const EnumDecl *D = nullptr;
63+
const Selection *S = nullptr;
64+
SourceManager *SM = nullptr;
65+
llvm::SmallVector<std::unique_ptr<llvm::MemoryBuffer>> ExtraBuffers;
66+
llvm::StringMap<StringRef> ContentPerFile;
67+
Effect E;
68+
};
69+
70+
REGISTER_TWEAK(ScopifyEnum)
71+
72+
bool ScopifyEnum::prepare(const Selection &Inputs) {
73+
if (!Inputs.AST->getLangOpts().CPlusPlus11)
74+
return false;
75+
const SelectionTree::Node *N = Inputs.ASTSelection.commonAncestor();
76+
if (!N)
77+
return false;
78+
D = N->ASTNode.get<EnumDecl>();
79+
return D && !D->isScoped() && D->isThisDeclarationADefinition();
80+
}
81+
82+
Expected<Tweak::Effect> ScopifyEnum::apply(const Selection &Inputs) {
83+
S = &Inputs;
84+
SM = &S->AST->getSourceManager();
85+
E.FormatEdits = false;
86+
ContentPerFile.insert(std::make_pair(SM->getFilename(D->getLocation()),
87+
SM->getBufferData(SM->getMainFileID())));
88+
89+
if (auto Err = addClassKeywordToDeclarations())
90+
return Err;
91+
if (auto Err = scopifyEnumValues())
92+
return Err;
93+
94+
return E;
95+
}
96+
97+
llvm::Error ScopifyEnum::addClassKeywordToDeclarations() {
98+
for (const auto &Ref :
99+
findReferences(*S->AST, getPosition(*D), 0, S->Index, false)
100+
.References) {
101+
if (!(Ref.Attributes & ReferencesResult::Declaration))
102+
continue;
103+
104+
static const auto MakeReplacement = [](StringRef FilePath,
105+
StringRef Content, unsigned Offset) {
106+
return tooling::Replacement(FilePath, Offset, 0, "class ");
107+
};
108+
if (auto Err = addReplacementForReference(Ref, MakeReplacement))
109+
return Err;
110+
}
111+
return llvm::Error::success();
112+
}
113+
114+
llvm::Error ScopifyEnum::scopifyEnumValues() {
115+
std::string PrefixToInsert(D->getName());
116+
PrefixToInsert += "::";
117+
for (auto E : D->enumerators()) {
118+
if (auto Err = scopifyEnumValue(*E, PrefixToInsert))
119+
return Err;
120+
}
121+
return llvm::Error::success();
122+
}
123+
124+
llvm::Error ScopifyEnum::scopifyEnumValue(const EnumConstantDecl &CD,
125+
StringRef Prefix) {
126+
for (const auto &Ref :
127+
findReferences(*S->AST, getPosition(CD), 0, S->Index, false)
128+
.References) {
129+
if (Ref.Attributes & ReferencesResult::Declaration)
130+
continue;
131+
132+
const auto MakeReplacement = [&Prefix](StringRef FilePath,
133+
StringRef Content, unsigned Offset) {
134+
const auto IsAlreadyScoped = [Content, Offset] {
135+
if (Offset < 2)
136+
return false;
137+
unsigned I = Offset;
138+
while (--I > 0) {
139+
switch (Content[I]) {
140+
case ' ':
141+
case '\t':
142+
case '\n':
143+
continue;
144+
case ':':
145+
if (Content[I - 1] == ':')
146+
return true;
147+
[[fallthrough]];
148+
default:
149+
return false;
150+
}
151+
}
152+
return false;
153+
};
154+
return IsAlreadyScoped()
155+
? tooling::Replacement()
156+
: tooling::Replacement(FilePath, Offset, 0, Prefix);
157+
};
158+
if (auto Err = addReplacementForReference(Ref, MakeReplacement))
159+
return Err;
160+
}
161+
162+
return llvm::Error::success();
163+
}
164+
165+
llvm::Expected<StringRef> ScopifyEnum::getContentForFile(StringRef FilePath) {
166+
if (auto It = ContentPerFile.find(FilePath); It != ContentPerFile.end())
167+
return It->second;
168+
auto Buffer = S->FS->getBufferForFile(FilePath);
169+
if (!Buffer)
170+
return llvm::errorCodeToError(Buffer.getError());
171+
StringRef Content = Buffer->get()->getBuffer();
172+
ExtraBuffers.push_back(std::move(*Buffer));
173+
ContentPerFile.insert(std::make_pair(FilePath, Content));
174+
return Content;
175+
}
176+
177+
unsigned int ScopifyEnum::getOffsetFromPosition(const Position &Pos,
178+
StringRef Content) const {
179+
unsigned int Offset = 0;
180+
181+
for (std::size_t LinesRemaining = Pos.line;
182+
Offset < Content.size() && LinesRemaining;) {
183+
if (Content[Offset++] == '\n')
184+
--LinesRemaining;
185+
}
186+
return Offset + Pos.character;
187+
}
188+
189+
llvm::Error
190+
ScopifyEnum::addReplacementForReference(const ReferencesResult::Reference &Ref,
191+
const MakeReplacement &GetReplacement) {
192+
StringRef FilePath = Ref.Loc.uri.file();
193+
auto Content = getContentForFile(FilePath);
194+
if (!Content)
195+
return Content.takeError();
196+
unsigned Offset = getOffsetFromPosition(Ref.Loc.range.start, *Content);
197+
tooling::Replacement Replacement = GetReplacement(FilePath, *Content, Offset);
198+
if (Replacement.isApplicable())
199+
return addReplacement(FilePath, *Content, Replacement);
200+
return llvm::Error::success();
201+
}
202+
203+
llvm::Error
204+
ScopifyEnum::addReplacement(StringRef FilePath, StringRef Content,
205+
const tooling::Replacement &Replacement) {
206+
Edit &TheEdit = E.ApplyEdits[FilePath];
207+
TheEdit.InitialCode = Content;
208+
if (auto Err = TheEdit.Replacements.add(Replacement))
209+
return Err;
210+
return llvm::Error::success();
211+
}
212+
213+
Position ScopifyEnum::getPosition(const Decl &D) const {
214+
const SourceLocation Loc = D.getLocation();
215+
Position Pos;
216+
Pos.line = SM->getSpellingLineNumber(Loc) - 1;
217+
Pos.character = SM->getSpellingColumnNumber(Loc) - 1;
218+
return Pos;
219+
}
220+
221+
} // namespace
222+
} // namespace clang::clangd

clang-tools-extra/clangd/unittests/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ add_unittest(ClangdUnitTests ClangdTests
131131
tweaks/PopulateSwitchTests.cpp
132132
tweaks/RawStringLiteralTests.cpp
133133
tweaks/RemoveUsingNamespaceTests.cpp
134+
tweaks/ScopifyEnumTests.cpp
134135
tweaks/ShowSelectionTreeTests.cpp
135136
tweaks/SpecialMembersTests.cpp
136137
tweaks/SwapIfBranchesTests.cpp
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
//===-- DefineOutline.cpp ---------------------------------------*- C++ -*-===//
2+
//
3+
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4+
// See https://llvm.org/LICENSE.txt for license information.
5+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6+
//
7+
//===----------------------------------------------------------------------===//
8+
9+
#include "TweakTesting.h"
10+
#include "gtest/gtest.h"
11+
12+
namespace clang::clangd {
13+
namespace {
14+
15+
TWEAK_TEST(ScopifyEnum);
16+
17+
TEST_F(ScopifyEnumTest, TriggersOnUnscopedEnumDecl) {
18+
FileName = "Test.hpp";
19+
// Not available for scoped enum.
20+
EXPECT_UNAVAILABLE(R"cpp(enum class ^E { V };)cpp");
21+
22+
// Not available for non-definition.
23+
EXPECT_UNAVAILABLE(R"cpp(
24+
enum E { V };
25+
enum ^E;
26+
)cpp");
27+
}
28+
29+
TEST_F(ScopifyEnumTest, ApplyTest) {
30+
std::string Original = R"cpp(
31+
enum ^E { EV1, EV2, EV3 };
32+
enum E;
33+
E func(E in)
34+
{
35+
E out = EV1;
36+
if (in == EV2)
37+
out = E::EV3;
38+
return out;
39+
}
40+
)cpp";
41+
std::string Expected = R"cpp(
42+
enum class E { EV1, EV2, EV3 };
43+
enum class E;
44+
E func(E in)
45+
{
46+
E out = E::EV1;
47+
if (in == E::EV2)
48+
out = E::EV3;
49+
return out;
50+
}
51+
)cpp";
52+
FileName = "Test.cpp";
53+
SCOPED_TRACE(Original);
54+
EXPECT_EQ(apply(Original), Expected);
55+
}
56+
57+
} // namespace
58+
} // namespace clang::clangd

clang-tools-extra/docs/ReleaseNotes.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ Code actions
7070
^^^^^^^^^^^^
7171

7272
- The extract variable tweak gained support for extracting lambda expressions to a variable.
73+
- A new tweak was added for turning unscoped into scoped enums.
7374

7475
Signature help
7576
^^^^^^^^^^^^^^

0 commit comments

Comments
 (0)