| 1 | //===--- ObjCMemberwiseInitializer.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 "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" |
| 25 | #include <optional> |
| 26 | |
| 27 | namespace clang { |
| 28 | namespace clangd { |
| 29 | namespace { |
| 30 | |
| 31 | static std::string capitalize(std::string Message) { |
| 32 | if (!Message.empty()) |
| 33 | Message[0] = llvm::toUpper(x: Message[0]); |
| 34 | return Message; |
| 35 | } |
| 36 | |
| 37 | static std::string getTypeStr(const QualType &OrigT, const Decl &D, |
| 38 | unsigned PropertyAttributes) { |
| 39 | QualType T = OrigT; |
| 40 | PrintingPolicy Policy(D.getASTContext().getLangOpts()); |
| 41 | Policy.SuppressStrongLifetime = true; |
| 42 | std::string Prefix; |
| 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)) { |
| 47 | switch (*Kind) { |
| 48 | case NullabilityKind::Nullable: |
| 49 | Prefix = "nullable " ; |
| 50 | break; |
| 51 | case NullabilityKind::NonNull: |
| 52 | Prefix = "nonnull " ; |
| 53 | break; |
| 54 | case NullabilityKind::Unspecified: |
| 55 | Prefix = "null_unspecified " ; |
| 56 | break; |
| 57 | case NullabilityKind::NullableResult: |
| 58 | T = OrigT; |
| 59 | break; |
| 60 | } |
| 61 | } |
| 62 | } |
| 63 | return Prefix + T.getAsString(Policy); |
| 64 | } |
| 65 | |
| 66 | struct MethodParameter { |
| 67 | // Parameter name. |
| 68 | llvm::StringRef Name; |
| 69 | |
| 70 | // Type of the parameter. |
| 71 | std::string Type; |
| 72 | |
| 73 | // Assignment target (LHS). |
| 74 | std::string Assignee; |
| 75 | |
| 76 | MethodParameter(const ObjCIvarDecl &ID) { |
| 77 | // Convention maps `@property int foo` to ivar `int _foo`, so drop the |
| 78 | // leading `_` if there is one. |
| 79 | Name = ID.getName(); |
| 80 | Name.consume_front(Prefix: "_" ); |
| 81 | Type = getTypeStr(ID.getType(), ID, ObjCPropertyAttribute::kind_noattr); |
| 82 | Assignee = ID.getName().str(); |
| 83 | } |
| 84 | MethodParameter(const ObjCPropertyDecl &PD) { |
| 85 | Name = PD.getName(); |
| 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(); |
| 91 | } |
| 92 | static std::optional<MethodParameter> parameterFor(const Decl &D) { |
| 93 | if (const auto *ID = dyn_cast<ObjCIvarDecl>(Val: &D)) |
| 94 | return MethodParameter(*ID); |
| 95 | if (const auto *PD = dyn_cast<ObjCPropertyDecl>(Val: &D)) |
| 96 | if (PD->isInstanceProperty()) |
| 97 | return MethodParameter(*PD); |
| 98 | return std::nullopt; |
| 99 | } |
| 100 | }; |
| 101 | |
| 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(V: P.Name).second) |
| 113 | Params.push_back(Elt: P); |
| 114 | } |
| 115 | for (const auto *Prop : ID->properties()) { |
| 116 | MethodParameter P(*Prop); |
| 117 | if (Names.insert(P.Name).second) |
| 118 | Params.push_back(P); |
| 119 | } |
| 120 | return Params; |
| 121 | } |
| 122 | |
| 123 | static std::string |
| 124 | initializerForParams(const SmallVector<MethodParameter, 8> &Params, |
| 125 | bool GenerateImpl) { |
| 126 | std::string Code; |
| 127 | llvm::raw_string_ostream Stream(Code); |
| 128 | |
| 129 | if (Params.empty()) { |
| 130 | if (GenerateImpl) { |
| 131 | Stream << |
| 132 | R"cpp(- (instancetype)init { |
| 133 | self = [super init]; |
| 134 | if (self) { |
| 135 | |
| 136 | } |
| 137 | return self; |
| 138 | })cpp" ; |
| 139 | } else { |
| 140 | Stream << "- (instancetype)init;" ; |
| 141 | } |
| 142 | } else { |
| 143 | const auto &First = Params.front(); |
| 144 | Stream << llvm::formatv(Fmt: "- (instancetype)initWith{0}:({1}){2}" , |
| 145 | Vals: capitalize(Message: First.Name.trim().str()), Vals: First.Type, |
| 146 | Vals: First.Name); |
| 147 | for (const auto &It : llvm::drop_begin(RangeOrContainer: Params)) |
| 148 | Stream << llvm::formatv(Fmt: " {0}:({1}){0}" , Vals: It.Name, Vals: It.Type); |
| 149 | |
| 150 | if (GenerateImpl) { |
| 151 | Stream << |
| 152 | R"cpp( { |
| 153 | self = [super init]; |
| 154 | if (self) {)cpp" ; |
| 155 | for (const auto &Param : Params) |
| 156 | Stream << llvm::formatv(Fmt: "\n {0} = {1};" , Vals: Param.Assignee, Vals: Param.Name); |
| 157 | Stream << |
| 158 | R"cpp( |
| 159 | } |
| 160 | return self; |
| 161 | })cpp" ; |
| 162 | } else { |
| 163 | Stream << ";" ; |
| 164 | } |
| 165 | } |
| 166 | Stream << "\n\n" ; |
| 167 | return Code; |
| 168 | } |
| 169 | |
| 170 | /// Generate an initializer for an Objective-C class based on selected |
| 171 | /// properties and instance variables. |
| 172 | class ObjCMemberwiseInitializer : public Tweak { |
| 173 | public: |
| 174 | const char *id() const final; |
| 175 | llvm::StringLiteral kind() const override { |
| 176 | return CodeAction::REFACTOR_KIND; |
| 177 | } |
| 178 | |
| 179 | bool prepare(const Selection &Inputs) override; |
| 180 | Expected<Tweak::Effect> apply(const Selection &Inputs) override; |
| 181 | std::string title() const override; |
| 182 | |
| 183 | private: |
| 184 | SmallVector<MethodParameter, 8> |
| 185 | paramsForSelection(const SelectionTree::Node *N); |
| 186 | |
| 187 | const ObjCInterfaceDecl *Interface = nullptr; |
| 188 | |
| 189 | // Will be nullptr if running on an interface. |
| 190 | const ObjCImplementationDecl *Impl = nullptr; |
| 191 | }; |
| 192 | |
| 193 | REGISTER_TWEAK(ObjCMemberwiseInitializer) |
| 194 | |
| 195 | bool ObjCMemberwiseInitializer::prepare(const Selection &Inputs) { |
| 196 | const SelectionTree::Node *N = Inputs.ASTSelection.commonAncestor(); |
| 197 | if (!N) |
| 198 | return false; |
| 199 | const Decl *D = N->ASTNode.get<Decl>(); |
| 200 | if (!D) |
| 201 | return false; |
| 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) |
| 205 | return false; |
| 206 | |
| 207 | // We support the following selected decls: |
| 208 | // - ObjCInterfaceDecl/ObjCImplementationDecl only - generate for all |
| 209 | // properties and ivars |
| 210 | // |
| 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()) |
| 217 | return false; |
| 218 | Interface = ID; |
| 219 | } else if (const auto *ID = dyn_cast<ObjCImplementationDecl>(D)) { |
| 220 | Interface = ID->getClassInterface(); |
| 221 | Impl = ID; |
| 222 | } else if (isa<ObjCPropertyDecl, ObjCIvarDecl>(Val: D)) { |
| 223 | const auto *DC = D->getDeclContext(); |
| 224 | if (const auto *ID = dyn_cast<ObjCInterfaceDecl>(DC)) { |
| 225 | Interface = ID; |
| 226 | } else if (const auto *ID = dyn_cast<ObjCImplementationDecl>(DC)) { |
| 227 | Interface = ID->getClassInterface(); |
| 228 | Impl = ID; |
| 229 | } |
| 230 | } |
| 231 | return Interface != nullptr; |
| 232 | } |
| 233 | |
| 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); |
| 241 | return Params; |
| 242 | } |
| 243 | } |
| 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(ID: Interface); |
| 249 | |
| 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>(); |
| 254 | if (!D) |
| 255 | continue; |
| 256 | if (auto P = MethodParameter::parameterFor(*D)) |
| 257 | if (Names.insert(P->Name).second) |
| 258 | Params.push_back(*P); |
| 259 | } |
| 260 | return Params; |
| 261 | } |
| 262 | |
| 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(); |
| 267 | if (!N) |
| 268 | return error(Fmt: "Invalid selection" ); |
| 269 | |
| 270 | SmallVector<MethodParameter, 8> Params = paramsForSelection(N); |
| 271 | |
| 272 | // Insert before the first non-init instance method. |
| 273 | std::vector<Anchor> Anchors = { |
| 274 | {.Match: [](const Decl *D) { |
| 275 | if (const auto *MD = llvm::dyn_cast<ObjCMethodDecl>(Val: D)) { |
| 276 | return MD->getMethodFamily() != OMF_init && MD->isInstanceMethod(); |
| 277 | } |
| 278 | return false; |
| 279 | }, |
| 280 | .Direction: Anchor::Above}}; |
| 281 | Effect E; |
| 282 | |
| 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, FID: SM.getFileID(Interface->getLocation()), |
| 289 | Replacements: tooling::Replacements(*InterfaceReplacement)); |
| 290 | if (!FE) |
| 291 | return FE.takeError(); |
| 292 | E.ApplyEdits.insert(std::move(*FE)); |
| 293 | |
| 294 | if (Impl) { |
| 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(); |
| 301 | |
| 302 | if (SM.isWrittenInSameFile(Loc1: Interface->getLocation(), Loc2: Impl->getLocation())) { |
| 303 | // Merge with previous edit if they are in the same file. |
| 304 | if (auto Err = |
| 305 | E.ApplyEdits.begin()->second.Replacements.add(*ImplReplacement)) |
| 306 | return std::move(Err); |
| 307 | } else { |
| 308 | // Generate a new edit if the interface and implementation are in |
| 309 | // different files. |
| 310 | auto FE = Effect::fileEdit(SM, FID: SM.getFileID(Impl->getLocation()), |
| 311 | Replacements: tooling::Replacements(*ImplReplacement)); |
| 312 | if (!FE) |
| 313 | return FE.takeError(); |
| 314 | E.ApplyEdits.insert(std::move(*FE)); |
| 315 | } |
| 316 | } |
| 317 | return E; |
| 318 | } |
| 319 | |
| 320 | std::string ObjCMemberwiseInitializer::title() const { |
| 321 | if (Impl) |
| 322 | return "Generate memberwise initializer" ; |
| 323 | return "Declare memberwise initializer" ; |
| 324 | } |
| 325 | |
| 326 | } // namespace |
| 327 | } // namespace clangd |
| 328 | } // namespace clang |
| 329 | |