//===--- ObjCMemberwiseInitializer.cpp ---------------------------*- C++-*-===// // // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. // See https://llvm.org/LICENSE.txt for license information. // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception // //===----------------------------------------------------------------------===// #include "ParsedAST.h" #include "SourceCode.h" #include "refactor/InsertionPoint.h" #include "refactor/Tweak.h" #include "support/Logger.h" #include "clang/AST/DeclObjC.h" #include "clang/AST/PrettyPrinter.h" #include "clang/Basic/LLVM.h" #include "clang/Basic/LangOptions.h" #include "clang/Basic/SourceLocation.h" #include "clang/Basic/SourceManager.h" #include "clang/Tooling/Core/Replacement.h" #include "llvm/ADT/StringRef.h" #include "llvm/ADT/iterator_range.h" #include "llvm/Support/Casting.h" #include "llvm/Support/Error.h" #include namespace clang { namespace clangd { namespace { static std::string capitalize(std::string Message) { if (!Message.empty()) Message[0] = llvm::toUpper(Message[0]); return Message; } static std::string getTypeStr(const QualType &OrigT, const Decl &D, unsigned PropertyAttributes) { QualType T = OrigT; PrintingPolicy Policy(D.getASTContext().getLangOpts()); Policy.SuppressStrongLifetime = true; std::string Prefix; // If the nullability is specified via a property attribute, use the shorter // `nullable` form for the method parameter. if (PropertyAttributes & ObjCPropertyAttribute::kind_nullability) { if (auto Kind = AttributedType::stripOuterNullability(T)) { switch (*Kind) { case NullabilityKind::Nullable: Prefix = "nullable "; break; case NullabilityKind::NonNull: Prefix = "nonnull "; break; case NullabilityKind::Unspecified: Prefix = "null_unspecified "; break; case NullabilityKind::NullableResult: T = OrigT; break; } } } return Prefix + T.getAsString(Policy); } struct MethodParameter { // Parameter name. llvm::StringRef Name; // Type of the parameter. std::string Type; // Assignment target (LHS). std::string Assignee; MethodParameter(const ObjCIvarDecl &ID) { // Convention maps `@property int foo` to ivar `int _foo`, so drop the // leading `_` if there is one. Name = ID.getName(); Name.consume_front("_"); Type = getTypeStr(ID.getType(), ID, ObjCPropertyAttribute::kind_noattr); Assignee = ID.getName().str(); } MethodParameter(const ObjCPropertyDecl &PD) { Name = PD.getName(); Type = getTypeStr(PD.getType(), PD, PD.getPropertyAttributes()); if (const auto *ID = PD.getPropertyIvarDecl()) Assignee = ID->getName().str(); else // Could be a dynamic property or a property in a header. Assignee = ("self." + Name).str(); } static std::optional parameterFor(const Decl &D) { if (const auto *ID = dyn_cast(&D)) return MethodParameter(*ID); if (const auto *PD = dyn_cast(&D)) if (PD->isInstanceProperty()) return MethodParameter(*PD); return std::nullopt; } }; static SmallVector getAllParams(const ObjCInterfaceDecl *ID) { SmallVector Params; // Currently we only generate based on the ivars and properties declared // in the interface. We could consider expanding this to include visible // categories + class extensions in the future (see // all_declared_ivar_begin). llvm::DenseSet Names; for (const auto *Ivar : ID->ivars()) { MethodParameter P(*Ivar); if (Names.insert(P.Name).second) Params.push_back(P); } for (const auto *Prop : ID->properties()) { MethodParameter P(*Prop); if (Names.insert(P.Name).second) Params.push_back(P); } return Params; } static std::string initializerForParams(const SmallVector &Params, bool GenerateImpl) { std::string Code; llvm::raw_string_ostream Stream(Code); if (Params.empty()) { if (GenerateImpl) { Stream << R"cpp(- (instancetype)init { self = [super init]; if (self) { } return self; })cpp"; } else { Stream << "- (instancetype)init;"; } } else { const auto &First = Params.front(); Stream << llvm::formatv("- (instancetype)initWith{0}:({1}){2}", capitalize(First.Name.trim().str()), First.Type, First.Name); for (const auto &It : llvm::drop_begin(Params)) Stream << llvm::formatv(" {0}:({1}){0}", It.Name, It.Type); if (GenerateImpl) { Stream << R"cpp( { self = [super init]; if (self) {)cpp"; for (const auto &Param : Params) Stream << llvm::formatv("\n {0} = {1};", Param.Assignee, Param.Name); Stream << R"cpp( } return self; })cpp"; } else { Stream << ";"; } } Stream << "\n\n"; return Code; } /// Generate an initializer for an Objective-C class based on selected /// properties and instance variables. class ObjCMemberwiseInitializer : public Tweak { public: const char *id() const final; llvm::StringLiteral kind() const override { return CodeAction::REFACTOR_KIND; } bool prepare(const Selection &Inputs) override; Expected apply(const Selection &Inputs) override; std::string title() const override; private: SmallVector paramsForSelection(const SelectionTree::Node *N); const ObjCInterfaceDecl *Interface = nullptr; // Will be nullptr if running on an interface. const ObjCImplementationDecl *Impl = nullptr; }; REGISTER_TWEAK(ObjCMemberwiseInitializer) bool ObjCMemberwiseInitializer::prepare(const Selection &Inputs) { const SelectionTree::Node *N = Inputs.ASTSelection.commonAncestor(); if (!N) return false; const Decl *D = N->ASTNode.get(); if (!D) return false; const auto &LangOpts = Inputs.AST->getLangOpts(); // Require ObjC w/ arc enabled since we don't emit retains. if (!LangOpts.ObjC || !LangOpts.ObjCAutoRefCount) return false; // We support the following selected decls: // - ObjCInterfaceDecl/ObjCImplementationDecl only - generate for all // properties and ivars // // - Specific ObjCPropertyDecl(s)/ObjCIvarDecl(s) - generate only for those // selected. Note that if only one is selected, the common ancestor will be // the ObjCPropertyDecl/ObjCIvarDecl itself instead of the container. if (const auto *ID = dyn_cast(D)) { // Ignore forward declarations (@class Name;). if (!ID->isThisDeclarationADefinition()) return false; Interface = ID; } else if (const auto *ID = dyn_cast(D)) { Interface = ID->getClassInterface(); Impl = ID; } else if (isa(D)) { const auto *DC = D->getDeclContext(); if (const auto *ID = dyn_cast(DC)) { Interface = ID; } else if (const auto *ID = dyn_cast(DC)) { Interface = ID->getClassInterface(); Impl = ID; } } return Interface != nullptr; } SmallVector ObjCMemberwiseInitializer::paramsForSelection(const SelectionTree::Node *N) { SmallVector Params; // Base case: selected a single ivar or property. if (const auto *D = N->ASTNode.get()) { if (auto Param = MethodParameter::parameterFor(*D)) { Params.push_back(*Param); return Params; } } const ObjCContainerDecl *Container = Impl ? static_cast(Impl) : static_cast(Interface); if (Container == N->ASTNode.get() && N->Children.empty()) return getAllParams(Interface); llvm::DenseSet Names; // Check for selecting multiple ivars/properties. for (const auto *CNode : N->Children) { const Decl *D = CNode->ASTNode.get(); if (!D) continue; if (auto P = MethodParameter::parameterFor(*D)) if (Names.insert(P->Name).second) Params.push_back(*P); } return Params; } Expected ObjCMemberwiseInitializer::apply(const Selection &Inputs) { const auto &SM = Inputs.AST->getASTContext().getSourceManager(); const SelectionTree::Node *N = Inputs.ASTSelection.commonAncestor(); if (!N) return error("Invalid selection"); SmallVector Params = paramsForSelection(N); // Insert before the first non-init instance method. std::vector Anchors = { {[](const Decl *D) { if (const auto *MD = llvm::dyn_cast(D)) { return MD->getMethodFamily() != OMF_init && MD->isInstanceMethod(); } return false; }, Anchor::Above}}; Effect E; auto InterfaceReplacement = insertDecl(initializerForParams(Params, /*GenerateImpl=*/false), *Interface, Anchors); if (!InterfaceReplacement) return InterfaceReplacement.takeError(); auto FE = Effect::fileEdit(SM, SM.getFileID(Interface->getLocation()), tooling::Replacements(*InterfaceReplacement)); if (!FE) return FE.takeError(); E.ApplyEdits.insert(std::move(*FE)); if (Impl) { // If we see the class implementation, add the initializer there too. // FIXME: merging the edits is awkward, do this elsewhere. auto ImplReplacement = insertDecl( initializerForParams(Params, /*GenerateImpl=*/true), *Impl, Anchors); if (!ImplReplacement) return ImplReplacement.takeError(); if (SM.isWrittenInSameFile(Interface->getLocation(), Impl->getLocation())) { // Merge with previous edit if they are in the same file. if (auto Err = E.ApplyEdits.begin()->second.Replacements.add(*ImplReplacement)) return std::move(Err); } else { // Generate a new edit if the interface and implementation are in // different files. auto FE = Effect::fileEdit(SM, SM.getFileID(Impl->getLocation()), tooling::Replacements(*ImplReplacement)); if (!FE) return FE.takeError(); E.ApplyEdits.insert(std::move(*FE)); } } return E; } std::string ObjCMemberwiseInitializer::title() const { if (Impl) return "Generate memberwise initializer"; return "Declare memberwise initializer"; } } // namespace } // namespace clangd } // namespace clang