diff --git a/clang-tools-extra/clangd/refactor/tweaks/CMakeLists.txt b/clang-tools-extra/clangd/refactor/tweaks/CMakeLists.txt --- a/clang-tools-extra/clangd/refactor/tweaks/CMakeLists.txt +++ b/clang-tools-extra/clangd/refactor/tweaks/CMakeLists.txt @@ -22,6 +22,7 @@ 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 --- /dev/null +++ b/clang-tools-extra/clangd/refactor/tweaks/ObjCMemberwiseInitializer.cpp @@ -0,0 +1,332 @@ +//===--- 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/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); +} + +// Returns the effective begin loc for D, including attached leading comments. +static SourceLocation effectiveBeginLoc(const Decl *D, + const SourceManager &SM) { + SourceLocation DeclStart = D->getBeginLoc(); + if (const auto *C = D->getASTContext().getRawCommentForDeclNoCache(D)) { + SourceLocation CommentStart = C->getBeginLoc(); + if (SM.isBeforeInTranslationUnit(CommentStart, DeclStart)) { + return CommentStart; + } + } + return DeclStart; +} + +// Returns the effective end loc for D, including attached trailing comments. +static SourceLocation effectiveEndLoc(const Decl *D, const SourceManager &SM) { + SourceLocation DeclEnd = D->getEndLoc(); + if (const auto *C = D->getASTContext().getRawCommentForDeclNoCache(D)) { + SourceLocation CommentEnd = C->getEndLoc(); + if (SM.isBeforeInTranslationUnit(DeclEnd, CommentEnd)) { + return CommentEnd; + } + } + return DeclEnd; +} + +struct LocationWithPadding { + SourceLocation Loc; + + // Numbers of newlines to pad start. + uint8_t PadStart; + + // Number of newlines to pad end. + uint8_t PadEnd; +}; + +static LocationWithPadding +locForNewInitializer(const ObjCContainerDecl *Container) { + const auto &SM = Container->getASTContext().getSourceManager(); + SourceLocation Loc; + + // We'll insert the new init before the first non-init instance method. + for (const auto *MD : Container->instance_methods()) { + // Skip over methods implicitly generated from @property. + if (MD->isImplicit()) + continue; + + if (MD->getMethodFamily() == OMF_init) { + Loc = effectiveEndLoc(MD, SM).getLocWithOffset(1); + } else if (Loc.isValid()) { + return {Loc, 2, 0}; + } else { + return {effectiveBeginLoc(MD, SM), 0, 2}; + } + } + if (Loc.isValid()) { + return {Loc, 2, 0}; + } + // Fallback: insert where the `@end` is. + return {Container->getEndLoc().getLocWithOffset(-1), 1, 2}; +} + +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(); + if (Name.startswith("_")) + Name = Name.substr(1); + 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; + } +}; + +/// 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: + void initParams(const SelectionTree::Node *N, + SmallVectorImpl &Params); + + /// Either a `ObjCImplementationDecl` or `ObjCInterfaceDecl`. + const ObjCContainerDecl *Container = 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; + Container = ID; + } else if (const auto *ID = dyn_cast(D)) { + Container = ID; + } else if (isa(D)) { + const auto *DC = D->getDeclContext(); + if (const auto *ID = dyn_cast(DC)) { + Container = ID; + } else if (const auto *ID = dyn_cast(DC)) { + Container = ID; + } + } + return Container != nullptr; +} + +void ObjCMemberwiseInitializer::initParams( + const SelectionTree::Node *N, SmallVectorImpl &Params) { + bool HadBadNode = false; + // 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; + } + } + llvm::DenseSet Names; + // Check for selecting multiple ivars/properties. + for (const auto *CNode : N->Children) { + const Decl *D = CNode->ASTNode.get(); + if (!D) { + HadBadNode = true; + continue; + } + if (auto P = MethodParameter::parameterFor(*D)) { + if (Names.insert(P->Name).second) { + Params.push_back(P.getValue()); + } + continue; + } else { + HadBadNode = true; + } + } + // If just the Container itself was selected, fetch all properties and ivars + // for the given class. + if (!HadBadNode && Params.empty()) { + const ObjCInterfaceDecl *Interface = nullptr; + if (const auto *ID = dyn_cast(Container)) { + Interface = ID; + } else if (const auto *ID = dyn_cast(Container)) { + Interface = ID->getClassInterface(); + } + if (Interface) { + // 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). + for (const auto *Ivar : Interface->ivars()) { + MethodParameter P(*Ivar); + if (Names.insert(P.Name).second) + Params.push_back(P); + } + for (const auto *Prop : Interface->properties()) { + MethodParameter P(*Prop); + if (Names.insert(P.Name).second) + Params.push_back(P); + } + } + } +} + +Expected +ObjCMemberwiseInitializer::apply(const Selection &Inputs) { + const SelectionTree::Node *N = Inputs.ASTSelection.commonAncestor(); + if (!N || !Container) + return error("Invalid selection"); + + SmallVector Params; + initParams(N, Params); + + LocationWithPadding LocPadding = locForNewInitializer(Container); + + bool GenerateImpl = isa(Container); + llvm::SmallString<256> Text(std::string(LocPadding.PadStart, '\n')); + + if (Params.empty()) { + if (GenerateImpl) { + Text.append({"- (instancetype)init {\n self = [super init];\n if " + "(self) {\n\n }\n return self;\n}"}); + } else { + Text.append({"- (instancetype)init;"}); + } + } else { + Text.append({"- (instancetype)initWith"}); + const auto &First = Params.front(); + Text.append({capitalize(First.Name.trim().str()), ":(", First.Type, ")", + First.Name}); + for (auto It = Params.begin() + 1; It != Params.end(); ++It) { + Text.append({" ", It->Name, ":(", It->Type, ")", It->Name}); + } + if (GenerateImpl) { + Text.append(" {\n self = [super init];\n if (self) {"); + for (const auto &Param : Params) { + Text.append({"\n ", Param.Assignee, " = ", Param.Name, ";"}); + } + Text.append("\n }\n return self;\n}"); + } else { + Text.append(";"); + } + } + Text.append(std::string(LocPadding.PadEnd, '\n')); + + const SourceManager &SM = Inputs.AST->getSourceManager(); + + return Effect::mainFileEdit(SM, tooling::Replacements(tooling::Replacement( + SM, LocPadding.Loc, 0, Text))); +} + +std::string ObjCMemberwiseInitializer::title() const { + return "Generate 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 --- a/clang-tools-extra/clangd/unittests/CMakeLists.txt +++ b/clang-tools-extra/clangd/unittests/CMakeLists.txt @@ -117,6 +117,7 @@ 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 --- /dev/null +++ b/clang-tools-extra/clangd/unittests/tweaks/ObjCMemberwiseInitializerTests.cpp @@ -0,0 +1,150 @@ +//===-- 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. + EXPECT_AVAILABLE(R"cpp( + @interface Fo^o + @end + )cpp"); + EXPECT_AVAILABLE(R"cpp( + @interface Foo + @end + + @implementation F^oo + @end + )cpp"); + + // 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; +@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