xref: /llvm-project/clang-tools-extra/clangd/refactor/tweaks/ScopifyEnum.cpp (revision 8d946c71712daeabf6636ff3844fa49e4638324b)
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 "Protocol.h"
11 #include "Selection.h"
12 #include "SourceCode.h"
13 #include "XRefs.h"
14 #include "refactor/Tweak.h"
15 #include "clang/AST/Decl.h"
16 #include "clang/AST/DeclBase.h"
17 #include "clang/Basic/LLVM.h"
18 #include "clang/Basic/SourceLocation.h"
19 #include "clang/Basic/SourceManager.h"
20 #include "clang/Tooling/Core/Replacement.h"
21 #include "llvm/ADT/SmallVector.h"
22 #include "llvm/ADT/StringMap.h"
23 #include "llvm/ADT/StringRef.h"
24 #include "llvm/Support/Error.h"
25 #include "llvm/Support/MemoryBuffer.h"
26 
27 #include <cstddef>
28 #include <functional>
29 #include <memory>
30 #include <string>
31 #include <utility>
32 
33 namespace clang::clangd {
34 namespace {
35 
36 /// Turns an unscoped into a scoped enum type.
37 /// Before:
38 ///   enum E { EV1, EV2 };
39 ///        ^
40 ///   void f() { E e1 = EV1; }
41 ///
42 /// After:
43 ///   enum class E { V1, V2 };
44 ///   void f() { E e1 = E::V1; }
45 ///
46 /// Note that the respective project code might not compile anymore
47 /// if it made use of the now-gone implicit conversion to int.
48 /// This is out of scope for this tweak.
49 
50 class ScopifyEnum : public Tweak {
51   const char *id() const final;
title() const52   std::string title() const override { return "Convert to scoped enum"; }
kind() const53   llvm::StringLiteral kind() const override {
54     return CodeAction::REFACTOR_KIND;
55   }
56   bool prepare(const Selection &Inputs) override;
57   Expected<Tweak::Effect> apply(const Selection &Inputs) override;
58 
59   using MakeReplacement =
60       std::function<tooling::Replacement(StringRef, StringRef, unsigned)>;
61   llvm::Error addClassKeywordToDeclarations();
62   llvm::Error scopifyEnumValues();
63   llvm::Error scopifyEnumValue(const EnumConstantDecl &CD, StringRef EnumName,
64                                bool StripPrefix);
65   llvm::Expected<StringRef> getContentForFile(StringRef FilePath);
66   llvm::Error addReplacementForReference(const ReferencesResult::Reference &Ref,
67                                          const MakeReplacement &GetReplacement);
68   llvm::Error addReplacement(StringRef FilePath, StringRef Content,
69                              const tooling::Replacement &Replacement);
70 
71   const EnumDecl *D = nullptr;
72   const Selection *S = nullptr;
73   SourceManager *SM = nullptr;
74   llvm::SmallVector<std::unique_ptr<llvm::MemoryBuffer>> ExtraBuffers;
75   llvm::StringMap<StringRef> ContentPerFile;
76   Effect E;
77 };
78 
REGISTER_TWEAK(ScopifyEnum)79 REGISTER_TWEAK(ScopifyEnum)
80 
81 bool ScopifyEnum::prepare(const Selection &Inputs) {
82   if (!Inputs.AST->getLangOpts().CPlusPlus11)
83     return false;
84   const SelectionTree::Node *N = Inputs.ASTSelection.commonAncestor();
85   if (!N)
86     return false;
87   D = N->ASTNode.get<EnumDecl>();
88   return D && !D->isScoped() && D->isThisDeclarationADefinition();
89 }
90 
apply(const Selection & Inputs)91 Expected<Tweak::Effect> ScopifyEnum::apply(const Selection &Inputs) {
92   S = &Inputs;
93   SM = &S->AST->getSourceManager();
94   E.FormatEdits = false;
95   ContentPerFile.insert(std::make_pair(SM->getFilename(D->getLocation()),
96                                        SM->getBufferData(SM->getMainFileID())));
97 
98   if (auto Err = addClassKeywordToDeclarations())
99     return std::move(Err);
100   if (auto Err = scopifyEnumValues())
101     return std::move(Err);
102 
103   return E;
104 }
105 
addClassKeywordToDeclarations()106 llvm::Error ScopifyEnum::addClassKeywordToDeclarations() {
107   for (const auto &Ref :
108        findReferences(*S->AST, sourceLocToPosition(*SM, D->getBeginLoc()), 0,
109                       S->Index, false)
110            .References) {
111     if (!(Ref.Attributes & ReferencesResult::Declaration))
112       continue;
113 
114     static const auto MakeReplacement = [](StringRef FilePath,
115                                            StringRef Content, unsigned Offset) {
116       return tooling::Replacement(FilePath, Offset, 0, "class ");
117     };
118     if (auto Err = addReplacementForReference(Ref, MakeReplacement))
119       return Err;
120   }
121   return llvm::Error::success();
122 }
123 
scopifyEnumValues()124 llvm::Error ScopifyEnum::scopifyEnumValues() {
125   StringRef EnumName(D->getName());
126   bool StripPrefix = true;
127   for (const EnumConstantDecl *E : D->enumerators()) {
128     if (!E->getName().starts_with(EnumName)) {
129       StripPrefix = false;
130       break;
131     }
132   }
133   for (const EnumConstantDecl *E : D->enumerators()) {
134     if (auto Err = scopifyEnumValue(*E, EnumName, StripPrefix))
135       return Err;
136   }
137   return llvm::Error::success();
138 }
139 
scopifyEnumValue(const EnumConstantDecl & CD,StringRef EnumName,bool StripPrefix)140 llvm::Error ScopifyEnum::scopifyEnumValue(const EnumConstantDecl &CD,
141                                           StringRef EnumName,
142                                           bool StripPrefix) {
143   for (const auto &Ref :
144        findReferences(*S->AST, sourceLocToPosition(*SM, CD.getBeginLoc()), 0,
145                       S->Index, false)
146            .References) {
147     if (Ref.Attributes & ReferencesResult::Declaration) {
148       if (StripPrefix) {
149         const auto MakeReplacement = [&EnumName](StringRef FilePath,
150                                                  StringRef Content,
151                                                  unsigned Offset) {
152           unsigned Length = EnumName.size();
153           if (Content[Offset + Length] == '_')
154             ++Length;
155           return tooling::Replacement(FilePath, Offset, Length, {});
156         };
157         if (auto Err = addReplacementForReference(Ref, MakeReplacement))
158           return Err;
159       }
160       continue;
161     }
162 
163     const auto MakeReplacement = [&](StringRef FilePath, StringRef Content,
164                                      unsigned Offset) {
165       const auto IsAlreadyScoped = [Content, Offset] {
166         if (Offset < 2)
167           return false;
168         unsigned I = Offset;
169         while (--I > 0) {
170           switch (Content[I]) {
171           case ' ':
172           case '\t':
173           case '\n':
174             continue;
175           case ':':
176             if (Content[I - 1] == ':')
177               return true;
178             [[fallthrough]];
179           default:
180             return false;
181           }
182         }
183         return false;
184       };
185       if (StripPrefix) {
186         const int ExtraLength =
187             Content[Offset + EnumName.size()] == '_' ? 1 : 0;
188         if (IsAlreadyScoped())
189           return tooling::Replacement(FilePath, Offset,
190                                       EnumName.size() + ExtraLength, {});
191         return tooling::Replacement(FilePath, Offset + EnumName.size(),
192                                     ExtraLength, "::");
193       }
194       return IsAlreadyScoped() ? tooling::Replacement()
195                                : tooling::Replacement(FilePath, Offset, 0,
196                                                       EnumName.str() + "::");
197     };
198     if (auto Err = addReplacementForReference(Ref, MakeReplacement))
199       return Err;
200   }
201 
202   return llvm::Error::success();
203 }
204 
getContentForFile(StringRef FilePath)205 llvm::Expected<StringRef> ScopifyEnum::getContentForFile(StringRef FilePath) {
206   if (auto It = ContentPerFile.find(FilePath); It != ContentPerFile.end())
207     return It->second;
208   auto Buffer = S->FS->getBufferForFile(FilePath);
209   if (!Buffer)
210     return llvm::errorCodeToError(Buffer.getError());
211   StringRef Content = Buffer->get()->getBuffer();
212   ExtraBuffers.push_back(std::move(*Buffer));
213   ContentPerFile.insert(std::make_pair(FilePath, Content));
214   return Content;
215 }
216 
217 llvm::Error
addReplacementForReference(const ReferencesResult::Reference & Ref,const MakeReplacement & GetReplacement)218 ScopifyEnum::addReplacementForReference(const ReferencesResult::Reference &Ref,
219                                         const MakeReplacement &GetReplacement) {
220   StringRef FilePath = Ref.Loc.uri.file();
221   llvm::Expected<StringRef> Content = getContentForFile(FilePath);
222   if (!Content)
223     return Content.takeError();
224   llvm::Expected<size_t> Offset =
225       positionToOffset(*Content, Ref.Loc.range.start);
226   if (!Offset)
227     return Offset.takeError();
228   tooling::Replacement Replacement =
229       GetReplacement(FilePath, *Content, *Offset);
230   if (Replacement.isApplicable())
231     return addReplacement(FilePath, *Content, Replacement);
232   return llvm::Error::success();
233 }
234 
235 llvm::Error
addReplacement(StringRef FilePath,StringRef Content,const tooling::Replacement & Replacement)236 ScopifyEnum::addReplacement(StringRef FilePath, StringRef Content,
237                             const tooling::Replacement &Replacement) {
238   Edit &TheEdit = E.ApplyEdits[FilePath];
239   TheEdit.InitialCode = Content;
240   if (auto Err = TheEdit.Replacements.add(Replacement))
241     return Err;
242   return llvm::Error::success();
243 }
244 
245 } // namespace
246 } // namespace clang::clangd
247