329 lines · cpp
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-exception6//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 27namespace clang {28namespace clangd {29namespace {30 31static std::string capitalize(std::string Message) {32 if (!Message.empty())33 Message[0] = llvm::toUpper(Message[0]);34 return Message;35}36 37static 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 shorter44 // `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 66struct 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 the78 // leading `_` if there is one.79 Name = ID.getName();80 Name.consume_front("_");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>(&D))94 return MethodParameter(*ID);95 if (const auto *PD = dyn_cast<ObjCPropertyDecl>(&D))96 if (PD->isInstanceProperty())97 return MethodParameter(*PD);98 return std::nullopt;99 }100};101 102static SmallVector<MethodParameter, 8>103getAllParams(const ObjCInterfaceDecl *ID) {104 SmallVector<MethodParameter, 8> Params;105 // Currently we only generate based on the ivars and properties declared106 // in the interface. We could consider expanding this to include visible107 // categories + class extensions in the future (see108 // 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)113 Params.push_back(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 123static std::string124initializerForParams(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("- (instancetype)initWith{0}:({1}){2}",145 capitalize(First.Name.trim().str()), First.Type,146 First.Name);147 for (const auto &It : llvm::drop_begin(Params))148 Stream << llvm::formatv(" {0}:({1}){0}", It.Name, 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("\n {0} = {1};", Param.Assignee, 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 selected171/// properties and instance variables.172class ObjCMemberwiseInitializer : public Tweak {173public: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 183private: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 193REGISTER_TWEAK(ObjCMemberwiseInitializer)194 195bool 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 all209 // properties and ivars210 //211 // - Specific ObjCPropertyDecl(s)/ObjCIvarDecl(s) - generate only for those212 // selected. Note that if only one is selected, the common ancestor will be213 // 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>(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 234SmallVector<MethodParameter, 8>235ObjCMemberwiseInitializer::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(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 263Expected<Tweak::Effect>264ObjCMemberwiseInitializer::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("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 {[](const Decl *D) {275 if (const auto *MD = llvm::dyn_cast<ObjCMethodDecl>(D)) {276 return MD->getMethodFamily() != OMF_init && MD->isInstanceMethod();277 }278 return false;279 },280 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, SM.getFileID(Interface->getLocation()),289 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(Interface->getLocation(), 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 in309 // different files.310 auto FE = Effect::fileEdit(SM, SM.getFileID(Impl->getLocation()),311 tooling::Replacements(*ImplReplacement));312 if (!FE)313 return FE.takeError();314 E.ApplyEdits.insert(std::move(*FE));315 }316 }317 return E;318}319 320std::string ObjCMemberwiseInitializer::title() const {321 if (Impl)322 return "Generate memberwise initializer";323 return "Declare memberwise initializer";324}325 326} // namespace327} // namespace clangd328} // namespace clang329