diff --git a/clang-tools-extra/clangd/ParsedAST.h b/clang-tools-extra/clangd/ParsedAST.h --- a/clang-tools-extra/clangd/ParsedAST.h +++ b/clang-tools-extra/clangd/ParsedAST.h @@ -77,6 +77,10 @@ return getASTContext().getSourceManager(); } + const LangOptions &getLangOpts() const { + return getASTContext().getLangOpts(); + } + /// This function returns top-level decls present in the main file of the AST. /// The result does not include the decls that come from the preamble. /// (These should be const, but RecursiveASTVisitor requires Decl*). 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 @@ -19,6 +19,7 @@ ExpandMacro.cpp ExtractFunction.cpp ExtractVariable.cpp + ObjCLocalizeStringLiteral.cpp RawStringLiteral.cpp RemoveUsingNamespace.cpp SwapIfBranches.cpp diff --git a/clang-tools-extra/clangd/refactor/tweaks/ObjCLocalizeStringLiteral.cpp b/clang-tools-extra/clangd/refactor/tweaks/ObjCLocalizeStringLiteral.cpp new file mode 100644 --- /dev/null +++ b/clang-tools-extra/clangd/refactor/tweaks/ObjCLocalizeStringLiteral.cpp @@ -0,0 +1,86 @@ +//===--- ObjcLocalizeStringLiteral.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 "Logger.h" +#include "ParsedAST.h" +#include "SourceCode.h" +#include "refactor/Tweak.h" +#include "clang/AST/ExprObjC.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 { + +/// Wraps an Objective-C string literal with the NSLocalizedString macro. +/// Before: +/// @"description" +/// ^^^ +/// After: +/// NSLocalizedString(@"description", "") +class ObjCLocalizeStringLiteral : public Tweak { +public: + const char *id() const override final; + Intent intent() const override { return Intent::Refactor; } + + bool prepare(const Selection &Inputs) override; + Expected apply(const Selection &Inputs) override; + std::string title() const override; + +private: + const clang::ObjCStringLiteral *Str = nullptr; +}; + +REGISTER_TWEAK(ObjCLocalizeStringLiteral) + +bool ObjCLocalizeStringLiteral::prepare(const Selection &Inputs) { + const SelectionTree::Node *N = Inputs.ASTSelection.commonAncestor(); + if (!N) + return false; + // Allow the refactoring even if the user selected only the C string part + // of the expression. + if (N->ASTNode.get()) { + if (N->Parent) + N = N->Parent; + } + Str = dyn_cast_or_null(N->ASTNode.get()); + return Str; +} + +Expected +ObjCLocalizeStringLiteral::apply(const Selection &Inputs) { + auto &SM = Inputs.AST.getSourceManager(); + auto Reps = tooling::Replacements(tooling::Replacement( + SM, CharSourceRange::getCharRange(Str->getBeginLoc()), + "NSLocalizedString(", Inputs.AST.getASTContext().getLangOpts())); + SourceLocation EndLoc = Lexer::getLocForEndOfToken( + Str->getEndLoc(), 0, Inputs.AST.getSourceManager(), + Inputs.AST.getLangOpts()); + if (auto Err = Reps.add(tooling::Replacement( + SM, CharSourceRange::getCharRange(EndLoc), ", @\"\")", + Inputs.AST.getASTContext().getLangOpts()))) + return std::move(Err); + return Effect::mainFileEdit(SM, std::move(Reps)); +} + +std::string ObjCLocalizeStringLiteral::title() const { + return "Wrap in NSLocalizedString"; +} + +} // namespace +} // namespace clangd +} // namespace clang diff --git a/clang-tools-extra/clangd/unittests/TweakTests.cpp b/clang-tools-extra/clangd/unittests/TweakTests.cpp --- a/clang-tools-extra/clangd/unittests/TweakTests.cpp +++ b/clang-tools-extra/clangd/unittests/TweakTests.cpp @@ -122,6 +122,29 @@ EXPECT_EQ(apply(Input), Output); } +TWEAK_TEST(ObjCLocalizeStringLiteral); +TEST_F(ObjCLocalizeStringLiteralTest, Test) { + ExtraArgs.push_back("-x"); + ExtraArgs.push_back("objective-c"); + + // Ensure the the action can be initiated in the string literal. + EXPECT_AVAILABLE(R"(id x = ^@^"^t^est^";)"); + EXPECT_AVAILABLE(R"(id x = [[@"test"]];)"); + EXPECT_AVAILABLE(R"(id x = @[["test"]];)"); + + // Ensure that the action can't be initiated in other places. + EXPECT_UNAVAILABLE(R"(i^d ^x ^= @"test";^)"); + EXPECT_UNAVAILABLE(R"(id [[x]] = @"test";)"); + EXPECT_UNAVAILABLE(R"([[id x = @"test";]])"); + + // Ensure that the action is not available for regular C strings. + EXPECT_UNAVAILABLE(R"(const char * x= "^test";)"); + + const char *Input = R"(id x = [[@"test"]];)"; + const char *Output = R"(id x = NSLocalizedString(@"test", @"");)"; + EXPECT_EQ(apply(Input), Output); +} + TWEAK_TEST(DumpAST); TEST_F(DumpASTTest, Test) { EXPECT_AVAILABLE("^int f^oo() { re^turn 2 ^+ 2; }");