//===-- RenameTests.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 "Annotations.h" #include "ClangdServer.h" #include "SyncAPI.h" #include "TestFS.h" #include "TestTU.h" #include "index/Ref.h" #include "refactor/Rename.h" #include "support/TestTracer.h" #include "clang/Tooling/Core/Replacement.h" #include "llvm/ADT/STLExtras.h" #include "llvm/Support/MemoryBuffer.h" #include #include "gmock/gmock.h" #include "gtest/gtest.h" namespace clang { namespace clangd { namespace { using testing::ElementsAre; using testing::Eq; using testing::IsEmpty; using testing::Pair; using testing::SizeIs; using testing::UnorderedElementsAre; using testing::UnorderedElementsAreArray; // Convert a Range to a Ref. Ref refWithRange(const clangd::Range &Range, const std::string &URI) { Ref Result; Result.Kind = RefKind::Reference | RefKind::Spelled; Result.Location.Start.setLine(Range.start.line); Result.Location.Start.setColumn(Range.start.character); Result.Location.End.setLine(Range.end.line); Result.Location.End.setColumn(Range.end.character); Result.Location.FileURI = URI.c_str(); return Result; } // Build a RefSlab from all marked ranges in the annotation. The ranges are // assumed to associate with the given SymbolName. std::unique_ptr buildRefSlab(const Annotations &Code, llvm::StringRef SymbolName, llvm::StringRef Path) { RefSlab::Builder Builder; TestTU TU; TU.HeaderCode = std::string(Code.code()); auto Symbols = TU.headerSymbols(); const auto &SymbolID = findSymbol(Symbols, SymbolName).ID; std::string PathURI = URI::create(Path).toString(); for (const auto &Range : Code.ranges()) Builder.insert(SymbolID, refWithRange(Range, PathURI)); return std::make_unique(std::move(Builder).build()); } std::vector< std::pair> applyEdits(FileEdits FE) { std::vector> Results; for (auto &It : FE) Results.emplace_back( It.first().str(), llvm::cantFail(tooling::applyAllReplacements( It.getValue().InitialCode, It.getValue().Replacements))); return Results; } // Generates an expected rename result by replacing all ranges in the given // annotation with the NewName. std::string expectedResult(Annotations Test, llvm::StringRef NewName) { std::string Result; unsigned NextChar = 0; llvm::StringRef Code = Test.code(); for (const auto &R : Test.llvm::Annotations::ranges()) { assert(R.Begin <= R.End && NextChar <= R.Begin); Result += Code.substr(NextChar, R.Begin - NextChar); Result += NewName; NextChar = R.End; } Result += Code.substr(NextChar); return Result; } TEST(RenameTest, WithinFileRename) { // For each "^" this test moves cursor to its location and applies renaming // while checking that all identifiers in [[]] ranges are also renamed. llvm::StringRef Tests[] = { // Function. R"cpp( void [[foo^]]() { [[fo^o]](); } )cpp", // Type. R"cpp( struct [[foo^]] {}; [[foo]] test() { [[f^oo]] x; return x; } )cpp", // Local variable. R"cpp( void bar() { if (auto [[^foo]] = 5) { [[foo]] = 3; } } )cpp", // Class, its constructor and destructor. R"cpp( class [[F^oo]] { [[F^oo]](); ~[[F^oo]](); [[F^oo]] *foo(int x); [[F^oo]] *Ptr; }; [[F^oo]]::[[Fo^o]]() {} [[F^oo]]::~[[Fo^o]]() {} [[F^oo]] *[[F^oo]]::foo(int x) { return Ptr; } )cpp", // Template class, its constructor and destructor. R"cpp( template class [[F^oo]] { [[F^oo]](); ~[[F^oo]](); void f([[F^oo]] x); }; template [[F^oo]]::[[Fo^o]]() {} template [[F^oo]]::~[[Fo^o]]() {} )cpp", // Template class constructor. R"cpp( class [[F^oo]] { template [[Fo^o]](); template [[F^oo]](T t); }; template [[F^oo]]::[[Fo^o]]() {} )cpp", // Class in template argument. R"cpp( class [[F^oo]] {}; template void func(); template class Baz {}; int main() { func<[[F^oo]]>(); Baz<[[F^oo]]> obj; return 0; } )cpp", // Forward class declaration without definition. R"cpp( class [[F^oo]]; [[F^oo]] *f(); )cpp", // Member function. R"cpp( struct X { void [[F^oo]]() {} void Baz() { [[F^oo]](); } }; )cpp", // Class methods overrides. R"cpp( struct A { virtual void [[f^oo]]() {} }; struct B : A { void [[f^oo]]() override {} }; struct C : B { void [[f^oo]]() override {} }; void func() { A().[[f^oo]](); B().[[f^oo]](); C().[[f^oo]](); } )cpp", // Templated method instantiation. R"cpp( template class Foo { public: static T [[f^oo]]() {} }; void bar() { Foo::[[f^oo]](); } )cpp", R"cpp( template class Foo { public: T [[f^oo]]() {} }; void bar() { Foo().[[f^oo]](); } )cpp", // Template class (partial) specializations. R"cpp( template class [[F^oo]] {}; template<> class [[F^oo]] {}; template class [[F^oo]] {}; void test() { [[F^oo]] x; [[F^oo]] y; [[F^oo]] z; } )cpp", // Incomplete class specializations R"cpp( template class [[Fo^o]] {}; void func([[F^oo]]); )cpp", // Template class instantiations. R"cpp( template class [[F^oo]] { public: T foo(T arg, T& ref, T* ptr) { T value; int number = 42; value = (T)number; value = static_cast(number); return value; } static void foo(T value) {} T member; }; template void func() { [[F^oo]] obj; obj.member = T(); [[Foo]]::foo(); } void test() { [[F^oo]] i; i.member = 0; [[F^oo]]::foo(0); [[F^oo]] b; b.member = false; [[F^oo]]::foo(false); } )cpp", // Template class methods. R"cpp( template class A { public: void [[f^oo]]() {} }; void func() { A().[[f^oo]](); A().[[f^oo]](); A().[[f^oo]](); } )cpp", // Templated class specialization. R"cpp( template class [[Foo^]]; template class [[Foo^]] {}; template class [[Foo^]]; )cpp", R"cpp( template class [[Foo^]]; template class [[Foo^]] {}; )cpp", // Function template specialization. R"cpp( template U [[foo^]](); template U [[foo^]]() {}; )cpp", R"cpp( template U [[foo^]]() {}; template U [[foo^]](); )cpp", R"cpp( template U [[foo^]](); template U [[foo^]](); )cpp", R"cpp( template void [[f^oo]](T t); template <> void [[f^oo]](int a); void test() { [[f^oo]](1); } )cpp", // Variable template. R"cpp( template bool [[F^oo]] = true; // Explicit template specialization template <> bool [[F^oo]] = false; // Partial template specialization template bool [[F^oo]] = false; void foo() { // Ref to the explicit template specialization [[F^oo]]; // Ref to the primary template. [[F^oo]]; } )cpp", // Complicated class type. R"cpp( // Forward declaration. class [[Fo^o]]; class Baz { virtual int getValue() const = 0; }; class [[F^oo]] : public Baz { public: [[F^oo]](int value = 0) : x(value) {} [[F^oo]] &operator++(int); bool operator<([[Foo]] const &rhs); int getValue() const; private: int x; }; void func() { [[F^oo]] *Pointer = 0; [[F^oo]] Variable = [[Foo]](10); for ([[F^oo]] it; it < Variable; it++); const [[F^oo]] *C = new [[Foo]](); const_cast<[[F^oo]] *>(C)->getValue(); [[F^oo]] foo; const Baz &BazReference = foo; const Baz *BazPointer = &foo; reinterpret_cast(BazPointer)->getValue(); static_cast(BazReference).getValue(); static_cast(BazPointer)->getValue(); } )cpp", // Static class member. R"cpp( struct Foo { static Foo *[[Static^Member]]; }; Foo* Foo::[[Static^Member]] = nullptr; void foo() { Foo* Pointer = Foo::[[Static^Member]]; } )cpp", // Reference in lambda parameters. R"cpp( template class function; template class function { public: template function(Functor f) {} function() {} R operator()(ArgTypes...) const {} }; namespace ns { class [[Old]] {}; void f() { function func; } } // namespace ns )cpp", // Destructor explicit call. R"cpp( class [[F^oo]] { public: ~[[^Foo]](); }; [[Foo^]]::~[[^Foo]]() {} int main() { [[Fo^o]] f; f.~/*something*/[[^Foo]](); f.~[[^Foo]](); } )cpp", // Derived destructor explicit call. R"cpp( class [[Bas^e]] {}; class Derived : public [[Bas^e]] {}; int main() { [[Bas^e]] *foo = new Derived(); foo->[[^Base]]::~[[^Base]](); } )cpp", // CXXConstructor initializer list. R"cpp( class Baz {}; class Qux { Baz [[F^oo]]; public: Qux(); }; Qux::Qux() : [[F^oo]]() {} )cpp", // DeclRefExpr. R"cpp( class C { public: static int [[F^oo]]; }; int foo(int x); #define MACRO(a) foo(a) void func() { C::[[F^oo]] = 1; MACRO(C::[[Foo]]); int y = C::[[F^oo]]; } )cpp", // Macros. R"cpp( // no rename inside macro body. #define M1 foo #define M2(x) x int [[fo^o]](); void boo(int); void qoo() { [[f^oo]](); boo([[f^oo]]()); M1(); boo(M1()); M2([[f^oo]]()); M2(M1()); // foo is inside the nested macro body. } )cpp", // MemberExpr in macros R"cpp( class Baz { public: int [[F^oo]]; }; int qux(int x); #define MACRO(a) qux(a) int main() { Baz baz; baz.[[F^oo]] = 1; MACRO(baz.[[F^oo]]); int y = baz.[[F^oo]]; } )cpp", // Fields in classes & partial and full specialiations. R"cpp( template struct Foo { T [[Vari^able]] = 42; }; void foo() { Foo f; f.[[Varia^ble]] = 9000; } )cpp", R"cpp( template struct Foo { T Variable[42]; U Another; void bar() {} }; template struct Foo { T [[Var^iable]]; void bar() { ++[[Var^iable]]; } }; void foo() { Foo f; f.[[Var^iable]] = 9000; } )cpp", R"cpp( template struct Foo { T Variable[42]; U Another; void bar() {} }; template struct Foo { T Variable; void bar() { ++Variable; } }; template<> struct Foo { unsigned [[Var^iable]]; void bar() { ++[[Var^iable]]; } }; void foo() { Foo f; f.[[Var^iable]] = 9000; } )cpp", // Static fields. R"cpp( struct Foo { static int [[Var^iable]]; }; int Foo::[[Var^iable]] = 42; void foo() { int LocalInt = Foo::[[Var^iable]]; } )cpp", R"cpp( template struct Foo { static T [[Var^iable]]; }; template <> int Foo::[[Var^iable]] = 42; template <> bool Foo::[[Var^iable]] = true; void foo() { int LocalInt = Foo::[[Var^iable]]; bool LocalBool = Foo::[[Var^iable]]; } )cpp", // Template parameters. R"cpp( template class Foo { [[T^]] foo([[T^]] arg, [[T^]]& ref, [[^T]]* ptr) { [[T]] value; int number = 42; value = ([[T^]])number; value = static_cast<[[^T]]>(number); return value; } static void foo([[T^]] value) {} [[T^]] member; }; )cpp", // Typedef. R"cpp( namespace ns { class basic_string {}; typedef basic_string [[s^tring]]; } // namespace ns ns::[[s^tring]] foo(); )cpp", // Variable. R"cpp( namespace A { int [[F^oo]]; } int Foo; int Qux = Foo; int Baz = A::[[^Foo]]; void fun() { struct { int Foo; } b = {100}; int Foo = 100; Baz = Foo; { extern int Foo; Baz = Foo; Foo = A::[[F^oo]] + Baz; A::[[Fo^o]] = b.Foo; } Foo = b.Foo; } )cpp", // Namespace alias. R"cpp( namespace a { namespace b { void foo(); } } namespace [[^x]] = a::b; void bar() { [[x^]]::foo(); } )cpp", // Enum. R"cpp( enum [[C^olor]] { Red, Green, Blue }; void foo() { [[C^olor]] c; c = [[C^olor]]::Blue; } )cpp", // Scoped enum. R"cpp( enum class [[K^ind]] { ABC }; void ff() { [[K^ind]] s; s = [[K^ind]]::ABC; } )cpp", // Template class in template argument list. R"cpp( template class [[Fo^o]] {}; template class Z> struct Bar { }; template <> struct Bar<[[F^oo]]> {}; )cpp", // Designated initializer. R"cpp( struct Bar { int [[Fo^o]]; }; Bar bar { .[[^Foo]] = 42 }; )cpp", // Nested designated initializer. R"cpp( struct Baz { int Field; }; struct Bar { Baz [[Fo^o]]; }; // FIXME: v selecting here results in renaming Field. Bar bar { .[[Foo]].Field = 42 }; )cpp", R"cpp( struct Baz { int [[Fiel^d]]; }; struct Bar { Baz Foo; }; Bar bar { .Foo.[[^Field]] = 42 }; )cpp", // Templated alias. R"cpp( template class X { T t; }; template using [[Fo^o]] = X; void bar() { [[Fo^o]] Bar; } )cpp", // Alias. R"cpp( class X {}; using [[F^oo]] = X; void bar() { [[Fo^o]] Bar; } )cpp", // Alias within a namespace. R"cpp( namespace x { class X {}; } namespace ns { using [[Fo^o]] = x::X; } void bar() { ns::[[Fo^o]] Bar; } )cpp", // Alias within macros. R"cpp( namespace x { class Old {}; } namespace ns { #define REF(alias) alias alias_var; #define ALIAS(old) \ using old##Alias = x::old; \ REF(old##Alias); ALIAS(Old); [[Old^Alias]] old_alias; } void bar() { ns::[[Old^Alias]] Bar; } )cpp", // User defined conversion. R"cpp( class [[F^oo]] { public: [[F^oo]]() {} }; class Baz { public: operator [[F^oo]]() { return [[F^oo]](); } }; int main() { Baz boo; [[F^oo]] foo = static_cast<[[F^oo]]>(boo); } )cpp", // ObjC, should not crash. R"cpp( @interface ObjC { char [[da^ta]]; } @end )cpp", }; llvm::StringRef NewName = "NewName"; for (llvm::StringRef T : Tests) { SCOPED_TRACE(T); Annotations Code(T); auto TU = TestTU::withCode(Code.code()); TU.ExtraArgs.push_back("-fno-delayed-template-parsing"); TU.ExtraArgs.push_back("-xobjective-c++"); auto AST = TU.build(); for (const auto &RenamePos : Code.points()) { auto RenameResult = rename({RenamePos, NewName, AST, testPath(TU.Filename)}); ASSERT_TRUE(bool(RenameResult)) << RenameResult.takeError(); ASSERT_EQ(1u, RenameResult->GlobalChanges.size()); EXPECT_EQ( applyEdits(std::move(RenameResult->GlobalChanges)).front().second, expectedResult(Code, NewName)); } } } TEST(RenameTest, Renameable) { struct Case { const char *Code; const char* ErrorMessage; // null if no error bool IsHeaderFile; const SymbolIndex *Index; llvm::StringRef NewName = "DummyName"; }; TestTU OtherFile = TestTU::withCode("Outside s; auto ss = &foo;"); const char *CommonHeader = R"cpp( class Outside {}; void foo(); )cpp"; OtherFile.HeaderCode = CommonHeader; OtherFile.Filename = "other.cc"; // The index has a "Outside" reference and a "foo" reference. auto OtherFileIndex = OtherFile.index(); const SymbolIndex *Index = OtherFileIndex.get(); const bool HeaderFile = true; Case Cases[] = { {R"cpp(// allow -- function-local void f(int [[Lo^cal]]) { [[Local]] = 2; } )cpp", nullptr, HeaderFile, Index}, {R"cpp(// allow -- symbol is indexable and has no refs in index. void [[On^lyInThisFile]](); )cpp", nullptr, HeaderFile, Index}, {R"cpp( void ^f(); )cpp", "keyword", HeaderFile, Index, "return"}, {R"cpp(// disallow -- symbol is indexable and has other refs in index. void f() { Out^side s; } )cpp", "used outside main file", HeaderFile, Index}, {R"cpp(// disallow -- symbol in anonymous namespace in header is not indexable. namespace { class Unin^dexable {}; } )cpp", "not eligible for indexing", HeaderFile, Index}, {R"cpp(// allow -- symbol in anonymous namespace in non-header file is indexable. namespace { class [[F^oo]] {}; } )cpp", nullptr, !HeaderFile, Index}, {R"cpp(// disallow -- namespace symbol isn't supported namespace n^s {} )cpp", "not a supported kind", HeaderFile, Index}, { R"cpp( #define MACRO 1 int s = MAC^RO; )cpp", "not a supported kind", HeaderFile, Index}, { R"cpp( struct X { X operator++(int); }; void f(X x) {x+^+;})cpp", "no symbol", HeaderFile, Index}, {R"cpp(// foo is declared outside the file. void fo^o() {} )cpp", "used outside main file", !HeaderFile /*cc file*/, Index}, {R"cpp( // We should detect the symbol is used outside the file from the AST. void fo^o() {})cpp", "used outside main file", !HeaderFile, nullptr /*no index*/}, {R"cpp(// disallow rename on excluded symbols (e.g. std symbols) namespace std { class str^ing {}; } )cpp", "not a supported kind", !HeaderFile, Index}, {R"cpp(// disallow rename on excluded symbols (e.g. std symbols) namespace std { inline namespace __u { class str^ing {}; } } )cpp", "not a supported kind", !HeaderFile, Index}, {R"cpp(// disallow rename on non-normal identifiers. @interface Foo {} -(int) fo^o:(int)x; // Token is an identifier, but declaration name isn't a simple identifier. @end )cpp", "not a supported kind", HeaderFile, Index}, {R"cpp( void foo(int); void foo(char); template void f(T t) { fo^o(t); })cpp", "multiple symbols", !HeaderFile, nullptr /*no index*/}, {R"cpp(// disallow rename on unrelated token. cl^ass Foo {}; )cpp", "no symbol", !HeaderFile, nullptr}, {R"cpp(// disallow rename on unrelated token. temp^late class Foo {}; )cpp", "no symbol", !HeaderFile, nullptr}, {R"cpp( namespace { int Conflict; int Va^r; } )cpp", "conflict", !HeaderFile, nullptr, "Conflict"}, {R"cpp( int Conflict; int Va^r; )cpp", "conflict", !HeaderFile, nullptr, "Conflict"}, {R"cpp( class Foo { int Conflict; int Va^r; }; )cpp", "conflict", !HeaderFile, nullptr, "Conflict"}, {R"cpp( enum E { Conflict, Fo^o, }; )cpp", "conflict", !HeaderFile, nullptr, "Conflict"}, {R"cpp( int Conflict; enum E { // transparent context. F^oo, }; )cpp", "conflict", !HeaderFile, nullptr, "Conflict"}, {R"cpp(// FIXME: detecting local variables is not supported yet. void func() { int Conflict; int [[V^ar]]; } )cpp", nullptr, !HeaderFile, nullptr, "Conflict"}, {R"cpp(// Trying to rename into the same name, SameName == SameName. void func() { int S^ameName; } )cpp", "new name is the same", !HeaderFile, nullptr, "SameName"}, {R"cpp(// Ensure it doesn't associate base specifier with base name. struct A {}; struct B : priv^ate A {}; )cpp", "Cannot rename symbol: there is no symbol at the given location", false, nullptr}, }; for (const auto& Case : Cases) { SCOPED_TRACE(Case.Code); Annotations T(Case.Code); TestTU TU = TestTU::withCode(T.code()); TU.HeaderCode = CommonHeader; TU.ExtraArgs.push_back("-fno-delayed-template-parsing"); if (Case.IsHeaderFile) { // We open the .h file as the main file. TU.Filename = "test.h"; // Parsing the .h file as C++ include. TU.ExtraArgs.push_back("-xobjective-c++-header"); } auto AST = TU.build(); llvm::StringRef NewName = Case.NewName; auto Results = rename({T.point(), NewName, AST, testPath(TU.Filename), Case.Index}); bool WantRename = true; if (T.ranges().empty()) WantRename = false; if (!WantRename) { assert(Case.ErrorMessage && "Error message must be set!"); EXPECT_FALSE(Results) << "expected rename returned an error: " << T.code(); auto ActualMessage = llvm::toString(Results.takeError()); EXPECT_THAT(ActualMessage, testing::HasSubstr(Case.ErrorMessage)); } else { EXPECT_TRUE(bool(Results)) << "rename returned an error: " << llvm::toString(Results.takeError()); ASSERT_EQ(1u, Results->GlobalChanges.size()); EXPECT_EQ(applyEdits(std::move(Results->GlobalChanges)).front().second, expectedResult(T, NewName)); } } } MATCHER_P(newText, T, "") { return arg.newText == T; } TEST(RenameTest, IndexMergeMainFile) { Annotations Code("int ^x();"); TestTU TU = TestTU::withCode(Code.code()); TU.Filename = "main.cc"; auto AST = TU.build(); auto Main = testPath("main.cc"); auto Rename = [&](const SymbolIndex *Idx) { auto GetDirtyBuffer = [&](PathRef Path) -> llvm::Optional { return Code.code().str(); // Every file has the same content. }; RenameOptions Opts; Opts.AllowCrossFile = true; RenameInputs Inputs{Code.point(), "xPrime", AST, Main, Idx, Opts, GetDirtyBuffer}; auto Results = rename(Inputs); EXPECT_TRUE(bool(Results)) << llvm::toString(Results.takeError()); return std::move(*Results); }; // We do not expect to see duplicated edits from AST vs index. auto Results = Rename(TU.index().get()); EXPECT_THAT(Results.GlobalChanges.keys(), ElementsAre(Main)); EXPECT_THAT(Results.GlobalChanges[Main].asTextEdits(), ElementsAre(newText("xPrime"))); // Sanity check: we do expect to see index results! TU.Filename = "other.cc"; Results = Rename(TU.index().get()); EXPECT_THAT(Results.GlobalChanges.keys(), UnorderedElementsAre(Main, testPath("other.cc"))); #ifdef CLANGD_PATH_CASE_INSENSITIVE // On case-insensitive systems, no duplicates if AST vs index case differs. // https://github.com/clangd/clangd/issues/665 TU.Filename = "MAIN.CC"; Results = Rename(TU.index().get()); EXPECT_THAT(Results.GlobalChanges.keys(), ElementsAre(Main)); EXPECT_THAT(Results.GlobalChanges[Main].asTextEdits(), ElementsAre(newText("xPrime"))); #endif } TEST(RenameTest, MainFileReferencesOnly) { // filter out references not from main file. llvm::StringRef Test = R"cpp( void test() { int [[fo^o]] = 1; // rename references not from main file are not included. #include "foo.inc" })cpp"; Annotations Code(Test); auto TU = TestTU::withCode(Code.code()); TU.AdditionalFiles["foo.inc"] = R"cpp( #define Macro(X) X &Macro(foo); &foo; )cpp"; auto AST = TU.build(); llvm::StringRef NewName = "abcde"; auto RenameResult = rename({Code.point(), NewName, AST, testPath(TU.Filename)}); ASSERT_TRUE(bool(RenameResult)) << RenameResult.takeError() << Code.point(); ASSERT_EQ(1u, RenameResult->GlobalChanges.size()); EXPECT_EQ(applyEdits(std::move(RenameResult->GlobalChanges)).front().second, expectedResult(Code, NewName)); } TEST(RenameTest, ProtobufSymbolIsExcluded) { Annotations Code("Prot^obuf buf;"); auto TU = TestTU::withCode(Code.code()); TU.HeaderCode = R"cpp(// Generated by the protocol buffer compiler. DO NOT EDIT! class Protobuf {}; )cpp"; TU.HeaderFilename = "protobuf.pb.h"; auto AST = TU.build(); auto Results = rename({Code.point(), "newName", AST, testPath(TU.Filename)}); EXPECT_FALSE(Results); EXPECT_THAT(llvm::toString(Results.takeError()), testing::HasSubstr("not a supported kind")); } TEST(RenameTest, PrepareRename) { Annotations FooH("void func();"); Annotations FooCC(R"cpp( #include "foo.h" void [[fu^nc]]() {} )cpp"); std::string FooHPath = testPath("foo.h"); std::string FooCCPath = testPath("foo.cc"); MockFS FS; FS.Files[FooHPath] = std::string(FooH.code()); FS.Files[FooCCPath] = std::string(FooCC.code()); auto ServerOpts = ClangdServer::optsForTest(); ServerOpts.BuildDynamicSymbolIndex = true; trace::TestTracer Tracer; MockCompilationDatabase CDB; ClangdServer Server(CDB, FS, ServerOpts); runAddDocument(Server, FooHPath, FooH.code()); runAddDocument(Server, FooCCPath, FooCC.code()); auto Results = runPrepareRename(Server, FooCCPath, FooCC.point(), /*NewName=*/llvm::None, {/*CrossFile=*/true}); // Verify that for multi-file rename, we only return main-file occurrences. ASSERT_TRUE(bool(Results)) << Results.takeError(); // We don't know the result is complete in prepareRename (passing a nullptr // index internally), so GlobalChanges should be empty. EXPECT_TRUE(Results->GlobalChanges.empty()); EXPECT_THAT(FooCC.ranges(), testing::UnorderedElementsAreArray(Results->LocalChanges)); // Name validation. Results = runPrepareRename(Server, FooCCPath, FooCC.point(), /*NewName=*/std::string("int"), {/*CrossFile=*/true}); EXPECT_FALSE(Results); EXPECT_THAT(llvm::toString(Results.takeError()), testing::HasSubstr("keyword")); EXPECT_THAT(Tracer.takeMetric("rename_name_invalid", "Keywords"), ElementsAre(1)); // Single-file rename on global symbols, we should report an error. Results = runPrepareRename(Server, FooCCPath, FooCC.point(), /*NewName=*/llvm::None, {/*CrossFile=*/false}); EXPECT_FALSE(Results); EXPECT_THAT(llvm::toString(Results.takeError()), testing::HasSubstr("is used outside")); } TEST(CrossFileRenameTests, DirtyBuffer) { Annotations FooCode("class [[Foo]] {};"); std::string FooPath = testPath("foo.cc"); Annotations FooDirtyBuffer("class [[Foo]] {};\n// this is dirty buffer"); Annotations BarCode("void [[Bar]]() {}"); std::string BarPath = testPath("bar.cc"); // Build the index, the index has "Foo" references from foo.cc and "Bar" // references from bar.cc. FileSymbols FSymbols; FSymbols.update(FooPath, nullptr, buildRefSlab(FooCode, "Foo", FooPath), nullptr, false); FSymbols.update(BarPath, nullptr, buildRefSlab(BarCode, "Bar", BarPath), nullptr, false); auto Index = FSymbols.buildIndex(IndexType::Light); Annotations MainCode("class [[Fo^o]] {};"); auto MainFilePath = testPath("main.cc"); // Dirty buffer for foo.cc. auto GetDirtyBuffer = [&](PathRef Path) -> llvm::Optional { if (Path == FooPath) return FooDirtyBuffer.code().str(); return llvm::None; }; // Run rename on Foo, there is a dirty buffer for foo.cc, rename should // respect the dirty buffer. TestTU TU = TestTU::withCode(MainCode.code()); auto AST = TU.build(); llvm::StringRef NewName = "newName"; auto Results = rename({MainCode.point(), NewName, AST, MainFilePath, Index.get(), {/*CrossFile=*/true}, GetDirtyBuffer}); ASSERT_TRUE(bool(Results)) << Results.takeError(); EXPECT_THAT( applyEdits(std::move(Results->GlobalChanges)), UnorderedElementsAre( Pair(Eq(FooPath), Eq(expectedResult(FooDirtyBuffer, NewName))), Pair(Eq(MainFilePath), Eq(expectedResult(MainCode, NewName))))); // Run rename on Bar, there is no dirty buffer for the affected file bar.cc, // so we should read file content from VFS. MainCode = Annotations("void [[Bar]]() { [[B^ar]](); }"); TU = TestTU::withCode(MainCode.code()); // Set a file "bar.cc" on disk. TU.AdditionalFiles["bar.cc"] = std::string(BarCode.code()); AST = TU.build(); Results = rename({MainCode.point(), NewName, AST, MainFilePath, Index.get(), {/*CrossFile=*/true}, GetDirtyBuffer}); ASSERT_TRUE(bool(Results)) << Results.takeError(); EXPECT_THAT( applyEdits(std::move(Results->GlobalChanges)), UnorderedElementsAre( Pair(Eq(BarPath), Eq(expectedResult(BarCode, NewName))), Pair(Eq(MainFilePath), Eq(expectedResult(MainCode, NewName))))); // Run rename on a pagination index which couldn't return all refs in one // request, we reject rename on this case. class PaginationIndex : public SymbolIndex { bool refs(const RefsRequest &Req, llvm::function_ref Callback) const override { return true; // has more references } bool fuzzyFind( const FuzzyFindRequest &Req, llvm::function_ref Callback) const override { return false; } void lookup(const LookupRequest &Req, llvm::function_ref Callback) const override {} void relations(const RelationsRequest &Req, llvm::function_ref Callback) const override {} llvm::unique_function indexedFiles() const override { return [](llvm::StringRef) { return false; }; } size_t estimateMemoryUsage() const override { return 0; } } PIndex; Results = rename({MainCode.point(), NewName, AST, MainFilePath, &PIndex, {/*CrossFile=*/true}, GetDirtyBuffer}); EXPECT_FALSE(Results); EXPECT_THAT(llvm::toString(Results.takeError()), testing::HasSubstr("too many occurrences")); } TEST(CrossFileRenameTests, DeduplicateRefsFromIndex) { auto MainCode = Annotations("int [[^x]] = 2;"); auto MainFilePath = testPath("main.cc"); auto BarCode = Annotations("int [[x]];"); auto BarPath = testPath("bar.cc"); auto TU = TestTU::withCode(MainCode.code()); // Set a file "bar.cc" on disk. TU.AdditionalFiles["bar.cc"] = std::string(BarCode.code()); auto AST = TU.build(); std::string BarPathURI = URI::create(BarPath).toString(); Ref XRefInBarCC = refWithRange(BarCode.range(), BarPathURI); // The index will return duplicated refs, our code should be robost to handle // it. class DuplicatedXRefIndex : public SymbolIndex { public: DuplicatedXRefIndex(const Ref &ReturnedRef) : ReturnedRef(ReturnedRef) {} bool refs(const RefsRequest &Req, llvm::function_ref Callback) const override { // Return two duplicated refs. Callback(ReturnedRef); Callback(ReturnedRef); return false; } bool fuzzyFind(const FuzzyFindRequest &, llvm::function_ref) const override { return false; } void lookup(const LookupRequest &, llvm::function_ref) const override {} void relations(const RelationsRequest &, llvm::function_ref) const override {} llvm::unique_function indexedFiles() const override { return [](llvm::StringRef) { return false; }; } size_t estimateMemoryUsage() const override { return 0; } Ref ReturnedRef; } DIndex(XRefInBarCC); llvm::StringRef NewName = "newName"; auto Results = rename({MainCode.point(), NewName, AST, MainFilePath, &DIndex, {/*CrossFile=*/true}}); ASSERT_TRUE(bool(Results)) << Results.takeError(); EXPECT_THAT( applyEdits(std::move(Results->GlobalChanges)), UnorderedElementsAre( Pair(Eq(BarPath), Eq(expectedResult(BarCode, NewName))), Pair(Eq(MainFilePath), Eq(expectedResult(MainCode, NewName))))); } TEST(CrossFileRenameTests, WithUpToDateIndex) { MockCompilationDatabase CDB; CDB.ExtraClangFlags = {"-xc++"}; // rename is runnning on all "^" points in FooH, and "[[]]" ranges are the // expected rename occurrences. struct Case { llvm::StringRef FooH; llvm::StringRef FooCC; } Cases[] = { { // classes. R"cpp( class [[Fo^o]] { [[Foo]](); ~[[Foo]](); }; )cpp", R"cpp( #include "foo.h" [[Foo]]::[[Foo]]() {} [[Foo]]::~[[Foo]]() {} void func() { [[Foo]] foo; } )cpp", }, { // class templates. R"cpp( template class [[Foo]] {}; // FIXME: explicit template specializations are not supported due the // clangd index limitations. template <> class Foo {}; )cpp", R"cpp( #include "foo.h" void func() { [[F^oo]] foo; } )cpp", }, { // class methods. R"cpp( class Foo { void [[f^oo]](); }; )cpp", R"cpp( #include "foo.h" void Foo::[[foo]]() {} void func(Foo* p) { p->[[foo]](); } )cpp", }, { // rename on constructor and destructor. R"cpp( class [[Foo]] { [[^Foo]](); ~[[Foo^]](); }; )cpp", R"cpp( #include "foo.h" [[Foo]]::[[Foo]]() {} [[Foo]]::~[[Foo]]() {} void func() { [[Foo]] foo; } )cpp", }, { // functions. R"cpp( void [[f^oo]](); )cpp", R"cpp( #include "foo.h" void [[foo]]() {} void func() { [[foo]](); } )cpp", }, { // typedefs. R"cpp( typedef int [[IN^T]]; [[INT]] foo(); )cpp", R"cpp( #include "foo.h" [[INT]] foo() {} )cpp", }, { // usings. R"cpp( using [[I^NT]] = int; [[INT]] foo(); )cpp", R"cpp( #include "foo.h" [[INT]] foo() {} )cpp", }, { // variables. R"cpp( static const int [[VA^R]] = 123; )cpp", R"cpp( #include "foo.h" int s = [[VAR]]; )cpp", }, { // scope enums. R"cpp( enum class [[K^ind]] { ABC }; )cpp", R"cpp( #include "foo.h" [[Kind]] ff() { return [[Kind]]::ABC; } )cpp", }, { // enum constants. R"cpp( enum class Kind { [[A^BC]] }; )cpp", R"cpp( #include "foo.h" Kind ff() { return Kind::[[ABC]]; } )cpp", }, { // Implicit references in macro expansions. R"cpp( class [[Fo^o]] {}; #define FooFoo Foo #define FOO Foo )cpp", R"cpp( #include "foo.h" void bar() { [[Foo]] x; FOO y; FooFoo z; } )cpp", }, }; trace::TestTracer Tracer; for (const auto &T : Cases) { SCOPED_TRACE(T.FooH); Annotations FooH(T.FooH); Annotations FooCC(T.FooCC); std::string FooHPath = testPath("foo.h"); std::string FooCCPath = testPath("foo.cc"); MockFS FS; FS.Files[FooHPath] = std::string(FooH.code()); FS.Files[FooCCPath] = std::string(FooCC.code()); auto ServerOpts = ClangdServer::optsForTest(); ServerOpts.BuildDynamicSymbolIndex = true; ClangdServer Server(CDB, FS, ServerOpts); // Add all files to clangd server to make sure the dynamic index has been // built. runAddDocument(Server, FooHPath, FooH.code()); runAddDocument(Server, FooCCPath, FooCC.code()); llvm::StringRef NewName = "NewName"; for (const auto &RenamePos : FooH.points()) { EXPECT_THAT(Tracer.takeMetric("rename_files"), SizeIs(0)); auto FileEditsList = llvm::cantFail(runRename( Server, FooHPath, RenamePos, NewName, {/*CrossFile=*/true})); EXPECT_THAT(Tracer.takeMetric("rename_files"), ElementsAre(2)); EXPECT_THAT( applyEdits(std::move(FileEditsList.GlobalChanges)), UnorderedElementsAre( Pair(Eq(FooHPath), Eq(expectedResult(T.FooH, NewName))), Pair(Eq(FooCCPath), Eq(expectedResult(T.FooCC, NewName))))); } } } TEST(CrossFileRenameTests, CrossFileOnLocalSymbol) { // cross-file rename should work for function-local symbols, even there is no // index provided. Annotations Code("void f(int [[abc]]) { [[a^bc]] = 3; }"); auto TU = TestTU::withCode(Code.code()); auto Path = testPath(TU.Filename); auto AST = TU.build(); llvm::StringRef NewName = "newName"; auto Results = rename({Code.point(), NewName, AST, Path}); ASSERT_TRUE(bool(Results)) << Results.takeError(); EXPECT_THAT( applyEdits(std::move(Results->GlobalChanges)), UnorderedElementsAre(Pair(Eq(Path), Eq(expectedResult(Code, NewName))))); } TEST(CrossFileRenameTests, BuildRenameEdits) { Annotations Code("[[😂]]"); auto LSPRange = Code.range(); llvm::StringRef FilePath = "/test/TestTU.cpp"; llvm::StringRef NewName = "abc"; auto Edit = buildRenameEdit(FilePath, Code.code(), {LSPRange}, NewName); ASSERT_TRUE(bool(Edit)) << Edit.takeError(); ASSERT_EQ(1UL, Edit->Replacements.size()); EXPECT_EQ(FilePath, Edit->Replacements.begin()->getFilePath()); EXPECT_EQ(4UL, Edit->Replacements.begin()->getLength()); // Test invalid range. LSPRange.end = {10, 0}; // out of range Edit = buildRenameEdit(FilePath, Code.code(), {LSPRange}, NewName); EXPECT_FALSE(Edit); EXPECT_THAT(llvm::toString(Edit.takeError()), testing::HasSubstr("fail to convert")); // Normal ascii characters. Annotations T(R"cpp( [[range]] [[range]] [[range]] )cpp"); Edit = buildRenameEdit(FilePath, T.code(), T.ranges(), NewName); ASSERT_TRUE(bool(Edit)) << Edit.takeError(); EXPECT_EQ(applyEdits(FileEdits{{T.code(), std::move(*Edit)}}).front().second, expectedResult(T, NewName)); } TEST(CrossFileRenameTests, adjustRenameRanges) { // Ranges in IndexedCode indicate the indexed occurrences; // ranges in DraftCode indicate the expected mapped result, empty indicates // we expect no matched result found. struct { llvm::StringRef IndexedCode; llvm::StringRef DraftCode; } Tests[] = { { // both line and column are changed, not a near miss. R"cpp( int [[x]] = 0; )cpp", R"cpp( // insert a line. double x = 0; )cpp", }, { // subset. R"cpp( int [[x]] = 0; )cpp", R"cpp( int [[x]] = 0; {int x = 0; } )cpp", }, { // shift columns. R"cpp(int [[x]] = 0; void foo(int x);)cpp", R"cpp(double [[x]] = 0; void foo(double x);)cpp", }, { // shift lines. R"cpp( int [[x]] = 0; void foo(int x); )cpp", R"cpp( // insert a line. int [[x]] = 0; void foo(int x); )cpp", }, }; LangOptions LangOpts; LangOpts.CPlusPlus = true; for (const auto &T : Tests) { SCOPED_TRACE(T.DraftCode); Annotations Draft(T.DraftCode); auto ActualRanges = adjustRenameRanges( Draft.code(), "x", Annotations(T.IndexedCode).ranges(), LangOpts); if (!ActualRanges) EXPECT_THAT(Draft.ranges(), testing::IsEmpty()); else EXPECT_THAT(Draft.ranges(), testing::UnorderedElementsAreArray(*ActualRanges)); } } TEST(RangePatchingHeuristic, GetMappedRanges) { // ^ in LexedCode marks the ranges we expect to be mapped; no ^ indicates // there are no mapped ranges. struct { llvm::StringRef IndexedCode; llvm::StringRef LexedCode; } Tests[] = { { // no lexed ranges. "[[]]", "", }, { // both line and column are changed, not a near miss. R"([[]])", R"( [[]] )", }, { // subset. "[[]]", "^[[]] [[]]" }, { // shift columns. "[[]] [[]]", " ^[[]] ^[[]] [[]]" }, { R"( [[]] [[]] [[]] )", R"( // insert a line ^[[]] ^[[]] ^[[]] )", }, { R"( [[]] [[]] [[]] )", R"( // insert a line ^[[]] ^[[]] ^[[]] // column is shifted. )", }, { R"( [[]] [[]] [[]] )", R"( // insert a line [[]] [[]] [[]] // not mapped (both line and column are changed). )", }, { R"( [[]] [[]] [[]] [[]] } )", R"( // insert a new line ^[[]] ^[[]] [[]] // additional range ^[[]] ^[[]] [[]] // additional range )", }, { // non-distinct result (two best results), not a near miss R"( [[]] [[]] [[]] )", R"( [[]] [[]] [[]] [[]] )", } }; for (const auto &T : Tests) { SCOPED_TRACE(T.IndexedCode); auto Lexed = Annotations(T.LexedCode); auto LexedRanges = Lexed.ranges(); std::vector ExpectedMatches; for (auto P : Lexed.points()) { auto Match = llvm::find_if(LexedRanges, [&P](const Range& R) { return R.start == P; }); ASSERT_NE(Match, LexedRanges.end()); ExpectedMatches.push_back(*Match); } auto Mapped = getMappedRanges(Annotations(T.IndexedCode).ranges(), LexedRanges); if (!Mapped) EXPECT_THAT(ExpectedMatches, IsEmpty()); else EXPECT_THAT(ExpectedMatches, UnorderedElementsAreArray(*Mapped)); } } TEST(CrossFileRenameTests, adjustmentCost) { struct { llvm::StringRef RangeCode; size_t ExpectedCost; } Tests[] = { { R"( $idx[[]]$lex[[]] // diff: 0 )", 0, }, { R"( $idx[[]] $lex[[]] // line diff: +1 $idx[[]] $lex[[]] // line diff: +1 $idx[[]] $lex[[]] // line diff: +1 $idx[[]] $lex[[]] // line diff: +2 )", 1 + 1 }, { R"( $idx[[]] $lex[[]] // line diff: +1 $idx[[]] $lex[[]] // line diff: +2 $idx[[]] $lex[[]] // line diff: +3 )", 1 + 1 + 1 }, { R"( $idx[[]] $lex[[]] // line diff: +3 $idx[[]] $lex[[]] // line diff: +2 $idx[[]] $lex[[]] // line diff: +1 )", 3 + 1 + 1 }, { R"( $idx[[]] $lex[[]] // line diff: +1 $lex[[]] // line diff: -2 $idx[[]] $idx[[]] $lex[[]] // line diff: +3 )", 1 + 3 + 5 }, { R"( $idx[[]] $lex[[]] // column diff: +1 $idx[[]]$lex[[]] // diff: 0 )", 1 }, { R"( $idx[[]] $lex[[]] // diff: +1 $idx[[]] $lex[[]] // column diff: +1 $idx[[]]$lex[[]] // diff: 0 )", 1 + 1 + 1 }, { R"( $idx[[]] $lex[[]] // column diff: +1 )", 1 }, { R"( // column diffs: +1, +2, +3 $idx[[]] $lex[[]] $idx[[]] $lex[[]] $idx[[]] $lex[[]] )", 1 + 1 + 1, }, }; for (const auto &T : Tests) { SCOPED_TRACE(T.RangeCode); Annotations C(T.RangeCode); std::vector MappedIndex; for (size_t I = 0; I < C.ranges("lex").size(); ++I) MappedIndex.push_back(I); EXPECT_EQ(renameRangeAdjustmentCost(C.ranges("idx"), C.ranges("lex"), MappedIndex), T.ExpectedCost); } } } // namespace } // namespace clangd } // namespace clang