diff --git a/clang-tools-extra/clangd/refactor/tweaks/CMakeLists.txt b/clang-tools-extra/clangd/refactor/tweaks/CMakeLists.txt index caa69947c47fb..ae279781a6f52 100644 --- a/clang-tools-extra/clangd/refactor/tweaks/CMakeLists.txt +++ b/clang-tools-extra/clangd/refactor/tweaks/CMakeLists.txt @@ -22,6 +22,7 @@ add_clang_library(clangDaemonTweaks OBJECT ExtractFunction.cpp ExtractVariable.cpp ObjCLocalizeStringLiteral.cpp + ObjCMemberwiseInitializer.cpp PopulateSwitch.cpp RawStringLiteral.cpp RemoveUsingNamespace.cpp diff --git a/clang-tools-extra/clangd/refactor/tweaks/ObjCMemberwiseInitializer.cpp b/clang-tools-extra/clangd/refactor/tweaks/ObjCMemberwiseInitializer.cpp new file mode 100644 index 0000000000000..2f8f8f7863409 --- /dev/null +++ b/clang-tools-extra/clangd/refactor/tweaks/ObjCMemberwiseInitializer.cpp @@ -0,0 +1,329 @@ +//===--- 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/None.h" +#include "llvm/ADT/Optional.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/ADT/iterator_range.h" +#include "llvm/Support/Casting.h" +#include "llvm/Support/Error.h" + +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.getValue()) { + 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 llvm::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 llvm::None; + } +}; + +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 (auto It = Params.begin() + 1; It != Params.end(); ++It) + 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 override 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.getValue()); + 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.getValue()); + } + 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 diff --git a/clang-tools-extra/clangd/unittests/CMakeLists.txt b/clang-tools-extra/clangd/unittests/CMakeLists.txt index 88af14700ec22..8d8efb9ecc3c7 100644 --- a/clang-tools-extra/clangd/unittests/CMakeLists.txt +++ b/clang-tools-extra/clangd/unittests/CMakeLists.txt @@ -118,6 +118,7 @@ add_unittest(ClangdUnitTests ClangdTests tweaks/ExtractFunctionTests.cpp tweaks/ExtractVariableTests.cpp tweaks/ObjCLocalizeStringLiteralTests.cpp + tweaks/ObjCMemberwiseInitializerTests.cpp tweaks/PopulateSwitchTests.cpp tweaks/RawStringLiteralTests.cpp tweaks/RemoveUsingNamespaceTests.cpp diff --git a/clang-tools-extra/clangd/unittests/tweaks/ObjCMemberwiseInitializerTests.cpp b/clang-tools-extra/clangd/unittests/tweaks/ObjCMemberwiseInitializerTests.cpp new file mode 100644 index 0000000000000..dc78d696bebd8 --- /dev/null +++ b/clang-tools-extra/clangd/unittests/tweaks/ObjCMemberwiseInitializerTests.cpp @@ -0,0 +1,153 @@ +//===-- ObjCMemberwiseInitializerTests.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 "TestTU.h" +#include "TweakTesting.h" +#include "gmock/gmock-matchers.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace clang { +namespace clangd { +namespace { + +TWEAK_TEST(ObjCMemberwiseInitializer); + +TEST_F(ObjCMemberwiseInitializerTest, TestAvailability) { + FileName = "TestTU.m"; + + // Ensure the action can't be triggered since arc is disabled. + EXPECT_UNAVAILABLE(R"cpp( + @interface Fo^o + @end + )cpp"); + + ExtraArgs.push_back("-fobjc-arc"); + + // Ensure the action can be initiated on the interface and implementation, + // but not on the forward declaration. + EXPECT_AVAILABLE(R"cpp( + @interface Fo^o + @end + )cpp"); + EXPECT_AVAILABLE(R"cpp( + @interface Foo + @end + + @implementation F^oo + @end + )cpp"); + EXPECT_UNAVAILABLE("@class Fo^o;"); + + // Ensure that the action can be triggered on ivars and properties, + // including selecting both. + EXPECT_AVAILABLE(R"cpp( + @interface Foo { + id _fi^eld; + } + @end + )cpp"); + EXPECT_AVAILABLE(R"cpp( + @interface Foo + @property(nonatomic) id fi^eld; + @end + )cpp"); + EXPECT_AVAILABLE(R"cpp( + @interface Foo { + id _fi^eld; + } + @property(nonatomic) id pr^op; + @end + )cpp"); + + // Ensure that the action can't be triggered on property synthesis + // and methods. + EXPECT_UNAVAILABLE(R"cpp( + @interface Foo + @property(nonatomic) id prop; + @end + + @implementation Foo + @dynamic pr^op; + @end + )cpp"); + EXPECT_UNAVAILABLE(R"cpp( + @interface Foo + @end + + @implementation Foo + - (void)fo^o {} + @end + )cpp"); +} + +TEST_F(ObjCMemberwiseInitializerTest, Test) { + FileName = "TestTU.m"; + ExtraArgs.push_back("-fobjc-arc"); + + const char *Input = R"cpp( +@interface Foo { + id [[_field; +} +@property(nonatomic) id prop]]; +@property(nonatomic) id notSelected; +@end)cpp"; + const char *Output = R"cpp( +@interface Foo { + id _field; +} +@property(nonatomic) id prop; +@property(nonatomic) id notSelected; +- (instancetype)initWithField:(id)field prop:(id)prop; + +@end)cpp"; + EXPECT_EQ(apply(Input), Output); + + Input = R"cpp( +@interface Foo +@property(nonatomic, nullable) id somePrettyLongPropertyName; +@property(nonatomic, nonnull) id someReallyLongPropertyName; +@end + +@implementation F^oo + +- (instancetype)init { + return self; +} + +@end)cpp"; + Output = R"cpp( +@interface Foo +@property(nonatomic, nullable) id somePrettyLongPropertyName; +@property(nonatomic, nonnull) id someReallyLongPropertyName; +- (instancetype)initWithSomePrettyLongPropertyName:(nullable id)somePrettyLongPropertyName someReallyLongPropertyName:(nonnull id)someReallyLongPropertyName; + +@end + +@implementation Foo + +- (instancetype)init { + return self; +} + +- (instancetype)initWithSomePrettyLongPropertyName:(nullable id)somePrettyLongPropertyName someReallyLongPropertyName:(nonnull id)someReallyLongPropertyName { + self = [super init]; + if (self) { + _somePrettyLongPropertyName = somePrettyLongPropertyName; + _someReallyLongPropertyName = someReallyLongPropertyName; + } + return self; +} + +@end)cpp"; + EXPECT_EQ(apply(Input), Output); +} + +} // namespace +} // namespace clangd +} // namespace clang