[clang-tools-extra] 1af5fbd - [clangd] Code action for creating an ObjC initializer
David Goldman via cfe-commits
cfe-commits at lists.llvm.org
Thu Mar 17 08:32:20 PDT 2022
Author: David Goldman
Date: 2022-03-17T11:31:14-04:00
New Revision: 1af5fbd5c605372963f78351f721fa28ee5ba60e
URL: https://github.com/llvm/llvm-project/commit/1af5fbd5c605372963f78351f721fa28ee5ba60e
DIFF: https://github.com/llvm/llvm-project/commit/1af5fbd5c605372963f78351f721fa28ee5ba60e.diff
LOG: [clangd] Code action for creating an ObjC initializer
The code action creates an initializer for the selected
ivars/properties, defaulting to all if only the interface/implementation
container is selected.
We add it based on the position of the first non initializer that we
see, and default to adding it where the @end token is.
We also use the ObjC parameter form of (nullable id) instead of
(id _Nullable) if the property has the nullable attribute.
Differential Revision: https://reviews.llvm.org/D116385
Added:
clang-tools-extra/clangd/refactor/tweaks/ObjCMemberwiseInitializer.cpp
clang-tools-extra/clangd/unittests/tweaks/ObjCMemberwiseInitializerTests.cpp
Modified:
clang-tools-extra/clangd/refactor/tweaks/CMakeLists.txt
clang-tools-extra/clangd/unittests/CMakeLists.txt
Removed:
################################################################################
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<MethodParameter> parameterFor(const Decl &D) {
+ if (const auto *ID = dyn_cast<ObjCIvarDecl>(&D))
+ return MethodParameter(*ID);
+ if (const auto *PD = dyn_cast<ObjCPropertyDecl>(&D))
+ if (PD->isInstanceProperty())
+ return MethodParameter(*PD);
+ return llvm::None;
+ }
+};
+
+static SmallVector<MethodParameter, 8>
+getAllParams(const ObjCInterfaceDecl *ID) {
+ SmallVector<MethodParameter, 8> 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<llvm::StringRef> 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<MethodParameter, 8> &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<Tweak::Effect> apply(const Selection &Inputs) override;
+ std::string title() const override;
+
+private:
+ SmallVector<MethodParameter, 8>
+ 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<Decl>();
+ 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<ObjCInterfaceDecl>(D)) {
+ // Ignore forward declarations (@class Name;).
+ if (!ID->isThisDeclarationADefinition())
+ return false;
+ Interface = ID;
+ } else if (const auto *ID = dyn_cast<ObjCImplementationDecl>(D)) {
+ Interface = ID->getClassInterface();
+ Impl = ID;
+ } else if (isa<ObjCPropertyDecl, ObjCIvarDecl>(D)) {
+ const auto *DC = D->getDeclContext();
+ if (const auto *ID = dyn_cast<ObjCInterfaceDecl>(DC)) {
+ Interface = ID;
+ } else if (const auto *ID = dyn_cast<ObjCImplementationDecl>(DC)) {
+ Interface = ID->getClassInterface();
+ Impl = ID;
+ }
+ }
+ return Interface != nullptr;
+}
+
+SmallVector<MethodParameter, 8>
+ObjCMemberwiseInitializer::paramsForSelection(const SelectionTree::Node *N) {
+ SmallVector<MethodParameter, 8> Params;
+ // Base case: selected a single ivar or property.
+ if (const auto *D = N->ASTNode.get<Decl>()) {
+ if (auto Param = MethodParameter::parameterFor(*D)) {
+ Params.push_back(Param.getValue());
+ return Params;
+ }
+ }
+ const ObjCContainerDecl *Container =
+ Impl ? static_cast<const ObjCContainerDecl *>(Impl)
+ : static_cast<const ObjCContainerDecl *>(Interface);
+ if (Container == N->ASTNode.get<ObjCContainerDecl>() && N->Children.empty())
+ return getAllParams(Interface);
+
+ llvm::DenseSet<llvm::StringRef> Names;
+ // Check for selecting multiple ivars/properties.
+ for (const auto *CNode : N->Children) {
+ const Decl *D = CNode->ASTNode.get<Decl>();
+ if (!D)
+ continue;
+ if (auto P = MethodParameter::parameterFor(*D))
+ if (Names.insert(P->Name).second)
+ Params.push_back(P.getValue());
+ }
+ return Params;
+}
+
+Expected<Tweak::Effect>
+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<MethodParameter, 8> Params = paramsForSelection(N);
+
+ // Insert before the first non-init instance method.
+ std::vector<Anchor> Anchors = {
+ {[](const Decl *D) {
+ if (const auto *MD = llvm::dyn_cast<ObjCMethodDecl>(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
+ //
diff erent 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(
+ at interface Foo {
+ id [[_field;
+}
+ at property(nonatomic) id prop]];
+ at property(nonatomic) id notSelected;
+ at end)cpp";
+ const char *Output = R"cpp(
+ at interface Foo {
+ id _field;
+}
+ at property(nonatomic) id prop;
+ at property(nonatomic) id notSelected;
+- (instancetype)initWithField:(id)field prop:(id)prop;
+
+ at end)cpp";
+ EXPECT_EQ(apply(Input), Output);
+
+ Input = R"cpp(
+ at interface Foo
+ at property(nonatomic, nullable) id somePrettyLongPropertyName;
+ at property(nonatomic, nonnull) id someReallyLongPropertyName;
+ at end
+
+ at implementation F^oo
+
+- (instancetype)init {
+ return self;
+}
+
+ at end)cpp";
+ Output = R"cpp(
+ at interface Foo
+ at property(nonatomic, nullable) id somePrettyLongPropertyName;
+ at property(nonatomic, nonnull) id someReallyLongPropertyName;
+- (instancetype)initWithSomePrettyLongPropertyName:(nullable id)somePrettyLongPropertyName someReallyLongPropertyName:(nonnull id)someReallyLongPropertyName;
+
+ at end
+
+ at 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;
+}
+
+ at end)cpp";
+ EXPECT_EQ(apply(Input), Output);
+}
+
+} // namespace
+} // namespace clangd
+} // namespace clang
More information about the cfe-commits
mailing list