328 lines
10 KiB
C++
328 lines
10 KiB
C++
//===--- 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 <optional>
|
|
|
|
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<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 std::nullopt;
|
|
}
|
|
};
|
|
|
|
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 (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<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);
|
|
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);
|
|
}
|
|
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
|
|
// 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
|