//===----------------------------------------------------------------------===// // // This source file is part of the Swift.org open source project // // Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors // Licensed under Apache License v2.0 with Runtime Library Exception // // See https://swift.org/LICENSE.txt for license information // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors // //===----------------------------------------------------------------------===// #include "NullEditorConsumer.h" #include "SourceKit/Core/Context.h" #include "SourceKit/Core/LangSupport.h" #include "SourceKit/Core/NotificationCenter.h" #include "SourceKit/Support/Concurrency.h" #include "SourceKit/SwiftLang/Factory.h" #include "swift/Basic/LLVMInitialize.h" #include "llvm/Support/MemoryBuffer.h" #include "llvm/Support/Path.h" #include "llvm/Support/TargetSelect.h" #include "gtest/gtest.h" using namespace SourceKit; using namespace llvm; static StringRef getRuntimeLibPath() { return sys::path::parent_path(SWIFTLIB_DIR); } static SmallString<128> getSwiftExecutablePath() { SmallString<128> path = sys::path::parent_path(getRuntimeLibPath()); sys::path::append(path, "bin", "swift-frontend"); return path; } static void *createCancellationToken() { static std::atomic handle(1000); return reinterpret_cast( handle.fetch_add(1, std::memory_order_relaxed)); } namespace { struct TestCursorInfo { // Empty if no error. std::string Error; std::string InternalDiagnostic; std::string Name; std::string Typename; std::string Filename; unsigned Offset; unsigned Length; }; class CursorInfoTest : public ::testing::Test { SourceKit::Context &Ctx; std::atomic NumTasks; NullEditorConsumer Consumer; public: SourceKit::Context &getContext() { return Ctx; } LangSupport &getLang() { return getContext().getSwiftLangSupport(); } void SetUp() override { NumTasks = 0; } CursorInfoTest() : Ctx(*new SourceKit::Context(getSwiftExecutablePath(), getRuntimeLibPath(), /*diagnosticDocumentationPath*/ "", SourceKit::createSwiftLangSupport, [](SourceKit::Context &Ctx){ return nullptr; }, /*dispatchOnMain=*/false)) { INITIALIZE_LLVM(); // This is avoiding destroying \p SourceKit::Context because another // thread may be active trying to use it to post notifications. // FIXME: Use shared_ptr ownership to avoid such issues. } void addNotificationReceiver(DocumentUpdateNotificationReceiver Receiver) { Ctx.getNotificationCenter()->addDocumentUpdateNotificationReceiver(Receiver); } void open(const char *DocName, StringRef Text, std::optional> CArgs = std::nullopt) { auto Args = CArgs.has_value() ? makeArgs(DocName, *CArgs) : std::vector{}; auto Buf = MemoryBuffer::getMemBufferCopy(Text, DocName); getLang().editorOpen(DocName, Buf.get(), Consumer, Args, std::nullopt); } void replaceText(StringRef DocName, unsigned Offset, unsigned Length, StringRef Text) { auto Buf = MemoryBuffer::getMemBufferCopy(Text, DocName); getLang().editorReplaceText(DocName, Buf.get(), Offset, Length, Consumer); } TestCursorInfo getCursor(const char *DocName, unsigned Offset, ArrayRef CArgs, SourceKitCancellationToken CancellationToken = nullptr, bool CancelOnSubsequentRequest = false) { auto Args = makeArgs(DocName, CArgs); Semaphore sema(0); TestCursorInfo TestInfo; getLang().getCursorInfo( DocName, DocName, Offset, /*Length=*/0, /*Actionables=*/false, /*SymbolGraph=*/false, CancelOnSubsequentRequest, Args, /*vfsOptions=*/std::nullopt, CancellationToken, [&](const RequestResult &Result) { assert(!Result.isCancelled()); if (Result.isError()) { TestInfo.Error = Result.getError().str(); sema.signal(); return; } const CursorInfoData &Info = Result.value(); TestInfo.InternalDiagnostic = Info.InternalDiagnostic.str(); if (!Info.Symbols.empty()) { const CursorSymbolInfo &MainSymbol = Info.Symbols[0]; TestInfo.Name = std::string(MainSymbol.Name.str()); TestInfo.Typename = MainSymbol.TypeName.str(); TestInfo.Filename = MainSymbol.Location.Filename.str(); TestInfo.Offset = MainSymbol.Location.Offset; TestInfo.Length = MainSymbol.Location.Length; } sema.signal(); }); bool expired = sema.wait(60 * 1000); if (expired) llvm::report_fatal_error("check took too long"); return TestInfo; } unsigned findOffset(StringRef Val, StringRef Text) { auto pos = Text.find(Val); assert(pos != StringRef::npos); return pos; } void setNeedsSema(bool needsSema) { Consumer.needsSema = needsSema; } private: std::vector makeArgs(const char *DocName, ArrayRef CArgs) { std::vector Args = CArgs; Args.push_back(DocName); return Args; } }; } // anonymous namespace TEST_F(CursorInfoTest, FileNotExist) { const char *DocName = "test.swift"; const char *Contents = "let foo = 0\n"; const char *Args[] = { "" }; open(DocName, Contents); auto FooOffs = findOffset("foo =", Contents); auto Info = getCursor(DocName, FooOffs, Args); EXPECT_STREQ("foo", Info.Name.c_str()); EXPECT_STREQ("Int", Info.Typename.c_str()); } static const char *ExpensiveInit = "[0:0,0:0,0:0,0:0,0:0,0:0,0:0]"; TEST_F(CursorInfoTest, EditAfter) { const char *DocName = "test.swift"; const char *Contents = "let value = foo\n" "let foo = 0\n"; const char *Args[] = { "-parse-as-library" }; open(DocName, Contents); auto FooRefOffs = findOffset("foo", Contents); auto FooOffs = findOffset("foo =", Contents); auto Info = getCursor(DocName, FooRefOffs, Args); EXPECT_STREQ("foo", Info.Name.c_str()); EXPECT_STREQ("Int", Info.Typename.c_str()); EXPECT_STREQ(DocName, Info.Filename.c_str()); EXPECT_EQ(FooOffs, Info.Offset); EXPECT_EQ(strlen("foo"), Info.Length); StringRef TextToReplace = "0"; replaceText(DocName, findOffset(TextToReplace, Contents), TextToReplace.size(), ExpensiveInit); // Insert a space in front of 'foo' decl. replaceText(DocName, FooOffs, 0, " "); ++FooOffs; // Should not wait for the new AST, it should give the previous answer. Info = getCursor(DocName, FooRefOffs, Args); EXPECT_STREQ("foo", Info.Name.c_str()); EXPECT_STREQ("Int", Info.Typename.c_str()); EXPECT_STREQ(DocName, Info.Filename.c_str()); EXPECT_EQ(FooOffs, Info.Offset); EXPECT_EQ(strlen("foo"), Info.Length); } TEST_F(CursorInfoTest, EditBefore) { const char *DocName = "test.swift"; const char *Contents = "let foo = 0\n" "let value = foo;\n"; const char *Args[] = { "-parse-as-library" }; open(DocName, Contents); auto FooRefOffs = findOffset("foo;", Contents); auto FooOffs = findOffset("foo =", Contents); auto Info = getCursor(DocName, FooRefOffs, Args); EXPECT_STREQ("", Info.Error.c_str()); EXPECT_STREQ("", Info.InternalDiagnostic.c_str()); EXPECT_STREQ("foo", Info.Name.c_str()); EXPECT_STREQ("Int", Info.Typename.c_str()); EXPECT_STREQ(DocName, Info.Filename.c_str()); EXPECT_EQ(FooOffs, Info.Offset); EXPECT_EQ(strlen("foo"), Info.Length); StringRef TextToReplace = "0"; replaceText(DocName, findOffset(TextToReplace, Contents), TextToReplace.size(), ExpensiveInit); FooRefOffs += StringRef(ExpensiveInit).size() - TextToReplace.size(); // Insert a space in front of 'foo' decl. replaceText(DocName, FooOffs, 0, " "); ++FooOffs; ++FooRefOffs; // Should not wait for the new AST, it should give the previous answer. Info = getCursor(DocName, FooRefOffs, Args); EXPECT_STREQ("", Info.Error.c_str()); EXPECT_STREQ("", Info.InternalDiagnostic.c_str()); EXPECT_STREQ("foo", Info.Name.c_str()); EXPECT_STREQ("Int", Info.Typename.c_str()); EXPECT_STREQ(DocName, Info.Filename.c_str()); EXPECT_EQ(FooOffs, Info.Offset); EXPECT_EQ(strlen("foo"), Info.Length); } TEST_F(CursorInfoTest, CursorInfoMustWaitDueDeclLoc) { const char *DocName = "test.swift"; const char *Contents = "let value = foo\n" "let foo = 0\n"; const char *Args[] = { "-parse-as-library" }; open(DocName, Contents); auto FooRefOffs = findOffset("foo", Contents); auto FooOffs = findOffset("foo =", Contents); auto Info = getCursor(DocName, FooRefOffs, Args); EXPECT_STREQ("", Info.Error.c_str()); EXPECT_STREQ("", Info.InternalDiagnostic.c_str()); EXPECT_STREQ("foo", Info.Name.c_str()); EXPECT_STREQ("Int", Info.Typename.c_str()); StringRef TextToReplace = "0"; replaceText(DocName, findOffset(TextToReplace, Contents), TextToReplace.size(), ExpensiveInit); // Edit over the 'foo' decl. replaceText(DocName, FooOffs, strlen("foo"), "foo"); // Should wait for the new AST, because the declaration location for the 'foo' // reference has been edited out. Info = getCursor(DocName, FooRefOffs, Args); EXPECT_STREQ("", Info.Error.c_str()); EXPECT_STREQ("", Info.InternalDiagnostic.c_str()); EXPECT_STREQ("foo", Info.Name.c_str()); EXPECT_STREQ("[Int : Int]", Info.Typename.c_str()); EXPECT_EQ(FooOffs, Info.Offset); EXPECT_EQ(strlen("foo"), Info.Length); } TEST_F(CursorInfoTest, CursorInfoMustWaitDueOffset) { const char *DocName = "test.swift"; const char *Contents = "let value = foo\n" "let foo = 0\n"; const char *Args[] = { "-parse-as-library" }; open(DocName, Contents); auto FooRefOffs = findOffset("foo", Contents); auto FooOffs = findOffset("foo =", Contents); auto Info = getCursor(DocName, FooRefOffs, Args); EXPECT_STREQ("foo", Info.Name.c_str()); EXPECT_STREQ("Int", Info.Typename.c_str()); StringRef TextToReplace = "0"; replaceText(DocName, findOffset(TextToReplace, Contents), TextToReplace.size(), ExpensiveInit); // Edit over the 'foo' reference. replaceText(DocName, FooRefOffs, strlen("foo"), "foo"); // Should wait for the new AST, because the cursor location has been edited // out. Info = getCursor(DocName, FooRefOffs, Args); EXPECT_STREQ("foo", Info.Name.c_str()); EXPECT_STREQ("[Int : Int]", Info.Typename.c_str()); EXPECT_EQ(FooOffs, Info.Offset); EXPECT_EQ(strlen("foo"), Info.Length); } TEST_F(CursorInfoTest, CursorInfoMustWaitDueToken) { const char *DocName = "test.swift"; const char *Contents = "let value = foo\n" "let foo = 0\n"; const char *Args[] = { "-parse-as-library" }; open(DocName, Contents); auto FooRefOffs = findOffset("foo", Contents); auto FooOffs = findOffset("foo =", Contents); auto Info = getCursor(DocName, FooRefOffs, Args); EXPECT_STREQ("foo", Info.Name.c_str()); EXPECT_STREQ("Int", Info.Typename.c_str()); StringRef TextToReplace = "0"; replaceText(DocName, findOffset(TextToReplace, Contents), TextToReplace.size(), ExpensiveInit); // Change 'foo' to 'fog' by replacing the last character. replaceText(DocName, FooOffs+2, 1, "g"); replaceText(DocName, FooRefOffs+2, 1, "g"); // Should wait for the new AST, because the cursor location points to a // different token. Info = getCursor(DocName, FooRefOffs, Args); EXPECT_STREQ("fog", Info.Name.c_str()); EXPECT_STREQ("[Int : Int]", Info.Typename.c_str()); EXPECT_EQ(FooOffs, Info.Offset); EXPECT_EQ(strlen("fog"), Info.Length); } TEST_F(CursorInfoTest, CursorInfoMustWaitDueTokenRace) { const char *DocName = "test.swift"; const char *Contents = "let value = foo\n" "let foo = 0\n"; const char *Args[] = {"-parse-as-library"}; auto FooRefOffs = findOffset("foo", Contents); auto FooOffs = findOffset("foo =", Contents); // Open with args, kicking off an ast build. The hope of this tests is for // this AST to still be in the process of building when we start the cursor // info, to ensure the ASTManager doesn't try to handle this cursor info with // the wrong AST. setNeedsSema(true); open(DocName, Contents, llvm::ArrayRef(Args)); // Change 'foo' to 'fog' by replacing the last character. replaceText(DocName, FooOffs + 2, 1, "g"); replaceText(DocName, FooRefOffs + 2, 1, "g"); // Should wait for the new AST, because the cursor location points to a // different token. auto Info = getCursor(DocName, FooRefOffs, Args); EXPECT_STREQ("fog", Info.Name.c_str()); EXPECT_STREQ("Int", Info.Typename.c_str()); EXPECT_EQ(FooOffs, Info.Offset); EXPECT_EQ(strlen("fog"), Info.Length); } TEST_F(CursorInfoTest, CursorInfoCancelsPreviousRequest) { // TODO: This test case relies on the following snippet being slow to type // check so that the first cursor info request takes longer to execute than it // takes time to schedule the second request. If that is fixed, we need to // find a new way to cause slow type checking. rdar://80582770 const char *SlowDocName = "slow.swift"; const char *SlowContents = "func foo(x: Invalid1, y: Invalid2) {\n" " x / y / x / y / x / y / x / y\n" "}\n"; auto SlowOffset = findOffset("x", SlowContents); const char *Args[] = {"-parse-as-library"}; std::vector ArgsForSlow = llvm::ArrayRef(Args).vec(); ArgsForSlow.push_back(SlowDocName); const char *FastDocName = "fast.swift"; const char *FastContents = "func bar() {\n" " let foo = 123\n" "}\n"; auto FastOffset = findOffset("foo", FastContents); std::vector ArgsForFast = llvm::ArrayRef(Args).vec(); ArgsForFast.push_back(FastDocName); open(SlowDocName, SlowContents, llvm::ArrayRef(Args)); open(FastDocName, FastContents, llvm::ArrayRef(Args)); // Schedule a cursor info request that takes long to execute. This should be // cancelled as the next cursor info (which is faster) gets requested. Semaphore FirstCursorInfoSema(0); getLang().getCursorInfo( SlowDocName, SlowDocName, SlowOffset, /*Length=*/0, /*Actionables=*/false, /*SymbolGraph=*/false, /*CancelOnSubsequentRequest=*/true, ArgsForSlow, /*vfsOptions=*/std::nullopt, /*CancellationToken=*/nullptr, [&](const RequestResult &Result) { EXPECT_TRUE(Result.isCancelled()); FirstCursorInfoSema.signal(); }); auto Info = getCursor(FastDocName, FastOffset, Args, /*CancellationToken=*/nullptr, /*CancelOnSubsequentRequest=*/true); EXPECT_STREQ("foo", Info.Name.c_str()); EXPECT_STREQ("Int", Info.Typename.c_str()); EXPECT_EQ(FastOffset, Info.Offset); EXPECT_EQ(strlen("foo"), Info.Length); bool expired = FirstCursorInfoSema.wait(30 * 1000); if (expired) llvm::report_fatal_error("Did not receive a response for the first request"); } TEST_F(CursorInfoTest, DISABLED_CursorInfoCancellation) { // Disabled due to a race condition (rdar://88652757) // TODO: This test case relies on the following snippet being slow to type // check so that the first cursor info request takes longer to execute than it // takes time to schedule the second request. If that is fixed, we need to // find a new way to cause slow type checking. rdar://80582770 const char *SlowDocName = "slow.swift"; const char *SlowContents = "func foo(x: Invalid1, y: Invalid2) {\n" " x / y / x / y / x / y / x / y\n" "}\n"; auto SlowOffset = findOffset("x", SlowContents); const char *Args[] = {"-parse-as-library"}; std::vector ArgsForSlow = llvm::ArrayRef(Args).vec(); ArgsForSlow.push_back(SlowDocName); open(SlowDocName, SlowContents, llvm::ArrayRef(Args)); SourceKitCancellationToken CancellationToken = createCancellationToken(); // Schedule a cursor info request that takes long to execute. This should be // cancelled as the next cursor info (which is faster) gets requested. Semaphore CursorInfoSema(0); getLang().getCursorInfo( SlowDocName, SlowDocName, SlowOffset, /*Length=*/0, /*Actionables=*/false, /*SymbolGraph=*/false, /*CancelOnSubsequentRequest=*/false, ArgsForSlow, /*vfsOptions=*/std::nullopt, /*CancellationToken=*/CancellationToken, [&](const RequestResult &Result) { EXPECT_TRUE(Result.isCancelled()); CursorInfoSema.signal(); }); getContext().getRequestTracker()->cancel(CancellationToken); bool expired = CursorInfoSema.wait(30 * 1000); if (expired) llvm::report_fatal_error("Did not receive a response for the first request"); }