1 //===--- ObjCMemberwiseInitializer.cpp ---------------------------*- C++-*-===//
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
7 //===----------------------------------------------------------------------===//
10 #include "SourceCode.h"
11 #include "refactor/InsertionPoint.h"
12 #include "refactor/Tweak.h"
13 #include "support/Logger.h"
14 #include "clang/AST/DeclObjC.h"
15 #include "clang/AST/PrettyPrinter.h"
16 #include "clang/Basic/LLVM.h"
17 #include "clang/Basic/LangOptions.h"
18 #include "clang/Basic/SourceLocation.h"
19 #include "clang/Basic/SourceManager.h"
20 #include "clang/Tooling/Core/Replacement.h"
21 #include "llvm/ADT/StringRef.h"
22 #include "llvm/ADT/iterator_range.h"
23 #include "llvm/Support/Casting.h"
24 #include "llvm/Support/Error.h"
31 static std::string
capitalize(std::string Message
) {
33 Message
[0] = llvm::toUpper(Message
[0]);
37 static std::string
getTypeStr(const QualType
&OrigT
, const Decl
&D
,
38 unsigned PropertyAttributes
) {
40 PrintingPolicy
Policy(D
.getASTContext().getLangOpts());
41 Policy
.SuppressStrongLifetime
= true;
43 // If the nullability is specified via a property attribute, use the shorter
44 // `nullable` form for the method parameter.
45 if (PropertyAttributes
& ObjCPropertyAttribute::kind_nullability
) {
46 if (auto Kind
= AttributedType::stripOuterNullability(T
)) {
48 case NullabilityKind::Nullable
:
51 case NullabilityKind::NonNull
:
54 case NullabilityKind::Unspecified
:
55 Prefix
= "null_unspecified ";
57 case NullabilityKind::NullableResult
:
63 return Prefix
+ T
.getAsString(Policy
);
66 struct MethodParameter
{
70 // Type of the parameter.
73 // Assignment target (LHS).
76 MethodParameter(const ObjCIvarDecl
&ID
) {
77 // Convention maps `@property int foo` to ivar `int _foo`, so drop the
78 // leading `_` if there is one.
80 Name
.consume_front("_");
81 Type
= getTypeStr(ID
.getType(), ID
, ObjCPropertyAttribute::kind_noattr
);
82 Assignee
= ID
.getName().str();
84 MethodParameter(const ObjCPropertyDecl
&PD
) {
86 Type
= getTypeStr(PD
.getType(), PD
, PD
.getPropertyAttributes());
87 if (const auto *ID
= PD
.getPropertyIvarDecl())
88 Assignee
= ID
->getName().str();
89 else // Could be a dynamic property or a property in a header.
90 Assignee
= ("self." + Name
).str();
92 static std::optional
<MethodParameter
> parameterFor(const Decl
&D
) {
93 if (const auto *ID
= dyn_cast
<ObjCIvarDecl
>(&D
))
94 return MethodParameter(*ID
);
95 if (const auto *PD
= dyn_cast
<ObjCPropertyDecl
>(&D
))
96 if (PD
->isInstanceProperty())
97 return MethodParameter(*PD
);
102 static SmallVector
<MethodParameter
, 8>
103 getAllParams(const ObjCInterfaceDecl
*ID
) {
104 SmallVector
<MethodParameter
, 8> Params
;
105 // Currently we only generate based on the ivars and properties declared
106 // in the interface. We could consider expanding this to include visible
107 // categories + class extensions in the future (see
108 // all_declared_ivar_begin).
109 llvm::DenseSet
<llvm::StringRef
> Names
;
110 for (const auto *Ivar
: ID
->ivars()) {
111 MethodParameter
P(*Ivar
);
112 if (Names
.insert(P
.Name
).second
)
115 for (const auto *Prop
: ID
->properties()) {
116 MethodParameter
P(*Prop
);
117 if (Names
.insert(P
.Name
).second
)
124 initializerForParams(const SmallVector
<MethodParameter
, 8> &Params
,
127 llvm::raw_string_ostream
Stream(Code
);
129 if (Params
.empty()) {
132 R
"cpp(- (instancetype)init {
140 Stream
<< "- (instancetype)init;";
143 const auto &First
= Params
.front();
144 Stream
<< llvm::formatv("- (instancetype)initWith{0}:({1}){2}",
145 capitalize(First
.Name
.trim().str()), First
.Type
,
147 for (const auto &It
: llvm::drop_begin(Params
))
148 Stream
<< llvm::formatv(" {0}:({1}){0}", It
.Name
, It
.Type
);
155 for (const auto &Param
: Params
)
156 Stream
<< llvm::formatv("\n {0} = {1};", Param
.Assignee
, Param
.Name
);
170 /// Generate an initializer for an Objective-C class based on selected
171 /// properties and instance variables.
172 class ObjCMemberwiseInitializer
: public Tweak
{
174 const char *id() const final
;
175 llvm::StringLiteral
kind() const override
{
176 return CodeAction::REFACTOR_KIND
;
179 bool prepare(const Selection
&Inputs
) override
;
180 Expected
<Tweak::Effect
> apply(const Selection
&Inputs
) override
;
181 std::string
title() const override
;
184 SmallVector
<MethodParameter
, 8>
185 paramsForSelection(const SelectionTree::Node
*N
);
187 const ObjCInterfaceDecl
*Interface
= nullptr;
189 // Will be nullptr if running on an interface.
190 const ObjCImplementationDecl
*Impl
= nullptr;
193 REGISTER_TWEAK(ObjCMemberwiseInitializer
)
195 bool ObjCMemberwiseInitializer::prepare(const Selection
&Inputs
) {
196 const SelectionTree::Node
*N
= Inputs
.ASTSelection
.commonAncestor();
199 const Decl
*D
= N
->ASTNode
.get
<Decl
>();
202 const auto &LangOpts
= Inputs
.AST
->getLangOpts();
203 // Require ObjC w/ arc enabled since we don't emit retains.
204 if (!LangOpts
.ObjC
|| !LangOpts
.ObjCAutoRefCount
)
207 // We support the following selected decls:
208 // - ObjCInterfaceDecl/ObjCImplementationDecl only - generate for all
209 // properties and ivars
211 // - Specific ObjCPropertyDecl(s)/ObjCIvarDecl(s) - generate only for those
212 // selected. Note that if only one is selected, the common ancestor will be
213 // the ObjCPropertyDecl/ObjCIvarDecl itself instead of the container.
214 if (const auto *ID
= dyn_cast
<ObjCInterfaceDecl
>(D
)) {
215 // Ignore forward declarations (@class Name;).
216 if (!ID
->isThisDeclarationADefinition())
219 } else if (const auto *ID
= dyn_cast
<ObjCImplementationDecl
>(D
)) {
220 Interface
= ID
->getClassInterface();
222 } else if (isa
<ObjCPropertyDecl
, ObjCIvarDecl
>(D
)) {
223 const auto *DC
= D
->getDeclContext();
224 if (const auto *ID
= dyn_cast
<ObjCInterfaceDecl
>(DC
)) {
226 } else if (const auto *ID
= dyn_cast
<ObjCImplementationDecl
>(DC
)) {
227 Interface
= ID
->getClassInterface();
231 return Interface
!= nullptr;
234 SmallVector
<MethodParameter
, 8>
235 ObjCMemberwiseInitializer::paramsForSelection(const SelectionTree::Node
*N
) {
236 SmallVector
<MethodParameter
, 8> Params
;
237 // Base case: selected a single ivar or property.
238 if (const auto *D
= N
->ASTNode
.get
<Decl
>()) {
239 if (auto Param
= MethodParameter::parameterFor(*D
)) {
240 Params
.push_back(*Param
);
244 const ObjCContainerDecl
*Container
=
245 Impl
? static_cast<const ObjCContainerDecl
*>(Impl
)
246 : static_cast<const ObjCContainerDecl
*>(Interface
);
247 if (Container
== N
->ASTNode
.get
<ObjCContainerDecl
>() && N
->Children
.empty())
248 return getAllParams(Interface
);
250 llvm::DenseSet
<llvm::StringRef
> Names
;
251 // Check for selecting multiple ivars/properties.
252 for (const auto *CNode
: N
->Children
) {
253 const Decl
*D
= CNode
->ASTNode
.get
<Decl
>();
256 if (auto P
= MethodParameter::parameterFor(*D
))
257 if (Names
.insert(P
->Name
).second
)
258 Params
.push_back(*P
);
263 Expected
<Tweak::Effect
>
264 ObjCMemberwiseInitializer::apply(const Selection
&Inputs
) {
265 const auto &SM
= Inputs
.AST
->getASTContext().getSourceManager();
266 const SelectionTree::Node
*N
= Inputs
.ASTSelection
.commonAncestor();
268 return error("Invalid selection");
270 SmallVector
<MethodParameter
, 8> Params
= paramsForSelection(N
);
272 // Insert before the first non-init instance method.
273 std::vector
<Anchor
> Anchors
= {
275 if (const auto *MD
= llvm::dyn_cast
<ObjCMethodDecl
>(D
)) {
276 return MD
->getMethodFamily() != OMF_init
&& MD
->isInstanceMethod();
283 auto InterfaceReplacement
=
284 insertDecl(initializerForParams(Params
, /*GenerateImpl=*/false),
285 *Interface
, Anchors
);
286 if (!InterfaceReplacement
)
287 return InterfaceReplacement
.takeError();
288 auto FE
= Effect::fileEdit(SM
, SM
.getFileID(Interface
->getLocation()),
289 tooling::Replacements(*InterfaceReplacement
));
291 return FE
.takeError();
292 E
.ApplyEdits
.insert(std::move(*FE
));
295 // If we see the class implementation, add the initializer there too.
296 // FIXME: merging the edits is awkward, do this elsewhere.
297 auto ImplReplacement
= insertDecl(
298 initializerForParams(Params
, /*GenerateImpl=*/true), *Impl
, Anchors
);
299 if (!ImplReplacement
)
300 return ImplReplacement
.takeError();
302 if (SM
.isWrittenInSameFile(Interface
->getLocation(), Impl
->getLocation())) {
303 // Merge with previous edit if they are in the same file.
305 E
.ApplyEdits
.begin()->second
.Replacements
.add(*ImplReplacement
))
306 return std::move(Err
);
308 // Generate a new edit if the interface and implementation are in
310 auto FE
= Effect::fileEdit(SM
, SM
.getFileID(Impl
->getLocation()),
311 tooling::Replacements(*ImplReplacement
));
313 return FE
.takeError();
314 E
.ApplyEdits
.insert(std::move(*FE
));
320 std::string
ObjCMemberwiseInitializer::title() const {
322 return "Generate memberwise initializer";
323 return "Declare memberwise initializer";
327 } // namespace clangd