[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