Index: clang-tools-extra/trunk/CMakeLists.txt =================================================================== --- clang-tools-extra/trunk/CMakeLists.txt +++ clang-tools-extra/trunk/CMakeLists.txt @@ -1,15 +1,3 @@ -if (NOT DEFINED CLANGD_BUILD_XPC) - if(${CMAKE_SYSTEM_NAME} MATCHES "Darwin") - set(CLANGD_BUILD_XPC_DEFAULT ON) - else () - set(CLANGD_BUILD_XPC_DEFAULT OFF) - endif () - - set(CLANGD_BUILD_XPC ${CLANGD_BUILD_XPC_DEFAULT} CACHE BOOL "Build XPC Support For Clangd." FORCE) - - unset(CLANGD_BUILD_XPC_DEFAULT) -endif () - add_subdirectory(clang-apply-replacements) add_subdirectory(clang-reorder-fields) add_subdirectory(modularize) Index: clang-tools-extra/trunk/clangd/CMakeLists.txt =================================================================== --- clang-tools-extra/trunk/clangd/CMakeLists.txt +++ clang-tools-extra/trunk/clangd/CMakeLists.txt @@ -1,6 +1,17 @@ # Configure the Features.inc file. -llvm_canonicalize_cmake_booleans( - CLANGD_BUILD_XPC) +if (NOT DEFINED CLANGD_BUILD_XPC) + if(${CMAKE_SYSTEM_NAME} MATCHES "Darwin") + set(CLANGD_BUILD_XPC_DEFAULT ON) + else () + set(CLANGD_BUILD_XPC_DEFAULT OFF) + endif () + + set(CLANGD_BUILD_XPC ${CLANGD_BUILD_XPC_DEFAULT} CACHE BOOL "Build XPC Support For Clangd." FORCE) + unset(CLANGD_BUILD_XPC_DEFAULT) +endif () + +llvm_canonicalize_cmake_booleans(CLANGD_BUILD_XPC) + configure_file( ${CMAKE_CURRENT_SOURCE_DIR}/Features.inc.in ${CMAKE_CURRENT_BINARY_DIR}/Features.inc @@ -130,3 +141,8 @@ if ( CLANGD_BUILD_XPC ) add_subdirectory(xpc) endif () + +if(CLANG_INCLUDE_TESTS) +add_subdirectory(test) +add_subdirectory(unittests) +endif() Index: clang-tools-extra/trunk/clangd/test/CMakeLists.txt =================================================================== --- clang-tools-extra/trunk/clangd/test/CMakeLists.txt +++ clang-tools-extra/trunk/clangd/test/CMakeLists.txt @@ -0,0 +1,28 @@ +set(CLANGD_TEST_DEPS + clangd + ClangdTests + # No tests for these, but we should still make sure they build. + clangd-indexer + dexp + ) + +if(CLANGD_BUILD_XPC) + list(APPEND CLANGD_TEST_DEPS clangd-xpc-test-client) +endif() + +foreach(dep FileCheck count not) + if(TARGET ${dep}) + list(APPEND CLANGD_TEST_DEPS ${dep}) + endif() +endforeach() + +configure_lit_site_cfg( + ${CMAKE_CURRENT_SOURCE_DIR}/lit.cfg.in + ${CMAKE_CURRENT_BINARY_DIR}/lit.cfg) +configure_lit_site_cfg( + ${CMAKE_CURRENT_SOURCE_DIR}/../unittests/lit.cfg.in + ${CMAKE_CURRENT_BINARY_DIR}/../unittests/lit.cfg) + +add_lit_testsuite(check-clangd "Running the Clangd regression tests" + ${CMAKE_CURRENT_BINARY_DIR}/Unit;${CMAKE_CURRENT_BINARY_DIR} + DEPENDS ${CLANGD_TEST_DEPS}) Index: clang-tools-extra/trunk/clangd/test/Inputs/BenchmarkHeader.h =================================================================== --- clang-tools-extra/trunk/clangd/test/Inputs/BenchmarkHeader.h +++ clang-tools-extra/trunk/clangd/test/Inputs/BenchmarkHeader.h @@ -0,0 +1,19 @@ +namespace clang { +namespace clangd { +namespace dex { +class Dex; +} // namespace dex +} // namespace clangd +} // namespace clang + +namespace llvm { +namespace sys { + +int getHostNumPhysicalCores(); + +} // namespace sys +} // namespace llvm + +namespace { +int Variable; +} // namespace Index: clang-tools-extra/trunk/clangd/test/Inputs/BenchmarkSource.cpp =================================================================== --- clang-tools-extra/trunk/clangd/test/Inputs/BenchmarkSource.cpp +++ clang-tools-extra/trunk/clangd/test/Inputs/BenchmarkSource.cpp @@ -0,0 +1 @@ +#include "BenchmarkHeader.h" Index: clang-tools-extra/trunk/clangd/test/Inputs/background-index/compile_commands.json =================================================================== --- clang-tools-extra/trunk/clangd/test/Inputs/background-index/compile_commands.json +++ clang-tools-extra/trunk/clangd/test/Inputs/background-index/compile_commands.json @@ -0,0 +1,5 @@ +[{ + "directory": "DIRECTORY", + "command": "clang foo.cpp", + "file": "DIRECTORY/foo.cpp" +}] Index: clang-tools-extra/trunk/clangd/test/Inputs/background-index/definition.jsonrpc =================================================================== --- clang-tools-extra/trunk/clangd/test/Inputs/background-index/definition.jsonrpc +++ clang-tools-extra/trunk/clangd/test/Inputs/background-index/definition.jsonrpc @@ -0,0 +1,51 @@ +{ + "jsonrpc": "2.0", + "id": 0, + "method": "initialize", + "params": { + "processId": 123, + "rootPath": "clangd", + "capabilities": {}, + "trace": "off" + } +} +--- +{ + "jsonrpc": "2.0", + "method": "textDocument/didOpen", + "params": { + "textDocument": { + "uri": "file://DIRECTORY/bar.cpp", + "languageId": "cpp", + "version": 1, + "text": "#include \"foo.h\"\nint main(){\nreturn foo();\n}" + } + } +} +--- +{ + "jsonrpc": "2.0", + "id": 1, + "method": "sync", + "params": null +} +--- +{ + "jsonrpc": "2.0", + "id": 2, + "method": "textDocument/definition", + "params": { + "textDocument": { + "uri": "file://DIRECTORY/bar.cpp" + }, + "position": { + "line": 2, + "character": 8 + } + } +} +# CHECK: "uri": "file://{{.*}}/foo.cpp" +--- +{"jsonrpc":"2.0","id":3,"method":"shutdown"} +--- +{"jsonrpc":"2.0","method":"exit"} Index: clang-tools-extra/trunk/clangd/test/Inputs/background-index/foo.h =================================================================== --- clang-tools-extra/trunk/clangd/test/Inputs/background-index/foo.h +++ clang-tools-extra/trunk/clangd/test/Inputs/background-index/foo.h @@ -0,0 +1,4 @@ +#ifndef FOO_H +#define FOO_H +int foo(); +#endif Index: clang-tools-extra/trunk/clangd/test/Inputs/background-index/foo.cpp =================================================================== --- clang-tools-extra/trunk/clangd/test/Inputs/background-index/foo.cpp +++ clang-tools-extra/trunk/clangd/test/Inputs/background-index/foo.cpp @@ -0,0 +1,2 @@ +#include "foo.h" +int foo() { return 42; } Index: clang-tools-extra/trunk/clangd/test/Inputs/requests.json =================================================================== --- clang-tools-extra/trunk/clangd/test/Inputs/requests.json +++ clang-tools-extra/trunk/clangd/test/Inputs/requests.json @@ -0,0 +1,7 @@ +[{"Limit":100,"ProximityPaths":["/usr/home/user/clang-tools-extra/clangd/benchmarks/IndexBenchmark.cpp"],"Query":"OMP","RestrictForCodeCompletion":true,"Scopes":["clang::"], "AnyScope":false}, +{"Limit":100,"ProximityPaths":[],"Query":"s","RestrictForCodeCompletion":true,"Scopes":["llvm::", ""], "AnyScope":false}, +{"Limit":100,"ProximityPaths":[],"Query":"sy","RestrictForCodeCompletion":true,"Scopes":["llvm::", ""], "AnyScope":false}, +{"Limit":100,"ProximityPaths":[],"Query":"sys","RestrictForCodeCompletion":true,"Scopes":["llvm::", ""], "AnyScope":false}, +{"Limit":100,"ProximityPaths":[],"Query":"sys","RestrictForCodeCompletion":true,"Scopes":["llvm::", ""], "AnyScope":false}, +{"Limit":100,"ProximityPaths":[],"Query":"Dex","RestrictForCodeCompletion":true,"Scopes":["clang::clangd::", "clang::", "clang::clangd::dex::"],"AnyScope":false}, +{"Limit":100,"ProximityPaths":[],"Query":"Variable","RestrictForCodeCompletion":true,"Scopes":[""], "AnyScope":false}] Index: clang-tools-extra/trunk/clangd/test/Inputs/symbols.test.yaml =================================================================== --- clang-tools-extra/trunk/clangd/test/Inputs/symbols.test.yaml +++ clang-tools-extra/trunk/clangd/test/Inputs/symbols.test.yaml @@ -0,0 +1,17 @@ +--- +!Symbol +ID: 057557CEBF6E6B2D +Name: 'vector' +Scope: 'std::' +SymInfo: + Kind: Class + Lang: Cpp +CanonicalDeclaration: + FileURI: 'file:///vector.h' + Start: + Line: 215 + Column: 10 + End: + Line: 215 + Column: 16 +... Index: clang-tools-extra/trunk/clangd/test/background-index.test =================================================================== --- clang-tools-extra/trunk/clangd/test/background-index.test +++ clang-tools-extra/trunk/clangd/test/background-index.test @@ -0,0 +1,20 @@ +# We need to splice paths into file:// URIs for this test. +# UNSUPPORTED: win32 + +# Use a copy of inputs, as we'll mutate it (as will the background index). +# RUN: rm -rf %t +# RUN: cp -r %S/Inputs/background-index %t +# Need to embed the correct temp path in the actual JSON-RPC requests. +# RUN: sed -i -e "s|DIRECTORY|%t|" %t/* + +# We're editing bar.cpp, which includes foo.h. +# foo() is declared in foo.h and defined in foo.cpp. +# The background index should allow us to go-to-definition on foo(). +# RUN: clangd -background-index -background-index-rebuild-period=0 -lit-test < %t/definition.jsonrpc | FileCheck %t/definition.jsonrpc + +# Test that the index is writing files in the expected location. +# RUN: ls %t/.clangd/index/foo.cpp.*.idx + +# Test the index is read from disk: delete code and restart clangd. +# RUN: rm %t/foo.cpp +# RUN: clangd -background-index -lit-test < %t/definition.jsonrpc | FileCheck %t/definition.jsonrpc Index: clang-tools-extra/trunk/clangd/test/compile-commands-path-in-initialize.test =================================================================== --- clang-tools-extra/trunk/clangd/test/compile-commands-path-in-initialize.test +++ clang-tools-extra/trunk/clangd/test/compile-commands-path-in-initialize.test @@ -0,0 +1,29 @@ +# Test that we can set choose a configuration/build directly in the initialize +# request. + +# RUN: rm -rf %t.dir/* && mkdir -p %t.dir +# RUN: mkdir %t.dir/build-1 +# RUN: echo '[{"directory": "%/t.dir", "command": "c++ the-file.cpp", "file": "the-file.cpp"}]' > %t.dir/compile_commands.json +# RUN: echo '[{"directory": "%/t.dir/build-1", "command": "c++ -DMACRO=1 the-file.cpp", "file": "../the-file.cpp"}]' > %t.dir/build-1/compile_commands.json + +# RUN: sed -e "s|INPUT_DIR|%/t.dir|g" %s > %t.test.1 + +# On Windows, we need the URI in didOpen to look like "uri":"file:///C:/..." +# (with the extra slash in the front), so we add it here. +# RUN: sed -e "s|file://\([A-Z]\):/|file:///\1:/|g" %t.test.1 > %t.test + +# RUN: clangd -lit-test < %t.test | FileCheck -strict-whitespace %t.test + +{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"initializationOptions":{"compilationDatabasePath":"INPUT_DIR/build-1"}}} +--- +{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"file://INPUT_DIR/the-file.cpp","languageId":"cpp","version":1,"text":"#if !defined(MACRO)\n#pragma message (\"MACRO is not defined\")\n#elif MACRO == 1\n#pragma message (\"MACRO is one\")\n#else\n#pragma message (\"woops\")\n#endif\nint main() {}\n"}}} +# CHECK: "method": "textDocument/publishDiagnostics", +# CHECK-NEXT: "params": { +# CHECK-NEXT: "diagnostics": [ +# CHECK-NEXT: { +# CHECK-NEXT: "code": "-W#pragma-messages", +# CHECK-NEXT: "message": "MACRO is one", +--- +{"jsonrpc":"2.0","id":10000,"method":"shutdown"} +--- +{"jsonrpc":"2.0","method":"exit"} Index: clang-tools-extra/trunk/clangd/test/completion-auto-trigger.test =================================================================== --- clang-tools-extra/trunk/clangd/test/completion-auto-trigger.test +++ clang-tools-extra/trunk/clangd/test/completion-auto-trigger.test @@ -0,0 +1,106 @@ +# RUN: clangd -lit-test < %s | FileCheck -strict-whitespace %s +{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootPath":"clangd","capabilities":{},"trace":"off"}} +--- +{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"test:///main.cpp","languageId":"cpp","version":1,"text":"namespace ns { int ns_member; } struct vector { int size; static int default_capacity; };\nvoid test(vector *a, vector *b) {\n if (a > b) {} \n a->size = 10;\n\n a ? a : b;\n ns::ns_member = 10;\n}"}}} +--- +{"jsonrpc":"2.0","id":1,"method":"textDocument/completion","params":{"textDocument":{"uri":"test:///main.cpp"},"position":{"line":2,"character":9},"context":{"triggerKind":2,"triggerCharacter":">"}}} +# CHECK: "error": { +# CHECK-NEXT: "code": -32001, +# CHECK-NEXT: "message": "ignored auto-triggered completion, preceding char did not match" +# CHECK-NEXT: }, +# CHECK-NEXT: "id": 1, +--- +{"jsonrpc":"2.0","id":2,"method":"textDocument/completion","params":{"textDocument":{"uri":"test:///main.cpp"},"position":{"line":3,"character":5},"context":{"triggerKind":2,"triggerCharacter":">"}}} +# CHECK: "id": 2, +# CHECK-NEXT: "jsonrpc": "2.0" +# CHECK-NEXT: "result": { +# CHECK-NEXT: "isIncomplete": false, +# CHECK-NEXT: "items": [ +# CHECK-NEXT: { +# CHECK-NEXT: "detail": "int", +# CHECK-NEXT: "filterText": "size", +# CHECK-NEXT: "insertText": "size", +# CHECK-NEXT: "insertTextFormat": 1, +# CHECK-NEXT: "kind": 5, +# CHECK-NEXT: "label": " size", +# CHECK-NEXT: "sortText": "3eacccccsize", +# CHECK-NEXT: "textEdit": { +# CHECK-NEXT: "newText": "size", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 5, +# CHECK-NEXT: "line": 3 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 5, +# CHECK-NEXT: "line": 3 +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: { +# CHECK-NEXT: "detail": "int", +# CHECK-NEXT: "filterText": "default_capacity", +# CHECK-NEXT: "insertText": "default_capacity", +# CHECK-NEXT: "insertTextFormat": 1, +# CHECK-NEXT: "kind": 10, +# CHECK-NEXT: "label": " default_capacity", +# CHECK-NEXT: "sortText": "3fd70a3ddefault_capacity", +# CHECK-NEXT: "textEdit": { +# CHECK-NEXT: "newText": "default_capacity", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 5, +# CHECK-NEXT: "line": 3 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 5, +# CHECK-NEXT: "line": 3 +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: ] +# CHECK-NEXT: } +--- +{"jsonrpc":"2.0","id":3,"method":"textDocument/completion","params":{"textDocument":{"uri":"test:///main.cpp"},"position":{"line":5,"character":9},"context":{"triggerKind":2,"triggerCharacter":":"}}} +# CHECK: "error": { +# CHECK-NEXT: "code": -32001, +# CHECK-NEXT: "message": "ignored auto-triggered completion, preceding char did not match" +# CHECK-NEXT: }, +# CHECK-NEXT: "id": 3, +--- +{"jsonrpc":"2.0","id":4,"method":"textDocument/completion","params":{"textDocument":{"uri":"test:///main.cpp"},"position":{"line":6,"character":6},"context":{"triggerKind":2,"triggerCharacter":":"}}} +--- +# CHECK: "id": 4, +# CHECK-NEXT: "jsonrpc": "2.0" +# CHECK-NEXT: "result": { +# CHECK-NEXT: "isIncomplete": false, +# CHECK-NEXT: "items": [ +# CHECK-NEXT: { +# CHECK-NEXT: "detail": "int", +# CHECK-NEXT: "filterText": "ns_member", +# CHECK-NEXT: "insertText": "ns_member", +# CHECK-NEXT: "insertTextFormat": 1, +# CHECK-NEXT: "kind": 6, +# CHECK-NEXT: "label": " ns_member", +# CHECK-NEXT: "sortText": "3f2cccccns_member", +# CHECK-NEXT: "textEdit": { +# CHECK-NEXT: "newText": "ns_member", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 6, +# CHECK-NEXT: "line": 6 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 6, +# CHECK-NEXT: "line": 6 +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: ] +# CHECK-NEXT: } +{"jsonrpc":"2.0","id":5,"method":"shutdown"} +--- +{"jsonrpc":"2.0","method":"exit"} Index: clang-tools-extra/trunk/clangd/test/completion-snippets.test =================================================================== --- clang-tools-extra/trunk/clangd/test/completion-snippets.test +++ clang-tools-extra/trunk/clangd/test/completion-snippets.test @@ -0,0 +1,56 @@ +# RUN: clangd -lit-test < %s | FileCheck -strict-whitespace %s +# RUN: clangd -lit-test -pch-storage=memory < %s | FileCheck -strict-whitespace %s +{ + "jsonrpc": "2.0", + "id": 0, + "method": "initialize", + "params": { + "processId": 123, + "rootPath": "clangd", + "capabilities": { + "textDocument": { + "completion": { + "completionItem": { + "snippetSupport": true + } + } + } + }, + "trace": "off" + } +} +--- +{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"test:///main.cpp","languageId":"cpp","version":1,"text":"int func_with_args(int a, int b);\nint main() {\nfunc_with\n}"}}} +--- +{"jsonrpc":"2.0","id":1,"method":"textDocument/completion","params":{"textDocument":{"uri":"test:///main.cpp"},"position":{"line":2,"character":7}}} +# CHECK: "id": 1 +# CHECK-NEXT: "jsonrpc": "2.0", +# CHECK-NEXT: "result": { +# CHECK-NEXT: "isIncomplete": {{.*}} +# CHECK-NEXT: "items": [ +# CHECK: "filterText": "func_with_args", +# CHECK-NEXT: "insertText": "func_with_args(${1:int a}, ${2:int b})", +# CHECK-NEXT: "insertTextFormat": 2, +# CHECK-NEXT: "kind": 3, +# CHECK-NEXT: "label": " func_with_args(int a, int b)", +# CHECK-NEXT: "sortText": "{{.*}}func_with_args" +# CHECK-NEXT: "textEdit": { +# CHECK-NEXT: "newText": "func_with_args(${1:int a}, ${2:int b})", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 7, +# CHECK-NEXT: "line": 2 +# CHECK-NEXT: } +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 0, +# CHECK-NEXT: "line": 2 +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: ] +# CHECK-NEXT: } +--- +{"jsonrpc":"2.0","id":4,"method":"shutdown"} +--- +{"jsonrpc":"2.0","method":"exit"} Index: clang-tools-extra/trunk/clangd/test/completion.test =================================================================== --- clang-tools-extra/trunk/clangd/test/completion.test +++ clang-tools-extra/trunk/clangd/test/completion.test @@ -0,0 +1,72 @@ +# RUN: clangd -lit-test < %s | FileCheck -strict-whitespace %s +# RUN: clangd -lit-test -pch-storage=memory < %s | FileCheck -strict-whitespace %s +{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootPath":"clangd","capabilities":{},"trace":"off"}} +--- +{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"test:///main.cpp","languageId":"cpp","version":1,"text":"struct S { int a; };\nint main() {\nS().\n}"}}} +--- +{"jsonrpc":"2.0","id":1,"method":"textDocument/completion","params":{"textDocument":{"uri":"test:///main.cpp"},"position":{"line":2,"character":4}}} +# CHECK: "id": 1 +# CHECK-NEXT: "jsonrpc": "2.0", +# CHECK-NEXT: "result": { +# CHECK-NEXT: "isIncomplete": false, +# CHECK-NEXT: "items": [ +# CHECK-NEXT: { +# CHECK-NEXT: "detail": "int", +# CHECK-NEXT: "filterText": "a", +# CHECK-NEXT: "insertText": "a", +# CHECK-NEXT: "insertTextFormat": 1, +# CHECK-NEXT: "kind": 5, +# CHECK-NEXT: "label": " a", +# CHECK-NEXT: "sortText": "{{.*}}a" +# CHECK-NEXT: "textEdit": { +# CHECK-NEXT: "newText": "a", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 4, +# CHECK-NEXT: "line": 2 +# CHECK-NEXT: } +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 4, +# CHECK-NEXT: "line": 2 +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: ] +--- +# Update the source file and check for completions again. +{"jsonrpc":"2.0","method":"textDocument/didChange","params":{"textDocument":{"uri":"test:///main.cpp","version":2},"contentChanges":[{"text":"struct S { int b; };\nint main() {\nS().\n}"}]}} +--- +{"jsonrpc":"2.0","id":3,"method":"textDocument/completion","params":{"textDocument":{"uri":"test:///main.cpp"},"position":{"line":2,"character":4}}} +# CHECK: "id": 3, +# CHECK-NEXT: "jsonrpc": "2.0", +# CHECK-NEXT: "result": { +# CHECK-NEXT: "isIncomplete": false, +# CHECK-NEXT: "items": [ +# CHECK-NEXT: { +# CHECK-NEXT: "detail": "int", +# CHECK-NEXT: "filterText": "b", +# CHECK-NEXT: "insertText": "b", +# CHECK-NEXT: "insertTextFormat": 1, +# CHECK-NEXT: "kind": 5, +# CHECK-NEXT: "label": " b", +# CHECK-NEXT: "sortText": "{{.*}}b" +# CHECK-NEXT: "textEdit": { +# CHECK-NEXT: "newText": "b", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 4, +# CHECK-NEXT: "line": 2 +# CHECK-NEXT: } +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 4, +# CHECK-NEXT: "line": 2 +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: ] +--- +{"jsonrpc":"2.0","id":4,"method":"shutdown"} +--- +{"jsonrpc":"2.0","method":"exit"} Index: clang-tools-extra/trunk/clangd/test/crash-non-added-files.test =================================================================== --- clang-tools-extra/trunk/clangd/test/crash-non-added-files.test +++ clang-tools-extra/trunk/clangd/test/crash-non-added-files.test @@ -0,0 +1,34 @@ +# RUN: clangd -lit-test < %s | FileCheck -strict-whitespace %s +{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootPath":"clangd","capabilities":{},"trace":"off"}} +--- +{"jsonrpc":"2.0","id":2,"method":"textDocument/codeAction","params":{"textDocument":{"uri":"test:///foo.c"},"range":{"start":{"line":104,"character":13},"end":{"line":0,"character":35}},"context":{"diagnostics":[{"range":{"start": {"line": 0, "character": 32}, "end": {"line": 0, "character": 37}},"severity":2,"message":"using the result of an assignment as a condition without parentheses"},{"range":{"start": {"line": 0, "character": 34}, "end": {"line": 0, "character": 35}},"severity":3,"message":"place parentheses around the assignment to silence this warning"},{"range":{"start": {"line": 0, "character": 34}, "end": {"line": 0, "character": 35}},"severity":3,"message":"use '==' to turn this assignment into an equality comparison"}]}}} +# CHECK: "error": { +# CHECK-NEXT: "code": -32602 +# CHECK-NEXT: "message": "onCodeAction called for non-added file" +# CHECK-NEXT: }, +# CHECK-NEXT: "id": 2, +--- +{"jsonrpc":"2.0","id":3,"method":"textDocument/rangeFormatting","params":{"textDocument":{"uri":"test:///foo.c"},"range":{"start":{"line":1,"character":4},"end":{"line":1,"character":12}},"options":{"tabSize":4,"insertSpaces":true}}} +# CHECK: "error": { +# CHECK-NEXT: "code": -32602 +# CHECK-NEXT: "message": "onDocumentRangeFormatting called for non-added file" +# CHECK-NEXT: }, +# CHECK-NEXT: "id": 3, +--- +{"jsonrpc":"2.0","id":4,"method":"textDocument/formatting","params":{"textDocument":{"uri":"test:///foo.c"},"options":{"tabSize":4,"insertSpaces":true}}} +# CHECK: "error": { +# CHECK-NEXT: "code": -32602 +# CHECK-NEXT: "message": "onDocumentFormatting called for non-added file" +# CHECK-NEXT: }, +# CHECK-NEXT: "id": 4, +--- +{"jsonrpc":"2.0","id":5,"method":"textDocument/onTypeFormatting","params":{"textDocument":{"uri":"test:///foo.c"},"position":{"line":3,"character":1},"ch":"}","options":{"tabSize":4,"insertSpaces":true}}} +# CHECK: "error": { +# CHECK-NEXT: "code": -32602 +# CHECK-NEXT: "message": "onDocumentOnTypeFormatting called for non-added file" +# CHECK-NEXT: }, +# CHECK-NEXT: "id": 5, +--- +{"jsonrpc":"2.0","id":6,"method":"shutdown"} +--- +{"jsonrpc":"2.0","method":"exit"} Index: clang-tools-extra/trunk/clangd/test/delimited-input-comment-at-the-end.test =================================================================== --- clang-tools-extra/trunk/clangd/test/delimited-input-comment-at-the-end.test +++ clang-tools-extra/trunk/clangd/test/delimited-input-comment-at-the-end.test @@ -0,0 +1,11 @@ +# RUN: clangd -input-style=delimited -run-synchronously -input-mirror-file %t < %s +# RUN: grep '{"jsonrpc":"2.0","id":3,"method":"exit"}' %t +# +# RUN: clangd -lit-test -input-mirror-file %t < %s +# RUN: grep '{"jsonrpc":"2.0","id":3,"method":"exit"}' %t +# +{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootPath":"clangd","capabilities":{},"trace":"off"}} +--- +{"jsonrpc":"2.0","id":3,"method":"shutdown"} +--- +{"jsonrpc":"2.0","method":"exit"} Index: clang-tools-extra/trunk/clangd/test/diagnostic-category.test =================================================================== --- clang-tools-extra/trunk/clangd/test/diagnostic-category.test +++ clang-tools-extra/trunk/clangd/test/diagnostic-category.test @@ -0,0 +1,45 @@ +# RUN: clangd -lit-test < %s | FileCheck -strict-whitespace %s +{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootPath":"clangd","capabilities":{"textDocument":{"publishDiagnostics":{"categorySupport":true}}},"trace":"off"}} +--- +{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"test:///foo.c","languageId":"c","version":1,"text":"struct Point {}; union Point p;"}}} +# CHECK: "method": "textDocument/publishDiagnostics", +# CHECK-NEXT: "params": { +# CHECK-NEXT: "diagnostics": [ +# CHECK-NEXT: { +# CHECK-NEXT: "category": "Semantic Issue", +# CHECK-NEXT: "code": "use_with_wrong_tag", +# CHECK-NEXT: "message": "Use of 'Point' with tag type that does not match previous declaration (fix available)\n\nfoo.c:1:8: note: previous use is here", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 22, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 17, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: "severity": 1, +# CHECK-NEXT: "source": "clang" +# CHECK-NEXT: }, +# CHECK-NEXT: { +# CHECK-NEXT: "message": "Previous use is here\n\nfoo.c:1:18: error: use of 'Point' with tag type that does not match previous declaration", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 12, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 7, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: "severity": 3 +# CHECK-NEXT: } +# CHECK-NEXT: ], +# CHECK-NEXT: "uri": "file://{{.*}}/foo.c" +# CHECK-NEXT: } +--- +{"jsonrpc":"2.0","id":4,"method":"shutdown"} +--- +{"jsonrpc":"2.0","method":"exit"} Index: clang-tools-extra/trunk/clangd/test/diagnostics-notes.test =================================================================== --- clang-tools-extra/trunk/clangd/test/diagnostics-notes.test +++ clang-tools-extra/trunk/clangd/test/diagnostics-notes.test @@ -0,0 +1,48 @@ +# RUN: clangd -lit-test < %s | FileCheck -strict-whitespace %s +{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootPath":"clangd","capabilities":{"textDocument":{"publishDiagnostics":{"relatedInformation":true}}},"trace":"off"}} +--- +{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"test:///foo.cc","languageId":"cpp","version":1,"text":"int x;\nint x;"}}} +# CHECK: "method": "textDocument/publishDiagnostics", +# CHECK-NEXT: "params": { +# CHECK-NEXT: "diagnostics": [ +# CHECK-NEXT: { +# CHECK-NEXT: "code": "redefinition", +# CHECK-NEXT: "message": "Redefinition of 'x'", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 5, +# CHECK-NEXT: "line": 1 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 4, +# CHECK-NEXT: "line": 1 +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: "relatedInformation": [ +# CHECK-NEXT: { +# CHECK-NEXT: "location": { +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 5, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 4, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: "uri": "{{.*}}foo.cc" +# CHECK-NEXT: }, +# CHECK-NEXT: "message": "Previous definition is here" +# CHECK-NEXT: } +# CHECK-NEXT: ], +# CHECK-NEXT: "severity": 1, +# CHECK-NEXT: "source": "clang" +# CHECK-NEXT: } +# CHECK-NEXT: ], +# CHECK-NEXT: "uri": "file://{{.*}}/foo.cc" +# CHECK-NEXT: } +--- +{"jsonrpc":"2.0","id":5,"method":"shutdown"} +--- +{"jsonrpc":"2.0","method":"exit"} Index: clang-tools-extra/trunk/clangd/test/diagnostics.test =================================================================== --- clang-tools-extra/trunk/clangd/test/diagnostics.test +++ clang-tools-extra/trunk/clangd/test/diagnostics.test @@ -0,0 +1,55 @@ +# RUN: clangd -lit-test -clang-tidy-checks=bugprone-sizeof-expression < %s | FileCheck -strict-whitespace %s +{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootPath":"clangd","capabilities":{},"trace":"off"}} +--- +{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"test:///foo.c","languageId":"c","version":1,"text":"void main() {\n(void)sizeof(42);\n}"}}} +# CHECK: "method": "textDocument/publishDiagnostics", +# CHECK-NEXT: "params": { +# CHECK-NEXT: "diagnostics": [ +# CHECK-NEXT: { +# CHECK-NEXT: "code": "-Wmain-return-type", +# CHECK-NEXT: "message": "Return type of 'main' is not 'int' (fix available)", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 4, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 0, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: "severity": 2, +# CHECK-NEXT: "source": "clang" +# CHECK-NEXT: }, +# CHECK-NEXT: { +# CHECK-NEXT: "code": "bugprone-sizeof-expression", +# CHECK-NEXT: "message": "Suspicious usage of 'sizeof(K)'; did you mean 'K'?", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 12, +# CHECK-NEXT: "line": 1 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 6, +# CHECK-NEXT: "line": 1 +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: "severity": 2, +# CHECK-NEXT: "source": "clang-tidy" +# CHECK-NEXT: } +# CHECK-NEXT: ], +# CHECK-NEXT: "uri": "file://{{.*}}/foo.c" +# CHECK-NEXT: } +--- +{"jsonrpc":"2.0","id":2,"method":"sync","params":null} +--- +{"jsonrpc":"2.0","method":"textDocument/didClose","params":{"textDocument":{"uri":"test:///foo.c"}}} +# CHECK: "method": "textDocument/publishDiagnostics", +# CHECK-NEXT: "params": { +# CHECK-NEXT: "diagnostics": [], +# CHECK-NEXT: "uri": "file://{{.*}}/foo.c" +# CHECK-NEXT: } +--- +{"jsonrpc":"2.0","id":5,"method":"shutdown"} +--- +{"jsonrpc":"2.0","method":"exit"} Index: clang-tools-extra/trunk/clangd/test/did-change-configuration-params.test =================================================================== --- clang-tools-extra/trunk/clangd/test/did-change-configuration-params.test +++ clang-tools-extra/trunk/clangd/test/did-change-configuration-params.test @@ -0,0 +1,56 @@ +# RUN: clangd -compile_args_from=lsp -lit-test < %s 2> %t | FileCheck -strict-whitespace %s +# RUN: cat %t | FileCheck --check-prefix=ERR %s +# UNSUPPORTED: windows-gnu,windows-msvc +{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootPath":"clangd","capabilities":{},"trace":"off"}} +--- +{"jsonrpc":"2.0","method":"workspace/didChangeConfiguration","params":{"settings":{"compilationDatabaseChanges":{"/clangd-test/foo.c": {"workingDirectory":"/clangd-test", "compilationCommand": ["clang", "-c", "foo.c"]}}}}} +--- +{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"test:///foo.c","languageId":"c","version":1,"text":"int main() { int i; return i; }"}}} +# CHECK: "method": "textDocument/publishDiagnostics", +# CHECK-NEXT: "params": { +# CHECK-NEXT: "diagnostics": [], +# CHECK-NEXT: "uri": "file://{{.*}}/foo.c" +# CHECK-NEXT: } +--- +{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"test:///bar.c","languageId":"c","version":1,"text":"int main() { int i; return i; }"}}} +# CHECK: "method": "textDocument/publishDiagnostics", +# CHECK-NEXT: "params": { +# CHECK-NEXT: "diagnostics": [], +# CHECK-NEXT: "uri": "file://{{.*}}/bar.c" +# CHECK-NEXT: } +--- +{"jsonrpc":"2.0","method":"workspace/didChangeConfiguration","params":{"settings":{"compilationDatabaseChanges":{"/clangd-test/foo.c": {"workingDirectory":"/clangd-test2", "compilationCommand": ["clang", "-c", "foo.c", "-Wall", "-Werror"]}}}}} +# CHECK: "method": "textDocument/publishDiagnostics", +# CHECK-NEXT: "params": { +# CHECK-NEXT: "diagnostics": [ +# CHECK-NEXT: { +# CHECK-NEXT: "code": "-Wuninitialized", +# CHECK-NEXT: "message": "Variable 'i' is uninitialized when used here (fix available)", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 28, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 27, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: "severity": 1, +# CHECK-NEXT: "source": "clang" +# CHECK-NEXT: } +# CHECK-NEXT: ], +# CHECK-NEXT: "uri": "file://{{.*}}/foo.c" +# CHECK-NEXT: } +# +# ERR: Updating file {{.*}}foo.c with command +# ERR: [{{.*}}clangd-test2] +# ERR: clang -c foo.c -Wall -Werror +# Don't reparse the second file: +# ERR: Skipping rebuild of the AST for {{.*}}bar.c +--- +{"jsonrpc":"2.0","id":5,"method":"shutdown"} +--- +{"jsonrpc":"2.0","method":"exit"} + + Index: clang-tools-extra/trunk/clangd/test/execute-command.test =================================================================== --- clang-tools-extra/trunk/clangd/test/execute-command.test +++ clang-tools-extra/trunk/clangd/test/execute-command.test @@ -0,0 +1,68 @@ +# RUN: clangd -lit-test < %s | FileCheck -strict-whitespace %s +{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootPath":"clangd","capabilities":{},"trace":"off"}} +--- +{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"test:///foo.c","languageId":"c","version":1,"text":"int main(int i, char **a) { if (i = 2) {}}"}}} +# CHECK: "method": "textDocument/publishDiagnostics", +# CHECK-NEXT: "params": { +# CHECK-NEXT: "diagnostics": [ +# CHECK-NEXT: { +# CHECK-NEXT: "code": "-Wparentheses", +# CHECK-NEXT: "message": "Using the result of an assignment as a condition without parentheses (fixes available)", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 37, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 32, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: "severity": 2, +# CHECK-NEXT: "source": "clang" +# CHECK-NEXT: } +# CHECK-NEXT: ], +# CHECK-NEXT: "uri": "file://{{.*}}/foo.c" +# CHECK-NEXT: } +--- +# No command name +{"jsonrpc":"2.0","id":3,"method":"workspace/executeCommand","params":{}} +--- +# Invalid, non-scalar command name +{"jsonrpc":"2.0","id":4,"method":"workspace/executeCommand","params":{"command": {}}} +--- +{"jsonrpc":"2.0","id":5,"method":"workspace/executeCommand","params":{"command":"clangd.applyFix","custom":"foo", "arguments":[{"changes":{"test:///foo.c":[{"range":{"start":{"line":0,"character":32},"end":{"line":0,"character":32}},"newText":"("},{"range":{"start":{"line":0,"character":37},"end":{"line":0,"character":37}},"newText":")"}]}}]}} +--- +# Arguments not a sequence. +{"jsonrpc":"2.0","id":6,"method":"workspace/executeCommand","params":{"command":"clangd.applyFix","arguments":"foo"}} +--- +# Unknown command. +{"jsonrpc":"2.0","id":7,"method":"workspace/executeCommand","params":{"command":"mycommand"}} +--- +# ApplyFix argument not a mapping node. +{"jsonrpc":"2.0","id":8,"method":"workspace/executeCommand","params":{"command":"clangd.applyFix","custom":"foo", "arguments":[""]}} +--- +# Custom field in WorkspaceEdit +{"jsonrpc":"2.0","id":9,"method":"workspace/executeCommand","params":{"command":"clangd.applyFix","arguments":[{"custom":"foo", "changes":{"test:///foo.c":[{"range":{"start":{"line":0,"character":32},"end":{"line":0,"character":32}},"newText":"("},{"range":{"start":{"line":0,"character":37},"end":{"line":0,"character":37}},"newText":")"}]}}]}} +--- +# changes in WorkspaceEdit with no mapping node +{"jsonrpc":"2.0","id":10,"method":"workspace/executeCommand","params":{"command":"clangd.applyFix","arguments":[{"changes":"foo"}]}} +--- +# Custom field in WorkspaceEditChange +{"jsonrpc":"2.0","id":11,"method":"workspace/executeCommand","params":{"command":"clangd.applyFix","arguments":[{"changes":{"test:///foo.c":[{"range":{"start":{"line":0,"character":32},"end":{"line":0,"character":32}},"newText":"("},{"range":{"start":{"line":0,"character":37},"end":{"line":0,"character":37}},"newText":")"}], "custom":"foo"}}]}} +--- +# No sequence node for TextEdits +{"jsonrpc":"2.0","id":12,"method":"workspace/executeCommand","params":{"command":"clangd.applyFix","arguments":[{"changes":{"test:///foo.c":"bar"}}]}} +--- +# No mapping node for TextEdit +{"jsonrpc":"2.0","id":13,"method":"workspace/executeCommand","params":{"command":"clangd.applyFix","arguments":[{"changes":{"test:///foo.c":[""]}}]}} +--- +# TextEdit not decoded +{"jsonrpc":"2.0","id":14,"method":"workspace/executeCommand","params":{"command":"clangd.applyFix","arguments":[{"changes":{"test:///foo.c":[{"range":{"start":{"line":0,"character":32},"end":{"line":0,"character":32}},"newText":"("},{"range":"","newText":")"}]}}]}} +--- +# Command name after arguments +{"jsonrpc":"2.0","id":9,"method":"workspace/executeCommand","params":{"arguments":[{"custom":"foo", "changes":{"test:///foo.c":[{"range":{"start":{"line":0,"character":32},"end":{"line":0,"character":32}},"newText":"("},{"range":{"start":{"line":0,"character":37},"end":{"line":0,"character":37}},"newText":")"}]}}],"command":"clangd.applyFix"}} +--- +{"jsonrpc":"2.0","id":3,"method":"shutdown"} +--- +{"jsonrpc":"2.0","method":"exit"} Index: clang-tools-extra/trunk/clangd/test/exit-with-shutdown.test =================================================================== --- clang-tools-extra/trunk/clangd/test/exit-with-shutdown.test +++ clang-tools-extra/trunk/clangd/test/exit-with-shutdown.test @@ -0,0 +1,6 @@ +# RUN: clangd -lit-test < %s +{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootPath":"clangd","capabilities":{},"trace":"off"}} +--- +{"jsonrpc":"2.0","id":3,"method":"shutdown"} +--- +{"jsonrpc":"2.0","method":"exit"} Index: clang-tools-extra/trunk/clangd/test/exit-without-shutdown.test =================================================================== --- clang-tools-extra/trunk/clangd/test/exit-without-shutdown.test +++ clang-tools-extra/trunk/clangd/test/exit-without-shutdown.test @@ -0,0 +1,4 @@ +# RUN: not clangd -lit-test < %s +{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootPath":"clangd","capabilities":{},"trace":"off"}} +--- +{"jsonrpc":"2.0","method":"exit"} Index: clang-tools-extra/trunk/clangd/test/filestatus.test =================================================================== --- clang-tools-extra/trunk/clangd/test/filestatus.test +++ clang-tools-extra/trunk/clangd/test/filestatus.test @@ -0,0 +1,13 @@ +# RUN: clangd -lit-test < %s | FileCheck -strict-whitespace %s +{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootPath":"clangd","capabilities":{},"initializationOptions":{"clangdFileStatus": true},"trace":"off"}} +--- +{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"test:///main.cpp","languageId":"cpp","version":1,"text":"int x; int y = x;"}}} +# CHECK: "method": "textDocument/clangd.fileStatus", +# CHECK-NEXT: "params": { +# CHECK-NEXT: "state": "parsing includes", +# CHECK-NEXT: "uri": "{{.*}}/main.cpp" +# CHECK-NEXT: } +--- +{"jsonrpc":"2.0","id":3,"method":"shutdown"} +--- +{"jsonrpc":"2.0","method":"exit"} Index: clang-tools-extra/trunk/clangd/test/fixits-codeaction.test =================================================================== --- clang-tools-extra/trunk/clangd/test/fixits-codeaction.test +++ clang-tools-extra/trunk/clangd/test/fixits-codeaction.test @@ -0,0 +1,132 @@ +# RUN: clangd -lit-test < %s | FileCheck -strict-whitespace %s +{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootPath":"clangd","capabilities":{"textDocument":{"codeAction":{"codeActionLiteralSupport":{}}}},"trace":"off"}} +--- +{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"test:///foo.c","languageId":"c","version":1,"text":"int main(int i, char **a) { if (i = 2) {}}"}}} +# CHECK: "method": "textDocument/publishDiagnostics", +# CHECK-NEXT: "params": { +# CHECK-NEXT: "diagnostics": [ +# CHECK-NEXT: { +# CHECK-NEXT: "code": "-Wparentheses", +# CHECK-NEXT: "message": "Using the result of an assignment as a condition without parentheses (fixes available)", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 37, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 32, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: "severity": 2, +# CHECK-NEXT: "source": "clang" +# CHECK-NEXT: } +# CHECK-NEXT: ], +# CHECK-NEXT: "uri": "file://{{.*}}/foo.c" +# CHECK-NEXT: } +--- +{"jsonrpc":"2.0","id":2,"method":"textDocument/codeAction","params":{"textDocument":{"uri":"test:///foo.c"},"range":{"start":{"line":0,"character":13},"end":{"line":0,"character":35}},"context":{"diagnostics":[{"range":{"start": {"line": 0, "character": 32}, "end": {"line": 0, "character": 37}},"severity":2,"message":"Using the result of an assignment as a condition without parentheses (fixes available)", "code": "-Wparentheses", "source": "clang"}]}}} +# CHECK: "id": 2, +# CHECK-NEXT: "jsonrpc": "2.0", +# CHECK-NEXT: "result": [ +# CHECK-NEXT: { +# CHECK-NEXT: "diagnostics": [ +# CHECK-NEXT: { +# CHECK-NEXT: "code": "-Wparentheses", +# CHECK-NEXT: "message": "Using the result of an assignment as a condition without parentheses (fixes available)", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 37, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 32, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: "severity": 2, +# CHECK-NEXT: "source": "clang" +# CHECK-NEXT: } +# CHECK-NEXT: ], +# CHECK-NEXT: "edit": { +# CHECK-NEXT: "changes": { +# CHECK-NEXT: "file://{{.*}}/foo.c": [ +# CHECK-NEXT: { +# CHECK-NEXT: "newText": "(", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 32, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 32, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: { +# CHECK-NEXT: "newText": ")", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 37, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 37, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: ] +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: "kind": "quickfix", +# CHECK-NEXT: "title": "place parentheses around the assignment to silence this warning" +# CHECK-NEXT: }, +# CHECK-NEXT: { +# CHECK-NEXT: "diagnostics": [ +# CHECK-NEXT: { +# CHECK-NEXT: "code": "-Wparentheses", +# CHECK-NEXT: "message": "Using the result of an assignment as a condition without parentheses (fixes available)", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 37, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 32, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: "severity": 2, +# CHECK-NEXT: "source": "clang" +# CHECK-NEXT: } +# CHECK-NEXT: ], +# CHECK-NEXT: "edit": { +# CHECK-NEXT: "changes": { +# CHECK-NEXT: "file://{{.*}}/foo.c": [ +# CHECK-NEXT: { +# CHECK-NEXT: "newText": "==", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 35, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 34, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: ] +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: "kind": "quickfix", +# CHECK-NEXT: "title": "use '==' to turn this assignment into an equality comparison" +# CHECK-NEXT: } +# CHECK-NEXT: ] +--- +{"jsonrpc":"2.0","id":4,"method":"shutdown"} +--- +{"jsonrpc":"2.0","method":"exit"} + Index: clang-tools-extra/trunk/clangd/test/fixits-command.test =================================================================== --- clang-tools-extra/trunk/clangd/test/fixits-command.test +++ clang-tools-extra/trunk/clangd/test/fixits-command.test @@ -0,0 +1,212 @@ +# RUN: clangd -lit-test < %s | FileCheck -strict-whitespace %s +{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootPath":"clangd","capabilities":{},"trace":"off"}} +--- +{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"test:///foo.c","languageId":"c","version":1,"text":"int main(int i, char **a) { if (i = 2) {}}"}}} +# CHECK: "method": "textDocument/publishDiagnostics", +# CHECK-NEXT: "params": { +# CHECK-NEXT: "diagnostics": [ +# CHECK-NEXT: { +# CHECK-NEXT: "code": "-Wparentheses", +# CHECK-NEXT: "message": "Using the result of an assignment as a condition without parentheses (fixes available)", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 37, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 32, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: "severity": 2, +# CHECK-NEXT: "source": "clang" +# CHECK-NEXT: } +# CHECK-NEXT: ], +# CHECK-NEXT: "uri": "file://{{.*}}/foo.c" +# CHECK-NEXT: } +--- +{"jsonrpc":"2.0","id":2,"method":"textDocument/codeAction","params":{"textDocument":{"uri":"test:///foo.c"},"range":{"start":{"line":0,"character":13},"end":{"line":0,"character":35}},"context":{"diagnostics":[{"range":{"start": {"line": 0, "character": 32}, "end": {"line": 0, "character": 37}},"severity":2,"message":"Using the result of an assignment as a condition without parentheses (fixes available)"}]}}} +# CHECK: "id": 2, +# CHECK-NEXT: "jsonrpc": "2.0", +# CHECK-NEXT: "result": [ +# CHECK-NEXT: { +# CHECK-NEXT: "arguments": [ +# CHECK-NEXT: { +# CHECK-NEXT: "changes": { +# CHECK-NEXT: "file://{{.*}}/foo.c": [ +# CHECK-NEXT: { +# CHECK-NEXT: "newText": "(", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 32, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 32, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: { +# CHECK-NEXT: "newText": ")", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 37, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 37, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: ] +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: ], +# CHECK-NEXT: "command": "clangd.applyFix", +# CHECK-NEXT: "title": "Apply fix: place parentheses around the assignment to silence this warning" +# CHECK-NEXT: }, +# CHECK-NEXT: { +# CHECK-NEXT: "arguments": [ +# CHECK-NEXT: { +# CHECK-NEXT: "changes": { +# CHECK-NEXT: "file://{{.*}}/foo.c": [ +# CHECK-NEXT: { +# CHECK-NEXT: "newText": "==", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 35, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 34, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: ] +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: ], +# CHECK-NEXT: "command": "clangd.applyFix", +# CHECK-NEXT: "title": "Apply fix: use '==' to turn this assignment into an equality comparison" +# CHECK-NEXT: } +# CHECK-NEXT: ] +--- +{"jsonrpc":"2.0","id":3,"method":"textDocument/codeAction","params":{"textDocument":{"uri":"test:///foo.c"},"range":{"start":{"line":0,"character":13},"end":{"line":0,"character":35}},"context":{"diagnostics":[{"range":{"start": {"line": 0, "character": 32}, "end": {"line": 0, "character": 37}},"severity":2,"message":"Using the result of an assignment as a condition without parentheses (fixes available)"}]}}} +# Make sure unused "code" and "source" fields ignored gracefully +# CHECK: "id": 3, +# CHECK-NEXT: "jsonrpc": "2.0", +# CHECK-NEXT: "result": [ +# CHECK-NEXT: { +# CHECK-NEXT: "arguments": [ +# CHECK-NEXT: { +# CHECK-NEXT: "changes": { +# CHECK-NEXT: "file://{{.*}}/foo.c": [ +# CHECK-NEXT: { +# CHECK-NEXT: "newText": "(", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 32, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 32, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: { +# CHECK-NEXT: "newText": ")", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 37, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 37, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: ] +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: ], +# CHECK-NEXT: "command": "clangd.applyFix", +# CHECK-NEXT: "title": "Apply fix: place parentheses around the assignment to silence this warning" +# CHECK-NEXT: }, +# CHECK-NEXT: { +# CHECK-NEXT: "arguments": [ +# CHECK-NEXT: { +# CHECK-NEXT: "changes": { +# CHECK-NEXT: "file://{{.*}}/foo.c": [ +# CHECK-NEXT: { +# CHECK-NEXT: "newText": "==", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 35, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 34, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: ] +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: ], +# CHECK-NEXT: "command": "clangd.applyFix", +# CHECK-NEXT: "title": "Apply fix: use '==' to turn this assignment into an equality comparison" +# CHECK-NEXT: } +# CHECK-NEXT: ] +--- +{"jsonrpc":"2.0","id":4,"method":"workspace/executeCommand","params":{"command":"clangd.applyFix","arguments":[{"changes":{"test:///foo.c":[{"range":{"start":{"line":0,"character":32},"end":{"line":0,"character":32}},"newText":"("},{"range":{"start":{"line":0,"character":37},"end":{"line":0,"character":37}},"newText":")"}]}}]}} +# CHECK: "id": 4, +# CHECK-NEXT: "jsonrpc": "2.0", +# CHECK-NEXT: "result": "Fix applied." +# +# CHECK: "id": 0, +# CHECK-NEXT: "jsonrpc": "2.0", +# CHECK-NEXT: "method": "workspace/applyEdit", +# CHECK-NEXT: "params": { +# CHECK-NEXT: "edit": { +# CHECK-NEXT: "changes": { +# CHECK-NEXT: "{{.*}}/foo.c": [ +# CHECK-NEXT: { +# CHECK-NEXT: "newText": "(", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 32, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 32, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: { +# CHECK-NEXT: "newText": ")", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 37, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 37, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: ] +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: } +--- +{"jsonrpc":"2.0","id":4,"method":"shutdown"} +--- +{"jsonrpc":"2.0","method":"exit"} Index: clang-tools-extra/trunk/clangd/test/fixits-embed-in-diagnostic.test =================================================================== --- clang-tools-extra/trunk/clangd/test/fixits-embed-in-diagnostic.test +++ clang-tools-extra/trunk/clangd/test/fixits-embed-in-diagnostic.test @@ -0,0 +1,69 @@ +# RUN: clangd -lit-test < %s | FileCheck -strict-whitespace %s +{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootPath":"clangd","capabilities":{"textDocument":{"publishDiagnostics":{"codeActionsInline":true}}},"trace":"off"}} +--- +{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"test:///foo.c","languageId":"c","version":1,"text":"struct Point {}; union Point p;"}}} +# CHECK: "method": "textDocument/publishDiagnostics", +# CHECK-NEXT: "params": { +# CHECK-NEXT: "diagnostics": [ +# CHECK-NEXT: { +# CHECK-NEXT: "code": "use_with_wrong_tag", +# CHECK-NEXT: "codeActions": [ +# CHECK-NEXT: { +# CHECK-NEXT: "edit": { +# CHECK-NEXT: "changes": { +# CHECK-NEXT: "file://{{.*}}/foo.c": [ +# CHECK-NEXT: { +# CHECK-NEXT: "newText": "struct", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 22, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 17, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: ] +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: "kind": "quickfix", +# CHECK-NEXT: "title": "change 'union' to 'struct'" +# CHECK-NEXT: } +# CHECK-NEXT: ], +# CHECK-NEXT: "message": "Use of 'Point' with tag type that does not match previous declaration (fix available)\n\nfoo.c:1:8: note: previous use is here", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 22, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 17, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: "severity": 1, +# CHECK-NEXT: "source": "clang" +# CHECK-NEXT: }, +# CHECK-NEXT: { +# CHECK-NEXT: "message": "Previous use is here\n\nfoo.c:1:18: error: use of 'Point' with tag type that does not match previous declaration", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 12, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 7, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: "severity": 3 +# CHECK-NEXT: } +# CHECK-NEXT: ], +# CHECK-NEXT: "uri": "file://{{.*}}/foo.c" +# CHECK-NEXT: } +--- +{"jsonrpc":"2.0","id":4,"method":"shutdown"} +--- +{"jsonrpc":"2.0","method":"exit"} Index: clang-tools-extra/trunk/clangd/test/formatting.test =================================================================== --- clang-tools-extra/trunk/clangd/test/formatting.test +++ clang-tools-extra/trunk/clangd/test/formatting.test @@ -0,0 +1,187 @@ +# RUN: clangd -lit-test < %s | FileCheck -strict-whitespace %s +{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootPath":"clangd","capabilities":{},"trace":"off"}} +--- +{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"test:///foo.c","languageId":"c","version":1,"text":"int foo ( int x ) {\n x = x+1;\n return x;\n }"}}} +--- +{"jsonrpc":"2.0","id":1,"method":"textDocument/rangeFormatting","params":{"textDocument":{"uri":"test:///foo.c"},"range":{"start":{"line":1,"character":4},"end":{"line":1,"character":12}},"options":{"tabSize":4,"insertSpaces":true}}} +# CHECK: "id": 1, +# CHECK-NEXT: "jsonrpc": "2.0", +# CHECK-NEXT: "result": [ +# CHECK-NEXT: { +# CHECK-NEXT: "newText": "\n ", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 4, +# CHECK-NEXT: "line": 1 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 19, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: { +# CHECK-NEXT: "newText": " ", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 9, +# CHECK-NEXT: "line": 1 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 9, +# CHECK-NEXT: "line": 1 +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: { +# CHECK-NEXT: "newText": " ", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 10, +# CHECK-NEXT: "line": 1 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 10, +# CHECK-NEXT: "line": 1 +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: { +# CHECK-NEXT: "newText": "\n ", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 4, +# CHECK-NEXT: "line": 2 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 12, +# CHECK-NEXT: "line": 1 +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: ] +--- +{"jsonrpc":"2.0","method":"textDocument/didChange","params":{"textDocument":{"uri":"test:///foo.c","version":5},"contentChanges":[{"text":"int foo ( int x ) {\n x = x + 1;\n return x;\n }"}]}} +# +# +--- +{"jsonrpc":"2.0","id":2,"method":"textDocument/rangeFormatting","params":{"textDocument":{"uri":"test:///foo.c"},"range":{"start":{"line":1,"character":2},"end":{"line":1,"character":12}},"options":{"tabSize":4,"insertSpaces":true}}} +# CHECK: "id": 2, +# CHECK-NEXT: "jsonrpc": "2.0", +# CHECK-NEXT: "result": [] +--- +{"jsonrpc":"2.0","id":3,"method":"textDocument/formatting","params":{"textDocument":{"uri":"test:///foo.c"},"options":{"tabSize":4,"insertSpaces":true}}} +# CHECK: "id": 3, +# CHECK-NEXT: "jsonrpc": "2.0", +# CHECK-NEXT: "result": [ +# CHECK-NEXT: { +# CHECK-NEXT: "newText": "", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 8, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 7, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: { +# CHECK-NEXT: "newText": "", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 10, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 9, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: { +# CHECK-NEXT: "newText": "", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 16, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 15, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: { +# CHECK-NEXT: "newText": "\n", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 4, +# CHECK-NEXT: "line": 3 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 11, +# CHECK-NEXT: "line": 2 +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: ] +--- +{"jsonrpc":"2.0","method":"textDocument/didChange","params":{"textDocument":{"uri":"test:///foo.c","version":9},"contentChanges":[{"text":"int foo(int x) {\n x = x + 1;\n return x;\n}"}]}} +--- +{"jsonrpc":"2.0","id":4,"method":"textDocument/formatting","params":{"textDocument":{"uri":"test:///foo.c"},"options":{"tabSize":4,"insertSpaces":true}}} +# CHECK: "id": 4, +# CHECK-NEXT: "jsonrpc": "2.0", +# CHECK-NEXT: "result": [] +--- +{"jsonrpc":"2.0","method":"textDocument/didChange","params":{"textDocument":{"uri":"test:///foo.c","version":5},"contentChanges":[{"text":"int foo ( int x ) {\n x = x + 1;\n return x;\n}"}]}} +--- +{"jsonrpc":"2.0","id":5,"method":"textDocument/onTypeFormatting","params":{"textDocument":{"uri":"test:///foo.c"},"position":{"line":3,"character":1},"ch":"}","options":{"tabSize":4,"insertSpaces":true}}} +# CHECK: "id": 5, +# CHECK-NEXT: "jsonrpc": "2.0", +# CHECK-NEXT: "result": [ +# CHECK-NEXT: { +# CHECK-NEXT: "newText": "", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 8, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 7, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: { +# CHECK-NEXT: "newText": "", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 10, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 9, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: { +# CHECK-NEXT: "newText": "", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 16, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 15, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: ] +--- +{"jsonrpc":"2.0","id":6,"method":"shutdown"} +--- +{"jsonrpc":"2.0","method":"exit"} Index: clang-tools-extra/trunk/clangd/test/hover.test =================================================================== --- clang-tools-extra/trunk/clangd/test/hover.test +++ clang-tools-extra/trunk/clangd/test/hover.test @@ -0,0 +1,24 @@ +# RUN: clangd -lit-test < %s | FileCheck %s +{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootPath":"clangd","capabilities":{},"trace":"off"}} +--- +{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"test:///main.cpp","languageId":"cpp","version":1,"text":"void foo(); int main() { foo(); }\n"}}} +--- +{"jsonrpc":"2.0","id":1,"method":"textDocument/hover","params":{"textDocument":{"uri":"test:///main.cpp"},"position":{"line":0,"character":27}}} +# CHECK: "id": 1, +# CHECK-NEXT: "jsonrpc": "2.0", +# CHECK-NEXT: "result": { +# CHECK-NEXT: "contents": { +# CHECK-NEXT: "kind": "plaintext", +# CHECK-NEXT: "value": "Declared in global namespace\n\nvoid foo()" +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT:} +--- +{"jsonrpc":"2.0","id":1,"method":"textDocument/hover","params":{"textDocument":{"uri":"test:///main.cpp"},"position":{"line":0,"character":10}}} +# CHECK: "id": 1, +# CHECK-NEXT: "jsonrpc": "2.0", +# CHECK-NEXT: "result": null +--- +{"jsonrpc":"2.0","id":3,"method":"shutdown"} +--- +{"jsonrpc":"2.0","method":"exit"} Index: clang-tools-extra/trunk/clangd/test/index-tools.test =================================================================== --- clang-tools-extra/trunk/clangd/test/index-tools.test +++ clang-tools-extra/trunk/clangd/test/index-tools.test @@ -0,0 +1,6 @@ +# RUN: clangd-indexer %p/Inputs/BenchmarkSource.cpp -- -I%p/Inputs > %t.index +# FIXME: By default, benchmarks are excluded from the list of default targets hence not built. Find a way to depend on benchmarks to run the next command. +# REQUIRES: shell +# RUN: if [ -f %clangd-benchmark-dir/IndexBenchmark ]; then %clangd-benchmark-dir/IndexBenchmark %t.index %p/Inputs/requests.json --benchmark_min_time=0.01 ; fi +# Pass invalid JSON file and check that IndexBenchmark fails to parse it. +# RUN: if [ -f %clangd-benchmark-dir/IndexBenchmark ]; then not %clangd-benchmark-dir/IndexBenchmark %t.index %t --benchmark_min_time=0.01 ; fi Index: clang-tools-extra/trunk/clangd/test/initialize-params-invalid.test =================================================================== --- clang-tools-extra/trunk/clangd/test/initialize-params-invalid.test +++ clang-tools-extra/trunk/clangd/test/initialize-params-invalid.test @@ -0,0 +1,12 @@ +# RUN: clangd -lit-test < %s | FileCheck -strict-whitespace %s +# Test with invalid initialize request parameters +{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":"","rootUri":"test:///workspace","capabilities":{},"trace":"off"}} +# CHECK: "id": 0, +# CHECK-NEXT: "jsonrpc": "2.0", +# CHECK-NEXT: "result": { +# CHECK-NEXT: "capabilities": { +# ... +--- +{"jsonrpc":"2.0","id":3,"method":"shutdown"} +--- +{"jsonrpc":"2.0","method":"exit"} Index: clang-tools-extra/trunk/clangd/test/initialize-params.test =================================================================== --- clang-tools-extra/trunk/clangd/test/initialize-params.test +++ clang-tools-extra/trunk/clangd/test/initialize-params.test @@ -0,0 +1,53 @@ +# RUN: clangd -lit-test < %s | FileCheck -strict-whitespace %s +# Test initialize request parameters with rootUri +{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootUri":"test:///workspace","capabilities":{},"trace":"off"}} +# CHECK: "id": 0, +# CHECK-NEXT: "jsonrpc": "2.0", +# CHECK-NEXT: "result": { +# CHECK-NEXT: "capabilities": { +# CHECK-NEXT: "codeActionProvider": true, +# CHECK-NEXT: "completionProvider": { +# CHECK-NEXT: "resolveProvider": false, +# CHECK-NEXT: "triggerCharacters": [ +# CHECK-NEXT: ".", +# CHECK-NEXT: ">", +# CHECK-NEXT: ":" +# CHECK-NEXT: ] +# CHECK-NEXT: }, +# CHECK-NEXT: "declarationProvider": true, +# CHECK-NEXT: "definitionProvider": true, +# CHECK-NEXT: "documentFormattingProvider": true, +# CHECK-NEXT: "documentHighlightProvider": true, +# CHECK-NEXT: "documentOnTypeFormattingProvider": { +# CHECK-NEXT: "firstTriggerCharacter": "}", +# CHECK-NEXT: "moreTriggerCharacter": [] +# CHECK-NEXT: }, +# CHECK-NEXT: "documentRangeFormattingProvider": true, +# CHECK-NEXT: "documentSymbolProvider": true, +# CHECK-NEXT: "executeCommandProvider": { +# CHECK-NEXT: "commands": [ +# CHECK-NEXT: "clangd.applyFix", +# CHECK-NEXT: "clangd.applyTweak" +# CHECK-NEXT: ] +# CHECK-NEXT: }, +# CHECK-NEXT: "hoverProvider": true, +# CHECK-NEXT: "referencesProvider": true, +# CHECK-NEXT: "renameProvider": true, +# CHECK-NEXT: "signatureHelpProvider": { +# CHECK-NEXT: "triggerCharacters": [ +# CHECK-NEXT: "(", +# CHECK-NEXT: "," +# CHECK-NEXT: ] +# CHECK-NEXT: }, +# CHECK-NEXT: "textDocumentSync": 2, +# CHECK-NEXT: "typeHierarchyProvider": true +# CHECK-NEXT: "workspaceSymbolProvider": true +# CHECK-NEXT: } +# CHECK-NEXT: } +--- +{"jsonrpc":"2.0","id":3,"method":"shutdown"} +# CHECK: "id": 3, +# CHECK-NEXT: "jsonrpc": "2.0", +# CHECK-NEXT: "result": null +--- +{"jsonrpc":"2.0","method":"exit"} Index: clang-tools-extra/trunk/clangd/test/initialize-sequence.test =================================================================== --- clang-tools-extra/trunk/clangd/test/initialize-sequence.test +++ clang-tools-extra/trunk/clangd/test/initialize-sequence.test @@ -0,0 +1,21 @@ +# RUN: clangd -lit-test < %s | FileCheck -strict-whitespace %s +{"jsonrpc":"2.0","id":0,"method":"workspace/symbol","params":{"query":"std::basic_ostringstream"}} +# CHECK: "error": { +# CHECK-NEXT: "code": -32002 +# CHECK-NEXT: "message": "server not initialized" +# CHECK-NEXT: }, +# CHECK-NEXT: "id": 0, +--- +{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"processId":123,"rootPath":"clangd","capabilities":{},"trace":"off"}} +--- +{"jsonrpc":"2.0","id":2,"method":"initialize","params":{"processId":123,"rootPath":"clangd","capabilities":{},"trace":"off"}} +# CHECK: "error": { +# CHECK-NEXT: "code": -32600 +# CHECK-NEXT: "message": "server already initialized" +# CHECK-NEXT: }, +# CHECK-NEXT: "id": 2, +--- +{"jsonrpc":"2.0","id":3,"method":"shutdown"} +--- +{"jsonrpc":"2.0","method":"exit"} + Index: clang-tools-extra/trunk/clangd/test/input-mirror.test =================================================================== --- clang-tools-extra/trunk/clangd/test/input-mirror.test +++ clang-tools-extra/trunk/clangd/test/input-mirror.test @@ -0,0 +1,17 @@ +# RUN: clangd -pretty -run-synchronously -input-mirror-file %t < %s +# Note that we have to use '-b' as -input-mirror-file does not have a newline at the end of file. +# RUN: diff -b %t %s +# It is absolutely vital that this file has CRLF line endings. +# +Content-Length: 125 + +{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootPath":"clangd","capabilities":{},"trace":"off"}} +Content-Length: 172 + +{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"test:///main.cpp","languageId":"cpp","version":1,"text":"int main() {\nint a;\na;\n}\n"}}} +Content-Length: 44 + +{"jsonrpc":"2.0","id":3,"method":"shutdown"} +Content-Length: 33 + +{"jsonrpc":"2.0","method":"exit"} Index: clang-tools-extra/trunk/clangd/test/lit.cfg.in =================================================================== --- clang-tools-extra/trunk/clangd/test/lit.cfg.in +++ clang-tools-extra/trunk/clangd/test/lit.cfg.in @@ -0,0 +1,29 @@ +@LIT_SITE_CFG_IN_HEADER@ + +import lit.llvm +import lit.formats + +# Reuse clang configuration (PATH setup, etc). +config.clang_tools_dir = "@CLANG_TOOLS_DIR@" +config.llvm_tools_dir = "@LLVM_TOOLS_DIR@" +config.clang_libs_dir = "@CLANG_LIBS_DIR@" +config.llvm_libs_dir = "@LLVM_LIBS_DIR@" +config.target_triple = "@TARGET_TRIPLE@" +config.host_triple = "@LLVM_HOST_TRIPLE@" +lit.llvm.initialize(lit_config, config) +lit.llvm.llvm_config.use_clang() + +config.name = 'Clangd' +config.suffixes = ['.test'] +config.excludes = ['Inputs'] +config.test_format = lit.formats.ShTest(not lit.llvm.llvm_config.use_lit_shell) +config.test_source_root = "@CMAKE_CURRENT_SOURCE_DIR@" +config.test_exec_root = "@CMAKE_CURRENT_BINARY_DIR@" + +# Clangd-specific lit environment. +config.substitutions.append(('%clangd-benchmark-dir', + "@CMAKE_CURRENT_BINARY_DIR@/../benchmarks")) + +if @CLANGD_BUILD_XPC@: + config.available_features.add('clangd-xpc-support') + Index: clang-tools-extra/trunk/clangd/test/lit.local.cfg =================================================================== --- clang-tools-extra/trunk/clangd/test/lit.local.cfg +++ clang-tools-extra/trunk/clangd/test/lit.local.cfg @@ -0,0 +1,6 @@ +import re +# We rely on the default -std being derived from the filetype. +# PS4 sets a different -std, and many tests break. +# FIXME: make our tests less brittle instead. +if re.match(r'.*-scei-ps4', config.target_triple): + config.unsupported = True Index: clang-tools-extra/trunk/clangd/test/protocol.test =================================================================== --- clang-tools-extra/trunk/clangd/test/protocol.test +++ clang-tools-extra/trunk/clangd/test/protocol.test @@ -0,0 +1,110 @@ +# RUN: not clangd -pretty -run-synchronously -enable-test-uri-scheme < %s | FileCheck -strict-whitespace %s +# RUN: not clangd -pretty -run-synchronously -enable-test-uri-scheme < %s 2>&1 | FileCheck -check-prefix=STDERR %s +# vim: fileformat=dos +# It is absolutely vital that this file has CRLF line endings. +# +# Note that we invert the test because we intent to let clangd exit prematurely. +# +# Test protocol parsing +Content-Length: 125 +Content-Type: application/vscode-jsonrpc; charset-utf-8 + +{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootPath":"clangd","capabilities":{},"trace":"off"}} +# Test message with Content-Type after Content-Length +# +# CHECK: "jsonrpc": "2.0", +# CHECK-NEXT: "result": { +# CHECK: } +Content-Length: 246 + +{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"test:///main.cpp","languageId":"cpp","version":1,"text":"struct fake { int a, bb, ccc; int f(int i, const float f) const; };\nint main() {\n fake f;\n f.\n}\n"}}} + +Content-Length: 104 + +{"jsonrpc":"2.0","method":"textDocument/didChange","params":{"textDocument":{"uri":"test:///main.cpp"}}} + +Content-Type: application/vscode-jsonrpc; charset-utf-8 +Content-Length: 146 + +{"jsonrpc":"2.0","id":1,"method":"textDocument/completion","params":{"textDocument":{"uri":"test:/main.cpp"},"position":{"line":3,"character":5}}} +# Test message with Content-Type before Content-Length +# +# CHECK: "id": 1, +# CHECK-NEXT: "jsonrpc": "2.0", +# CHECK-NEXT: "result": { +# CHECK-NEXT: "isIncomplete": false, +# CHECK-NEXT: "items": [ +# CHECK: "filterText": "a", +# CHECK-NEXT: "insertText": "a", +# CHECK-NEXT: "insertTextFormat": 1, +# CHECK-NEXT: "kind": 5, +# CHECK-NEXT: "label": " a", +# CHECK-NEXT: "sortText": "{{.*}}" +# CHECK: ] +# CHECK-NEXT: } + +X-Test: Testing +Content-Type: application/vscode-jsonrpc; charset-utf-8 +Content-Length: 146 +Content-Type: application/vscode-jsonrpc; charset-utf-8 +X-Testing: Test + +{"jsonrpc":"2.0","id":2,"method":"textDocument/completion","params":{"textDocument":{"uri":"test:/main.cpp"},"position":{"line":3,"character":5}}} + +Content-Type: application/vscode-jsonrpc; charset-utf-8 +Content-Length: 10 +Content-Length: 146 + +{"jsonrpc":"2.0","id":3,"method":"textDocument/completion","params":{"textDocument":{"uri":"test:/main.cpp"},"position":{"line":3,"character":5}}} +# Test message with duplicate Content-Length headers +# +# CHECK: "id": 3, +# CHECK-NEXT: "jsonrpc": "2.0", +# CHECK-NEXT: "result": { +# CHECK-NEXT: "isIncomplete": false, +# CHECK-NEXT: "items": [ +# CHECK: "filterText": "a", +# CHECK-NEXT: "insertText": "a", +# CHECK-NEXT: "insertTextFormat": 1, +# CHECK-NEXT: "kind": 5, +# CHECK-NEXT: "label": " a", +# CHECK-NEXT: "sortText": "{{.*}}" +# CHECK: ] +# CHECK-NEXT: } +# STDERR: Warning: Duplicate Content-Length header received. The previous value for this message (10) was ignored. + +Content-Type: application/vscode-jsonrpc; charset-utf-8 +Content-Length: 10 + +{"jsonrpc":"2.0","id":4,"method":"textDocument/completion","params":{"textDocument":{"uri":"test:/main.cpp"},"position":{"line":3,"character":5}}} +# Test message with malformed Content-Length +# +# STDERR: JSON parse error +# Ensure we recover by sending another (valid) message + +Content-Length: 146 + +{"jsonrpc":"2.0","id":5,"method":"textDocument/completion","params":{"textDocument":{"uri":"test:/main.cpp"},"position":{"line":3,"character":5}}} +# Test message with Content-Type before Content-Length +# +# CHECK: "id": 5, +# CHECK-NEXT: "jsonrpc": "2.0", +# CHECK-NEXT: "result": { +# CHECK-NEXT: "isIncomplete": false, +# CHECK-NEXT: "items": [ +# CHECK: "filterText": "a", +# CHECK-NEXT: "insertText": "a", +# CHECK-NEXT: "insertTextFormat": 1, +# CHECK-NEXT: "kind": 5, +# CHECK-NEXT: "label": " a", +# CHECK-NEXT: "sortText": "{{.*}}" +# CHECK: ] +# CHECK-NEXT: } +Content-Length: 1024 + +{"jsonrpc":"2.0","id":5,"method":"textDocument/completion","params":{"textDocument":{"uri":"test:/main.cpp"},"position":{"line":3,"character":5}}} +# Test message which reads beyond the end of the stream. +# +# Ensure this is the last test in the file! +# STDERR: Input was aborted. Read only {{[0-9]+}} bytes of expected {{[0-9]+}}. + Index: clang-tools-extra/trunk/clangd/test/references.test =================================================================== --- clang-tools-extra/trunk/clangd/test/references.test +++ clang-tools-extra/trunk/clangd/test/references.test @@ -0,0 +1,40 @@ +# RUN: clangd -lit-test < %s | FileCheck -strict-whitespace %s +{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootPath":"clangd","capabilities":{},"trace":"off"}} +--- +{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"test:///main.cpp","languageId":"cpp","version":1,"text":"int x; int y = x;"}}} +--- +{"jsonrpc":"2.0","id":1,"method":"textDocument/references","params":{"textDocument":{"uri":"test:///main.cpp"},"position":{"line":0,"character":4}}} +# CHECK: "id": 1 +# CHECK-NEXT: "jsonrpc": "2.0", +# CHECK-NEXT: "result": [ +# CHECK-NEXT: { +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 5, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 4, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: "uri": "{{.*}}/main.cpp" +# CHECK-NEXT: }, +# CHECK-NEXT: { +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 16, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 15, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: "uri": "{{.*}}/main.cpp" +# CHECK-NEXT: } +# CHECK-NEXT: ] +--- +{"jsonrpc":"2.0","id":3,"method":"shutdown"} +--- +{"jsonrpc":"2.0","method":"exit"} Index: clang-tools-extra/trunk/clangd/test/rename.test =================================================================== --- clang-tools-extra/trunk/clangd/test/rename.test +++ clang-tools-extra/trunk/clangd/test/rename.test @@ -0,0 +1,39 @@ +# RUN: clangd -lit-test < %s | FileCheck -strict-whitespace %s +{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootPath":"clangd","capabilities":{},"trace":"off"}} +--- +{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"test:///foo.cpp","languageId":"cpp","version":1,"text":"int foo;"}}} +--- +{"jsonrpc":"2.0","id":1,"method":"textDocument/rename","params":{"textDocument":{"uri":"test:///foo.cpp"},"position":{"line":0,"character":5},"newName":"bar"}} +# CHECK: "id": 1, +# CHECK-NEXT: "jsonrpc": "2.0", +# CHECK-NEXT: "result": { +# CHECK-NEXT: "changes": { +# CHECK-NEXT: "file://{{.*}}/foo.cpp": [ +# CHECK-NEXT: { +# CHECK-NEXT: "newText": "bar", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 7 +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 4 +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: ] +# CHECK-NEXT: } +# CHECK-NEXT: } +--- +{"jsonrpc":"2.0","id":2,"method":"textDocument/rename","params":{"textDocument":{"uri":"test:///foo.cpp"},"position":{"line":0,"character":2},"newName":"bar"}} +# CHECK: "error": { +# CHECK-NEXT: "code": -32001, +# CHECK-NEXT: "message": "there is no symbol at the given location" +# CHECK-NEXT: }, +# CHECK-NEXT: "id": 2, +# CHECK-NEXT: "jsonrpc": "2.0" +--- +{"jsonrpc":"2.0","id":3,"method":"shutdown"} +--- +{"jsonrpc":"2.0","method":"exit"} Index: clang-tools-extra/trunk/clangd/test/signature-help.test =================================================================== --- clang-tools-extra/trunk/clangd/test/signature-help.test +++ clang-tools-extra/trunk/clangd/test/signature-help.test @@ -0,0 +1,27 @@ +# RUN: clangd -lit-test < %s | FileCheck -strict-whitespace %s +# Start a session. +{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootPath":"clangd","capabilities":{},"trace":"off"}} +--- +{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"test:///main.cpp","languageId":"cpp","version":1,"text":"void x(int);\nint main(){\nx("}}} +--- +{"jsonrpc":"2.0","id":1,"method":"textDocument/signatureHelp","params":{"textDocument":{"uri":"test:///main.cpp"},"position":{"line":2,"character":2}}} +# CHECK: "id": 1, +# CHECK-NEXT: "jsonrpc": "2.0", +# CHECK-NEXT: "result": { +# CHECK-NEXT: "activeParameter": 0, +# CHECK-NEXT: "activeSignature": 0, +# CHECK-NEXT: "signatures": [ +# CHECK-NEXT: { +# CHECK-NEXT: "label": "x(int) -> void", +# CHECK-NEXT: "parameters": [ +# CHECK-NEXT: { +# CHECK-NEXT: "label": "int" +# CHECK-NEXT: } +# CHECK-NEXT: ] +# CHECK-NEXT: } +# CHECK-NEXT: ] +# CHECK-NEXT: } +--- +{"jsonrpc":"2.0","id":100000,"method":"shutdown"} +--- +{"jsonrpc":"2.0","method":"exit"} Index: clang-tools-extra/trunk/clangd/test/spaces-in-delimited-input.test =================================================================== --- clang-tools-extra/trunk/clangd/test/spaces-in-delimited-input.test +++ clang-tools-extra/trunk/clangd/test/spaces-in-delimited-input.test @@ -0,0 +1,13 @@ +# RUN: clangd -input-style=delimited -run-synchronously < %s 2>&1 | FileCheck %s +# RUN: clangd -lit-test -run-synchronously < %s 2>&1 | FileCheck %s +# +{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootPath":"clangd","capabilities":{},"trace":"off"}} + +--- + +{"jsonrpc":"2.0","id":3,"method":"shutdown"} + +--- + +{"jsonrpc":"2.0","method":"exit"} +# CHECK-NOT: JSON parse error Index: clang-tools-extra/trunk/clangd/test/symbol-info.test =================================================================== --- clang-tools-extra/trunk/clangd/test/symbol-info.test +++ clang-tools-extra/trunk/clangd/test/symbol-info.test @@ -0,0 +1,14 @@ +# RUN: clangd -lit-test < %s | FileCheck %s +{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootPath":"clangd","capabilities":{},"trace":"off"}} +--- +{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"test:///simple.cpp","languageId":"cpp","version":1,"text":"void foo(); int main() { foo(); }\n"}}} +--- +{"jsonrpc":"2.0","id":1,"method":"textDocument/symbolInfo","params":{"textDocument":{"uri":"test:///simple.cpp"},"position":{"line":0,"character":27}}} +# CHECK: "containerName": null, +# CHECK-NEXT: "id": "CA2EBE44A1D76D2A", +# CHECK-NEXT: "name": "foo", +# CHECK-NEXT: "usr": "c:@F@foo#" +--- +{"jsonrpc":"2.0","id":3,"method":"shutdown"} +--- +{"jsonrpc":"2.0","method":"exit"} Index: clang-tools-extra/trunk/clangd/test/symbols.test =================================================================== --- clang-tools-extra/trunk/clangd/test/symbols.test +++ clang-tools-extra/trunk/clangd/test/symbols.test @@ -0,0 +1,84 @@ +# RUN: clangd --index-file=%S/Inputs/symbols.test.yaml -lit-test < %s | FileCheck %s +{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootPath":"clangd","capabilities":{"workspace":{"symbol":{"symbolKind":{"valueSet": [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26]}}}},"trace":"off"}} +--- +{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"test:///main.cpp","languageId":"cpp","version":1,"text":"void foo(); int main() { foo(); }\n"}}} +--- +{"jsonrpc":"2.0","id":1,"method":"workspace/symbol","params":{"query":"vector"}} +# CHECK: "id": 1, +# CHECK-NEXT: "jsonrpc": "2.0", +# CHECK-NEXT: "result": [ +# CHECK-NEXT: { +# CHECK-NEXT: "containerName": "std", +# CHECK-NEXT: "kind": 5, +# CHECK-NEXT: "location": { +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": {{.*}}, +# CHECK-NEXT: "line": {{.*}} +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": {{.*}}, +# CHECK-NEXT: "line": {{.*}} +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: "uri": "file:///vector.h" +# CHECK-NEXT: }, +# CHECK-NEXT: "name": "vector" +# CHECK-NEXT: } +# CHECK-NEXT: ] +# CHECK-NEXT:} +--- +{"jsonrpc":"2.0","id":2,"method":"textDocument/documentSymbol","params":{"textDocument":{"uri":"test:///main.cpp"}}} +# CHECK: "id": 2, +# CHECK-NEXT: "jsonrpc": "2.0", +# CHECK-NEXT: "result": [ +# CHECK-NEXT: { +# CHECK-NEXT: "containerName": "", +# CHECK-NEXT: "kind": 12, +# CHECK-NEXT: "location": { +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": {{.*}}, +# CHECK-NEXT: "line": {{.*}} +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": {{.*}}, +# CHECK-NEXT: "line": {{.*}} +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: "uri": "file://{{.*}}/main.cpp" +# CHECK-NEXT: }, +# CHECK-NEXT: "name": "foo" +# CHECK-NEXT: } +# CHECK-NEXT: { +# CHECK-NEXT: "containerName": "", +# CHECK-NEXT: "kind": 12, +# CHECK-NEXT: "location": { +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": {{.*}}, +# CHECK-NEXT: "line": {{.*}} +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": {{.*}}, +# CHECK-NEXT: "line": {{.*}} +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: "uri": "file://{{.*}}/main.cpp" +# CHECK-NEXT: }, +# CHECK-NEXT: "name": "main" +# CHECK-NEXT: } +# CHECK-NEXT: ] +# CHECK-NEXT:} +--- +{"jsonrpc":"2.0","id":3,"method":"textDocument/documentSymbol","params":{"textDocument":{"uri":"test:///foo.cpp"}}} +# CHECK: "error": { +# CHECK-NEXT: "code": -32602, +# CHECK-NEXT: "message": "trying to get AST for non-added document" +# CHECK-NEXT: }, +# CHECK-NEXT: "id": 3, +# CHECK-NEXT: "jsonrpc": "2.0" +--- +{"jsonrpc":"2.0","id":3,"method":"shutdown"} +--- +{"jsonrpc":"2.0","method":"exit"} Index: clang-tools-extra/trunk/clangd/test/test-uri-posix.test =================================================================== --- clang-tools-extra/trunk/clangd/test/test-uri-posix.test +++ clang-tools-extra/trunk/clangd/test/test-uri-posix.test @@ -0,0 +1,11 @@ +# RUN: clangd -lit-test < %s | FileCheck -strict-whitespace %s +# UNSUPPORTED: windows-gnu,windows-msvc +# Test authority-less URI +{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootPath":"clangd","capabilities":{},"trace":"off"}} +--- +{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"test:///foo.c","languageId":"c","version":1,"text":"void main() {}"}}} +# CHECK: "uri": "file:///clangd-test/foo.c" +--- +{"jsonrpc":"2.0","id":5,"method":"shutdown"} +--- +{"jsonrpc":"2.0","method":"exit"} Index: clang-tools-extra/trunk/clangd/test/test-uri-windows.test =================================================================== --- clang-tools-extra/trunk/clangd/test/test-uri-windows.test +++ clang-tools-extra/trunk/clangd/test/test-uri-windows.test @@ -0,0 +1,11 @@ +# RUN: clangd -lit-test < %s | FileCheck -strict-whitespace %s +# REQUIRES: windows-gnu || windows-msvc +# Test authority-less URI +{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootPath":"clangd","capabilities":{},"trace":"off"}} +--- +{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"test:///foo.c","languageId":"c","version":1,"text":"void main() {}"}}} +# CHECK: "uri": "file:///C:/clangd-test/foo.c" +--- +{"jsonrpc":"2.0","id":5,"method":"shutdown"} +--- +{"jsonrpc":"2.0","method":"exit"} Index: clang-tools-extra/trunk/clangd/test/textdocument-didchange-fail.test =================================================================== --- clang-tools-extra/trunk/clangd/test/textdocument-didchange-fail.test +++ clang-tools-extra/trunk/clangd/test/textdocument-didchange-fail.test @@ -0,0 +1,39 @@ +# RUN: clangd -lit-test < %s | FileCheck -strict-whitespace %s +# RUN: clangd -lit-test -pch-storage=memory < %s | FileCheck -strict-whitespace %s +{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootPath":"clangd","capabilities":{},"trace":"off"}} +--- +{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"test:///main.cpp","languageId":"cpp","version":1,"text":"int main() {}\n"}}} +--- +{"jsonrpc":"2.0","id":1,"method":"textDocument/definition","params":{"textDocument":{"uri":"test:///main.cpp"},"position":{"line":0,"character":6}}} +# CHECK: "id": 1, +# CHECK-NEXT: "jsonrpc": "2.0", +# CHECK-NEXT: "result": [ +# CHECK-NEXT: { +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 8, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 4, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: "uri": "file://{{.*}}/clangd-test/main.cpp" +# CHECK-NEXT: } +# CHECK-NEXT: ] +--- +{"jsonrpc":"2.0","method":"textDocument/didChange","params":{"textDocument":{"uri":"test:///main.cpp"},"contentChanges":[{"range":{"start":{"line":100,"character":0},"end":{"line":100,"character":0}},"text": "foo"}]}} +--- +{"jsonrpc":"2.0","id":1,"method":"textDocument/definition","params":{"textDocument":{"uri":"test:///main.cpp"},"position":{"line":0,"character":6}}} +# CHECK: "error": { +# CHECK-NEXT: "code": -32602, +# CHECK-NEXT: "message": "trying to get AST for non-added document" +# CHECK-NEXT: }, +# CHECK-NEXT: "id": 1, +# CHECK-NEXT: "jsonrpc": "2.0" +# CHECK-NEXT:} +--- +{"jsonrpc":"2.0","id":4,"method":"shutdown"} +--- +{"jsonrpc":"2.0","method":"exit"} Index: clang-tools-extra/trunk/clangd/test/too_large.test =================================================================== --- clang-tools-extra/trunk/clangd/test/too_large.test +++ clang-tools-extra/trunk/clangd/test/too_large.test @@ -0,0 +1,7 @@ +# RUN: not clangd -run-synchronously < %s 2>&1 | FileCheck -check-prefix=STDERR %s +# vim: fileformat=dos +# It is absolutely vital that this file has CRLF line endings. +# +Content-Length: 2147483648 + +# STDERR: Refusing to read message Index: clang-tools-extra/trunk/clangd/test/trace.test =================================================================== --- clang-tools-extra/trunk/clangd/test/trace.test +++ clang-tools-extra/trunk/clangd/test/trace.test @@ -0,0 +1,28 @@ +# RUN: env CLANGD_TRACE=%t clangd -lit-test < %s && FileCheck %s < %t +{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootPath":"clangd","capabilities":{},"trace":"off"}} +--- +{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"test:///foo.c","languageId":"c","version":1,"text":"void main() {}"}}} +# These assertions are a bit loose, to avoid brittleness. +# CHECK: { +# CHECK: "displayTimeUnit": "ns", +# CHECK: "traceEvents": [ +# CHECK: { +# CHECK: "ph": "X", +# CHECK: "name": "BuildPreamble", +# CHECK: "args": { +# CHECK: "File": "{{.*(/|\\)}}foo.c" +# CHECK: }, +# CHECK: } +# CHECK: { +# CHECK: "ph": "X", +# CHECK: "name": "BuildAST", +# CHECK: "args": { +# CHECK: "File": "{{.*(/|\\)}}foo.c" +# CHECK: }, +# CHECK: } +# CHECK: ] +# CHECK: } +--- +{"jsonrpc":"2.0","id":5,"method":"shutdown"} +--- +{"jsonrpc":"2.0","method":"exit"} Index: clang-tools-extra/trunk/clangd/test/tweaks-format.test =================================================================== --- clang-tools-extra/trunk/clangd/test/tweaks-format.test +++ clang-tools-extra/trunk/clangd/test/tweaks-format.test @@ -0,0 +1,50 @@ +# RUN: clangd -lit-test < %s | FileCheck %s +{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootPath":"clangd","capabilities":{},"trace":"off"}} +--- +{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"test:///main.cc","languageId":"cpp","version":1,"text":"int f() { if (true) { return 1; } else {} }"}}} +--- +{"jsonrpc":"2.0","id":5,"method":"textDocument/codeAction","params":{"textDocument":{"uri":"test:///main.cc"},"range":{"start":{"line":0,"character":11},"end":{"line":0,"character":11}},"context":{"diagnostics":[]}}} +--- +{"jsonrpc":"2.0","id":6,"method":"workspace/executeCommand","params":{"command":"clangd.applyTweak","arguments":[{"file":"test:///main.cc","selection":{"end":{"character":11,"line":0},"start":{"character":11,"line":0}},"tweakID":"SwapIfBranches"}]}} +# CHECK: "newText": "\n ", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 10, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 9, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: { +# CHECK-NEXT: "newText": "{\n }", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 33, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 20, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: { +# CHECK-NEXT: "newText": "{\n return 1;\n }\n", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 42, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 39, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: } +--- +{"jsonrpc":"2.0","id":3,"method":"shutdown"} +--- +{"jsonrpc":"2.0","method":"exit"} Index: clang-tools-extra/trunk/clangd/test/type-hierarchy.test =================================================================== --- clang-tools-extra/trunk/clangd/test/type-hierarchy.test +++ clang-tools-extra/trunk/clangd/test/type-hierarchy.test @@ -0,0 +1,92 @@ +# RUN: clangd -lit-test < %s | FileCheck -strict-whitespace %s +{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootPath":"clangd","capabilities":{},"trace":"off"}} +--- +{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"test:///main.cpp","languageId":"cpp","version":1,"text":"struct Parent {};\nstruct Child1 : Parent {};\nstruct Child2 : Child1 {};"}}} +--- +{"jsonrpc":"2.0","id":1,"method":"textDocument/typeHierarchy","params":{"textDocument":{"uri":"test:///main.cpp"},"position":{"line":2,"character":11},"direction":1,"resolve":1}} +# CHECK: "id": 1 +# CHECK-NEXT: "jsonrpc": "2.0", +# CHECK-NEXT: "result": { +# CHECK-NEXT: "kind": 23, +# CHECK-NEXT: "name": "Child2", +# CHECK-NEXT: "parents": [ +# CHECK-NEXT: { +# CHECK-NEXT: "kind": 23, +# CHECK-NEXT: "name": "Child1", +# CHECK-NEXT: "parents": [ +# CHECK-NEXT: { +# CHECK-NEXT: "kind": 23, +# CHECK-NEXT: "name": "Parent", +# CHECK-NEXT: "parents": [], +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 15, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 0, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: "selectionRange": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 13, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 7, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: "uri": "file:///clangd-test/main.cpp" +# CHECK-NEXT: } +# CHECK-NEXT: ], +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 24, +# CHECK-NEXT: "line": 1 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 0, +# CHECK-NEXT: "line": 1 +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: "selectionRange": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 13, +# CHECK-NEXT: "line": 1 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 7, +# CHECK-NEXT: "line": 1 +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: "uri": "file:///clangd-test/main.cpp" +# CHECK-NEXT: } +# CHECK-NEXT: ], +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 24, +# CHECK-NEXT: "line": 2 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 0, +# CHECK-NEXT: "line": 2 +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: "selectionRange": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 13, +# CHECK-NEXT: "line": 2 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 7, +# CHECK-NEXT: "line": 2 +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: "uri": "file:///clangd-test/main.cpp" +# CHECK-NEXT: } +--- +{"jsonrpc":"2.0","id":2,"method":"shutdown"} +--- +{"jsonrpc":"2.0","method":"exit"} Index: clang-tools-extra/trunk/clangd/test/unsupported-method.test =================================================================== --- clang-tools-extra/trunk/clangd/test/unsupported-method.test +++ clang-tools-extra/trunk/clangd/test/unsupported-method.test @@ -0,0 +1,16 @@ +# RUN: clangd -lit-test < %s | FileCheck -strict-whitespace %s +{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootPath":"clangd","capabilities":{},"trace":"off"}} +--- +{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"test:///main.cpp","languageId":"cpp","version":1,"text":""}}} +--- +{"jsonrpc":"2.0","id":1,"method":"textDocument/jumpInTheAirLikeYouJustDontCare","params":{}} +# CHECK: "error": { +# CHECK-NEXT: "code": -32601, +# CHECK-NEXT: "message": "method not found" +# CHECK-NEXT: }, +# CHECK-NEXT: "id": 1, +# CHECK-NEXT: "jsonrpc": "2.0" +--- +{"jsonrpc":"2.0","id":2,"method":"shutdown"} +--- +{"jsonrpc":"2.0","method":"exit"} Index: clang-tools-extra/trunk/clangd/test/utf8.test =================================================================== --- clang-tools-extra/trunk/clangd/test/utf8.test +++ clang-tools-extra/trunk/clangd/test/utf8.test @@ -0,0 +1,32 @@ +# RUN: clangd -lit-test < %s | FileCheck -strict-whitespace %s +# This test verifies that we can negotiate UTF-8 offsets via protocol extension. +{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootPath":"clangd","capabilities":{"offsetEncoding":["utf-8","utf-16"]},"trace":"off"}} +# CHECK: "offsetEncoding": "utf-8" +--- +{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"test:///main.cpp","languageId":"cpp","version":1,"text":"/*ö*/int x;\nint y=x;"}}} +--- +{"jsonrpc":"2.0","id":1,"method":"textDocument/definition","params":{"textDocument":{"uri":"test:///main.cpp"},"position":{"line":1,"character":6}}} +# /*ö*/int x; +# 01234567890 +# x is character (and utf-16) range [9,10) but byte range [10,11). +# CHECK: "id": 1, +# CHECK-NEXT: "jsonrpc": "2.0", +# CHECK-NEXT: "result": [ +# CHECK-NEXT: { +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 11, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 10, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: "uri": "file://{{.*}}/main.cpp" +# CHECK-NEXT: } +# CHECK-NEXT: ] +--- +{"jsonrpc":"2.0","id":10000,"method":"shutdown"} +--- +{"jsonrpc":"2.0","method":"exit"} Index: clang-tools-extra/trunk/clangd/test/xpc/initialize.test =================================================================== --- clang-tools-extra/trunk/clangd/test/xpc/initialize.test +++ clang-tools-extra/trunk/clangd/test/xpc/initialize.test @@ -0,0 +1,10 @@ +# RUN: clangd-xpc-test-client < %s | FileCheck %s +# REQUIRES: clangd-xpc-support + +{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootUri":"test:///workspace","capabilities":{},"trace":"off"}} +# CHECK: {"id":0,"jsonrpc":"2.0","result":{"capabilities" + +{"jsonrpc":"2.0","id":3,"method":"shutdown"} +# CHECK: {"id":3,"jsonrpc":"2.0","result":null} + +{"jsonrpc":"2.0","method":"exit"} Index: clang-tools-extra/trunk/clangd/test/xrefs.test =================================================================== --- clang-tools-extra/trunk/clangd/test/xrefs.test +++ clang-tools-extra/trunk/clangd/test/xrefs.test @@ -0,0 +1,92 @@ +# RUN: clangd -lit-test < %s | FileCheck -strict-whitespace %s +{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootPath":"clangd","capabilities":{},"trace":"off"}} +--- +{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"test:///main.cpp","languageId":"cpp","version":1,"text":"extern int x;\nint x = 0;\nint y = x;"}}} +--- +{"jsonrpc":"2.0","id":1,"method":"textDocument/definition","params":{"textDocument":{"uri":"test:///main.cpp"},"position":{"line":2,"character":8}}} +# CHECK: "id": 1, +# CHECK-NEXT: "jsonrpc": "2.0", +# CHECK-NEXT: "result": [ +# CHECK-NEXT: { +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 5, +# CHECK-NEXT: "line": 1 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 4, +# CHECK-NEXT: "line": 1 +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: "uri": "file://{{.*}}/{{([A-Z]:/)?}}main.cpp" +# CHECK-NEXT: } +# CHECK-NEXT: ] +--- +# Toggle: we're on the definition, so jump to the declaration. +{"jsonrpc":"2.0","id":1,"method":"textDocument/definition","params":{"textDocument":{"uri":"test:///main.cpp"},"position":{"line":1,"character":4}}} +# CHECK: "id": 1, +# CHECK-NEXT: "jsonrpc": "2.0", +# CHECK-NEXT: "result": [ +# CHECK-NEXT: { +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 12, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 11, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: "uri": "file://{{.*}}/{{([A-Z]:/)?}}main.cpp" +# CHECK-NEXT: } +# CHECK-NEXT: ] +--- +{"jsonrpc":"2.0","id":1,"method":"textDocument/documentHighlight","params":{"textDocument":{"uri":"test:///main.cpp"},"position":{"line":2,"character":8}}} +# CHECK: "id": 1 +# CHECK-NEXT: "jsonrpc": "2.0", +# CHECK-NEXT: "result": [ +# CHECK-NEXT: { +# CHECK-NEXT: "kind": 1, +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 12, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 11, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: { +# CHECK-NEXT: "kind": 1, +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 5, +# CHECK-NEXT: "line": 1 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 4, +# CHECK-NEXT: "line": 1 +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: { +# CHECK-NEXT: "kind": 2, +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 9, +# CHECK-NEXT: "line": 2 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 8, +# CHECK-NEXT: "line": 2 +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: ] +--- +{"jsonrpc":"2.0","id":10000,"method":"shutdown"} +--- +{"jsonrpc":"2.0","method":"exit"} Index: clang-tools-extra/trunk/clangd/unittests/Annotations.h =================================================================== --- clang-tools-extra/trunk/clangd/unittests/Annotations.h +++ clang-tools-extra/trunk/clangd/unittests/Annotations.h @@ -0,0 +1,39 @@ +//===--- Annotations.h - Annotated source code for tests ---------*- 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 +// +//===----------------------------------------------------------------------===// +// A clangd-specific version of llvm/Testing/Support/Annotations.h, replaces +// offsets and offset-based ranges with types from the LSP protocol. +//===---------------------------------------------------------------------===// + +#ifndef LLVM_CLANG_TOOLS_EXTRA_UNITTESTS_CLANGD_ANNOTATIONS_H +#define LLVM_CLANG_TOOLS_EXTRA_UNITTESTS_CLANGD_ANNOTATIONS_H + +#include "Protocol.h" +#include "llvm/Testing/Support/Annotations.h" + +namespace clang { +namespace clangd { + +/// Same as llvm::Annotations, but adjusts functions to LSP-specific types for +/// positions and ranges. +class Annotations : public llvm::Annotations { + using Base = llvm::Annotations; + +public: + using llvm::Annotations::Annotations; + + Position point(llvm::StringRef Name = "") const; + std::vector points(llvm::StringRef Name = "") const; + + clangd::Range range(llvm::StringRef Name = "") const; + std::vector ranges(llvm::StringRef Name = "") const; +}; + +} // namespace clangd +} // namespace clang + +#endif // LLVM_CLANG_TOOLS_EXTRA_UNITTESTS_CLANGD_ANNOTATIONS_H Index: clang-tools-extra/trunk/clangd/unittests/Annotations.cpp =================================================================== --- clang-tools-extra/trunk/clangd/unittests/Annotations.cpp +++ clang-tools-extra/trunk/clangd/unittests/Annotations.cpp @@ -0,0 +1,53 @@ +//===--- Annotations.cpp - Annotated source code for unit tests --*- 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 "SourceCode.h" + +namespace clang { +namespace clangd { + +Position Annotations::point(llvm::StringRef Name) const { + return offsetToPosition(code(), Base::point(Name)); +} + +std::vector Annotations::points(llvm::StringRef Name) const { + auto Offsets = Base::points(Name); + + std::vector Ps; + Ps.reserve(Offsets.size()); + for (size_t O : Offsets) + Ps.push_back(offsetToPosition(code(), O)); + + return Ps; +} + +static clangd::Range toLSPRange(llvm::StringRef Code, Annotations::Range R) { + clangd::Range LSPRange; + LSPRange.start = offsetToPosition(Code, R.Begin); + LSPRange.end = offsetToPosition(Code, R.End); + return LSPRange; +} + +clangd::Range Annotations::range(llvm::StringRef Name) const { + return toLSPRange(code(), Base::range(Name)); +} + +std::vector Annotations::ranges(llvm::StringRef Name) const { + auto OffsetRanges = Base::ranges(Name); + + std::vector Rs; + Rs.reserve(OffsetRanges.size()); + for (Annotations::Range R : OffsetRanges) + Rs.push_back(toLSPRange(code(), R)); + + return Rs; +} + +} // namespace clangd +} // namespace clang Index: clang-tools-extra/trunk/clangd/unittests/BackgroundIndexTests.cpp =================================================================== --- clang-tools-extra/trunk/clangd/unittests/BackgroundIndexTests.cpp +++ clang-tools-extra/trunk/clangd/unittests/BackgroundIndexTests.cpp @@ -0,0 +1,465 @@ +#include "SyncAPI.h" +#include "TestFS.h" +#include "index/Background.h" +#include "llvm/Support/ScopedPrinter.h" +#include "llvm/Support/Threading.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include + +using testing::_; +using testing::AllOf; +using testing::Contains; +using testing::ElementsAre; +using testing::Not; +using testing::UnorderedElementsAre; + +namespace clang { +namespace clangd { + +MATCHER_P(Named, N, "") { return arg.Name == N; } +MATCHER(Declared, "") { + return !StringRef(arg.CanonicalDeclaration.FileURI).empty(); +} +MATCHER(Defined, "") { return !StringRef(arg.Definition.FileURI).empty(); } +MATCHER_P(FileURI, F, "") { return StringRef(arg.Location.FileURI) == F; } +testing::Matcher +RefsAre(std::vector> Matchers) { + return ElementsAre(testing::Pair(_, UnorderedElementsAreArray(Matchers))); +} +// URI cannot be empty since it references keys in the IncludeGraph. +MATCHER(EmptyIncludeNode, "") { + return !arg.IsTU && !arg.URI.empty() && arg.Digest == FileDigest{{0}} && + arg.DirectIncludes.empty(); +} + +class MemoryShardStorage : public BackgroundIndexStorage { + mutable std::mutex StorageMu; + llvm::StringMap &Storage; + size_t &CacheHits; + +public: + MemoryShardStorage(llvm::StringMap &Storage, size_t &CacheHits) + : Storage(Storage), CacheHits(CacheHits) {} + llvm::Error storeShard(llvm::StringRef ShardIdentifier, + IndexFileOut Shard) const override { + std::lock_guard Lock(StorageMu); + AccessedPaths.insert(ShardIdentifier); + Storage[ShardIdentifier] = llvm::to_string(Shard); + return llvm::Error::success(); + } + std::unique_ptr + loadShard(llvm::StringRef ShardIdentifier) const override { + std::lock_guard Lock(StorageMu); + AccessedPaths.insert(ShardIdentifier); + if (Storage.find(ShardIdentifier) == Storage.end()) { + return nullptr; + } + auto IndexFile = readIndexFile(Storage[ShardIdentifier]); + if (!IndexFile) { + ADD_FAILURE() << "Error while reading " << ShardIdentifier << ':' + << IndexFile.takeError(); + return nullptr; + } + CacheHits++; + return llvm::make_unique(std::move(*IndexFile)); + } + + mutable llvm::StringSet<> AccessedPaths; +}; + +class BackgroundIndexTest : public ::testing::Test { +protected: + BackgroundIndexTest() { BackgroundIndex::preventThreadStarvationInTests(); } +}; + +TEST_F(BackgroundIndexTest, NoCrashOnErrorFile) { + MockFSProvider FS; + FS.Files[testPath("root/A.cc")] = "error file"; + llvm::StringMap Storage; + size_t CacheHits = 0; + MemoryShardStorage MSS(Storage, CacheHits); + OverlayCDB CDB(/*Base=*/nullptr); + BackgroundIndex Idx(Context::empty(), FS, CDB, + [&](llvm::StringRef) { return &MSS; }); + + tooling::CompileCommand Cmd; + Cmd.Filename = testPath("root/A.cc"); + Cmd.Directory = testPath("root"); + Cmd.CommandLine = {"clang++", "-DA=1", testPath("root/A.cc")}; + CDB.setCompileCommand(testPath("root/A.cc"), Cmd); + + ASSERT_TRUE(Idx.blockUntilIdleForTest()); +} + +TEST_F(BackgroundIndexTest, IndexTwoFiles) { + MockFSProvider FS; + // a.h yields different symbols when included by A.cc vs B.cc. + FS.Files[testPath("root/A.h")] = R"cpp( + void common(); + void f_b(); + #if A + class A_CC {}; + #else + class B_CC{}; + #endif + )cpp"; + FS.Files[testPath("root/A.cc")] = + "#include \"A.h\"\nvoid g() { (void)common; }"; + FS.Files[testPath("root/B.cc")] = + R"cpp( + #define A 0 + #include "A.h" + void f_b() { + (void)common; + })cpp"; + llvm::StringMap Storage; + size_t CacheHits = 0; + MemoryShardStorage MSS(Storage, CacheHits); + OverlayCDB CDB(/*Base=*/nullptr); + BackgroundIndex Idx(Context::empty(), FS, CDB, + [&](llvm::StringRef) { return &MSS; }); + + tooling::CompileCommand Cmd; + Cmd.Filename = testPath("root/A.cc"); + Cmd.Directory = testPath("root"); + Cmd.CommandLine = {"clang++", "-DA=1", testPath("root/A.cc")}; + CDB.setCompileCommand(testPath("root/A.cc"), Cmd); + + ASSERT_TRUE(Idx.blockUntilIdleForTest()); + EXPECT_THAT( + runFuzzyFind(Idx, ""), + UnorderedElementsAre(Named("common"), Named("A_CC"), Named("g"), + AllOf(Named("f_b"), Declared(), Not(Defined())))); + + Cmd.Filename = testPath("root/B.cc"); + Cmd.CommandLine = {"clang++", Cmd.Filename}; + CDB.setCompileCommand(testPath("root/A.cc"), Cmd); + + ASSERT_TRUE(Idx.blockUntilIdleForTest()); + // B_CC is dropped as we don't collect symbols from A.h in this compilation. + EXPECT_THAT(runFuzzyFind(Idx, ""), + UnorderedElementsAre(Named("common"), Named("A_CC"), Named("g"), + AllOf(Named("f_b"), Declared(), Defined()))); + + auto Syms = runFuzzyFind(Idx, "common"); + EXPECT_THAT(Syms, UnorderedElementsAre(Named("common"))); + auto Common = *Syms.begin(); + EXPECT_THAT(getRefs(Idx, Common.ID), + RefsAre({FileURI("unittest:///root/A.h"), + FileURI("unittest:///root/A.cc"), + FileURI("unittest:///root/B.cc")})); +} + +TEST_F(BackgroundIndexTest, ShardStorageTest) { + MockFSProvider FS; + FS.Files[testPath("root/A.h")] = R"cpp( + void common(); + void f_b(); + class A_CC {}; + )cpp"; + std::string A_CC = "#include \"A.h\"\nvoid g() { (void)common; }"; + FS.Files[testPath("root/A.cc")] = A_CC; + + llvm::StringMap Storage; + size_t CacheHits = 0; + MemoryShardStorage MSS(Storage, CacheHits); + + tooling::CompileCommand Cmd; + Cmd.Filename = testPath("root/A.cc"); + Cmd.Directory = testPath("root"); + Cmd.CommandLine = {"clang++", testPath("root/A.cc")}; + // Check nothing is loaded from Storage, but A.cc and A.h has been stored. + { + OverlayCDB CDB(/*Base=*/nullptr); + BackgroundIndex Idx(Context::empty(), FS, CDB, + [&](llvm::StringRef) { return &MSS; }); + CDB.setCompileCommand(testPath("root/A.cc"), Cmd); + ASSERT_TRUE(Idx.blockUntilIdleForTest()); + } + EXPECT_EQ(CacheHits, 0U); + EXPECT_EQ(Storage.size(), 2U); + + { + OverlayCDB CDB(/*Base=*/nullptr); + BackgroundIndex Idx(Context::empty(), FS, CDB, + [&](llvm::StringRef) { return &MSS; }); + CDB.setCompileCommand(testPath("root"), Cmd); + ASSERT_TRUE(Idx.blockUntilIdleForTest()); + } + EXPECT_EQ(CacheHits, 2U); // Check both A.cc and A.h loaded from cache. + EXPECT_EQ(Storage.size(), 2U); + + auto ShardHeader = MSS.loadShard(testPath("root/A.h")); + EXPECT_NE(ShardHeader, nullptr); + EXPECT_THAT( + *ShardHeader->Symbols, + UnorderedElementsAre(Named("common"), Named("A_CC"), + AllOf(Named("f_b"), Declared(), Not(Defined())))); + for (const auto &Ref : *ShardHeader->Refs) + EXPECT_THAT(Ref.second, + UnorderedElementsAre(FileURI("unittest:///root/A.h"))); + + auto ShardSource = MSS.loadShard(testPath("root/A.cc")); + EXPECT_NE(ShardSource, nullptr); + EXPECT_THAT(*ShardSource->Symbols, UnorderedElementsAre(Named("g"))); + EXPECT_THAT(*ShardSource->Refs, RefsAre({FileURI("unittest:///root/A.cc")})); +} + +TEST_F(BackgroundIndexTest, DirectIncludesTest) { + MockFSProvider FS; + FS.Files[testPath("root/B.h")] = ""; + FS.Files[testPath("root/A.h")] = R"cpp( + #include "B.h" + void common(); + void f_b(); + class A_CC {}; + )cpp"; + std::string A_CC = "#include \"A.h\"\nvoid g() { (void)common; }"; + FS.Files[testPath("root/A.cc")] = A_CC; + + llvm::StringMap Storage; + size_t CacheHits = 0; + MemoryShardStorage MSS(Storage, CacheHits); + + tooling::CompileCommand Cmd; + Cmd.Filename = testPath("root/A.cc"); + Cmd.Directory = testPath("root"); + Cmd.CommandLine = {"clang++", testPath("root/A.cc")}; + { + OverlayCDB CDB(/*Base=*/nullptr); + BackgroundIndex Idx(Context::empty(), FS, CDB, + [&](llvm::StringRef) { return &MSS; }); + CDB.setCompileCommand(testPath("root/A.cc"), Cmd); + ASSERT_TRUE(Idx.blockUntilIdleForTest()); + } + + auto ShardSource = MSS.loadShard(testPath("root/A.cc")); + EXPECT_TRUE(ShardSource->Sources); + EXPECT_EQ(ShardSource->Sources->size(), 2U); // A.cc, A.h + EXPECT_THAT( + ShardSource->Sources->lookup("unittest:///root/A.cc").DirectIncludes, + UnorderedElementsAre("unittest:///root/A.h")); + EXPECT_NE(ShardSource->Sources->lookup("unittest:///root/A.cc").Digest, + FileDigest{{0}}); + EXPECT_THAT(ShardSource->Sources->lookup("unittest:///root/A.h"), + EmptyIncludeNode()); + + auto ShardHeader = MSS.loadShard(testPath("root/A.h")); + EXPECT_TRUE(ShardHeader->Sources); + EXPECT_EQ(ShardHeader->Sources->size(), 2U); // A.h, B.h + EXPECT_THAT( + ShardHeader->Sources->lookup("unittest:///root/A.h").DirectIncludes, + UnorderedElementsAre("unittest:///root/B.h")); + EXPECT_NE(ShardHeader->Sources->lookup("unittest:///root/A.h").Digest, + FileDigest{{0}}); + EXPECT_THAT(ShardHeader->Sources->lookup("unittest:///root/B.h"), + EmptyIncludeNode()); +} + +// FIXME: figure out the right timeouts or rewrite to not use the timeouts and +// re-enable. +TEST_F(BackgroundIndexTest, DISABLED_PeriodicalIndex) { + MockFSProvider FS; + llvm::StringMap Storage; + size_t CacheHits = 0; + MemoryShardStorage MSS(Storage, CacheHits); + OverlayCDB CDB(/*Base=*/nullptr); + BackgroundIndex Idx( + Context::empty(), FS, CDB, [&](llvm::StringRef) { return &MSS; }, + /*BuildIndexPeriodMs=*/500); + + FS.Files[testPath("root/A.cc")] = "#include \"A.h\""; + + tooling::CompileCommand Cmd; + FS.Files[testPath("root/A.h")] = "class X {};"; + Cmd.Filename = testPath("root/A.cc"); + Cmd.CommandLine = {"clang++", Cmd.Filename}; + CDB.setCompileCommand(testPath("root/A.cc"), Cmd); + + ASSERT_TRUE(Idx.blockUntilIdleForTest()); + EXPECT_THAT(runFuzzyFind(Idx, ""), ElementsAre()); + std::this_thread::sleep_for(std::chrono::milliseconds(1000)); + EXPECT_THAT(runFuzzyFind(Idx, ""), ElementsAre(Named("X"))); + + FS.Files[testPath("root/A.h")] = "class Y {};"; + FS.Files[testPath("root/A.cc")] += " "; // Force reindex the file. + Cmd.CommandLine = {"clang++", "-DA=1", testPath("root/A.cc")}; + CDB.setCompileCommand(testPath("root/A.cc"), Cmd); + + ASSERT_TRUE(Idx.blockUntilIdleForTest()); + EXPECT_THAT(runFuzzyFind(Idx, ""), ElementsAre(Named("X"))); + std::this_thread::sleep_for(std::chrono::milliseconds(1000)); + EXPECT_THAT(runFuzzyFind(Idx, ""), ElementsAre(Named("Y"))); +} + +TEST_F(BackgroundIndexTest, ShardStorageLoad) { + MockFSProvider FS; + FS.Files[testPath("root/A.h")] = R"cpp( + void common(); + void f_b(); + class A_CC {}; + )cpp"; + FS.Files[testPath("root/A.cc")] = + "#include \"A.h\"\nvoid g() { (void)common; }"; + + llvm::StringMap Storage; + size_t CacheHits = 0; + MemoryShardStorage MSS(Storage, CacheHits); + + tooling::CompileCommand Cmd; + Cmd.Filename = testPath("root/A.cc"); + Cmd.Directory = testPath("root"); + Cmd.CommandLine = {"clang++", testPath("root/A.cc")}; + // Check nothing is loaded from Storage, but A.cc and A.h has been stored. + { + OverlayCDB CDB(/*Base=*/nullptr); + BackgroundIndex Idx(Context::empty(), FS, CDB, + [&](llvm::StringRef) { return &MSS; }); + CDB.setCompileCommand(testPath("root/A.cc"), Cmd); + ASSERT_TRUE(Idx.blockUntilIdleForTest()); + } + + // Change header. + FS.Files[testPath("root/A.h")] = R"cpp( + void common(); + void f_b(); + class A_CC {}; + class A_CCnew {}; + )cpp"; + { + OverlayCDB CDB(/*Base=*/nullptr); + BackgroundIndex Idx(Context::empty(), FS, CDB, + [&](llvm::StringRef) { return &MSS; }); + CDB.setCompileCommand(testPath("root"), Cmd); + ASSERT_TRUE(Idx.blockUntilIdleForTest()); + } + EXPECT_EQ(CacheHits, 2U); // Check both A.cc and A.h loaded from cache. + + // Check if the new symbol has arrived. + auto ShardHeader = MSS.loadShard(testPath("root/A.h")); + EXPECT_NE(ShardHeader, nullptr); + EXPECT_THAT(*ShardHeader->Symbols, Contains(Named("A_CCnew"))); + + // Change source. + FS.Files[testPath("root/A.cc")] = + "#include \"A.h\"\nvoid g() { (void)common; }\nvoid f_b() {}"; + { + CacheHits = 0; + OverlayCDB CDB(/*Base=*/nullptr); + BackgroundIndex Idx(Context::empty(), FS, CDB, + [&](llvm::StringRef) { return &MSS; }); + CDB.setCompileCommand(testPath("root"), Cmd); + ASSERT_TRUE(Idx.blockUntilIdleForTest()); + } + EXPECT_EQ(CacheHits, 2U); // Check both A.cc and A.h loaded from cache. + + // Check if the new symbol has arrived. + ShardHeader = MSS.loadShard(testPath("root/A.h")); + EXPECT_NE(ShardHeader, nullptr); + EXPECT_THAT(*ShardHeader->Symbols, Contains(Named("A_CCnew"))); + auto ShardSource = MSS.loadShard(testPath("root/A.cc")); + EXPECT_NE(ShardSource, nullptr); + EXPECT_THAT(*ShardSource->Symbols, + Contains(AllOf(Named("f_b"), Declared(), Defined()))); +} + +TEST_F(BackgroundIndexTest, ShardStorageEmptyFile) { + MockFSProvider FS; + FS.Files[testPath("root/A.h")] = R"cpp( + void common(); + void f_b(); + class A_CC {}; + )cpp"; + FS.Files[testPath("root/B.h")] = R"cpp( + #include "A.h" + )cpp"; + FS.Files[testPath("root/A.cc")] = + "#include \"B.h\"\nvoid g() { (void)common; }"; + + llvm::StringMap Storage; + size_t CacheHits = 0; + MemoryShardStorage MSS(Storage, CacheHits); + + tooling::CompileCommand Cmd; + Cmd.Filename = testPath("root/A.cc"); + Cmd.Directory = testPath("root"); + Cmd.CommandLine = {"clang++", testPath("root/A.cc")}; + // Check that A.cc, A.h and B.h has been stored. + { + OverlayCDB CDB(/*Base=*/nullptr); + BackgroundIndex Idx(Context::empty(), FS, CDB, + [&](llvm::StringRef) { return &MSS; }); + CDB.setCompileCommand(testPath("root/A.cc"), Cmd); + ASSERT_TRUE(Idx.blockUntilIdleForTest()); + } + EXPECT_THAT(Storage.keys(), + UnorderedElementsAre(testPath("root/A.cc"), testPath("root/A.h"), + testPath("root/B.h"))); + auto ShardHeader = MSS.loadShard(testPath("root/B.h")); + EXPECT_NE(ShardHeader, nullptr); + EXPECT_TRUE(ShardHeader->Symbols->empty()); + + // Check that A.cc, A.h and B.h has been loaded. + { + CacheHits = 0; + OverlayCDB CDB(/*Base=*/nullptr); + BackgroundIndex Idx(Context::empty(), FS, CDB, + [&](llvm::StringRef) { return &MSS; }); + CDB.setCompileCommand(testPath("root/A.cc"), Cmd); + ASSERT_TRUE(Idx.blockUntilIdleForTest()); + } + EXPECT_EQ(CacheHits, 3U); + + // Update B.h to contain some symbols. + FS.Files[testPath("root/B.h")] = R"cpp( + #include "A.h" + void new_func(); + )cpp"; + // Check that B.h has been stored with new contents. + { + CacheHits = 0; + OverlayCDB CDB(/*Base=*/nullptr); + BackgroundIndex Idx(Context::empty(), FS, CDB, + [&](llvm::StringRef) { return &MSS; }); + CDB.setCompileCommand(testPath("root/A.cc"), Cmd); + ASSERT_TRUE(Idx.blockUntilIdleForTest()); + } + EXPECT_EQ(CacheHits, 3U); + ShardHeader = MSS.loadShard(testPath("root/B.h")); + EXPECT_NE(ShardHeader, nullptr); + EXPECT_THAT(*ShardHeader->Symbols, + Contains(AllOf(Named("new_func"), Declared(), Not(Defined())))); +} + +TEST_F(BackgroundIndexTest, NoDotsInAbsPath) { + MockFSProvider FS; + llvm::StringMap Storage; + size_t CacheHits = 0; + MemoryShardStorage MSS(Storage, CacheHits); + OverlayCDB CDB(/*Base=*/nullptr); + BackgroundIndex Idx(Context::empty(), FS, CDB, + [&](llvm::StringRef) { return &MSS; }); + + tooling::CompileCommand Cmd; + FS.Files[testPath("root/A.cc")] = ""; + Cmd.Filename = "../A.cc"; + Cmd.Directory = testPath("root/build"); + Cmd.CommandLine = {"clang++", "../A.cc"}; + CDB.setCompileCommand(testPath("root/build/../A.cc"), Cmd); + + FS.Files[testPath("root/B.cc")] = ""; + Cmd.Filename = "./B.cc"; + Cmd.Directory = testPath("root"); + Cmd.CommandLine = {"clang++", "./B.cc"}; + CDB.setCompileCommand(testPath("root/./B.cc"), Cmd); + + ASSERT_TRUE(Idx.blockUntilIdleForTest()); + for (llvm::StringRef AbsPath : MSS.AccessedPaths.keys()) { + EXPECT_FALSE(AbsPath.contains("./")) << AbsPath; + EXPECT_FALSE(AbsPath.contains("../")) << AbsPath; + } +} + +} // namespace clangd +} // namespace clang Index: clang-tools-extra/trunk/clangd/unittests/CMakeLists.txt =================================================================== --- clang-tools-extra/trunk/clangd/unittests/CMakeLists.txt +++ clang-tools-extra/trunk/clangd/unittests/CMakeLists.txt @@ -0,0 +1,92 @@ +set(LLVM_LINK_COMPONENTS + support + ) + +get_filename_component(CLANGD_SOURCE_DIR + ${CMAKE_CURRENT_SOURCE_DIR}/../../clangd REALPATH) +get_filename_component(CLANGD_BINARY_DIR + ${CMAKE_CURRENT_BINARY_DIR}/../../clangd REALPATH) +include_directories( + ${CLANGD_SOURCE_DIR} + ${CLANGD_BINARY_DIR} + ) + +if(CLANG_BUILT_STANDALONE) + # LLVMTestingSupport library is needed for clangd tests. + if (EXISTS ${LLVM_MAIN_SRC_DIR}/lib/Testing/Support + AND NOT TARGET LLVMTestingSupport) + add_subdirectory(${LLVM_MAIN_SRC_DIR}/lib/Testing/Support + lib/Testing/Support) + endif() +endif() + +add_custom_target(ClangdUnitTests) +add_unittest(ClangdUnitTests ClangdTests + Annotations.cpp + BackgroundIndexTests.cpp + CancellationTests.cpp + ClangdTests.cpp + ClangdUnitTests.cpp + CodeCompleteTests.cpp + CodeCompletionStringsTests.cpp + ContextTests.cpp + DexTests.cpp + DiagnosticsTests.cpp + DraftStoreTests.cpp + ExpectedTypeTest.cpp + FileDistanceTests.cpp + FileIndexTests.cpp + FindSymbolsTests.cpp + FSTests.cpp + FunctionTests.cpp + FuzzyMatchTests.cpp + GlobalCompilationDatabaseTests.cpp + HeadersTests.cpp + IndexActionTests.cpp + IndexTests.cpp + JSONTransportTests.cpp + PrintASTTests.cpp + QualityTests.cpp + RIFFTests.cpp + SelectionTests.cpp + SerializationTests.cpp + SourceCodeTests.cpp + SymbolCollectorTests.cpp + SymbolInfoTests.cpp + SyncAPI.cpp + TUSchedulerTests.cpp + TestFS.cpp + TestIndex.cpp + TestTU.cpp + ThreadingTests.cpp + TraceTests.cpp + TypeHierarchyTests.cpp + TweakTests.cpp + URITests.cpp + XRefsTests.cpp + + $ + ) + +target_link_libraries(ClangdTests + PRIVATE + clangAST + clangBasic + clangDaemon + clangFormat + clangFrontend + clangIndex + clangLex + clangSema + clangSerialization + clangTidy + clangTooling + clangToolingCore + clangToolingInclusions + LLVMSupport + LLVMTestingSupport + ) + +if (CLANGD_BUILD_XPC) + add_subdirectory(xpc) +endif () Index: clang-tools-extra/trunk/clangd/unittests/CancellationTests.cpp =================================================================== --- clang-tools-extra/trunk/clangd/unittests/CancellationTests.cpp +++ clang-tools-extra/trunk/clangd/unittests/CancellationTests.cpp @@ -0,0 +1,65 @@ +#include "Cancellation.h" +#include "Context.h" +#include "Threading.h" +#include "llvm/Support/Error.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include +#include +#include + +namespace clang { +namespace clangd { +namespace { + +TEST(CancellationTest, CancellationTest) { + auto Task = cancelableTask(); + WithContext ContextWithCancellation(std::move(Task.first)); + EXPECT_FALSE(isCancelled()); + Task.second(); + EXPECT_TRUE(isCancelled()); +} + +TEST(CancellationTest, CancelerDiesContextLives) { + llvm::Optional ContextWithCancellation; + { + auto Task = cancelableTask(); + ContextWithCancellation.emplace(std::move(Task.first)); + EXPECT_FALSE(isCancelled()); + Task.second(); + EXPECT_TRUE(isCancelled()); + } + EXPECT_TRUE(isCancelled()); +} + +TEST(CancellationTest, TaskContextDiesHandleLives) { + auto Task = cancelableTask(); + { + WithContext ContextWithCancellation(std::move(Task.first)); + EXPECT_FALSE(isCancelled()); + Task.second(); + EXPECT_TRUE(isCancelled()); + } + // Still should be able to cancel without any problems. + Task.second(); +} + +TEST(CancellationTest, AsynCancellationTest) { + std::atomic HasCancelled(false); + Notification Cancelled; + auto TaskToBeCancelled = [&](Context Ctx) { + WithContext ContextGuard(std::move(Ctx)); + Cancelled.wait(); + HasCancelled = isCancelled(); + }; + auto Task = cancelableTask(); + std::thread AsyncTask(TaskToBeCancelled, std::move(Task.first)); + Task.second(); + Cancelled.notify(); + AsyncTask.join(); + + EXPECT_TRUE(HasCancelled); +} +} // namespace +} // namespace clangd +} // namespace clang Index: clang-tools-extra/trunk/clangd/unittests/ClangdTests.cpp =================================================================== --- clang-tools-extra/trunk/clangd/unittests/ClangdTests.cpp +++ clang-tools-extra/trunk/clangd/unittests/ClangdTests.cpp @@ -0,0 +1,1162 @@ +//===-- ClangdTests.cpp - Clangd unit tests ---------------------*- 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 "ClangdLSPServer.h" +#include "ClangdServer.h" +#include "GlobalCompilationDatabase.h" +#include "Matchers.h" +#include "SyncAPI.h" +#include "TestFS.h" +#include "Threading.h" +#include "URI.h" +#include "clang/Config/config.h" +#include "clang/Sema/CodeCompleteConsumer.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/StringMap.h" +#include "llvm/Support/Errc.h" +#include "llvm/Support/Path.h" +#include "llvm/Support/Regex.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include +#include +#include +#include +#include +#include +#include + +namespace clang { +namespace clangd { + +namespace { + +using ::testing::ElementsAre; +using ::testing::Field; +using ::testing::Gt; +using ::testing::IsEmpty; +using ::testing::Pair; +using ::testing::UnorderedElementsAre; + +MATCHER_P2(DeclAt, File, Range, "") { + return arg.PreferredDeclaration == + Location{URIForFile::canonicalize(File, testRoot()), Range}; +} + +bool diagsContainErrors(const std::vector &Diagnostics) { + for (auto D : Diagnostics) { + if (D.Severity == DiagnosticsEngine::Error || + D.Severity == DiagnosticsEngine::Fatal) + return true; + } + return false; +} + +class ErrorCheckingDiagConsumer : public DiagnosticsConsumer { +public: + void onDiagnosticsReady(PathRef File, + std::vector Diagnostics) override { + bool HadError = diagsContainErrors(Diagnostics); + std::lock_guard Lock(Mutex); + HadErrorInLastDiags = HadError; + } + + bool hadErrorInLastDiags() { + std::lock_guard Lock(Mutex); + return HadErrorInLastDiags; + } + +private: + std::mutex Mutex; + bool HadErrorInLastDiags = false; +}; + +/// For each file, record whether the last published diagnostics contained at +/// least one error. +class MultipleErrorCheckingDiagConsumer : public DiagnosticsConsumer { +public: + void onDiagnosticsReady(PathRef File, + std::vector Diagnostics) override { + bool HadError = diagsContainErrors(Diagnostics); + + std::lock_guard Lock(Mutex); + LastDiagsHadError[File] = HadError; + } + + /// Exposes all files consumed by onDiagnosticsReady in an unspecified order. + /// For each file, a bool value indicates whether the last diagnostics + /// contained an error. + std::vector> filesWithDiags() const { + std::vector> Result; + std::lock_guard Lock(Mutex); + + for (const auto &it : LastDiagsHadError) { + Result.emplace_back(it.first(), it.second); + } + + return Result; + } + + void clear() { + std::lock_guard Lock(Mutex); + LastDiagsHadError.clear(); + } + +private: + mutable std::mutex Mutex; + llvm::StringMap LastDiagsHadError; +}; + +/// Replaces all patterns of the form 0x123abc with spaces +std::string replacePtrsInDump(std::string const &Dump) { + llvm::Regex RE("0x[0-9a-fA-F]+"); + llvm::SmallVector Matches; + llvm::StringRef Pending = Dump; + + std::string Result; + while (RE.match(Pending, &Matches)) { + assert(Matches.size() == 1 && "Exactly one match expected"); + auto MatchPos = Matches[0].data() - Pending.data(); + + Result += Pending.take_front(MatchPos); + Pending = Pending.drop_front(MatchPos + Matches[0].size()); + } + Result += Pending; + + return Result; +} + +std::string dumpASTWithoutMemoryLocs(ClangdServer &Server, PathRef File) { + auto DumpWithMemLocs = runDumpAST(Server, File); + return replacePtrsInDump(DumpWithMemLocs); +} + +class ClangdVFSTest : public ::testing::Test { +protected: + std::string parseSourceAndDumpAST( + PathRef SourceFileRelPath, llvm::StringRef SourceContents, + std::vector> ExtraFiles = {}, + bool ExpectErrors = false) { + MockFSProvider FS; + ErrorCheckingDiagConsumer DiagConsumer; + MockCompilationDatabase CDB; + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + for (const auto &FileWithContents : ExtraFiles) + FS.Files[testPath(FileWithContents.first)] = FileWithContents.second; + + auto SourceFilename = testPath(SourceFileRelPath); + Server.addDocument(SourceFilename, SourceContents); + auto Result = dumpASTWithoutMemoryLocs(Server, SourceFilename); + EXPECT_TRUE(Server.blockUntilIdleForTest()) << "Waiting for diagnostics"; + EXPECT_EQ(ExpectErrors, DiagConsumer.hadErrorInLastDiags()); + return Result; + } +}; + +TEST_F(ClangdVFSTest, Parse) { + // FIXME: figure out a stable format for AST dumps, so that we can check the + // output of the dump itself is equal to the expected one, not just that it's + // different. + auto Empty = parseSourceAndDumpAST("foo.cpp", "", {}); + auto OneDecl = parseSourceAndDumpAST("foo.cpp", "int a;", {}); + auto SomeDecls = parseSourceAndDumpAST("foo.cpp", "int a; int b; int c;", {}); + EXPECT_NE(Empty, OneDecl); + EXPECT_NE(Empty, SomeDecls); + EXPECT_NE(SomeDecls, OneDecl); + + auto Empty2 = parseSourceAndDumpAST("foo.cpp", ""); + auto OneDecl2 = parseSourceAndDumpAST("foo.cpp", "int a;"); + auto SomeDecls2 = parseSourceAndDumpAST("foo.cpp", "int a; int b; int c;"); + EXPECT_EQ(Empty, Empty2); + EXPECT_EQ(OneDecl, OneDecl2); + EXPECT_EQ(SomeDecls, SomeDecls2); +} + +TEST_F(ClangdVFSTest, ParseWithHeader) { + parseSourceAndDumpAST("foo.cpp", "#include \"foo.h\"", {}, + /*ExpectErrors=*/true); + parseSourceAndDumpAST("foo.cpp", "#include \"foo.h\"", {{"foo.h", ""}}, + /*ExpectErrors=*/false); + + const auto SourceContents = R"cpp( +#include "foo.h" +int b = a; +)cpp"; + parseSourceAndDumpAST("foo.cpp", SourceContents, {{"foo.h", ""}}, + /*ExpectErrors=*/true); + parseSourceAndDumpAST("foo.cpp", SourceContents, {{"foo.h", "int a;"}}, + /*ExpectErrors=*/false); +} + +TEST_F(ClangdVFSTest, Reparse) { + MockFSProvider FS; + ErrorCheckingDiagConsumer DiagConsumer; + MockCompilationDatabase CDB; + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + + const auto SourceContents = R"cpp( +#include "foo.h" +int b = a; +)cpp"; + + auto FooCpp = testPath("foo.cpp"); + + FS.Files[testPath("foo.h")] = "int a;"; + FS.Files[FooCpp] = SourceContents; + + Server.addDocument(FooCpp, SourceContents); + auto DumpParse1 = dumpASTWithoutMemoryLocs(Server, FooCpp); + ASSERT_TRUE(Server.blockUntilIdleForTest()) << "Waiting for diagnostics"; + EXPECT_FALSE(DiagConsumer.hadErrorInLastDiags()); + + Server.addDocument(FooCpp, ""); + auto DumpParseEmpty = dumpASTWithoutMemoryLocs(Server, FooCpp); + ASSERT_TRUE(Server.blockUntilIdleForTest()) << "Waiting for diagnostics"; + EXPECT_FALSE(DiagConsumer.hadErrorInLastDiags()); + + Server.addDocument(FooCpp, SourceContents); + auto DumpParse2 = dumpASTWithoutMemoryLocs(Server, FooCpp); + ASSERT_TRUE(Server.blockUntilIdleForTest()) << "Waiting for diagnostics"; + EXPECT_FALSE(DiagConsumer.hadErrorInLastDiags()); + + EXPECT_EQ(DumpParse1, DumpParse2); + EXPECT_NE(DumpParse1, DumpParseEmpty); +} + +TEST_F(ClangdVFSTest, ReparseOnHeaderChange) { + MockFSProvider FS; + ErrorCheckingDiagConsumer DiagConsumer; + MockCompilationDatabase CDB; + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + + const auto SourceContents = R"cpp( +#include "foo.h" +int b = a; +)cpp"; + + auto FooCpp = testPath("foo.cpp"); + auto FooH = testPath("foo.h"); + + FS.Files[FooH] = "int a;"; + FS.Files[FooCpp] = SourceContents; + + Server.addDocument(FooCpp, SourceContents); + auto DumpParse1 = dumpASTWithoutMemoryLocs(Server, FooCpp); + ASSERT_TRUE(Server.blockUntilIdleForTest()) << "Waiting for diagnostics"; + EXPECT_FALSE(DiagConsumer.hadErrorInLastDiags()); + + FS.Files[FooH] = ""; + Server.addDocument(FooCpp, SourceContents); + auto DumpParseDifferent = dumpASTWithoutMemoryLocs(Server, FooCpp); + ASSERT_TRUE(Server.blockUntilIdleForTest()) << "Waiting for diagnostics"; + EXPECT_TRUE(DiagConsumer.hadErrorInLastDiags()); + + FS.Files[FooH] = "int a;"; + Server.addDocument(FooCpp, SourceContents); + auto DumpParse2 = dumpASTWithoutMemoryLocs(Server, FooCpp); + ASSERT_TRUE(Server.blockUntilIdleForTest()) << "Waiting for diagnostics"; + EXPECT_FALSE(DiagConsumer.hadErrorInLastDiags()); + + EXPECT_EQ(DumpParse1, DumpParse2); + EXPECT_NE(DumpParse1, DumpParseDifferent); +} + +TEST_F(ClangdVFSTest, PropagatesContexts) { + static Key Secret; + struct FSProvider : public FileSystemProvider { + IntrusiveRefCntPtr getFileSystem() const override { + Got = Context::current().getExisting(Secret); + return buildTestFS({}); + } + mutable int Got; + } FS; + struct DiagConsumer : public DiagnosticsConsumer { + void onDiagnosticsReady(PathRef File, + std::vector Diagnostics) override { + Got = Context::current().getExisting(Secret); + } + int Got; + } DiagConsumer; + MockCompilationDatabase CDB; + + // Verify that the context is plumbed to the FS provider and diagnostics. + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + { + WithContextValue Entrypoint(Secret, 42); + Server.addDocument(testPath("foo.cpp"), "void main(){}"); + } + ASSERT_TRUE(Server.blockUntilIdleForTest()); + EXPECT_EQ(FS.Got, 42); + EXPECT_EQ(DiagConsumer.Got, 42); +} + +// Only enable this test on Unix +#ifdef LLVM_ON_UNIX +TEST_F(ClangdVFSTest, SearchLibDir) { + // Checks that searches for GCC installation is done through vfs. + MockFSProvider FS; + ErrorCheckingDiagConsumer DiagConsumer; + MockCompilationDatabase CDB; + CDB.ExtraClangFlags.insert(CDB.ExtraClangFlags.end(), + {"-xc++", "-target", "x86_64-linux-unknown", + "-m64", "--gcc-toolchain=/randomusr", + "-stdlib=libstdc++"}); + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + + // Just a random gcc version string + SmallString<8> Version("4.9.3"); + + // A lib dir for gcc installation + SmallString<64> LibDir("/randomusr/lib/gcc/x86_64-linux-gnu"); + llvm::sys::path::append(LibDir, Version); + + // Put crtbegin.o into LibDir/64 to trick clang into thinking there's a gcc + // installation there. + SmallString<64> DummyLibFile; + llvm::sys::path::append(DummyLibFile, LibDir, "64", "crtbegin.o"); + FS.Files[DummyLibFile] = ""; + + SmallString<64> IncludeDir("/randomusr/include/c++"); + llvm::sys::path::append(IncludeDir, Version); + + SmallString<64> StringPath; + llvm::sys::path::append(StringPath, IncludeDir, "string"); + FS.Files[StringPath] = "class mock_string {};"; + + auto FooCpp = testPath("foo.cpp"); + const auto SourceContents = R"cpp( +#include +mock_string x; +)cpp"; + FS.Files[FooCpp] = SourceContents; + + runAddDocument(Server, FooCpp, SourceContents); + EXPECT_FALSE(DiagConsumer.hadErrorInLastDiags()); + + const auto SourceContentsWithError = R"cpp( +#include +std::string x; +)cpp"; + runAddDocument(Server, FooCpp, SourceContentsWithError); + EXPECT_TRUE(DiagConsumer.hadErrorInLastDiags()); +} +#endif // LLVM_ON_UNIX + +TEST_F(ClangdVFSTest, ForceReparseCompileCommand) { + MockFSProvider FS; + ErrorCheckingDiagConsumer DiagConsumer; + MockCompilationDatabase CDB; + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + + auto FooCpp = testPath("foo.cpp"); + const auto SourceContents1 = R"cpp( +template +struct foo { T x; }; +)cpp"; + const auto SourceContents2 = R"cpp( +template +struct bar { T x; }; +)cpp"; + + FS.Files[FooCpp] = ""; + + // First parse files in C mode and check they produce errors. + CDB.ExtraClangFlags = {"-xc"}; + runAddDocument(Server, FooCpp, SourceContents1); + EXPECT_TRUE(DiagConsumer.hadErrorInLastDiags()); + runAddDocument(Server, FooCpp, SourceContents2); + EXPECT_TRUE(DiagConsumer.hadErrorInLastDiags()); + + // Now switch to C++ mode. + CDB.ExtraClangFlags = {"-xc++"}; + runAddDocument(Server, FooCpp, SourceContents2, WantDiagnostics::Auto); + EXPECT_FALSE(DiagConsumer.hadErrorInLastDiags()); + // Subsequent addDocument calls should finish without errors too. + runAddDocument(Server, FooCpp, SourceContents1); + EXPECT_FALSE(DiagConsumer.hadErrorInLastDiags()); + runAddDocument(Server, FooCpp, SourceContents2); + EXPECT_FALSE(DiagConsumer.hadErrorInLastDiags()); +} + +TEST_F(ClangdVFSTest, ForceReparseCompileCommandDefines) { + MockFSProvider FS; + ErrorCheckingDiagConsumer DiagConsumer; + MockCompilationDatabase CDB; + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + + auto FooCpp = testPath("foo.cpp"); + const auto SourceContents = R"cpp( +#ifdef WITH_ERROR +this +#endif + +int main() { return 0; } +)cpp"; + FS.Files[FooCpp] = ""; + + // Parse with define, we expect to see the errors. + CDB.ExtraClangFlags = {"-DWITH_ERROR"}; + runAddDocument(Server, FooCpp, SourceContents); + EXPECT_TRUE(DiagConsumer.hadErrorInLastDiags()); + + // Parse without the define, no errors should be produced. + CDB.ExtraClangFlags = {}; + runAddDocument(Server, FooCpp, SourceContents, WantDiagnostics::Auto); + ASSERT_TRUE(Server.blockUntilIdleForTest()); + EXPECT_FALSE(DiagConsumer.hadErrorInLastDiags()); + // Subsequent addDocument call should finish without errors too. + runAddDocument(Server, FooCpp, SourceContents); + EXPECT_FALSE(DiagConsumer.hadErrorInLastDiags()); +} + +// Test ClangdServer.reparseOpenedFiles. +TEST_F(ClangdVFSTest, ReparseOpenedFiles) { + Annotations FooSource(R"cpp( +#ifdef MACRO +static void $one[[bob]]() {} +#else +static void $two[[bob]]() {} +#endif + +int main () { bo^b (); return 0; } +)cpp"); + + Annotations BarSource(R"cpp( +#ifdef MACRO +this is an error +#endif +)cpp"); + + Annotations BazSource(R"cpp( +int hello; +)cpp"); + + MockFSProvider FS; + MockCompilationDatabase CDB; + MultipleErrorCheckingDiagConsumer DiagConsumer; + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + + auto FooCpp = testPath("foo.cpp"); + auto BarCpp = testPath("bar.cpp"); + auto BazCpp = testPath("baz.cpp"); + + FS.Files[FooCpp] = ""; + FS.Files[BarCpp] = ""; + FS.Files[BazCpp] = ""; + + CDB.ExtraClangFlags = {"-DMACRO=1"}; + Server.addDocument(FooCpp, FooSource.code()); + Server.addDocument(BarCpp, BarSource.code()); + Server.addDocument(BazCpp, BazSource.code()); + ASSERT_TRUE(Server.blockUntilIdleForTest()); + + EXPECT_THAT(DiagConsumer.filesWithDiags(), + UnorderedElementsAre(Pair(FooCpp, false), Pair(BarCpp, true), + Pair(BazCpp, false))); + + auto Locations = runLocateSymbolAt(Server, FooCpp, FooSource.point()); + EXPECT_TRUE(bool(Locations)); + EXPECT_THAT(*Locations, ElementsAre(DeclAt(FooCpp, FooSource.range("one")))); + + // Undefine MACRO, close baz.cpp. + CDB.ExtraClangFlags.clear(); + DiagConsumer.clear(); + Server.removeDocument(BazCpp); + Server.addDocument(FooCpp, FooSource.code(), WantDiagnostics::Auto); + Server.addDocument(BarCpp, BarSource.code(), WantDiagnostics::Auto); + ASSERT_TRUE(Server.blockUntilIdleForTest()); + + EXPECT_THAT(DiagConsumer.filesWithDiags(), + UnorderedElementsAre(Pair(FooCpp, false), Pair(BarCpp, false))); + + Locations = runLocateSymbolAt(Server, FooCpp, FooSource.point()); + EXPECT_TRUE(bool(Locations)); + EXPECT_THAT(*Locations, ElementsAre(DeclAt(FooCpp, FooSource.range("two")))); +} + +TEST_F(ClangdVFSTest, MemoryUsage) { + MockFSProvider FS; + ErrorCheckingDiagConsumer DiagConsumer; + MockCompilationDatabase CDB; + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + + Path FooCpp = testPath("foo.cpp"); + const auto SourceContents = R"cpp( +struct Something { + int method(); +}; +)cpp"; + Path BarCpp = testPath("bar.cpp"); + + FS.Files[FooCpp] = ""; + FS.Files[BarCpp] = ""; + + EXPECT_THAT(Server.getUsedBytesPerFile(), IsEmpty()); + + Server.addDocument(FooCpp, SourceContents); + Server.addDocument(BarCpp, SourceContents); + ASSERT_TRUE(Server.blockUntilIdleForTest()); + + EXPECT_THAT(Server.getUsedBytesPerFile(), + UnorderedElementsAre(Pair(FooCpp, Gt(0u)), Pair(BarCpp, Gt(0u)))); + + Server.removeDocument(FooCpp); + ASSERT_TRUE(Server.blockUntilIdleForTest()); + EXPECT_THAT(Server.getUsedBytesPerFile(), ElementsAre(Pair(BarCpp, Gt(0u)))); + + Server.removeDocument(BarCpp); + ASSERT_TRUE(Server.blockUntilIdleForTest()); + EXPECT_THAT(Server.getUsedBytesPerFile(), IsEmpty()); +} + +TEST_F(ClangdVFSTest, InvalidCompileCommand) { + MockFSProvider FS; + ErrorCheckingDiagConsumer DiagConsumer; + MockCompilationDatabase CDB; + + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + + auto FooCpp = testPath("foo.cpp"); + // clang cannot create CompilerInvocation if we pass two files in the + // CompileCommand. We pass the file in ExtraFlags once and CDB adds another + // one in getCompileCommand(). + CDB.ExtraClangFlags.push_back(FooCpp); + + // Clang can't parse command args in that case, but we shouldn't crash. + runAddDocument(Server, FooCpp, "int main() {}"); + + EXPECT_EQ(runDumpAST(Server, FooCpp), ""); + EXPECT_ERROR(runLocateSymbolAt(Server, FooCpp, Position())); + EXPECT_ERROR(runFindDocumentHighlights(Server, FooCpp, Position())); + EXPECT_ERROR(runRename(Server, FooCpp, Position(), "new_name")); + // Identifier-based fallback completion. + EXPECT_THAT(cantFail(runCodeComplete(Server, FooCpp, Position(), + clangd::CodeCompleteOptions())) + .Completions, + ElementsAre(Field(&CodeCompletion::Name, "int"), + Field(&CodeCompletion::Name, "main"))); + auto SigHelp = runSignatureHelp(Server, FooCpp, Position()); + ASSERT_TRUE(bool(SigHelp)) << "signatureHelp returned an error"; + EXPECT_THAT(SigHelp->signatures, IsEmpty()); +} + +class ClangdThreadingTest : public ClangdVFSTest {}; + +TEST_F(ClangdThreadingTest, StressTest) { + // Without 'static' clang gives an error for a usage inside TestDiagConsumer. + static const unsigned FilesCount = 5; + const unsigned RequestsCount = 500; + // Blocking requests wait for the parsing to complete, they slow down the test + // dramatically, so they are issued rarely. Each + // BlockingRequestInterval-request will be a blocking one. + const unsigned BlockingRequestInterval = 40; + + const auto SourceContentsWithoutErrors = R"cpp( +int a; +int b; +int c; +int d; +)cpp"; + + const auto SourceContentsWithErrors = R"cpp( +int a = x; +int b; +int c; +int d; +)cpp"; + + // Giving invalid line and column number should not crash ClangdServer, but + // just to make sure we're sometimes hitting the bounds inside the file we + // limit the intervals of line and column number that are generated. + unsigned MaxLineForFileRequests = 7; + unsigned MaxColumnForFileRequests = 10; + + std::vector FilePaths; + MockFSProvider FS; + for (unsigned I = 0; I < FilesCount; ++I) { + std::string Name = std::string("Foo") + std::to_string(I) + ".cpp"; + FS.Files[Name] = ""; + FilePaths.push_back(testPath(Name)); + } + + struct FileStat { + unsigned HitsWithoutErrors = 0; + unsigned HitsWithErrors = 0; + bool HadErrorsInLastDiags = false; + }; + + class TestDiagConsumer : public DiagnosticsConsumer { + public: + TestDiagConsumer() : Stats(FilesCount, FileStat()) {} + + void onDiagnosticsReady(PathRef File, + std::vector Diagnostics) override { + StringRef FileIndexStr = llvm::sys::path::stem(File); + ASSERT_TRUE(FileIndexStr.consume_front("Foo")); + + unsigned long FileIndex = std::stoul(FileIndexStr.str()); + + bool HadError = diagsContainErrors(Diagnostics); + + std::lock_guard Lock(Mutex); + if (HadError) + Stats[FileIndex].HitsWithErrors++; + else + Stats[FileIndex].HitsWithoutErrors++; + Stats[FileIndex].HadErrorsInLastDiags = HadError; + } + + std::vector takeFileStats() { + std::lock_guard Lock(Mutex); + return std::move(Stats); + } + + private: + std::mutex Mutex; + std::vector Stats; + }; + + struct RequestStats { + unsigned RequestsWithoutErrors = 0; + unsigned RequestsWithErrors = 0; + bool LastContentsHadErrors = false; + bool FileIsRemoved = true; + }; + + std::vector ReqStats; + ReqStats.reserve(FilesCount); + for (unsigned FileIndex = 0; FileIndex < FilesCount; ++FileIndex) + ReqStats.emplace_back(); + + TestDiagConsumer DiagConsumer; + { + MockCompilationDatabase CDB; + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + + // Prepare some random distributions for the test. + std::random_device RandGen; + + std::uniform_int_distribution FileIndexDist(0, FilesCount - 1); + // Pass a text that contains compiler errors to addDocument in about 20% of + // all requests. + std::bernoulli_distribution ShouldHaveErrorsDist(0.2); + // Line and Column numbers for requests that need them. + std::uniform_int_distribution LineDist(0, MaxLineForFileRequests); + std::uniform_int_distribution ColumnDist(0, MaxColumnForFileRequests); + + // Some helpers. + auto UpdateStatsOnAddDocument = [&](unsigned FileIndex, bool HadErrors) { + auto &Stats = ReqStats[FileIndex]; + + if (HadErrors) + ++Stats.RequestsWithErrors; + else + ++Stats.RequestsWithoutErrors; + Stats.LastContentsHadErrors = HadErrors; + Stats.FileIsRemoved = false; + }; + + auto UpdateStatsOnRemoveDocument = [&](unsigned FileIndex) { + auto &Stats = ReqStats[FileIndex]; + + Stats.FileIsRemoved = true; + }; + + auto AddDocument = [&](unsigned FileIndex, bool SkipCache) { + bool ShouldHaveErrors = ShouldHaveErrorsDist(RandGen); + Server.addDocument(FilePaths[FileIndex], + ShouldHaveErrors ? SourceContentsWithErrors + : SourceContentsWithoutErrors, + WantDiagnostics::Auto); + UpdateStatsOnAddDocument(FileIndex, ShouldHaveErrors); + }; + + // Various requests that we would randomly run. + auto AddDocumentRequest = [&]() { + unsigned FileIndex = FileIndexDist(RandGen); + AddDocument(FileIndex, /*SkipCache=*/false); + }; + + auto ForceReparseRequest = [&]() { + unsigned FileIndex = FileIndexDist(RandGen); + AddDocument(FileIndex, /*SkipCache=*/true); + }; + + auto RemoveDocumentRequest = [&]() { + unsigned FileIndex = FileIndexDist(RandGen); + // Make sure we don't violate the ClangdServer's contract. + if (ReqStats[FileIndex].FileIsRemoved) + AddDocument(FileIndex, /*SkipCache=*/false); + + Server.removeDocument(FilePaths[FileIndex]); + UpdateStatsOnRemoveDocument(FileIndex); + }; + + auto CodeCompletionRequest = [&]() { + unsigned FileIndex = FileIndexDist(RandGen); + // Make sure we don't violate the ClangdServer's contract. + if (ReqStats[FileIndex].FileIsRemoved) + AddDocument(FileIndex, /*SkipCache=*/false); + + Position Pos; + Pos.line = LineDist(RandGen); + Pos.character = ColumnDist(RandGen); + // FIXME(ibiryukov): Also test async completion requests. + // Simply putting CodeCompletion into async requests now would make + // tests slow, since there's no way to cancel previous completion + // requests as opposed to AddDocument/RemoveDocument, which are implicitly + // cancelled by any subsequent AddDocument/RemoveDocument request to the + // same file. + cantFail(runCodeComplete(Server, FilePaths[FileIndex], Pos, + clangd::CodeCompleteOptions())); + }; + + auto LocateSymbolRequest = [&]() { + unsigned FileIndex = FileIndexDist(RandGen); + // Make sure we don't violate the ClangdServer's contract. + if (ReqStats[FileIndex].FileIsRemoved) + AddDocument(FileIndex, /*SkipCache=*/false); + + Position Pos; + Pos.line = LineDist(RandGen); + Pos.character = ColumnDist(RandGen); + + ASSERT_TRUE(!!runLocateSymbolAt(Server, FilePaths[FileIndex], Pos)); + }; + + std::vector> AsyncRequests = { + AddDocumentRequest, ForceReparseRequest, RemoveDocumentRequest}; + std::vector> BlockingRequests = { + CodeCompletionRequest, LocateSymbolRequest}; + + // Bash requests to ClangdServer in a loop. + std::uniform_int_distribution AsyncRequestIndexDist( + 0, AsyncRequests.size() - 1); + std::uniform_int_distribution BlockingRequestIndexDist( + 0, BlockingRequests.size() - 1); + for (unsigned I = 1; I <= RequestsCount; ++I) { + if (I % BlockingRequestInterval != 0) { + // Issue an async request most of the time. It should be fast. + unsigned RequestIndex = AsyncRequestIndexDist(RandGen); + AsyncRequests[RequestIndex](); + } else { + // Issue a blocking request once in a while. + auto RequestIndex = BlockingRequestIndexDist(RandGen); + BlockingRequests[RequestIndex](); + } + } + ASSERT_TRUE(Server.blockUntilIdleForTest()); + } + + // Check some invariants about the state of the program. + std::vector Stats = DiagConsumer.takeFileStats(); + for (unsigned I = 0; I < FilesCount; ++I) { + if (!ReqStats[I].FileIsRemoved) { + ASSERT_EQ(Stats[I].HadErrorsInLastDiags, + ReqStats[I].LastContentsHadErrors); + } + + ASSERT_LE(Stats[I].HitsWithErrors, ReqStats[I].RequestsWithErrors); + ASSERT_LE(Stats[I].HitsWithoutErrors, ReqStats[I].RequestsWithoutErrors); + } +} + +TEST_F(ClangdVFSTest, CheckSourceHeaderSwitch) { + MockFSProvider FS; + ErrorCheckingDiagConsumer DiagConsumer; + MockCompilationDatabase CDB; + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + + auto SourceContents = R"cpp( + #include "foo.h" + int b = a; + )cpp"; + + auto FooCpp = testPath("foo.cpp"); + auto FooH = testPath("foo.h"); + auto Invalid = testPath("main.cpp"); + + FS.Files[FooCpp] = SourceContents; + FS.Files[FooH] = "int a;"; + FS.Files[Invalid] = "int main() { \n return 0; \n }"; + + Optional PathResult = Server.switchSourceHeader(FooCpp); + EXPECT_TRUE(PathResult.hasValue()); + ASSERT_EQ(PathResult.getValue(), FooH); + + PathResult = Server.switchSourceHeader(FooH); + EXPECT_TRUE(PathResult.hasValue()); + ASSERT_EQ(PathResult.getValue(), FooCpp); + + SourceContents = R"c( + #include "foo.HH" + int b = a; + )c"; + + // Test with header file in capital letters and different extension, source + // file with different extension + auto FooC = testPath("bar.c"); + auto FooHH = testPath("bar.HH"); + + FS.Files[FooC] = SourceContents; + FS.Files[FooHH] = "int a;"; + + PathResult = Server.switchSourceHeader(FooC); + EXPECT_TRUE(PathResult.hasValue()); + ASSERT_EQ(PathResult.getValue(), FooHH); + + // Test with both capital letters + auto Foo2C = testPath("foo2.C"); + auto Foo2HH = testPath("foo2.HH"); + FS.Files[Foo2C] = SourceContents; + FS.Files[Foo2HH] = "int a;"; + + PathResult = Server.switchSourceHeader(Foo2C); + EXPECT_TRUE(PathResult.hasValue()); + ASSERT_EQ(PathResult.getValue(), Foo2HH); + + // Test with source file as capital letter and .hxx header file + auto Foo3C = testPath("foo3.C"); + auto Foo3HXX = testPath("foo3.hxx"); + + SourceContents = R"c( + #include "foo3.hxx" + int b = a; + )c"; + + FS.Files[Foo3C] = SourceContents; + FS.Files[Foo3HXX] = "int a;"; + + PathResult = Server.switchSourceHeader(Foo3C); + EXPECT_TRUE(PathResult.hasValue()); + ASSERT_EQ(PathResult.getValue(), Foo3HXX); + + // Test if asking for a corresponding file that doesn't exist returns an empty + // string. + PathResult = Server.switchSourceHeader(Invalid); + EXPECT_FALSE(PathResult.hasValue()); +} + +TEST_F(ClangdThreadingTest, NoConcurrentDiagnostics) { + class NoConcurrentAccessDiagConsumer : public DiagnosticsConsumer { + public: + std::atomic Count = {0}; + + NoConcurrentAccessDiagConsumer(std::promise StartSecondReparse) + : StartSecondReparse(std::move(StartSecondReparse)) {} + + void onDiagnosticsReady(PathRef, std::vector) override { + ++Count; + std::unique_lock Lock(Mutex, std::try_to_lock_t()); + ASSERT_TRUE(Lock.owns_lock()) + << "Detected concurrent onDiagnosticsReady calls for the same file."; + + // If we started the second parse immediately, it might cancel the first. + // So we don't allow it to start until the first has delivered diags... + if (FirstRequest) { + FirstRequest = false; + StartSecondReparse.set_value(); + // ... but then we wait long enough that the callbacks would overlap. + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + } + } + + private: + std::mutex Mutex; + bool FirstRequest = true; + std::promise StartSecondReparse; + }; + + const auto SourceContentsWithoutErrors = R"cpp( +int a; +int b; +int c; +int d; +)cpp"; + + const auto SourceContentsWithErrors = R"cpp( +int a = x; +int b; +int c; +int d; +)cpp"; + + auto FooCpp = testPath("foo.cpp"); + MockFSProvider FS; + FS.Files[FooCpp] = ""; + + std::promise StartSecondPromise; + std::future StartSecond = StartSecondPromise.get_future(); + + NoConcurrentAccessDiagConsumer DiagConsumer(std::move(StartSecondPromise)); + MockCompilationDatabase CDB; + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + Server.addDocument(FooCpp, SourceContentsWithErrors); + StartSecond.wait(); + Server.addDocument(FooCpp, SourceContentsWithoutErrors); + ASSERT_TRUE(Server.blockUntilIdleForTest()) << "Waiting for diagnostics"; + ASSERT_EQ(DiagConsumer.Count, 2); // Sanity check - we actually ran both? +} + +TEST_F(ClangdVFSTest, FormatCode) { + MockFSProvider FS; + ErrorCheckingDiagConsumer DiagConsumer; + MockCompilationDatabase CDB; + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + + auto Path = testPath("foo.cpp"); + std::string Code = R"cpp( +#include "x.h" +#include "y.h" + +void f( ) {} +)cpp"; + std::string Expected = R"cpp( +#include "x.h" +#include "y.h" + +void f() {} +)cpp"; + FS.Files[Path] = Code; + runAddDocument(Server, Path, Code); + + auto Replaces = Server.formatFile(Code, Path); + EXPECT_TRUE(static_cast(Replaces)); + auto Changed = tooling::applyAllReplacements(Code, *Replaces); + EXPECT_TRUE(static_cast(Changed)); + EXPECT_EQ(Expected, *Changed); +} + +TEST_F(ClangdVFSTest, ChangedHeaderFromISystem) { + MockFSProvider FS; + ErrorCheckingDiagConsumer DiagConsumer; + MockCompilationDatabase CDB; + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + + auto SourcePath = testPath("source/foo.cpp"); + auto HeaderPath = testPath("headers/foo.h"); + FS.Files[HeaderPath] = "struct X { int bar; };"; + Annotations Code(R"cpp( + #include "foo.h" + + int main() { + X().ba^ + })cpp"); + CDB.ExtraClangFlags.push_back("-xc++"); + CDB.ExtraClangFlags.push_back("-isystem" + testPath("headers")); + + runAddDocument(Server, SourcePath, Code.code()); + auto Completions = cantFail(runCodeComplete(Server, SourcePath, Code.point(), + clangd::CodeCompleteOptions())) + .Completions; + EXPECT_THAT(Completions, ElementsAre(Field(&CodeCompletion::Name, "bar"))); + // Update the header and rerun addDocument to make sure we get the updated + // files. + FS.Files[HeaderPath] = "struct X { int bar; int baz; };"; + runAddDocument(Server, SourcePath, Code.code()); + Completions = cantFail(runCodeComplete(Server, SourcePath, Code.point(), + clangd::CodeCompleteOptions())) + .Completions; + // We want to make sure we see the updated version. + EXPECT_THAT(Completions, ElementsAre(Field(&CodeCompletion::Name, "bar"), + Field(&CodeCompletion::Name, "baz"))); +} + +// FIXME(ioeric): make this work for windows again. +#ifndef _WIN32 +// Check that running code completion doesn't stat() a bunch of files from the +// preamble again. (They should be using the preamble's stat-cache) +TEST(ClangdTests, PreambleVFSStatCache) { + class ListenStatsFSProvider : public FileSystemProvider { + public: + ListenStatsFSProvider(llvm::StringMap &CountStats) + : CountStats(CountStats) {} + + IntrusiveRefCntPtr getFileSystem() const override { + class ListenStatVFS : public llvm::vfs::ProxyFileSystem { + public: + ListenStatVFS(IntrusiveRefCntPtr FS, + llvm::StringMap &CountStats) + : ProxyFileSystem(std::move(FS)), CountStats(CountStats) {} + + llvm::ErrorOr> + openFileForRead(const Twine &Path) override { + ++CountStats[llvm::sys::path::filename(Path.str())]; + return ProxyFileSystem::openFileForRead(Path); + } + llvm::ErrorOr status(const Twine &Path) override { + ++CountStats[llvm::sys::path::filename(Path.str())]; + return ProxyFileSystem::status(Path); + } + + private: + llvm::StringMap &CountStats; + }; + + return IntrusiveRefCntPtr( + new ListenStatVFS(buildTestFS(Files), CountStats)); + } + + // If relative paths are used, they are resolved with testPath(). + llvm::StringMap Files; + llvm::StringMap &CountStats; + }; + + llvm::StringMap CountStats; + ListenStatsFSProvider FS(CountStats); + ErrorCheckingDiagConsumer DiagConsumer; + MockCompilationDatabase CDB; + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + + auto SourcePath = testPath("foo.cpp"); + auto HeaderPath = testPath("foo.h"); + FS.Files[HeaderPath] = "struct TestSym {};"; + Annotations Code(R"cpp( + #include "foo.h" + + int main() { + TestSy^ + })cpp"); + + runAddDocument(Server, SourcePath, Code.code()); + + unsigned Before = CountStats["foo.h"]; + EXPECT_GT(Before, 0u); + auto Completions = cantFail(runCodeComplete(Server, SourcePath, Code.point(), + clangd::CodeCompleteOptions())) + .Completions; + EXPECT_EQ(CountStats["foo.h"], Before); + EXPECT_THAT(Completions, + ElementsAre(Field(&CodeCompletion::Name, "TestSym"))); +} +#endif + +TEST_F(ClangdVFSTest, FlagsWithPlugins) { + MockFSProvider FS; + ErrorCheckingDiagConsumer DiagConsumer; + MockCompilationDatabase CDB; + CDB.ExtraClangFlags = { + "-Xclang", + "-add-plugin", + "-Xclang", + "random-plugin", + }; + OverlayCDB OCDB(&CDB); + ClangdServer Server(OCDB, FS, DiagConsumer, ClangdServer::optsForTest()); + + auto FooCpp = testPath("foo.cpp"); + const auto SourceContents = "int main() { return 0; }"; + FS.Files[FooCpp] = FooCpp; + Server.addDocument(FooCpp, SourceContents); + auto Result = dumpASTWithoutMemoryLocs(Server, FooCpp); + EXPECT_TRUE(Server.blockUntilIdleForTest()) << "Waiting for diagnostics"; + EXPECT_NE(Result, ""); +} + +TEST_F(ClangdVFSTest, FallbackWhenPreambleIsNotReady) { + MockFSProvider FS; + ErrorCheckingDiagConsumer DiagConsumer; + MockCompilationDatabase CDB; + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + + auto FooCpp = testPath("foo.cpp"); + Annotations Code(R"cpp( + namespace ns { int xyz; } + using namespace ns; + int main() { + xy^ + })cpp"); + FS.Files[FooCpp] = FooCpp; + + auto Opts = clangd::CodeCompleteOptions(); + Opts.AllowFallback = true; + + // This will make compile command broken and preamble absent. + CDB.ExtraClangFlags = {"yolo.cc"}; + Server.addDocument(FooCpp, Code.code()); + ASSERT_TRUE(Server.blockUntilIdleForTest()); + auto Res = cantFail(runCodeComplete(Server, FooCpp, Code.point(), Opts)); + EXPECT_EQ(Res.Context, CodeCompletionContext::CCC_Recovery); + // Identifier-based fallback completion doesn't know about "symbol" scope. + EXPECT_THAT(Res.Completions, + ElementsAre(AllOf(Field(&CodeCompletion::Name, "xyz"), + Field(&CodeCompletion::Scope, "")))); + + // Make the compile command work again. + CDB.ExtraClangFlags = {"-std=c++11"}; + Server.addDocument(FooCpp, Code.code()); + ASSERT_TRUE(Server.blockUntilIdleForTest()); + EXPECT_THAT(cantFail(runCodeComplete(Server, FooCpp, Code.point(), + clangd::CodeCompleteOptions())) + .Completions, + ElementsAre(AllOf(Field(&CodeCompletion::Name, "xyz"), + Field(&CodeCompletion::Scope, "ns::")))); +} + +TEST_F(ClangdVFSTest, FallbackWhenWaitingForCompileCommand) { + MockFSProvider FS; + ErrorCheckingDiagConsumer DiagConsumer; + // Returns compile command only when notified. + class DelayedCompilationDatabase : public GlobalCompilationDatabase { + public: + DelayedCompilationDatabase(Notification &CanReturnCommand) + : CanReturnCommand(CanReturnCommand) {} + + llvm::Optional + getCompileCommand(PathRef File, ProjectInfo * = nullptr) const override { + // FIXME: make this timeout and fail instead of waiting forever in case + // something goes wrong. + CanReturnCommand.wait(); + auto FileName = llvm::sys::path::filename(File); + std::vector CommandLine = {"clangd", "-ffreestanding", File}; + return {tooling::CompileCommand(llvm::sys::path::parent_path(File), + FileName, std::move(CommandLine), "")}; + } + + std::vector ExtraClangFlags; + + private: + Notification &CanReturnCommand; + }; + + Notification CanReturnCommand; + DelayedCompilationDatabase CDB(CanReturnCommand); + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + + auto FooCpp = testPath("foo.cpp"); + Annotations Code(R"cpp( + namespace ns { int xyz; } + using namespace ns; + int main() { + xy^ + })cpp"); + FS.Files[FooCpp] = FooCpp; + Server.addDocument(FooCpp, Code.code()); + + // Sleep for some time to make sure code completion is not run because update + // hasn't been scheduled. + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + auto Opts = clangd::CodeCompleteOptions(); + Opts.AllowFallback = true; + + auto Res = cantFail(runCodeComplete(Server, FooCpp, Code.point(), Opts)); + EXPECT_EQ(Res.Context, CodeCompletionContext::CCC_Recovery); + + CanReturnCommand.notify(); + ASSERT_TRUE(Server.blockUntilIdleForTest()); + EXPECT_THAT(cantFail(runCodeComplete(Server, FooCpp, Code.point(), + clangd::CodeCompleteOptions())) + .Completions, + ElementsAre(AllOf(Field(&CodeCompletion::Name, "xyz"), + Field(&CodeCompletion::Scope, "ns::")))); +} + +} // namespace +} // namespace clangd +} // namespace clang Index: clang-tools-extra/trunk/clangd/unittests/ClangdUnitTests.cpp =================================================================== --- clang-tools-extra/trunk/clangd/unittests/ClangdUnitTests.cpp +++ clang-tools-extra/trunk/clangd/unittests/ClangdUnitTests.cpp @@ -0,0 +1,86 @@ +//===-- ClangdUnitTests.cpp - ClangdUnit tests ------------------*- 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 "ClangdUnit.h" +#include "SourceCode.h" +#include "TestTU.h" +#include "llvm/Support/ScopedPrinter.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace clang { +namespace clangd { +namespace { + +using testing::ElementsAre; + +TEST(ClangdUnitTest, GetBeginningOfIdentifier) { + std::string Preamble = R"cpp( +struct Bar { int func(); }; +#define MACRO(X) void f() { X; } +Bar* bar; + )cpp"; + // First ^ is the expected beginning, last is the search position. + for (std::string Text : std::vector{ + "int ^f^oo();", // inside identifier + "int ^foo();", // beginning of identifier + "int ^foo^();", // end of identifier + "int foo(^);", // non-identifier + "^int foo();", // beginning of file (can't back up) + "int ^f0^0();", // after a digit (lexing at N-1 is wrong) + "int ^λλ^λ();", // UTF-8 handled properly when backing up + + // identifier in macro arg + "MACRO(bar->^func())", // beginning of identifier + "MACRO(bar->^fun^c())", // inside identifier + "MACRO(bar->^func^())", // end of identifier + "MACRO(^bar->func())", // begin identifier + "MACRO(^bar^->func())", // end identifier + "^MACRO(bar->func())", // beginning of macro name + "^MAC^RO(bar->func())", // inside macro name + "^MACRO^(bar->func())", // end of macro name + }) { + std::string WithPreamble = Preamble + Text; + Annotations TestCase(WithPreamble); + auto AST = TestTU::withCode(TestCase.code()).build(); + const auto &SourceMgr = AST.getASTContext().getSourceManager(); + SourceLocation Actual = getBeginningOfIdentifier( + AST, TestCase.points().back(), SourceMgr.getMainFileID()); + Position ActualPos = offsetToPosition( + TestCase.code(), + SourceMgr.getFileOffset(SourceMgr.getSpellingLoc(Actual))); + EXPECT_EQ(TestCase.points().front(), ActualPos) << Text; + } +} + +MATCHER_P(DeclNamed, Name, "") { + if (NamedDecl *ND = dyn_cast(arg)) + if (ND->getName() == Name) + return true; + if (auto *Stream = result_listener->stream()) { + llvm::raw_os_ostream OS(*Stream); + arg->dump(OS); + } + return false; +} + +TEST(ClangdUnitTest, TopLevelDecls) { + TestTU TU; + TU.HeaderCode = R"( + int header1(); + int header2; + )"; + TU.Code = "int main();"; + auto AST = TU.build(); + EXPECT_THAT(AST.getLocalTopLevelDecls(), ElementsAre(DeclNamed("main"))); +} + +} // namespace +} // namespace clangd +} // namespace clang Index: clang-tools-extra/trunk/clangd/unittests/CodeCompleteTests.cpp =================================================================== --- clang-tools-extra/trunk/clangd/unittests/CodeCompleteTests.cpp +++ clang-tools-extra/trunk/clangd/unittests/CodeCompleteTests.cpp @@ -0,0 +1,2523 @@ +//===-- CodeCompleteTests.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 "CodeComplete.h" +#include "Compiler.h" +#include "Matchers.h" +#include "Protocol.h" +#include "Quality.h" +#include "SourceCode.h" +#include "SyncAPI.h" +#include "TestFS.h" +#include "TestIndex.h" +#include "TestTU.h" +#include "index/Index.h" +#include "index/MemIndex.h" +#include "clang/Sema/CodeCompleteConsumer.h" +#include "clang/Tooling/CompilationDatabase.h" +#include "llvm/Support/Error.h" +#include "llvm/Support/Path.h" +#include "llvm/Testing/Support/Error.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace clang { +namespace clangd { + +namespace { +using ::llvm::Failed; +using ::testing::AllOf; +using ::testing::Contains; +using ::testing::ElementsAre; +using ::testing::Field; +using ::testing::HasSubstr; +using ::testing::IsEmpty; +using ::testing::Not; +using ::testing::UnorderedElementsAre; + +class IgnoreDiagnostics : public DiagnosticsConsumer { + void onDiagnosticsReady(PathRef File, + std::vector Diagnostics) override {} +}; + +// GMock helpers for matching completion items. +MATCHER_P(Named, Name, "") { return arg.Name == Name; } +MATCHER_P(Scope, S, "") { return arg.Scope == S; } +MATCHER_P(Qualifier, Q, "") { return arg.RequiredQualifier == Q; } +MATCHER_P(Labeled, Label, "") { + return arg.RequiredQualifier + arg.Name + arg.Signature == Label; +} +MATCHER_P(SigHelpLabeled, Label, "") { return arg.label == Label; } +MATCHER_P(Kind, K, "") { return arg.Kind == K; } +MATCHER_P(Doc, D, "") { return arg.Documentation == D; } +MATCHER_P(ReturnType, D, "") { return arg.ReturnType == D; } +MATCHER_P(HasInclude, IncludeHeader, "") { + return !arg.Includes.empty() && arg.Includes[0].Header == IncludeHeader; +} +MATCHER_P(InsertInclude, IncludeHeader, "") { + return !arg.Includes.empty() && arg.Includes[0].Header == IncludeHeader && + bool(arg.Includes[0].Insertion); +} +MATCHER(InsertInclude, "") { + return !arg.Includes.empty() && bool(arg.Includes[0].Insertion); +} +MATCHER_P(SnippetSuffix, Text, "") { return arg.SnippetSuffix == Text; } +MATCHER_P(Origin, OriginSet, "") { return arg.Origin == OriginSet; } +MATCHER_P(Signature, S, "") { return arg.Signature == S; } + +// Shorthand for Contains(Named(Name)). +Matcher &> Has(std::string Name) { + return Contains(Named(std::move(Name))); +} +Matcher &> Has(std::string Name, + CompletionItemKind K) { + return Contains(AllOf(Named(std::move(Name)), Kind(K))); +} +MATCHER(IsDocumented, "") { return !arg.Documentation.empty(); } +MATCHER(Deprecated, "") { return arg.Deprecated; } + +std::unique_ptr memIndex(std::vector Symbols) { + SymbolSlab::Builder Slab; + for (const auto &Sym : Symbols) + Slab.insert(Sym); + return MemIndex::build(std::move(Slab).build(), RefSlab()); +} + +CodeCompleteResult completions(ClangdServer &Server, llvm::StringRef TestCode, + Position point, + std::vector IndexSymbols = {}, + clangd::CodeCompleteOptions Opts = {}) { + std::unique_ptr OverrideIndex; + if (!IndexSymbols.empty()) { + assert(!Opts.Index && "both Index and IndexSymbols given!"); + OverrideIndex = memIndex(std::move(IndexSymbols)); + Opts.Index = OverrideIndex.get(); + } + + auto File = testPath("foo.cpp"); + runAddDocument(Server, File, TestCode); + auto CompletionList = + llvm::cantFail(runCodeComplete(Server, File, point, Opts)); + return CompletionList; +} + +CodeCompleteResult completions(ClangdServer &Server, llvm::StringRef Text, + std::vector IndexSymbols = {}, + clangd::CodeCompleteOptions Opts = {}, + PathRef FilePath = "foo.cpp") { + std::unique_ptr OverrideIndex; + if (!IndexSymbols.empty()) { + assert(!Opts.Index && "both Index and IndexSymbols given!"); + OverrideIndex = memIndex(std::move(IndexSymbols)); + Opts.Index = OverrideIndex.get(); + } + + auto File = testPath(FilePath); + Annotations Test(Text); + runAddDocument(Server, File, Test.code()); + auto CompletionList = + llvm::cantFail(runCodeComplete(Server, File, Test.point(), Opts)); + return CompletionList; +} + +// Builds a server and runs code completion. +// If IndexSymbols is non-empty, an index will be built and passed to opts. +CodeCompleteResult completions(llvm::StringRef Text, + std::vector IndexSymbols = {}, + clangd::CodeCompleteOptions Opts = {}, + PathRef FilePath = "foo.cpp") { + MockFSProvider FS; + MockCompilationDatabase CDB; + IgnoreDiagnostics DiagConsumer; + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + return completions(Server, Text, std::move(IndexSymbols), std::move(Opts), + FilePath); +} + +// Builds a server and runs code completion. +// If IndexSymbols is non-empty, an index will be built and passed to opts. +CodeCompleteResult completionsNoCompile(llvm::StringRef Text, + std::vector IndexSymbols = {}, + clangd::CodeCompleteOptions Opts = {}, + PathRef FilePath = "foo.cpp") { + std::unique_ptr OverrideIndex; + if (!IndexSymbols.empty()) { + assert(!Opts.Index && "both Index and IndexSymbols given!"); + OverrideIndex = memIndex(std::move(IndexSymbols)); + Opts.Index = OverrideIndex.get(); + } + + MockFSProvider FS; + Annotations Test(Text); + return codeComplete(FilePath, tooling::CompileCommand(), /*Preamble=*/nullptr, + Test.code(), Test.point(), FS.getFileSystem(), Opts); +} + +Symbol withReferences(int N, Symbol S) { + S.References = N; + return S; +} + +TEST(CompletionTest, Limit) { + clangd::CodeCompleteOptions Opts; + Opts.Limit = 2; + auto Results = completions(R"cpp( +struct ClassWithMembers { + int AAA(); + int BBB(); + int CCC(); +}; +int main() { ClassWithMembers().^ } + )cpp", + /*IndexSymbols=*/{}, Opts); + + EXPECT_TRUE(Results.HasMore); + EXPECT_THAT(Results.Completions, ElementsAre(Named("AAA"), Named("BBB"))); +} + +TEST(CompletionTest, Filter) { + std::string Body = R"cpp( + #define MotorCar + int Car; + struct S { + int FooBar; + int FooBaz; + int Qux; + }; + )cpp"; + + // Only items matching the fuzzy query are returned. + EXPECT_THAT(completions(Body + "int main() { S().Foba^ }").Completions, + AllOf(Has("FooBar"), Has("FooBaz"), Not(Has("Qux")))); + + // Macros require prefix match. + EXPECT_THAT(completions(Body + "int main() { C^ }").Completions, + AllOf(Has("Car"), Not(Has("MotorCar")))); +} + +void TestAfterDotCompletion(clangd::CodeCompleteOptions Opts) { + auto Results = completions( + R"cpp( + int global_var; + + int global_func(); + + // Make sure this is not in preamble. + #define MACRO X + + struct GlobalClass {}; + + struct ClassWithMembers { + /// Doc for method. + int method(); + + int field; + private: + int private_field; + }; + + int test() { + struct LocalClass {}; + + /// Doc for local_var. + int local_var; + + ClassWithMembers().^ + } + )cpp", + {cls("IndexClass"), var("index_var"), func("index_func")}, Opts); + + // Class members. The only items that must be present in after-dot + // completion. + EXPECT_THAT(Results.Completions, + AllOf(Has("method"), Has("field"), Not(Has("ClassWithMembers")), + Not(Has("operator=")), Not(Has("~ClassWithMembers")))); + EXPECT_IFF(Opts.IncludeIneligibleResults, Results.Completions, + Has("private_field")); + // Global items. + EXPECT_THAT( + Results.Completions, + Not(AnyOf(Has("global_var"), Has("index_var"), Has("global_func"), + Has("global_func()"), Has("index_func"), Has("GlobalClass"), + Has("IndexClass"), Has("MACRO"), Has("LocalClass")))); + // There should be no code patterns (aka snippets) in after-dot + // completion. At least there aren't any we're aware of. + EXPECT_THAT(Results.Completions, + Not(Contains(Kind(CompletionItemKind::Snippet)))); + // Check documentation. + EXPECT_IFF(Opts.IncludeComments, Results.Completions, + Contains(IsDocumented())); +} + +void TestGlobalScopeCompletion(clangd::CodeCompleteOptions Opts) { + auto Results = completions( + R"cpp( + int global_var; + int global_func(); + + // Make sure this is not in preamble. + #define MACRO X + + struct GlobalClass {}; + + struct ClassWithMembers { + /// Doc for method. + int method(); + }; + + int test() { + struct LocalClass {}; + + /// Doc for local_var. + int local_var; + + ^ + } + )cpp", + {cls("IndexClass"), var("index_var"), func("index_func")}, Opts); + + // Class members. Should never be present in global completions. + EXPECT_THAT(Results.Completions, + Not(AnyOf(Has("method"), Has("method()"), Has("field")))); + // Global items. + EXPECT_THAT(Results.Completions, + AllOf(Has("global_var"), Has("index_var"), Has("global_func"), + Has("index_func" /* our fake symbol doesn't include () */), + Has("GlobalClass"), Has("IndexClass"))); + // A macro. + EXPECT_IFF(Opts.IncludeMacros, Results.Completions, Has("MACRO")); + // Local items. Must be present always. + EXPECT_THAT(Results.Completions, + AllOf(Has("local_var"), Has("LocalClass"), + Contains(Kind(CompletionItemKind::Snippet)))); + // Check documentation. + EXPECT_IFF(Opts.IncludeComments, Results.Completions, + Contains(IsDocumented())); +} + +TEST(CompletionTest, CompletionOptions) { + auto Test = [&](const clangd::CodeCompleteOptions &Opts) { + TestAfterDotCompletion(Opts); + TestGlobalScopeCompletion(Opts); + }; + // We used to test every combination of options, but that got too slow (2^N). + auto Flags = { + &clangd::CodeCompleteOptions::IncludeMacros, + &clangd::CodeCompleteOptions::IncludeComments, + &clangd::CodeCompleteOptions::IncludeCodePatterns, + &clangd::CodeCompleteOptions::IncludeIneligibleResults, + }; + // Test default options. + Test({}); + // Test with one flag flipped. + for (auto &F : Flags) { + clangd::CodeCompleteOptions O; + O.*F ^= true; + Test(O); + } +} + +TEST(CompletionTest, Priorities) { + auto Internal = completions(R"cpp( + class Foo { + public: void pub(); + protected: void prot(); + private: void priv(); + }; + void Foo::pub() { this->^ } + )cpp"); + EXPECT_THAT(Internal.Completions, + HasSubsequence(Named("priv"), Named("prot"), Named("pub"))); + + auto External = completions(R"cpp( + class Foo { + public: void pub(); + protected: void prot(); + private: void priv(); + }; + void test() { + Foo F; + F.^ + } + )cpp"); + EXPECT_THAT(External.Completions, + AllOf(Has("pub"), Not(Has("prot")), Not(Has("priv")))); +} + +TEST(CompletionTest, Qualifiers) { + auto Results = completions(R"cpp( + class Foo { + public: int foo() const; + int bar() const; + }; + class Bar : public Foo { + int foo() const; + }; + void test() { Bar().^ } + )cpp"); + EXPECT_THAT(Results.Completions, + Contains(AllOf(Qualifier(""), Named("bar")))); + // Hidden members are not shown. + EXPECT_THAT(Results.Completions, + Not(Contains(AllOf(Qualifier("Foo::"), Named("foo"))))); + // Private members are not shown. + EXPECT_THAT(Results.Completions, + Not(Contains(AllOf(Qualifier(""), Named("foo"))))); +} + +TEST(CompletionTest, InjectedTypename) { + // These are suppressed when accessed as a member... + EXPECT_THAT(completions("struct X{}; void foo(){ X().^ }").Completions, + Not(Has("X"))); + EXPECT_THAT(completions("struct X{ void foo(){ this->^ } };").Completions, + Not(Has("X"))); + // ...but accessible in other, more useful cases. + EXPECT_THAT(completions("struct X{ void foo(){ ^ } };").Completions, + Has("X")); + EXPECT_THAT( + completions("struct Y{}; struct X:Y{ void foo(){ ^ } };").Completions, + Has("Y")); + EXPECT_THAT( + completions( + "template struct Y{}; struct X:Y{ void foo(){ ^ } };") + .Completions, + Has("Y")); + // This case is marginal (`using X::X` is useful), we allow it for now. + EXPECT_THAT(completions("struct X{}; void foo(){ X::^ }").Completions, + Has("X")); +} + +TEST(CompletionTest, SkipInjectedWhenUnqualified) { + EXPECT_THAT(completions("struct X { void f() { X^ }};").Completions, + ElementsAre(Named("X"), Named("~X"))); +} + +TEST(CompletionTest, Snippets) { + clangd::CodeCompleteOptions Opts; + auto Results = completions( + R"cpp( + struct fake { + int a; + int f(int i, const float f) const; + }; + int main() { + fake f; + f.^ + } + )cpp", + /*IndexSymbols=*/{}, Opts); + EXPECT_THAT( + Results.Completions, + HasSubsequence(Named("a"), + SnippetSuffix("(${1:int i}, ${2:const float f})"))); +} + +TEST(CompletionTest, Kinds) { + auto Results = completions( + R"cpp( + int variable; + struct Struct {}; + int function(); + // make sure MACRO is not included in preamble. + #define MACRO 10 + int X = ^ + )cpp", + {func("indexFunction"), var("indexVariable"), cls("indexClass")}); + EXPECT_THAT(Results.Completions, + AllOf(Has("function", CompletionItemKind::Function), + Has("variable", CompletionItemKind::Variable), + Has("int", CompletionItemKind::Keyword), + Has("Struct", CompletionItemKind::Class), + Has("MACRO", CompletionItemKind::Text), + Has("indexFunction", CompletionItemKind::Function), + Has("indexVariable", CompletionItemKind::Variable), + Has("indexClass", CompletionItemKind::Class))); + + Results = completions("nam^"); + EXPECT_THAT(Results.Completions, + Has("namespace", CompletionItemKind::Snippet)); +} + +TEST(CompletionTest, NoDuplicates) { + auto Results = completions( + R"cpp( + class Adapter { + }; + + void f() { + Adapter^ + } + )cpp", + {cls("Adapter")}); + + // Make sure there are no duplicate entries of 'Adapter'. + EXPECT_THAT(Results.Completions, ElementsAre(Named("Adapter"))); +} + +TEST(CompletionTest, ScopedNoIndex) { + auto Results = completions( + R"cpp( + namespace fake { int BigBang, Babble, Box; }; + int main() { fake::ba^ } + ")cpp"); + // Babble is a better match than BigBang. Box doesn't match at all. + EXPECT_THAT(Results.Completions, + ElementsAre(Named("Babble"), Named("BigBang"))); +} + +TEST(CompletionTest, Scoped) { + auto Results = completions( + R"cpp( + namespace fake { int Babble, Box; }; + int main() { fake::ba^ } + ")cpp", + {var("fake::BigBang")}); + EXPECT_THAT(Results.Completions, + ElementsAre(Named("Babble"), Named("BigBang"))); +} + +TEST(CompletionTest, ScopedWithFilter) { + auto Results = completions( + R"cpp( + void f() { ns::x^ } + )cpp", + {cls("ns::XYZ"), func("ns::foo")}); + EXPECT_THAT(Results.Completions, UnorderedElementsAre(Named("XYZ"))); +} + +TEST(CompletionTest, ReferencesAffectRanking) { + auto Results = completions("int main() { abs^ }", {ns("absl"), func("absb")}); + EXPECT_THAT(Results.Completions, + HasSubsequence(Named("absb"), Named("absl"))); + Results = completions("int main() { abs^ }", + {withReferences(10000, ns("absl")), func("absb")}); + EXPECT_THAT(Results.Completions, + HasSubsequence(Named("absl"), Named("absb"))); +} + +TEST(CompletionTest, GlobalQualified) { + auto Results = completions( + R"cpp( + void f() { ::^ } + )cpp", + {cls("XYZ")}); + EXPECT_THAT(Results.Completions, + AllOf(Has("XYZ", CompletionItemKind::Class), + Has("f", CompletionItemKind::Function))); +} + +TEST(CompletionTest, FullyQualified) { + auto Results = completions( + R"cpp( + namespace ns { void bar(); } + void f() { ::ns::^ } + )cpp", + {cls("ns::XYZ")}); + EXPECT_THAT(Results.Completions, + AllOf(Has("XYZ", CompletionItemKind::Class), + Has("bar", CompletionItemKind::Function))); +} + +TEST(CompletionTest, SemaIndexMerge) { + auto Results = completions( + R"cpp( + namespace ns { int local; void both(); } + void f() { ::ns::^ } + )cpp", + {func("ns::both"), cls("ns::Index")}); + // We get results from both index and sema, with no duplicates. + EXPECT_THAT(Results.Completions, + UnorderedElementsAre( + AllOf(Named("local"), Origin(SymbolOrigin::AST)), + AllOf(Named("Index"), Origin(SymbolOrigin::Static)), + AllOf(Named("both"), + Origin(SymbolOrigin::AST | SymbolOrigin::Static)))); +} + +TEST(CompletionTest, SemaIndexMergeWithLimit) { + clangd::CodeCompleteOptions Opts; + Opts.Limit = 1; + auto Results = completions( + R"cpp( + namespace ns { int local; void both(); } + void f() { ::ns::^ } + )cpp", + {func("ns::both"), cls("ns::Index")}, Opts); + EXPECT_EQ(Results.Completions.size(), Opts.Limit); + EXPECT_TRUE(Results.HasMore); +} + +TEST(CompletionTest, IncludeInsertionPreprocessorIntegrationTests) { + MockFSProvider FS; + MockCompilationDatabase CDB; + std::string Subdir = testPath("sub"); + std::string SearchDirArg = (Twine("-I") + Subdir).str(); + CDB.ExtraClangFlags = {SearchDirArg.c_str()}; + std::string BarHeader = testPath("sub/bar.h"); + FS.Files[BarHeader] = ""; + + IgnoreDiagnostics DiagConsumer; + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + auto BarURI = URI::create(BarHeader).toString(); + Symbol Sym = cls("ns::X"); + Sym.CanonicalDeclaration.FileURI = BarURI.c_str(); + Sym.IncludeHeaders.emplace_back(BarURI, 1); + // Shoten include path based on search dirctory and insert. + auto Results = completions(Server, + R"cpp( + int main() { ns::^ } + )cpp", + {Sym}); + EXPECT_THAT(Results.Completions, + ElementsAre(AllOf(Named("X"), InsertInclude("\"bar.h\"")))); + // Can be disabled via option. + CodeCompleteOptions NoInsertion; + NoInsertion.InsertIncludes = CodeCompleteOptions::NeverInsert; + Results = completions(Server, + R"cpp( + int main() { ns::^ } + )cpp", + {Sym}, NoInsertion); + EXPECT_THAT(Results.Completions, + ElementsAre(AllOf(Named("X"), Not(InsertInclude())))); + // Duplicate based on inclusions in preamble. + Results = completions(Server, + R"cpp( + #include "sub/bar.h" // not shortest, so should only match resolved. + int main() { ns::^ } + )cpp", + {Sym}); + EXPECT_THAT(Results.Completions, ElementsAre(AllOf(Named("X"), Labeled("X"), + Not(InsertInclude())))); +} + +TEST(CompletionTest, NoIncludeInsertionWhenDeclFoundInFile) { + MockFSProvider FS; + MockCompilationDatabase CDB; + + IgnoreDiagnostics DiagConsumer; + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + Symbol SymX = cls("ns::X"); + Symbol SymY = cls("ns::Y"); + std::string BarHeader = testPath("bar.h"); + auto BarURI = URI::create(BarHeader).toString(); + SymX.CanonicalDeclaration.FileURI = BarURI.c_str(); + SymY.CanonicalDeclaration.FileURI = BarURI.c_str(); + SymX.IncludeHeaders.emplace_back("", 1); + SymY.IncludeHeaders.emplace_back("", 1); + // Shoten include path based on search dirctory and insert. + auto Results = completions(Server, + R"cpp( + namespace ns { + class X; + class Y {}; + } + int main() { ns::^ } + )cpp", + {SymX, SymY}); + EXPECT_THAT(Results.Completions, + ElementsAre(AllOf(Named("X"), Not(InsertInclude())), + AllOf(Named("Y"), Not(InsertInclude())))); +} + +TEST(CompletionTest, IndexSuppressesPreambleCompletions) { + MockFSProvider FS; + MockCompilationDatabase CDB; + IgnoreDiagnostics DiagConsumer; + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + + FS.Files[testPath("bar.h")] = + R"cpp(namespace ns { struct preamble { int member; }; })cpp"; + auto File = testPath("foo.cpp"); + Annotations Test(R"cpp( + #include "bar.h" + namespace ns { int local; } + void f() { ns::^; } + void f2() { ns::preamble().$2^; } + )cpp"); + runAddDocument(Server, File, Test.code()); + clangd::CodeCompleteOptions Opts = {}; + + auto I = memIndex({var("ns::index")}); + Opts.Index = I.get(); + auto WithIndex = cantFail(runCodeComplete(Server, File, Test.point(), Opts)); + EXPECT_THAT(WithIndex.Completions, + UnorderedElementsAre(Named("local"), Named("index"))); + auto ClassFromPreamble = + cantFail(runCodeComplete(Server, File, Test.point("2"), Opts)); + EXPECT_THAT(ClassFromPreamble.Completions, Contains(Named("member"))); + + Opts.Index = nullptr; + auto WithoutIndex = + cantFail(runCodeComplete(Server, File, Test.point(), Opts)); + EXPECT_THAT(WithoutIndex.Completions, + UnorderedElementsAre(Named("local"), Named("preamble"))); +} + +// This verifies that we get normal preprocessor completions in the preamble. +// This is a regression test for an old bug: if we override the preamble and +// try to complete inside it, clang kicks our completion point just outside the +// preamble, resulting in always getting top-level completions. +TEST(CompletionTest, CompletionInPreamble) { + auto Results = completions(R"cpp( + #ifnd^ef FOO_H_ + #define BAR_H_ + #include + int foo() {} + #endif + )cpp") + .Completions; + EXPECT_THAT(Results, ElementsAre(Named("ifndef"))); +} + +TEST(CompletionTest, DynamicIndexIncludeInsertion) { + MockFSProvider FS; + MockCompilationDatabase CDB; + IgnoreDiagnostics DiagConsumer; + ClangdServer::Options Opts = ClangdServer::optsForTest(); + Opts.BuildDynamicSymbolIndex = true; + ClangdServer Server(CDB, FS, DiagConsumer, Opts); + + FS.Files[testPath("foo_header.h")] = R"cpp( + #pragma once + struct Foo { + // Member doc + int foo(); + }; + )cpp"; + const std::string FileContent(R"cpp( + #include "foo_header.h" + int Foo::foo() { + return 42; + } + )cpp"); + Server.addDocument(testPath("foo_impl.cpp"), FileContent); + // Wait for the dynamic index being built. + ASSERT_TRUE(Server.blockUntilIdleForTest()); + EXPECT_THAT(completions(Server, "Foo^ foo;").Completions, + ElementsAre(AllOf(Named("Foo"), + HasInclude('"' + + llvm::sys::path::convert_to_slash( + testPath("foo_header.h")) + + '"'), + InsertInclude()))); +} + +TEST(CompletionTest, DynamicIndexMultiFile) { + MockFSProvider FS; + MockCompilationDatabase CDB; + IgnoreDiagnostics DiagConsumer; + auto Opts = ClangdServer::optsForTest(); + Opts.BuildDynamicSymbolIndex = true; + ClangdServer Server(CDB, FS, DiagConsumer, Opts); + + FS.Files[testPath("foo.h")] = R"cpp( + namespace ns { class XYZ {}; void foo(int x) {} } + )cpp"; + runAddDocument(Server, testPath("foo.cpp"), R"cpp( + #include "foo.h" + )cpp"); + + auto File = testPath("bar.cpp"); + Annotations Test(R"cpp( + namespace ns { + class XXX {}; + /// Doooc + void fooooo() {} + } + void f() { ns::^ } + )cpp"); + runAddDocument(Server, File, Test.code()); + + auto Results = cantFail(runCodeComplete(Server, File, Test.point(), {})); + // "XYZ" and "foo" are not included in the file being completed but are still + // visible through the index. + EXPECT_THAT(Results.Completions, Has("XYZ", CompletionItemKind::Class)); + EXPECT_THAT(Results.Completions, Has("foo", CompletionItemKind::Function)); + EXPECT_THAT(Results.Completions, Has("XXX", CompletionItemKind::Class)); + EXPECT_THAT(Results.Completions, + Contains((Named("fooooo"), Kind(CompletionItemKind::Function), + Doc("Doooc"), ReturnType("void")))); +} + +TEST(CompletionTest, Documentation) { + auto Results = completions( + R"cpp( + // Non-doxygen comment. + int foo(); + /// Doxygen comment. + /// \param int a + int bar(int a); + /* Multi-line + block comment + */ + int baz(); + + int x = ^ + )cpp"); + EXPECT_THAT(Results.Completions, + Contains(AllOf(Named("foo"), Doc("Non-doxygen comment.")))); + EXPECT_THAT( + Results.Completions, + Contains(AllOf(Named("bar"), Doc("Doxygen comment.\n\\param int a")))); + EXPECT_THAT(Results.Completions, + Contains(AllOf(Named("baz"), Doc("Multi-line\nblock comment")))); +} + +TEST(CompletionTest, GlobalCompletionFiltering) { + + Symbol Class = cls("XYZ"); + Class.Flags = static_cast( + Class.Flags & ~(Symbol::IndexedForCodeCompletion)); + Symbol Func = func("XYZ::foooo"); + Func.Flags = static_cast( + Func.Flags & ~(Symbol::IndexedForCodeCompletion)); + + auto Results = completions(R"(// void f() { + XYZ::foooo^ + })", + {Class, Func}); + EXPECT_THAT(Results.Completions, IsEmpty()); +} + +TEST(CodeCompleteTest, DisableTypoCorrection) { + auto Results = completions(R"cpp( + namespace clang { int v; } + void f() { clangd::^ + )cpp"); + EXPECT_TRUE(Results.Completions.empty()); +} + +TEST(CodeCompleteTest, NoColonColonAtTheEnd) { + auto Results = completions(R"cpp( + namespace clang { } + void f() { + clan^ + } + )cpp"); + + EXPECT_THAT(Results.Completions, Contains(Labeled("clang"))); + EXPECT_THAT(Results.Completions, Not(Contains(Labeled("clang::")))); +} + +TEST(CompletionTest, BacktrackCrashes) { + // Sema calls code completion callbacks twice in these cases. + auto Results = completions(R"cpp( + namespace ns { + struct FooBarBaz {}; + } // namespace ns + + int foo(ns::FooBar^ + )cpp"); + + EXPECT_THAT(Results.Completions, ElementsAre(Labeled("FooBarBaz"))); + + // Check we don't crash in that case too. + completions(R"cpp( + struct FooBarBaz {}; + void test() { + if (FooBarBaz * x^) {} + } +)cpp"); +} + +TEST(CompletionTest, CompleteInMacroWithStringification) { + auto Results = completions(R"cpp( +void f(const char *, int x); +#define F(x) f(#x, x) + +namespace ns { +int X; +int Y; +} // namespace ns + +int f(int input_num) { + F(ns::^) +} +)cpp"); + + EXPECT_THAT(Results.Completions, + UnorderedElementsAre(Named("X"), Named("Y"))); +} + +TEST(CompletionTest, CompleteInMacroAndNamespaceWithStringification) { + auto Results = completions(R"cpp( +void f(const char *, int x); +#define F(x) f(#x, x) + +namespace ns { +int X; + +int f(int input_num) { + F(^) +} +} // namespace ns +)cpp"); + + EXPECT_THAT(Results.Completions, Contains(Named("X"))); +} + +TEST(CompletionTest, IgnoreCompleteInExcludedPPBranchWithRecoveryContext) { + auto Results = completions(R"cpp( + int bar(int param_in_bar) { + } + + int foo(int param_in_foo) { +#if 0 + // In recorvery mode, "param_in_foo" will also be suggested among many other + // unrelated symbols; however, this is really a special case where this works. + // If the #if block is outside of the function, "param_in_foo" is still + // suggested, but "bar" and "foo" are missing. So the recovery mode doesn't + // really provide useful results in excluded branches. + par^ +#endif + } +)cpp"); + + EXPECT_TRUE(Results.Completions.empty()); +} +SignatureHelp signatures(llvm::StringRef Text, Position Point, + std::vector IndexSymbols = {}) { + std::unique_ptr Index; + if (!IndexSymbols.empty()) + Index = memIndex(IndexSymbols); + + MockFSProvider FS; + MockCompilationDatabase CDB; + IgnoreDiagnostics DiagConsumer; + ClangdServer::Options Opts = ClangdServer::optsForTest(); + Opts.StaticIndex = Index.get(); + + ClangdServer Server(CDB, FS, DiagConsumer, Opts); + auto File = testPath("foo.cpp"); + runAddDocument(Server, File, Text); + return llvm::cantFail(runSignatureHelp(Server, File, Point)); +} + +SignatureHelp signatures(llvm::StringRef Text, + std::vector IndexSymbols = {}) { + Annotations Test(Text); + return signatures(Test.code(), Test.point(), std::move(IndexSymbols)); +} + +MATCHER_P(ParamsAre, P, "") { + if (P.size() != arg.parameters.size()) + return false; + for (unsigned I = 0; I < P.size(); ++I) + if (P[I] != arg.parameters[I].label) + return false; + return true; +} +MATCHER_P(SigDoc, Doc, "") { return arg.documentation == Doc; } + +Matcher Sig(std::string Label, + std::vector Params) { + return AllOf(SigHelpLabeled(Label), ParamsAre(Params)); +} + +TEST(SignatureHelpTest, Overloads) { + auto Results = signatures(R"cpp( + void foo(int x, int y); + void foo(int x, float y); + void foo(float x, int y); + void foo(float x, float y); + void bar(int x, int y = 0); + int main() { foo(^); } + )cpp"); + EXPECT_THAT(Results.signatures, + UnorderedElementsAre( + Sig("foo(float x, float y) -> void", {"float x", "float y"}), + Sig("foo(float x, int y) -> void", {"float x", "int y"}), + Sig("foo(int x, float y) -> void", {"int x", "float y"}), + Sig("foo(int x, int y) -> void", {"int x", "int y"}))); + // We always prefer the first signature. + EXPECT_EQ(0, Results.activeSignature); + EXPECT_EQ(0, Results.activeParameter); +} + +TEST(SignatureHelpTest, DefaultArgs) { + auto Results = signatures(R"cpp( + void bar(int x, int y = 0); + void bar(float x = 0, int y = 42); + int main() { bar(^ + )cpp"); + EXPECT_THAT(Results.signatures, + UnorderedElementsAre( + Sig("bar(int x, int y = 0) -> void", {"int x", "int y = 0"}), + Sig("bar(float x = 0, int y = 42) -> void", + {"float x = 0", "int y = 42"}))); + EXPECT_EQ(0, Results.activeSignature); + EXPECT_EQ(0, Results.activeParameter); +} + +TEST(SignatureHelpTest, ActiveArg) { + auto Results = signatures(R"cpp( + int baz(int a, int b, int c); + int main() { baz(baz(1,2,3), ^); } + )cpp"); + EXPECT_THAT(Results.signatures, + ElementsAre(Sig("baz(int a, int b, int c) -> int", + {"int a", "int b", "int c"}))); + EXPECT_EQ(0, Results.activeSignature); + EXPECT_EQ(1, Results.activeParameter); +} + +TEST(SignatureHelpTest, OpeningParen) { + llvm::StringLiteral Tests[] = {// Recursive function call. + R"cpp( + int foo(int a, int b, int c); + int main() { + foo(foo $p^( foo(10, 10, 10), ^ ))); + })cpp", + // Functional type cast. + R"cpp( + struct Foo { + Foo(int a, int b, int c); + }; + int main() { + Foo $p^( 10, ^ ); + })cpp", + // New expression. + R"cpp( + struct Foo { + Foo(int a, int b, int c); + }; + int main() { + new Foo $p^( 10, ^ ); + })cpp", + // Macro expansion. + R"cpp( + int foo(int a, int b, int c); + #define FOO foo( + + int main() { + // Macro expansions. + $p^FOO 10, ^ ); + })cpp", + // Macro arguments. + R"cpp( + int foo(int a, int b, int c); + int main() { + #define ID(X) X + ID(foo $p^( foo(10), ^ )) + })cpp"}; + + for (auto Test : Tests) { + Annotations Code(Test); + EXPECT_EQ(signatures(Code.code(), Code.point()).argListStart, + Code.point("p")) + << "Test source:" << Test; + } +} + +class IndexRequestCollector : public SymbolIndex { +public: + bool + fuzzyFind(const FuzzyFindRequest &Req, + llvm::function_ref Callback) const override { + std::lock_guard Lock(Mut); + Requests.push_back(Req); + return true; + } + + void lookup(const LookupRequest &, + llvm::function_ref) const override {} + + void refs(const RefsRequest &, + llvm::function_ref) const override {} + + // This is incorrect, but IndexRequestCollector is not an actual index and it + // isn't used in production code. + size_t estimateMemoryUsage() const override { return 0; } + + const std::vector consumeRequests() const { + std::lock_guard Lock(Mut); + auto Reqs = std::move(Requests); + Requests = {}; + return Reqs; + } + +private: + // We need a mutex to handle async fuzzy find requests. + mutable std::mutex Mut; + mutable std::vector Requests; +}; + +std::vector captureIndexRequests(llvm::StringRef Code) { + clangd::CodeCompleteOptions Opts; + IndexRequestCollector Requests; + Opts.Index = &Requests; + completions(Code, {}, Opts); + return Requests.consumeRequests(); +} + +TEST(CompletionTest, UnqualifiedIdQuery) { + auto Requests = captureIndexRequests(R"cpp( + namespace std {} + using namespace std; + namespace ns { + void f() { + vec^ + } + } + )cpp"); + + EXPECT_THAT(Requests, + ElementsAre(Field(&FuzzyFindRequest::Scopes, + UnorderedElementsAre("", "ns::", "std::")))); +} + +TEST(CompletionTest, EnclosingScopeComesFirst) { + auto Requests = captureIndexRequests(R"cpp( + namespace std {} + using namespace std; + namespace nx { + namespace ns { + namespace { + void f() { + vec^ + } + } + } + } + )cpp"); + + EXPECT_THAT(Requests, + ElementsAre(Field( + &FuzzyFindRequest::Scopes, + UnorderedElementsAre("", "std::", "nx::ns::", "nx::")))); + EXPECT_EQ(Requests[0].Scopes[0], "nx::ns::"); +} + +TEST(CompletionTest, ResolvedQualifiedIdQuery) { + auto Requests = captureIndexRequests(R"cpp( + namespace ns1 {} + namespace ns2 {} // ignore + namespace ns3 { namespace nns3 {} } + namespace foo { + using namespace ns1; + using namespace ns3::nns3; + } + namespace ns { + void f() { + foo::^ + } + } + )cpp"); + + EXPECT_THAT(Requests, + ElementsAre(Field( + &FuzzyFindRequest::Scopes, + UnorderedElementsAre("foo::", "ns1::", "ns3::nns3::")))); +} + +TEST(CompletionTest, UnresolvedQualifierIdQuery) { + auto Requests = captureIndexRequests(R"cpp( + namespace a {} + using namespace a; + namespace ns { + void f() { + bar::^ + } + } // namespace ns + )cpp"); + + EXPECT_THAT(Requests, + ElementsAre(Field( + &FuzzyFindRequest::Scopes, + UnorderedElementsAre("a::bar::", "ns::bar::", "bar::")))); +} + +TEST(CompletionTest, UnresolvedNestedQualifierIdQuery) { + auto Requests = captureIndexRequests(R"cpp( + namespace a {} + using namespace a; + namespace ns { + void f() { + ::a::bar::^ + } + } // namespace ns + )cpp"); + + EXPECT_THAT(Requests, ElementsAre(Field(&FuzzyFindRequest::Scopes, + UnorderedElementsAre("a::bar::")))); +} + +TEST(CompletionTest, EmptyQualifiedQuery) { + auto Requests = captureIndexRequests(R"cpp( + namespace ns { + void f() { + ^ + } + } // namespace ns + )cpp"); + + EXPECT_THAT(Requests, ElementsAre(Field(&FuzzyFindRequest::Scopes, + UnorderedElementsAre("", "ns::")))); +} + +TEST(CompletionTest, GlobalQualifiedQuery) { + auto Requests = captureIndexRequests(R"cpp( + namespace ns { + void f() { + ::^ + } + } // namespace ns + )cpp"); + + EXPECT_THAT(Requests, ElementsAre(Field(&FuzzyFindRequest::Scopes, + UnorderedElementsAre("")))); +} + +TEST(CompletionTest, NoDuplicatedQueryScopes) { + auto Requests = captureIndexRequests(R"cpp( + namespace {} + + namespace na { + namespace {} + namespace nb { + ^ + } // namespace nb + } // namespace na + )cpp"); + + EXPECT_THAT(Requests, + ElementsAre(Field(&FuzzyFindRequest::Scopes, + UnorderedElementsAre("na::", "na::nb::", "")))); +} + +TEST(CompletionTest, NoIndexCompletionsInsideClasses) { + auto Completions = completions( + R"cpp( + struct Foo { + int SomeNameOfField; + typedef int SomeNameOfTypedefField; + }; + + Foo::^)cpp", + {func("::SomeNameInTheIndex"), func("::Foo::SomeNameInTheIndex")}); + + EXPECT_THAT(Completions.Completions, + AllOf(Contains(Labeled("SomeNameOfField")), + Contains(Labeled("SomeNameOfTypedefField")), + Not(Contains(Labeled("SomeNameInTheIndex"))))); +} + +TEST(CompletionTest, NoIndexCompletionsInsideDependentCode) { + { + auto Completions = completions( + R"cpp( + template + void foo() { + T::^ + } + )cpp", + {func("::SomeNameInTheIndex")}); + + EXPECT_THAT(Completions.Completions, + Not(Contains(Labeled("SomeNameInTheIndex")))); + } + + { + auto Completions = completions( + R"cpp( + template + void foo() { + T::template Y::^ + } + )cpp", + {func("::SomeNameInTheIndex")}); + + EXPECT_THAT(Completions.Completions, + Not(Contains(Labeled("SomeNameInTheIndex")))); + } + + { + auto Completions = completions( + R"cpp( + template + void foo() { + T::foo::^ + } + )cpp", + {func("::SomeNameInTheIndex")}); + + EXPECT_THAT(Completions.Completions, + Not(Contains(Labeled("SomeNameInTheIndex")))); + } +} + +TEST(CompletionTest, OverloadBundling) { + clangd::CodeCompleteOptions Opts; + Opts.BundleOverloads = true; + + std::string Context = R"cpp( + struct X { + // Overload with int + int a(int); + // Overload with bool + int a(bool); + int b(float); + }; + int GFuncC(int); + int GFuncD(int); + )cpp"; + + // Member completions are bundled. + EXPECT_THAT(completions(Context + "int y = X().^", {}, Opts).Completions, + UnorderedElementsAre(Labeled("a(…)"), Labeled("b(float)"))); + + // Non-member completions are bundled, including index+sema. + Symbol NoArgsGFunc = func("GFuncC"); + EXPECT_THAT( + completions(Context + "int y = GFunc^", {NoArgsGFunc}, Opts).Completions, + UnorderedElementsAre(Labeled("GFuncC(…)"), Labeled("GFuncD(int)"))); + + // Differences in header-to-insert suppress bundling. + std::string DeclFile = URI::create(testPath("foo")).toString(); + NoArgsGFunc.CanonicalDeclaration.FileURI = DeclFile.c_str(); + NoArgsGFunc.IncludeHeaders.emplace_back("", 1); + EXPECT_THAT( + completions(Context + "int y = GFunc^", {NoArgsGFunc}, Opts).Completions, + UnorderedElementsAre(AllOf(Named("GFuncC"), InsertInclude("")), + Labeled("GFuncC(int)"), Labeled("GFuncD(int)"))); + + // Examine a bundled completion in detail. + auto A = + completions(Context + "int y = X().a^", {}, Opts).Completions.front(); + EXPECT_EQ(A.Name, "a"); + EXPECT_EQ(A.Signature, "(…)"); + EXPECT_EQ(A.BundleSize, 2u); + EXPECT_EQ(A.Kind, CompletionItemKind::Method); + EXPECT_EQ(A.ReturnType, "int"); // All overloads return int. + // For now we just return one of the doc strings arbitrarily. + EXPECT_THAT(A.Documentation, AnyOf(HasSubstr("Overload with int"), + HasSubstr("Overload with bool"))); + EXPECT_EQ(A.SnippetSuffix, "($0)"); +} + +TEST(CompletionTest, DocumentationFromChangedFileCrash) { + MockFSProvider FS; + auto FooH = testPath("foo.h"); + auto FooCpp = testPath("foo.cpp"); + FS.Files[FooH] = R"cpp( + // this is my documentation comment. + int func(); + )cpp"; + FS.Files[FooCpp] = ""; + + MockCompilationDatabase CDB; + IgnoreDiagnostics DiagConsumer; + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + + Annotations Source(R"cpp( + #include "foo.h" + int func() { + // This makes sure we have func from header in the AST. + } + int a = fun^ + )cpp"); + Server.addDocument(FooCpp, Source.code(), WantDiagnostics::Yes); + // We need to wait for preamble to build. + ASSERT_TRUE(Server.blockUntilIdleForTest()); + + // Change the header file. Completion will reuse the old preamble! + FS.Files[FooH] = R"cpp( + int func(); + )cpp"; + + clangd::CodeCompleteOptions Opts; + Opts.IncludeComments = true; + CodeCompleteResult Completions = + cantFail(runCodeComplete(Server, FooCpp, Source.point(), Opts)); + // We shouldn't crash. Unfortunately, current workaround is to not produce + // comments for symbols from headers. + EXPECT_THAT(Completions.Completions, + Contains(AllOf(Not(IsDocumented()), Named("func")))); +} + +TEST(CompletionTest, NonDocComments) { + MockFSProvider FS; + auto FooCpp = testPath("foo.cpp"); + FS.Files[FooCpp] = ""; + + MockCompilationDatabase CDB; + IgnoreDiagnostics DiagConsumer; + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + + Annotations Source(R"cpp( + // We ignore namespace comments, for rationale see CodeCompletionStrings.h. + namespace comments_ns { + } + + // ------------------ + int comments_foo(); + + // A comment and a decl are separated by newlines. + // Therefore, the comment shouldn't show up as doc comment. + + int comments_bar(); + + // this comment should be in the results. + int comments_baz(); + + + template + struct Struct { + int comments_qux(); + int comments_quux(); + }; + + + // This comment should not be there. + + template + int Struct::comments_qux() { + } + + // This comment **should** be in results. + template + int Struct::comments_quux() { + int a = comments^; + } + )cpp"); + // FIXME: Auto-completion in a template requires disabling delayed template + // parsing. + CDB.ExtraClangFlags.push_back("-fno-delayed-template-parsing"); + runAddDocument(Server, FooCpp, Source.code(), WantDiagnostics::Yes); + CodeCompleteResult Completions = cantFail(runCodeComplete( + Server, FooCpp, Source.point(), clangd::CodeCompleteOptions())); + + // We should not get any of those comments in completion. + EXPECT_THAT( + Completions.Completions, + UnorderedElementsAre(AllOf(Not(IsDocumented()), Named("comments_foo")), + AllOf(IsDocumented(), Named("comments_baz")), + AllOf(IsDocumented(), Named("comments_quux")), + AllOf(Not(IsDocumented()), Named("comments_ns")), + // FIXME(ibiryukov): the following items should have + // empty documentation, since they are separated from + // a comment with an empty line. Unfortunately, I + // couldn't make Sema tests pass if we ignore those. + AllOf(IsDocumented(), Named("comments_bar")), + AllOf(IsDocumented(), Named("comments_qux")))); +} + +TEST(CompletionTest, CompleteOnInvalidLine) { + auto FooCpp = testPath("foo.cpp"); + + MockCompilationDatabase CDB; + IgnoreDiagnostics DiagConsumer; + MockFSProvider FS; + FS.Files[FooCpp] = "// empty file"; + + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + // Run completion outside the file range. + Position Pos; + Pos.line = 100; + Pos.character = 0; + EXPECT_THAT_EXPECTED( + runCodeComplete(Server, FooCpp, Pos, clangd::CodeCompleteOptions()), + Failed()); +} + +TEST(CompletionTest, QualifiedNames) { + auto Results = completions( + R"cpp( + namespace ns { int local; void both(); } + void f() { ::ns::^ } + )cpp", + {func("ns::both"), cls("ns::Index")}); + // We get results from both index and sema, with no duplicates. + EXPECT_THAT( + Results.Completions, + UnorderedElementsAre(Scope("ns::"), Scope("ns::"), Scope("ns::"))); +} + +TEST(CompletionTest, Render) { + CodeCompletion C; + C.Name = "x"; + C.Signature = "(bool) const"; + C.SnippetSuffix = "(${0:bool})"; + C.ReturnType = "int"; + C.RequiredQualifier = "Foo::"; + C.Scope = "ns::Foo::"; + C.Documentation = "This is x()."; + C.Includes.emplace_back(); + auto &Include = C.Includes.back(); + Include.Header = "\"foo.h\""; + C.Kind = CompletionItemKind::Method; + C.Score.Total = 1.0; + C.Origin = SymbolOrigin::AST | SymbolOrigin::Static; + + CodeCompleteOptions Opts; + Opts.IncludeIndicator.Insert = "^"; + Opts.IncludeIndicator.NoInsert = ""; + Opts.EnableSnippets = false; + + auto R = C.render(Opts); + EXPECT_EQ(R.label, "Foo::x(bool) const"); + EXPECT_EQ(R.insertText, "Foo::x"); + EXPECT_EQ(R.insertTextFormat, InsertTextFormat::PlainText); + EXPECT_EQ(R.filterText, "x"); + EXPECT_EQ(R.detail, "int\n\"foo.h\""); + EXPECT_EQ(R.documentation, "This is x()."); + EXPECT_THAT(R.additionalTextEdits, IsEmpty()); + EXPECT_EQ(R.sortText, sortText(1.0, "x")); + EXPECT_FALSE(R.deprecated); + + Opts.EnableSnippets = true; + R = C.render(Opts); + EXPECT_EQ(R.insertText, "Foo::x(${0:bool})"); + EXPECT_EQ(R.insertTextFormat, InsertTextFormat::Snippet); + + Include.Insertion.emplace(); + R = C.render(Opts); + EXPECT_EQ(R.label, "^Foo::x(bool) const"); + EXPECT_THAT(R.additionalTextEdits, Not(IsEmpty())); + + Opts.ShowOrigins = true; + R = C.render(Opts); + EXPECT_EQ(R.label, "^[AS]Foo::x(bool) const"); + + C.BundleSize = 2; + R = C.render(Opts); + EXPECT_EQ(R.detail, "[2 overloads]\n\"foo.h\""); + + C.Deprecated = true; + R = C.render(Opts); + EXPECT_TRUE(R.deprecated); +} + +TEST(CompletionTest, IgnoreRecoveryResults) { + auto Results = completions( + R"cpp( + namespace ns { int NotRecovered() { return 0; } } + void f() { + // Sema enters recovery mode first and then normal mode. + if (auto x = ns::NotRecover^) + } + )cpp"); + EXPECT_THAT(Results.Completions, UnorderedElementsAre(Named("NotRecovered"))); +} + +TEST(CompletionTest, ScopeOfClassFieldInConstructorInitializer) { + auto Results = completions( + R"cpp( + namespace ns { + class X { public: X(); int x_; }; + X::X() : x_^(0) {} + } + )cpp"); + EXPECT_THAT(Results.Completions, + UnorderedElementsAre(AllOf(Scope("ns::X::"), Named("x_")))); +} + +TEST(CompletionTest, CodeCompletionContext) { + auto Results = completions( + R"cpp( + namespace ns { + class X { public: X(); int x_; }; + void f() { + X x; + x.^; + } + } + )cpp"); + + EXPECT_THAT(Results.Context, CodeCompletionContext::CCC_DotMemberAccess); +} + +TEST(CompletionTest, FixItForArrowToDot) { + MockFSProvider FS; + MockCompilationDatabase CDB; + IgnoreDiagnostics DiagConsumer; + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + + CodeCompleteOptions Opts; + Opts.IncludeFixIts = true; + Annotations TestCode( + R"cpp( + class Auxilary { + public: + void AuxFunction(); + }; + class ClassWithPtr { + public: + void MemberFunction(); + Auxilary* operator->() const; + Auxilary* Aux; + }; + void f() { + ClassWithPtr x; + x[[->]]^; + } + )cpp"); + auto Results = + completions(Server, TestCode.code(), TestCode.point(), {}, Opts); + EXPECT_EQ(Results.Completions.size(), 3u); + + TextEdit ReplacementEdit; + ReplacementEdit.range = TestCode.range(); + ReplacementEdit.newText = "."; + for (const auto &C : Results.Completions) { + EXPECT_TRUE(C.FixIts.size() == 1u || C.Name == "AuxFunction"); + if (!C.FixIts.empty()) { + EXPECT_THAT(C.FixIts, ElementsAre(ReplacementEdit)); + } + } +} + +TEST(CompletionTest, FixItForDotToArrow) { + MockFSProvider FS; + MockCompilationDatabase CDB; + IgnoreDiagnostics DiagConsumer; + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + + CodeCompleteOptions Opts; + Opts.IncludeFixIts = true; + Annotations TestCode( + R"cpp( + class Auxilary { + public: + void AuxFunction(); + }; + class ClassWithPtr { + public: + void MemberFunction(); + Auxilary* operator->() const; + Auxilary* Aux; + }; + void f() { + ClassWithPtr x; + x[[.]]^; + } + )cpp"); + auto Results = + completions(Server, TestCode.code(), TestCode.point(), {}, Opts); + EXPECT_EQ(Results.Completions.size(), 3u); + + TextEdit ReplacementEdit; + ReplacementEdit.range = TestCode.range(); + ReplacementEdit.newText = "->"; + for (const auto &C : Results.Completions) { + EXPECT_TRUE(C.FixIts.empty() || C.Name == "AuxFunction"); + if (!C.FixIts.empty()) { + EXPECT_THAT(C.FixIts, ElementsAre(ReplacementEdit)); + } + } +} + +TEST(CompletionTest, RenderWithFixItMerged) { + TextEdit FixIt; + FixIt.range.end.character = 5; + FixIt.newText = "->"; + + CodeCompletion C; + C.Name = "x"; + C.RequiredQualifier = "Foo::"; + C.FixIts = {FixIt}; + C.CompletionTokenRange.start.character = 5; + + CodeCompleteOptions Opts; + Opts.IncludeFixIts = true; + + auto R = C.render(Opts); + EXPECT_TRUE(R.textEdit); + EXPECT_EQ(R.textEdit->newText, "->Foo::x"); + EXPECT_TRUE(R.additionalTextEdits.empty()); +} + +TEST(CompletionTest, RenderWithFixItNonMerged) { + TextEdit FixIt; + FixIt.range.end.character = 4; + FixIt.newText = "->"; + + CodeCompletion C; + C.Name = "x"; + C.RequiredQualifier = "Foo::"; + C.FixIts = {FixIt}; + C.CompletionTokenRange.start.character = 5; + + CodeCompleteOptions Opts; + Opts.IncludeFixIts = true; + + auto R = C.render(Opts); + EXPECT_TRUE(R.textEdit); + EXPECT_EQ(R.textEdit->newText, "Foo::x"); + EXPECT_THAT(R.additionalTextEdits, UnorderedElementsAre(FixIt)); +} + +TEST(CompletionTest, CompletionTokenRange) { + MockFSProvider FS; + MockCompilationDatabase CDB; + IgnoreDiagnostics DiagConsumer; + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + + constexpr const char *TestCodes[] = { + R"cpp( + class Auxilary { + public: + void AuxFunction(); + }; + void f() { + Auxilary x; + x.[[Aux]]^; + } + )cpp", + R"cpp( + class Auxilary { + public: + void AuxFunction(); + }; + void f() { + Auxilary x; + x.[[]]^; + } + )cpp"}; + for (const auto &Text : TestCodes) { + Annotations TestCode(Text); + auto Results = completions(Server, TestCode.code(), TestCode.point()); + + EXPECT_EQ(Results.Completions.size(), 1u); + EXPECT_THAT(Results.Completions.front().CompletionTokenRange, + TestCode.range()); + } +} + +TEST(SignatureHelpTest, OverloadsOrdering) { + const auto Results = signatures(R"cpp( + void foo(int x); + void foo(int x, float y); + void foo(float x, int y); + void foo(float x, float y); + void foo(int x, int y = 0); + int main() { foo(^); } + )cpp"); + EXPECT_THAT( + Results.signatures, + ElementsAre( + Sig("foo(int x) -> void", {"int x"}), + Sig("foo(int x, int y = 0) -> void", {"int x", "int y = 0"}), + Sig("foo(float x, int y) -> void", {"float x", "int y"}), + Sig("foo(int x, float y) -> void", {"int x", "float y"}), + Sig("foo(float x, float y) -> void", {"float x", "float y"}))); + // We always prefer the first signature. + EXPECT_EQ(0, Results.activeSignature); + EXPECT_EQ(0, Results.activeParameter); +} + +TEST(SignatureHelpTest, InstantiatedSignatures) { + StringRef Sig0 = R"cpp( + template + void foo(T, T, T); + + int main() { + foo(^); + } + )cpp"; + + EXPECT_THAT(signatures(Sig0).signatures, + ElementsAre(Sig("foo(T, T, T) -> void", {"T", "T", "T"}))); + + StringRef Sig1 = R"cpp( + template + void foo(T, T, T); + + int main() { + foo(10, ^); + })cpp"; + + EXPECT_THAT(signatures(Sig1).signatures, + ElementsAre(Sig("foo(T, T, T) -> void", {"T", "T", "T"}))); + + StringRef Sig2 = R"cpp( + template + void foo(T...); + + int main() { + foo(^); + } + )cpp"; + + EXPECT_THAT(signatures(Sig2).signatures, + ElementsAre(Sig("foo(T...) -> void", {"T..."}))); + + // It is debatable whether we should substitute the outer template parameter + // ('T') in that case. Currently we don't substitute it in signature help, but + // do substitute in code complete. + // FIXME: make code complete and signature help consistent, figure out which + // way is better. + StringRef Sig3 = R"cpp( + template + struct X { + template + void foo(T, U); + }; + + int main() { + X().foo(^) + } + )cpp"; + + EXPECT_THAT(signatures(Sig3).signatures, + ElementsAre(Sig("foo(T, U) -> void", {"T", "U"}))); +} + +TEST(SignatureHelpTest, IndexDocumentation) { + Symbol Foo0 = sym("foo", index::SymbolKind::Function, "@F@\\0#"); + Foo0.Documentation = "Doc from the index"; + Symbol Foo1 = sym("foo", index::SymbolKind::Function, "@F@\\0#I#"); + Foo1.Documentation = "Doc from the index"; + Symbol Foo2 = sym("foo", index::SymbolKind::Function, "@F@\\0#I#I#"); + + StringRef Sig0 = R"cpp( + int foo(); + int foo(double); + + void test() { + foo(^); + } + )cpp"; + + EXPECT_THAT( + signatures(Sig0, {Foo0}).signatures, + ElementsAre(AllOf(Sig("foo() -> int", {}), SigDoc("Doc from the index")), + AllOf(Sig("foo(double) -> int", {"double"}), SigDoc("")))); + + StringRef Sig1 = R"cpp( + int foo(); + // Overriden doc from sema + int foo(int); + // Doc from sema + int foo(int, int); + + void test() { + foo(^); + } + )cpp"; + + EXPECT_THAT( + signatures(Sig1, {Foo0, Foo1, Foo2}).signatures, + ElementsAre(AllOf(Sig("foo() -> int", {}), SigDoc("Doc from the index")), + AllOf(Sig("foo(int) -> int", {"int"}), + SigDoc("Overriden doc from sema")), + AllOf(Sig("foo(int, int) -> int", {"int", "int"}), + SigDoc("Doc from sema")))); +} + +TEST(SignatureHelpTest, DynamicIndexDocumentation) { + MockFSProvider FS; + MockCompilationDatabase CDB; + IgnoreDiagnostics DiagConsumer; + ClangdServer::Options Opts = ClangdServer::optsForTest(); + Opts.BuildDynamicSymbolIndex = true; + ClangdServer Server(CDB, FS, DiagConsumer, Opts); + + FS.Files[testPath("foo.h")] = R"cpp( + struct Foo { + // Member doc + int foo(); + }; + )cpp"; + Annotations FileContent(R"cpp( + #include "foo.h" + void test() { + Foo f; + f.foo(^); + } + )cpp"); + auto File = testPath("test.cpp"); + Server.addDocument(File, FileContent.code()); + // Wait for the dynamic index being built. + ASSERT_TRUE(Server.blockUntilIdleForTest()); + EXPECT_THAT( + llvm::cantFail(runSignatureHelp(Server, File, FileContent.point())) + .signatures, + ElementsAre(AllOf(Sig("foo() -> int", {}), SigDoc("Member doc")))); +} + +TEST(CompletionTest, CompletionFunctionArgsDisabled) { + CodeCompleteOptions Opts; + Opts.EnableSnippets = true; + Opts.EnableFunctionArgSnippets = false; + + { + auto Results = completions( + R"cpp( + void xfoo(); + void xfoo(int x, int y); + void f() { xfo^ })cpp", + {}, Opts); + EXPECT_THAT( + Results.Completions, + UnorderedElementsAre(AllOf(Named("xfoo"), SnippetSuffix("()")), + AllOf(Named("xfoo"), SnippetSuffix("($0)")))); + } + { + auto Results = completions( + R"cpp( + void xbar(); + void f() { xba^ })cpp", + {}, Opts); + EXPECT_THAT(Results.Completions, UnorderedElementsAre(AllOf( + Named("xbar"), SnippetSuffix("()")))); + } + { + Opts.BundleOverloads = true; + auto Results = completions( + R"cpp( + void xfoo(); + void xfoo(int x, int y); + void f() { xfo^ })cpp", + {}, Opts); + EXPECT_THAT( + Results.Completions, + UnorderedElementsAre(AllOf(Named("xfoo"), SnippetSuffix("($0)")))); + } + { + auto Results = completions( + R"cpp( + template + void xfoo(int a, U b); + void f() { xfo^ })cpp", + {}, Opts); + EXPECT_THAT( + Results.Completions, + UnorderedElementsAre(AllOf(Named("xfoo"), SnippetSuffix("<$1>($0)")))); + } + { + auto Results = completions( + R"cpp( + template + class foo_class{}; + template + using foo_alias = T**; + void f() { foo_^ })cpp", + {}, Opts); + EXPECT_THAT( + Results.Completions, + UnorderedElementsAre(AllOf(Named("foo_class"), SnippetSuffix("<$0>")), + AllOf(Named("foo_alias"), SnippetSuffix("<$0>")))); + } +} + +TEST(CompletionTest, SuggestOverrides) { + constexpr const char *const Text(R"cpp( + class A { + public: + virtual void vfunc(bool param); + virtual void vfunc(bool param, int p); + void func(bool param); + }; + class B : public A { + virtual void ttt(bool param) const; + void vfunc(bool param, int p) override; + }; + class C : public B { + public: + void vfunc(bool param) override; + ^ + }; + )cpp"); + const auto Results = completions(Text); + EXPECT_THAT(Results.Completions, + AllOf(Contains(Labeled("void vfunc(bool param, int p) override")), + Contains(Labeled("void ttt(bool param) const override")), + Not(Contains(Labeled("void vfunc(bool param) override"))))); +} + +TEST(CompletionTest, OverridesNonIdentName) { + // Check the completions call does not crash. + completions(R"cpp( + struct Base { + virtual ~Base() = 0; + virtual operator int() = 0; + virtual Base& operator+(Base&) = 0; + }; + + struct Derived : Base { + ^ + }; + )cpp"); +} + +TEST(GuessCompletionPrefix, Filters) { + for (llvm::StringRef Case : { + "[[scope::]][[ident]]^", + "[[]][[]]^", + "\n[[]][[]]^", + "[[]][[ab]]^", + "x.[[]][[ab]]^", + "x.[[]][[]]^", + "[[x::]][[ab]]^", + "[[x::]][[]]^", + "[[::x::]][[ab]]^", + "some text [[scope::more::]][[identif]]^ier", + "some text [[scope::]][[mor]]^e::identifier", + "weird case foo::[[::bar::]][[baz]]^", + }) { + Annotations F(Case); + auto Offset = cantFail(positionToOffset(F.code(), F.point())); + auto ToStringRef = [&](Range R) { + return F.code().slice(cantFail(positionToOffset(F.code(), R.start)), + cantFail(positionToOffset(F.code(), R.end))); + }; + auto WantQualifier = ToStringRef(F.ranges()[0]), + WantName = ToStringRef(F.ranges()[1]); + + auto Prefix = guessCompletionPrefix(F.code(), Offset); + // Even when components are empty, check their offsets are correct. + EXPECT_EQ(WantQualifier, Prefix.Qualifier) << Case; + EXPECT_EQ(WantQualifier.begin(), Prefix.Qualifier.begin()) << Case; + EXPECT_EQ(WantName, Prefix.Name) << Case; + EXPECT_EQ(WantName.begin(), Prefix.Name.begin()) << Case; + } +} + +TEST(CompletionTest, EnableSpeculativeIndexRequest) { + MockFSProvider FS; + MockCompilationDatabase CDB; + IgnoreDiagnostics DiagConsumer; + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + + auto File = testPath("foo.cpp"); + Annotations Test(R"cpp( + namespace ns1 { int abc; } + namespace ns2 { int abc; } + void f() { ns1::ab$1^; ns1::ab$2^; } + void f2() { ns2::ab$3^; } + )cpp"); + runAddDocument(Server, File, Test.code()); + clangd::CodeCompleteOptions Opts = {}; + + IndexRequestCollector Requests; + Opts.Index = &Requests; + Opts.SpeculativeIndexRequest = true; + + auto CompleteAtPoint = [&](StringRef P) { + cantFail(runCodeComplete(Server, File, Test.point(P), Opts)); + // Sleep for a while to make sure asynchronous call (if applicable) is also + // triggered before callback is invoked. + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + }; + + CompleteAtPoint("1"); + auto Reqs1 = Requests.consumeRequests(); + ASSERT_EQ(Reqs1.size(), 1u); + EXPECT_THAT(Reqs1[0].Scopes, UnorderedElementsAre("ns1::")); + + CompleteAtPoint("2"); + auto Reqs2 = Requests.consumeRequests(); + // Speculation succeeded. Used speculative index result. + ASSERT_EQ(Reqs2.size(), 1u); + EXPECT_EQ(Reqs2[0], Reqs1[0]); + + CompleteAtPoint("3"); + // Speculation failed. Sent speculative index request and the new index + // request after sema. + auto Reqs3 = Requests.consumeRequests(); + ASSERT_EQ(Reqs3.size(), 2u); +} + +TEST(CompletionTest, InsertTheMostPopularHeader) { + std::string DeclFile = URI::create(testPath("foo")).toString(); + Symbol sym = func("Func"); + sym.CanonicalDeclaration.FileURI = DeclFile.c_str(); + sym.IncludeHeaders.emplace_back("\"foo.h\"", 2); + sym.IncludeHeaders.emplace_back("\"bar.h\"", 1000); + + auto Results = completions("Fun^", {sym}).Completions; + assert(!Results.empty()); + EXPECT_THAT(Results[0], AllOf(Named("Func"), InsertInclude("\"bar.h\""))); + EXPECT_EQ(Results[0].Includes.size(), 2u); +} + +TEST(CompletionTest, NoInsertIncludeIfOnePresent) { + MockFSProvider FS; + MockCompilationDatabase CDB; + + std::string FooHeader = testPath("foo.h"); + FS.Files[FooHeader] = ""; + + IgnoreDiagnostics DiagConsumer; + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + + std::string DeclFile = URI::create(testPath("foo")).toString(); + Symbol sym = func("Func"); + sym.CanonicalDeclaration.FileURI = DeclFile.c_str(); + sym.IncludeHeaders.emplace_back("\"foo.h\"", 2); + sym.IncludeHeaders.emplace_back("\"bar.h\"", 1000); + + EXPECT_THAT( + completions(Server, "#include \"foo.h\"\nFun^", {sym}).Completions, + UnorderedElementsAre( + AllOf(Named("Func"), HasInclude("\"foo.h\""), Not(InsertInclude())))); +} + +TEST(CompletionTest, MergeMacrosFromIndexAndSema) { + Symbol Sym; + Sym.Name = "Clangd_Macro_Test"; + Sym.ID = SymbolID("c:foo.cpp@8@macro@Clangd_Macro_Test"); + Sym.SymInfo.Kind = index::SymbolKind::Macro; + Sym.Flags |= Symbol::IndexedForCodeCompletion; + EXPECT_THAT(completions("#define Clangd_Macro_Test\nClangd_Macro_T^", {Sym}) + .Completions, + UnorderedElementsAre(Named("Clangd_Macro_Test"))); +} + +TEST(CompletionTest, NoMacroFromPreambleIfIndexIsSet) { + auto Results = completions( + R"cpp(#define CLANGD_PREAMBLE x + + int x = 0; + #define CLANGD_MAIN x + void f() { CLANGD_^ } + )cpp", + {func("CLANGD_INDEX")}); + // Index is overriden in code completion options, so the preamble symbol is + // not seen. + EXPECT_THAT(Results.Completions, UnorderedElementsAre(Named("CLANGD_MAIN"), + Named("CLANGD_INDEX"))); +} + +TEST(CompletionTest, DeprecatedResults) { + std::string Body = R"cpp( + void TestClangd(); + void TestClangc() __attribute__((deprecated("", ""))); + )cpp"; + + EXPECT_THAT( + completions(Body + "int main() { TestClang^ }").Completions, + UnorderedElementsAre(AllOf(Named("TestClangd"), Not(Deprecated())), + AllOf(Named("TestClangc"), Deprecated()))); +} + +TEST(SignatureHelpTest, InsideArgument) { + { + const auto Results = signatures(R"cpp( + void foo(int x); + void foo(int x, int y); + int main() { foo(1+^); } + )cpp"); + EXPECT_THAT( + Results.signatures, + ElementsAre(Sig("foo(int x) -> void", {"int x"}), + Sig("foo(int x, int y) -> void", {"int x", "int y"}))); + EXPECT_EQ(0, Results.activeParameter); + } + { + const auto Results = signatures(R"cpp( + void foo(int x); + void foo(int x, int y); + int main() { foo(1^); } + )cpp"); + EXPECT_THAT( + Results.signatures, + ElementsAre(Sig("foo(int x) -> void", {"int x"}), + Sig("foo(int x, int y) -> void", {"int x", "int y"}))); + EXPECT_EQ(0, Results.activeParameter); + } + { + const auto Results = signatures(R"cpp( + void foo(int x); + void foo(int x, int y); + int main() { foo(1^0); } + )cpp"); + EXPECT_THAT( + Results.signatures, + ElementsAre(Sig("foo(int x) -> void", {"int x"}), + Sig("foo(int x, int y) -> void", {"int x", "int y"}))); + EXPECT_EQ(0, Results.activeParameter); + } + { + const auto Results = signatures(R"cpp( + void foo(int x); + void foo(int x, int y); + int bar(int x, int y); + int main() { bar(foo(2, 3^)); } + )cpp"); + EXPECT_THAT(Results.signatures, ElementsAre(Sig("foo(int x, int y) -> void", + {"int x", "int y"}))); + EXPECT_EQ(1, Results.activeParameter); + } +} + +TEST(SignatureHelpTest, ConstructorInitializeFields) { + { + const auto Results = signatures(R"cpp( + struct A { + A(int); + }; + struct B { + B() : a_elem(^) {} + A a_elem; + }; + )cpp"); + EXPECT_THAT(Results.signatures, + UnorderedElementsAre(Sig("A(int)", {"int"}), + Sig("A(A &&)", {"A &&"}), + Sig("A(const A &)", {"const A &"}))); + } + { + const auto Results = signatures(R"cpp( + struct A { + A(int); + }; + struct C { + C(int); + C(A); + }; + struct B { + B() : c_elem(A(1^)) {} + C c_elem; + }; + )cpp"); + EXPECT_THAT(Results.signatures, + UnorderedElementsAre(Sig("A(int)", {"int"}), + Sig("A(A &&)", {"A &&"}), + Sig("A(const A &)", {"const A &"}))); + } +} + +TEST(CompletionTest, IncludedCompletionKinds) { + MockFSProvider FS; + MockCompilationDatabase CDB; + std::string Subdir = testPath("sub"); + std::string SearchDirArg = (Twine("-I") + Subdir).str(); + CDB.ExtraClangFlags = {SearchDirArg.c_str()}; + std::string BarHeader = testPath("sub/bar.h"); + FS.Files[BarHeader] = ""; + IgnoreDiagnostics DiagConsumer; + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + auto Results = completions(Server, + R"cpp( + #include "^" + )cpp"); + EXPECT_THAT(Results.Completions, + AllOf(Has("sub/", CompletionItemKind::Folder), + Has("bar.h\"", CompletionItemKind::File))); +} + +TEST(CompletionTest, NoCrashAtNonAlphaIncludeHeader) { + auto Results = completions( + R"cpp( + #include "./^" + )cpp"); + EXPECT_TRUE(Results.Completions.empty()); +} + +TEST(CompletionTest, NoAllScopesCompletionWhenQualified) { + clangd::CodeCompleteOptions Opts = {}; + Opts.AllScopes = true; + + auto Results = completions( + R"cpp( + void f() { na::Clangd^ } + )cpp", + {cls("na::ClangdA"), cls("nx::ClangdX"), cls("Clangd3")}, Opts); + EXPECT_THAT(Results.Completions, + UnorderedElementsAre( + AllOf(Qualifier(""), Scope("na::"), Named("ClangdA")))); +} + +TEST(CompletionTest, AllScopesCompletion) { + clangd::CodeCompleteOptions Opts = {}; + Opts.AllScopes = true; + + auto Results = completions( + R"cpp( + namespace na { + void f() { Clangd^ } + } + )cpp", + {cls("nx::Clangd1"), cls("ny::Clangd2"), cls("Clangd3"), + cls("na::nb::Clangd4")}, + Opts); + EXPECT_THAT( + Results.Completions, + UnorderedElementsAre(AllOf(Qualifier("nx::"), Named("Clangd1")), + AllOf(Qualifier("ny::"), Named("Clangd2")), + AllOf(Qualifier(""), Scope(""), Named("Clangd3")), + AllOf(Qualifier("nb::"), Named("Clangd4")))); +} + +TEST(CompletionTest, NoQualifierIfShadowed) { + clangd::CodeCompleteOptions Opts = {}; + Opts.AllScopes = true; + + auto Results = completions(R"cpp( + namespace nx { class Clangd1 {}; } + using nx::Clangd1; + void f() { Clangd^ } + )cpp", + {cls("nx::Clangd1"), cls("nx::Clangd2")}, Opts); + // Although Clangd1 is from another namespace, Sema tells us it's in-scope and + // needs no qualifier. + EXPECT_THAT(Results.Completions, + UnorderedElementsAre(AllOf(Qualifier(""), Named("Clangd1")), + AllOf(Qualifier("nx::"), Named("Clangd2")))); +} + +TEST(CompletionTest, NoCompletionsForNewNames) { + clangd::CodeCompleteOptions Opts; + Opts.AllScopes = true; + auto Results = completions(R"cpp( + void f() { int n^ } + )cpp", + {cls("naber"), cls("nx::naber")}, Opts); + EXPECT_THAT(Results.Completions, UnorderedElementsAre()); +} + +TEST(CompletionTest, ObjectiveCMethodNoArguments) { + auto Results = completions(R"objc( + @interface Foo + @property(nonatomic, setter=setXToIgnoreComplete:) int value; + @end + Foo *foo = [Foo new]; int y = [foo v^] + )objc", + /*IndexSymbols=*/{}, + /*Opts=*/{}, "Foo.m"); + + auto C = Results.Completions; + EXPECT_THAT(C, ElementsAre(Named("value"))); + EXPECT_THAT(C, ElementsAre(Kind(CompletionItemKind::Method))); + EXPECT_THAT(C, ElementsAre(ReturnType("int"))); + EXPECT_THAT(C, ElementsAre(Signature(""))); + EXPECT_THAT(C, ElementsAre(SnippetSuffix(""))); +} + +TEST(CompletionTest, ObjectiveCMethodOneArgument) { + auto Results = completions(R"objc( + @interface Foo + - (int)valueForCharacter:(char)c; + @end + Foo *foo = [Foo new]; int y = [foo v^] + )objc", + /*IndexSymbols=*/{}, + /*Opts=*/{}, "Foo.m"); + + auto C = Results.Completions; + EXPECT_THAT(C, ElementsAre(Named("valueForCharacter:"))); + EXPECT_THAT(C, ElementsAre(Kind(CompletionItemKind::Method))); + EXPECT_THAT(C, ElementsAre(ReturnType("int"))); + EXPECT_THAT(C, ElementsAre(Signature("(char)"))); + EXPECT_THAT(C, ElementsAre(SnippetSuffix("${1:(char)}"))); +} + +TEST(CompletionTest, ObjectiveCMethodTwoArgumentsFromBeginning) { + auto Results = completions(R"objc( + @interface Foo + + (id)fooWithValue:(int)value fooey:(unsigned int)fooey; + @end + id val = [Foo foo^] + )objc", + /*IndexSymbols=*/{}, + /*Opts=*/{}, "Foo.m"); + + auto C = Results.Completions; + EXPECT_THAT(C, ElementsAre(Named("fooWithValue:"))); + EXPECT_THAT(C, ElementsAre(Kind(CompletionItemKind::Method))); + EXPECT_THAT(C, ElementsAre(ReturnType("id"))); + EXPECT_THAT(C, ElementsAre(Signature("(int) fooey:(unsigned int)"))); + EXPECT_THAT( + C, ElementsAre(SnippetSuffix("${1:(int)} fooey:${2:(unsigned int)}"))); +} + +TEST(CompletionTest, ObjectiveCMethodTwoArgumentsFromMiddle) { + auto Results = completions(R"objc( + @interface Foo + + (id)fooWithValue:(int)value fooey:(unsigned int)fooey; + @end + id val = [Foo fooWithValue:10 f^] + )objc", + /*IndexSymbols=*/{}, + /*Opts=*/{}, "Foo.m"); + + auto C = Results.Completions; + EXPECT_THAT(C, ElementsAre(Named("fooey:"))); + EXPECT_THAT(C, ElementsAre(Kind(CompletionItemKind::Method))); + EXPECT_THAT(C, ElementsAre(ReturnType("id"))); + EXPECT_THAT(C, ElementsAre(Signature("(unsigned int)"))); + EXPECT_THAT(C, ElementsAre(SnippetSuffix("${1:(unsigned int)}"))); +} + +TEST(CompletionTest, WorksWithNullType) { + auto R = completions(R"cpp( + int main() { + for (auto [loopVar] : y ) { // y has to be unresolved. + int z = loopV^; + } + } + )cpp"); + EXPECT_THAT(R.Completions, ElementsAre(Named("loopVar"))); +} + +TEST(CompletionTest, UsingDecl) { + const char *Header(R"cpp( + void foo(int); + namespace std { + using ::foo; + })cpp"); + const char *Source(R"cpp( + void bar() { + std::^; + })cpp"); + auto Index = TestTU::withHeaderCode(Header).index(); + clangd::CodeCompleteOptions Opts; + Opts.Index = Index.get(); + Opts.AllScopes = true; + auto R = completions(Source, {}, Opts); + EXPECT_THAT(R.Completions, + ElementsAre(AllOf(Scope("std::"), Named("foo"), + Kind(CompletionItemKind::Reference)))); +} + +TEST(CompletionTest, ScopeIsUnresolved) { + clangd::CodeCompleteOptions Opts = {}; + Opts.AllScopes = true; + + auto Results = completions(R"cpp( + namespace a { + void f() { b::X^ } + } + )cpp", + {cls("a::b::XYZ")}, Opts); + EXPECT_THAT(Results.Completions, + UnorderedElementsAre(AllOf(Qualifier(""), Named("XYZ")))); +} + +TEST(CompletionTest, NestedScopeIsUnresolved) { + clangd::CodeCompleteOptions Opts = {}; + Opts.AllScopes = true; + + auto Results = completions(R"cpp( + namespace a { + namespace b {} + void f() { b::c::X^ } + } + )cpp", + {cls("a::b::c::XYZ")}, Opts); + EXPECT_THAT(Results.Completions, + UnorderedElementsAre(AllOf(Qualifier(""), Named("XYZ")))); +} + +// Clang parser gets confused here and doesn't report the ns:: prefix. +// Naive behavior is to insert it again. We examine the source and recover. +TEST(CompletionTest, NamespaceDoubleInsertion) { + clangd::CodeCompleteOptions Opts = {}; + + auto Results = completions(R"cpp( + namespace foo { + namespace ns {} + #define M(X) < X + M(ns::ABC^ + } + )cpp", + {cls("foo::ns::ABCDE")}, Opts); + EXPECT_THAT(Results.Completions, + UnorderedElementsAre(AllOf(Qualifier(""), Named("ABCDE")))); +} + +TEST(NoCompileCompletionTest, Basic) { + auto Results = completionsNoCompile(R"cpp( + void func() { + int xyz; + int abc; + ^ + } + )cpp"); + EXPECT_THAT(Results.Completions, + UnorderedElementsAre(Named("void"), Named("func"), Named("int"), + Named("xyz"), Named("abc"))); +} + +TEST(NoCompileCompletionTest, WithFilter) { + auto Results = completionsNoCompile(R"cpp( + void func() { + int sym1; + int sym2; + int xyz1; + int xyz2; + sy^ + } + )cpp"); + EXPECT_THAT(Results.Completions, + UnorderedElementsAre(Named("sym1"), Named("sym2"))); +} + +TEST(NoCompileCompletionTest, WithIndex) { + std::vector Syms = {func("xxx"), func("a::xxx"), func("ns::b::xxx"), + func("c::xxx"), func("ns::d::xxx")}; + auto Results = completionsNoCompile( + R"cpp( + // Current-scopes, unqualified completion. + using namespace a; + namespace ns { + using namespace b; + void foo() { + xx^ + } + )cpp", + Syms); + EXPECT_THAT(Results.Completions, + UnorderedElementsAre(AllOf(Qualifier(""), Scope("")), + AllOf(Qualifier(""), Scope("a::")), + AllOf(Qualifier(""), Scope("ns::b::")))); + CodeCompleteOptions Opts; + Opts.AllScopes = true; + Results = completionsNoCompile( + R"cpp( + // All-scopes unqualified completion. + using namespace a; + namespace ns { + using namespace b; + void foo() { + xx^ + } + )cpp", + Syms, Opts); + EXPECT_THAT(Results.Completions, + UnorderedElementsAre(AllOf(Qualifier(""), Scope("")), + AllOf(Qualifier(""), Scope("a::")), + AllOf(Qualifier(""), Scope("ns::b::")), + AllOf(Qualifier("c::"), Scope("c::")), + AllOf(Qualifier("d::"), Scope("ns::d::")))); + Results = completionsNoCompile( + R"cpp( + // Qualified completion. + using namespace a; + namespace ns { + using namespace b; + void foo() { + b::xx^ + } + )cpp", + Syms, Opts); + EXPECT_THAT(Results.Completions, + ElementsAre(AllOf(Qualifier(""), Scope("ns::b::")))); + Results = completionsNoCompile( + R"cpp( + // Absolutely qualified completion. + using namespace a; + namespace ns { + using namespace b; + void foo() { + ::a::xx^ + } + )cpp", + Syms, Opts); + EXPECT_THAT(Results.Completions, + ElementsAre(AllOf(Qualifier(""), Scope("a::")))); +} + +} // namespace +} // namespace clangd +} // namespace clang Index: clang-tools-extra/trunk/clangd/unittests/CodeCompletionStringsTests.cpp =================================================================== --- clang-tools-extra/trunk/clangd/unittests/CodeCompletionStringsTests.cpp +++ clang-tools-extra/trunk/clangd/unittests/CodeCompletionStringsTests.cpp @@ -0,0 +1,160 @@ +//===-- CodeCompletionStringsTests.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 "CodeCompletionStrings.h" +#include "clang/Sema/CodeCompleteConsumer.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace clang { +namespace clangd { +namespace { + +class CompletionStringTest : public ::testing::Test { +public: + CompletionStringTest() + : Allocator(std::make_shared()), + CCTUInfo(Allocator), Builder(*Allocator, CCTUInfo) {} + +protected: + void computeSignature(const CodeCompletionString &CCS) { + Signature.clear(); + Snippet.clear(); + getSignature(CCS, &Signature, &Snippet); + } + + std::shared_ptr Allocator; + CodeCompletionTUInfo CCTUInfo; + CodeCompletionBuilder Builder; + std::string Signature; + std::string Snippet; +}; + +TEST_F(CompletionStringTest, ReturnType) { + Builder.AddResultTypeChunk("result"); + Builder.AddResultTypeChunk("redundant result no no"); + EXPECT_EQ(getReturnType(*Builder.TakeString()), "result"); +} + +TEST_F(CompletionStringTest, Documentation) { + Builder.addBriefComment("This is ignored"); + EXPECT_EQ(formatDocumentation(*Builder.TakeString(), "Is this brief?"), + "Is this brief?"); +} + +TEST_F(CompletionStringTest, DocumentationWithAnnotation) { + Builder.addBriefComment("This is ignored"); + Builder.AddAnnotation("Ano"); + EXPECT_EQ(formatDocumentation(*Builder.TakeString(), "Is this brief?"), + "Annotation: Ano\n\nIs this brief?"); +} + +TEST_F(CompletionStringTest, MultipleAnnotations) { + Builder.AddAnnotation("Ano1"); + Builder.AddAnnotation("Ano2"); + Builder.AddAnnotation("Ano3"); + + EXPECT_EQ(formatDocumentation(*Builder.TakeString(), ""), + "Annotations: Ano1 Ano2 Ano3\n"); +} + +TEST_F(CompletionStringTest, EmptySignature) { + Builder.AddTypedTextChunk("X"); + Builder.AddResultTypeChunk("result no no"); + computeSignature(*Builder.TakeString()); + EXPECT_EQ(Signature, ""); + EXPECT_EQ(Snippet, ""); +} + +TEST_F(CompletionStringTest, Function) { + Builder.AddResultTypeChunk("result no no"); + Builder.addBriefComment("This comment is ignored"); + Builder.AddTypedTextChunk("Foo"); + Builder.AddChunk(CodeCompletionString::CK_LeftParen); + Builder.AddPlaceholderChunk("p1"); + Builder.AddChunk(CodeCompletionString::CK_Comma); + Builder.AddPlaceholderChunk("p2"); + Builder.AddChunk(CodeCompletionString::CK_RightParen); + + auto *CCS = Builder.TakeString(); + computeSignature(*CCS); + EXPECT_EQ(Signature, "(p1, p2)"); + EXPECT_EQ(Snippet, "(${1:p1}, ${2:p2})"); + EXPECT_EQ(formatDocumentation(*CCS, "Foo's comment"), "Foo's comment"); +} + +TEST_F(CompletionStringTest, EscapeSnippet) { + Builder.AddTypedTextChunk("Foo"); + Builder.AddChunk(CodeCompletionString::CK_LeftParen); + Builder.AddPlaceholderChunk("$p}1\\"); + Builder.AddChunk(CodeCompletionString::CK_RightParen); + + computeSignature(*Builder.TakeString()); + EXPECT_EQ(Signature, "($p}1\\)"); + EXPECT_EQ(Snippet, "(${1:\\$p\\}1\\\\})"); +} + +TEST_F(CompletionStringTest, IgnoreInformativeQualifier) { + Builder.AddTypedTextChunk("X"); + Builder.AddInformativeChunk("info ok"); + Builder.AddInformativeChunk("info no no::"); + computeSignature(*Builder.TakeString()); + EXPECT_EQ(Signature, "info ok"); + EXPECT_EQ(Snippet, ""); +} + +TEST_F(CompletionStringTest, ObjectiveCMethodNoArguments) { + Builder.AddResultTypeChunk("void"); + Builder.AddTypedTextChunk("methodName"); + + auto *CCS = Builder.TakeString(); + computeSignature(*CCS); + EXPECT_EQ(Signature, ""); + EXPECT_EQ(Snippet, ""); +} + +TEST_F(CompletionStringTest, ObjectiveCMethodOneArgument) { + Builder.AddResultTypeChunk("void"); + Builder.AddTypedTextChunk("methodWithArg:"); + Builder.AddPlaceholderChunk("(type)"); + + auto *CCS = Builder.TakeString(); + computeSignature(*CCS); + EXPECT_EQ(Signature, "(type)"); + EXPECT_EQ(Snippet, "${1:(type)}"); +} + +TEST_F(CompletionStringTest, ObjectiveCMethodTwoArgumentsFromBeginning) { + Builder.AddResultTypeChunk("int"); + Builder.AddTypedTextChunk("withFoo:"); + Builder.AddPlaceholderChunk("(type)"); + Builder.AddChunk(CodeCompletionString::CK_HorizontalSpace); + Builder.AddTypedTextChunk("bar:"); + Builder.AddPlaceholderChunk("(type2)"); + + auto *CCS = Builder.TakeString(); + computeSignature(*CCS); + EXPECT_EQ(Signature, "(type) bar:(type2)"); + EXPECT_EQ(Snippet, "${1:(type)} bar:${2:(type2)}"); +} + +TEST_F(CompletionStringTest, ObjectiveCMethodTwoArgumentsFromMiddle) { + Builder.AddResultTypeChunk("int"); + Builder.AddInformativeChunk("withFoo:"); + Builder.AddTypedTextChunk("bar:"); + Builder.AddPlaceholderChunk("(type2)"); + + auto *CCS = Builder.TakeString(); + computeSignature(*CCS); + EXPECT_EQ(Signature, "(type2)"); + EXPECT_EQ(Snippet, "${1:(type2)}"); +} + +} // namespace +} // namespace clangd +} // namespace clang Index: clang-tools-extra/trunk/clangd/unittests/ContextTests.cpp =================================================================== --- clang-tools-extra/trunk/clangd/unittests/ContextTests.cpp +++ clang-tools-extra/trunk/clangd/unittests/ContextTests.cpp @@ -0,0 +1,56 @@ +//===-- ContextTests.cpp - Context tests ------------------------*- 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 "Context.h" + +#include "gtest/gtest.h" + +namespace clang { +namespace clangd { + +TEST(ContextTests, Simple) { + Key IntParam; + Key ExtraIntParam; + + Context Ctx = Context::empty().derive(IntParam, 10).derive(ExtraIntParam, 20); + + EXPECT_EQ(*Ctx.get(IntParam), 10); + EXPECT_EQ(*Ctx.get(ExtraIntParam), 20); +} + +TEST(ContextTests, MoveOps) { + Key> Param; + + Context Ctx = Context::empty().derive(Param, llvm::make_unique(10)); + EXPECT_EQ(**Ctx.get(Param), 10); + + Context NewCtx = std::move(Ctx); + EXPECT_EQ(**NewCtx.get(Param), 10); +} + +TEST(ContextTests, Builders) { + Key ParentParam; + Key ParentAndChildParam; + Key ChildParam; + + Context ParentCtx = + Context::empty().derive(ParentParam, 10).derive(ParentAndChildParam, 20); + Context ChildCtx = + ParentCtx.derive(ParentAndChildParam, 30).derive(ChildParam, 40); + + EXPECT_EQ(*ParentCtx.get(ParentParam), 10); + EXPECT_EQ(*ParentCtx.get(ParentAndChildParam), 20); + EXPECT_EQ(ParentCtx.get(ChildParam), nullptr); + + EXPECT_EQ(*ChildCtx.get(ParentParam), 10); + EXPECT_EQ(*ChildCtx.get(ParentAndChildParam), 30); + EXPECT_EQ(*ChildCtx.get(ChildParam), 40); +} + +} // namespace clangd +} // namespace clang Index: clang-tools-extra/trunk/clangd/unittests/DexTests.cpp =================================================================== --- clang-tools-extra/trunk/clangd/unittests/DexTests.cpp +++ clang-tools-extra/trunk/clangd/unittests/DexTests.cpp @@ -0,0 +1,753 @@ +//===-- DexTests.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 "FuzzyMatch.h" +#include "TestFS.h" +#include "TestIndex.h" +#include "index/Index.h" +#include "index/Merge.h" +#include "index/SymbolID.h" +#include "index/dex/Dex.h" +#include "index/dex/Iterator.h" +#include "index/dex/Token.h" +#include "index/dex/Trigram.h" +#include "llvm/Support/ScopedPrinter.h" +#include "llvm/Support/raw_ostream.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include +#include + +using ::testing::AnyOf; +using ::testing::ElementsAre; +using ::testing::IsEmpty; +using ::testing::UnorderedElementsAre; + +namespace clang { +namespace clangd { +namespace dex { +namespace { + +//===----------------------------------------------------------------------===// +// Query iterator tests. +//===----------------------------------------------------------------------===// + +std::vector consumeIDs(Iterator &It) { + auto IDAndScore = consume(It); + std::vector IDs(IDAndScore.size()); + for (size_t I = 0; I < IDAndScore.size(); ++I) + IDs[I] = IDAndScore[I].first; + return IDs; +} + +TEST(DexIterators, DocumentIterator) { + const PostingList L({4, 7, 8, 20, 42, 100}); + auto DocIterator = L.iterator(); + + EXPECT_EQ(DocIterator->peek(), 4U); + EXPECT_FALSE(DocIterator->reachedEnd()); + + DocIterator->advance(); + EXPECT_EQ(DocIterator->peek(), 7U); + EXPECT_FALSE(DocIterator->reachedEnd()); + + DocIterator->advanceTo(20); + EXPECT_EQ(DocIterator->peek(), 20U); + EXPECT_FALSE(DocIterator->reachedEnd()); + + DocIterator->advanceTo(65); + EXPECT_EQ(DocIterator->peek(), 100U); + EXPECT_FALSE(DocIterator->reachedEnd()); + + DocIterator->advanceTo(420); + EXPECT_TRUE(DocIterator->reachedEnd()); +} + +TEST(DexIterators, AndTwoLists) { + Corpus C{10000}; + const PostingList L0({0, 5, 7, 10, 42, 320, 9000}); + const PostingList L1({0, 4, 7, 10, 30, 60, 320, 9000}); + + auto And = C.intersect(L1.iterator(), L0.iterator()); + + EXPECT_FALSE(And->reachedEnd()); + EXPECT_THAT(consumeIDs(*And), ElementsAre(0U, 7U, 10U, 320U, 9000U)); + + And = C.intersect(L0.iterator(), L1.iterator()); + + And->advanceTo(0); + EXPECT_EQ(And->peek(), 0U); + And->advanceTo(5); + EXPECT_EQ(And->peek(), 7U); + And->advanceTo(10); + EXPECT_EQ(And->peek(), 10U); + And->advanceTo(42); + EXPECT_EQ(And->peek(), 320U); + And->advanceTo(8999); + EXPECT_EQ(And->peek(), 9000U); + And->advanceTo(9001); +} + +TEST(DexIterators, AndThreeLists) { + Corpus C{10000}; + const PostingList L0({0, 5, 7, 10, 42, 320, 9000}); + const PostingList L1({0, 4, 7, 10, 30, 60, 320, 9000}); + const PostingList L2({1, 4, 7, 11, 30, 60, 320, 9000}); + + auto And = C.intersect(L0.iterator(), L1.iterator(), L2.iterator()); + EXPECT_EQ(And->peek(), 7U); + And->advanceTo(300); + EXPECT_EQ(And->peek(), 320U); + And->advanceTo(100000); + + EXPECT_TRUE(And->reachedEnd()); +} + +TEST(DexIterators, AndEmpty) { + Corpus C{10000}; + const PostingList L1{1}; + const PostingList L2{2}; + // These iterators are empty, but the optimizer can't tell. + auto Empty1 = C.intersect(L1.iterator(), L2.iterator()); + auto Empty2 = C.intersect(L1.iterator(), L2.iterator()); + // And syncs iterators on construction, and used to fail on empty children. + auto And = C.intersect(std::move(Empty1), std::move(Empty2)); + EXPECT_TRUE(And->reachedEnd()); +} + +TEST(DexIterators, OrTwoLists) { + Corpus C{10000}; + const PostingList L0({0, 5, 7, 10, 42, 320, 9000}); + const PostingList L1({0, 4, 7, 10, 30, 60, 320, 9000}); + + auto Or = C.unionOf(L0.iterator(), L1.iterator()); + + EXPECT_FALSE(Or->reachedEnd()); + EXPECT_EQ(Or->peek(), 0U); + Or->advance(); + EXPECT_EQ(Or->peek(), 4U); + Or->advance(); + EXPECT_EQ(Or->peek(), 5U); + Or->advance(); + EXPECT_EQ(Or->peek(), 7U); + Or->advance(); + EXPECT_EQ(Or->peek(), 10U); + Or->advance(); + EXPECT_EQ(Or->peek(), 30U); + Or->advanceTo(42); + EXPECT_EQ(Or->peek(), 42U); + Or->advanceTo(300); + EXPECT_EQ(Or->peek(), 320U); + Or->advanceTo(9000); + EXPECT_EQ(Or->peek(), 9000U); + Or->advanceTo(9001); + EXPECT_TRUE(Or->reachedEnd()); + + Or = C.unionOf(L0.iterator(), L1.iterator()); + + EXPECT_THAT(consumeIDs(*Or), + ElementsAre(0U, 4U, 5U, 7U, 10U, 30U, 42U, 60U, 320U, 9000U)); +} + +TEST(DexIterators, OrThreeLists) { + Corpus C{10000}; + const PostingList L0({0, 5, 7, 10, 42, 320, 9000}); + const PostingList L1({0, 4, 7, 10, 30, 60, 320, 9000}); + const PostingList L2({1, 4, 7, 11, 30, 60, 320, 9000}); + + auto Or = C.unionOf(L0.iterator(), L1.iterator(), L2.iterator()); + + EXPECT_FALSE(Or->reachedEnd()); + EXPECT_EQ(Or->peek(), 0U); + + Or->advance(); + EXPECT_EQ(Or->peek(), 1U); + + Or->advance(); + EXPECT_EQ(Or->peek(), 4U); + + Or->advanceTo(7); + + Or->advanceTo(59); + EXPECT_EQ(Or->peek(), 60U); + + Or->advanceTo(9001); + EXPECT_TRUE(Or->reachedEnd()); +} + +// FIXME(kbobyrev): The testcase below is similar to what is expected in real +// queries. It should be updated once new iterators (such as boosting, limiting, +// etc iterators) appear. However, it is not exhaustive and it would be +// beneficial to implement automatic generation (e.g. fuzzing) of query trees +// for more comprehensive testing. +TEST(DexIterators, QueryTree) { + // + // +-----------------+ + // |And Iterator:1, 5| + // +--------+--------+ + // | + // | + // +-------------+----------------------+ + // | | + // | | + // +----------v----------+ +----------v------------+ + // |And Iterator: 1, 5, 9| |Or Iterator: 0, 1, 3, 5| + // +----------+----------+ +----------+------------+ + // | | + // +------+-----+ ------------+ + // | | | | + // +-------v-----+ +----+---+ +---v----+ +----v---+ + // |1, 3, 5, 8, 9| |Boost: 2| |Boost: 3| |Boost: 4| + // +-------------+ +----+---+ +---+----+ +----+---+ + // | | | + // +----v-----+ +-v--+ +---v---+ + // |1, 5, 7, 9| |1, 5| |0, 3, 5| + // +----------+ +----+ +-------+ + // + Corpus C{10}; + const PostingList L0({1, 3, 5, 8, 9}); + const PostingList L1({1, 5, 7, 9}); + const PostingList L2({1, 5}); + const PostingList L3({0, 3, 5}); + + // Root of the query tree: [1, 5] + auto Root = C.intersect( + // Lower And Iterator: [1, 5, 9] + C.intersect(L0.iterator(), C.boost(L1.iterator(), 2U)), + // Lower Or Iterator: [0, 1, 5] + C.unionOf(C.boost(L2.iterator(), 3U), C.boost(L3.iterator(), 4U))); + + EXPECT_FALSE(Root->reachedEnd()); + EXPECT_EQ(Root->peek(), 1U); + Root->advanceTo(0); + // Advance multiple times. Shouldn't do anything. + Root->advanceTo(1); + Root->advanceTo(0); + EXPECT_EQ(Root->peek(), 1U); + auto ElementBoost = Root->consume(); + EXPECT_THAT(ElementBoost, 6); + Root->advance(); + EXPECT_EQ(Root->peek(), 5U); + Root->advanceTo(5); + EXPECT_EQ(Root->peek(), 5U); + ElementBoost = Root->consume(); + EXPECT_THAT(ElementBoost, 8); + Root->advanceTo(9000); + EXPECT_TRUE(Root->reachedEnd()); +} + +TEST(DexIterators, StringRepresentation) { + Corpus C{10}; + const PostingList L1({1, 3, 5}); + const PostingList L2({1, 7, 9}); + + // No token given, prints full posting list. + auto I1 = L1.iterator(); + EXPECT_EQ(llvm::to_string(*I1), "[1 3 5]"); + + // Token given, uses token's string representation. + Token Tok(Token::Kind::Trigram, "L2"); + auto I2 = L1.iterator(&Tok); + EXPECT_EQ(llvm::to_string(*I2), "T=L2"); + + auto Tree = C.limit(C.intersect(move(I1), move(I2)), 10); + // AND reorders its children, we don't care which order it prints. + EXPECT_THAT(llvm::to_string(*Tree), AnyOf("(LIMIT 10 (& [1 3 5] T=L2))", + "(LIMIT 10 (& T=L2 [1 3 5]))")); +} + +TEST(DexIterators, Limit) { + Corpus C{10000}; + const PostingList L0({3, 6, 7, 20, 42, 100}); + const PostingList L1({1, 3, 5, 6, 7, 30, 100}); + const PostingList L2({0, 3, 5, 7, 8, 100}); + + auto DocIterator = C.limit(L0.iterator(), 42); + EXPECT_THAT(consumeIDs(*DocIterator), ElementsAre(3, 6, 7, 20, 42, 100)); + + DocIterator = C.limit(L0.iterator(), 3); + EXPECT_THAT(consumeIDs(*DocIterator), ElementsAre(3, 6, 7)); + + DocIterator = C.limit(L0.iterator(), 0); + EXPECT_THAT(consumeIDs(*DocIterator), ElementsAre()); + + auto AndIterator = + C.intersect(C.limit(C.all(), 343), C.limit(L0.iterator(), 2), + C.limit(L1.iterator(), 3), C.limit(L2.iterator(), 42)); + EXPECT_THAT(consumeIDs(*AndIterator), ElementsAre(3, 7)); +} + +TEST(DexIterators, True) { + EXPECT_TRUE(Corpus{0}.all()->reachedEnd()); + EXPECT_THAT(consumeIDs(*Corpus{4}.all()), ElementsAre(0, 1, 2, 3)); +} + +TEST(DexIterators, Boost) { + Corpus C{5}; + auto BoostIterator = C.boost(C.all(), 42U); + EXPECT_FALSE(BoostIterator->reachedEnd()); + auto ElementBoost = BoostIterator->consume(); + EXPECT_THAT(ElementBoost, 42U); + + const PostingList L0({2, 4}); + const PostingList L1({1, 4}); + auto Root = C.unionOf(C.all(), C.boost(L0.iterator(), 2U), + C.boost(L1.iterator(), 3U)); + + ElementBoost = Root->consume(); + EXPECT_THAT(ElementBoost, 1); + Root->advance(); + EXPECT_THAT(Root->peek(), 1U); + ElementBoost = Root->consume(); + EXPECT_THAT(ElementBoost, 3); + + Root->advance(); + EXPECT_THAT(Root->peek(), 2U); + ElementBoost = Root->consume(); + EXPECT_THAT(ElementBoost, 2); + + Root->advanceTo(4); + ElementBoost = Root->consume(); + EXPECT_THAT(ElementBoost, 3); +} + +TEST(DexIterators, Optimizations) { + Corpus C{5}; + const PostingList L1{1}; + const PostingList L2{2}; + const PostingList L3{3}; + + // empty and/or yield true/false + EXPECT_EQ(llvm::to_string(*C.intersect()), "true"); + EXPECT_EQ(llvm::to_string(*C.unionOf()), "false"); + + // true/false inside and/or short-circuit + EXPECT_EQ(llvm::to_string(*C.intersect(L1.iterator(), C.all())), "[1]"); + EXPECT_EQ(llvm::to_string(*C.intersect(L1.iterator(), C.none())), "false"); + // Not optimized to avoid breaking boosts. + EXPECT_EQ(llvm::to_string(*C.unionOf(L1.iterator(), C.all())), + "(| [1] true)"); + EXPECT_EQ(llvm::to_string(*C.unionOf(L1.iterator(), C.none())), "[1]"); + + // and/or nested inside and/or are flattened + EXPECT_EQ(llvm::to_string(*C.intersect( + L1.iterator(), C.intersect(L1.iterator(), L1.iterator()))), + "(& [1] [1] [1])"); + EXPECT_EQ(llvm::to_string(*C.unionOf( + L1.iterator(), C.unionOf(L2.iterator(), L3.iterator()))), + "(| [1] [2] [3])"); + + // optimizations combine over multiple levels + EXPECT_EQ(llvm::to_string(*C.intersect( + C.intersect(L1.iterator(), C.intersect()), C.unionOf(C.all()))), + "[1]"); +} + +//===----------------------------------------------------------------------===// +// Search token tests. +//===----------------------------------------------------------------------===// + +testing::Matcher> +tokensAre(std::initializer_list Strings, Token::Kind Kind) { + std::vector Tokens; + for (const auto &TokenData : Strings) { + Tokens.push_back(Token(Kind, TokenData)); + } + return testing::UnorderedElementsAreArray(Tokens); +} + +testing::Matcher> +trigramsAre(std::initializer_list Trigrams) { + return tokensAre(Trigrams, Token::Kind::Trigram); +} + +TEST(DexTrigrams, IdentifierTrigrams) { + EXPECT_THAT(generateIdentifierTrigrams("X86"), + trigramsAre({"x86", "x", "x8"})); + + EXPECT_THAT(generateIdentifierTrigrams("nl"), trigramsAre({"nl", "n"})); + + EXPECT_THAT(generateIdentifierTrigrams("n"), trigramsAre({"n"})); + + EXPECT_THAT(generateIdentifierTrigrams("clangd"), + trigramsAre({"c", "cl", "cla", "lan", "ang", "ngd"})); + + EXPECT_THAT(generateIdentifierTrigrams("abc_def"), + trigramsAre({"a", "ab", "ad", "abc", "abd", "ade", "bcd", "bde", + "cde", "def"})); + + EXPECT_THAT(generateIdentifierTrigrams("a_b_c_d_e_"), + trigramsAre({"a", "a_", "ab", "abc", "bcd", "cde"})); + + EXPECT_THAT(generateIdentifierTrigrams("unique_ptr"), + trigramsAre({"u", "un", "up", "uni", "unp", "upt", "niq", "nip", + "npt", "iqu", "iqp", "ipt", "que", "qup", "qpt", + "uep", "ept", "ptr"})); + + EXPECT_THAT( + generateIdentifierTrigrams("TUDecl"), + trigramsAre({"t", "tu", "td", "tud", "tde", "ude", "dec", "ecl"})); + + EXPECT_THAT(generateIdentifierTrigrams("IsOK"), + trigramsAre({"i", "is", "io", "iso", "iok", "sok"})); + + EXPECT_THAT( + generateIdentifierTrigrams("abc_defGhij__klm"), + trigramsAre({"a", "ab", "ad", "abc", "abd", "ade", "adg", "bcd", + "bde", "bdg", "cde", "cdg", "def", "deg", "dgh", "dgk", + "efg", "egh", "egk", "fgh", "fgk", "ghi", "ghk", "gkl", + "hij", "hik", "hkl", "ijk", "ikl", "jkl", "klm"})); +} + +TEST(DexTrigrams, QueryTrigrams) { + EXPECT_THAT(generateQueryTrigrams("c"), trigramsAre({"c"})); + EXPECT_THAT(generateQueryTrigrams("cl"), trigramsAre({"cl"})); + EXPECT_THAT(generateQueryTrigrams("cla"), trigramsAre({"cla"})); + + EXPECT_THAT(generateQueryTrigrams(""), trigramsAre({})); + EXPECT_THAT(generateQueryTrigrams("_"), trigramsAre({"_"})); + EXPECT_THAT(generateQueryTrigrams("__"), trigramsAre({"__"})); + EXPECT_THAT(generateQueryTrigrams("___"), trigramsAre({})); + + EXPECT_THAT(generateQueryTrigrams("X86"), trigramsAre({"x86"})); + + EXPECT_THAT(generateQueryTrigrams("clangd"), + trigramsAre({"cla", "lan", "ang", "ngd"})); + + EXPECT_THAT(generateQueryTrigrams("abc_def"), + trigramsAre({"abc", "bcd", "cde", "def"})); + + EXPECT_THAT(generateQueryTrigrams("a_b_c_d_e_"), + trigramsAre({"abc", "bcd", "cde"})); + + EXPECT_THAT(generateQueryTrigrams("unique_ptr"), + trigramsAre({"uni", "niq", "iqu", "que", "uep", "ept", "ptr"})); + + EXPECT_THAT(generateQueryTrigrams("TUDecl"), + trigramsAre({"tud", "ude", "dec", "ecl"})); + + EXPECT_THAT(generateQueryTrigrams("IsOK"), trigramsAre({"iso", "sok"})); + + EXPECT_THAT(generateQueryTrigrams("abc_defGhij__klm"), + trigramsAre({"abc", "bcd", "cde", "def", "efg", "fgh", "ghi", + "hij", "ijk", "jkl", "klm"})); +} + +TEST(DexSearchTokens, SymbolPath) { + EXPECT_THAT(generateProximityURIs( + "unittest:///clang-tools-extra/clangd/index/Token.h"), + ElementsAre("unittest:///clang-tools-extra/clangd/index/Token.h", + "unittest:///clang-tools-extra/clangd/index", + "unittest:///clang-tools-extra/clangd", + "unittest:///clang-tools-extra", "unittest:///")); + + EXPECT_THAT(generateProximityURIs("unittest:///a/b/c.h"), + ElementsAre("unittest:///a/b/c.h", "unittest:///a/b", + "unittest:///a", "unittest:///")); +} + +//===----------------------------------------------------------------------===// +// Index tests. +//===----------------------------------------------------------------------===// + +TEST(Dex, Lookup) { + auto I = Dex::build(generateSymbols({"ns::abc", "ns::xyz"}), RefSlab()); + EXPECT_THAT(lookup(*I, SymbolID("ns::abc")), UnorderedElementsAre("ns::abc")); + EXPECT_THAT(lookup(*I, {SymbolID("ns::abc"), SymbolID("ns::xyz")}), + UnorderedElementsAre("ns::abc", "ns::xyz")); + EXPECT_THAT(lookup(*I, {SymbolID("ns::nonono"), SymbolID("ns::xyz")}), + UnorderedElementsAre("ns::xyz")); + EXPECT_THAT(lookup(*I, SymbolID("ns::nonono")), UnorderedElementsAre()); +} + +TEST(Dex, FuzzyFind) { + auto Index = + Dex::build(generateSymbols({"ns::ABC", "ns::BCD", "::ABC", + "ns::nested::ABC", "other::ABC", "other::A"}), + RefSlab()); + FuzzyFindRequest Req; + Req.Query = "ABC"; + Req.Scopes = {"ns::"}; + EXPECT_THAT(match(*Index, Req), UnorderedElementsAre("ns::ABC")); + Req.Scopes = {"ns::", "ns::nested::"}; + EXPECT_THAT(match(*Index, Req), + UnorderedElementsAre("ns::ABC", "ns::nested::ABC")); + Req.Query = "A"; + Req.Scopes = {"other::"}; + EXPECT_THAT(match(*Index, Req), + UnorderedElementsAre("other::A", "other::ABC")); + Req.Query = ""; + Req.Scopes = {}; + Req.AnyScope = true; + EXPECT_THAT(match(*Index, Req), + UnorderedElementsAre("ns::ABC", "ns::BCD", "::ABC", + "ns::nested::ABC", "other::ABC", + "other::A")); +} + +TEST(DexTest, DexLimitedNumMatches) { + auto I = Dex::build(generateNumSymbols(0, 100), RefSlab()); + FuzzyFindRequest Req; + Req.Query = "5"; + Req.AnyScope = true; + Req.Limit = 3; + bool Incomplete; + auto Matches = match(*I, Req, &Incomplete); + EXPECT_TRUE(Req.Limit); + EXPECT_EQ(Matches.size(), *Req.Limit); + EXPECT_TRUE(Incomplete); +} + +TEST(DexTest, FuzzyMatch) { + auto I = Dex::build( + generateSymbols({"LaughingOutLoud", "LionPopulation", "LittleOldLady"}), + RefSlab()); + FuzzyFindRequest Req; + Req.Query = "lol"; + Req.AnyScope = true; + Req.Limit = 2; + EXPECT_THAT(match(*I, Req), + UnorderedElementsAre("LaughingOutLoud", "LittleOldLady")); +} + +TEST(DexTest, ShortQuery) { + auto I = Dex::build(generateSymbols({"OneTwoThreeFour"}), RefSlab()); + FuzzyFindRequest Req; + Req.AnyScope = true; + bool Incomplete; + + EXPECT_THAT(match(*I, Req, &Incomplete), ElementsAre("OneTwoThreeFour")); + EXPECT_FALSE(Incomplete) << "Empty string is not a short query"; + + Req.Query = "t"; + EXPECT_THAT(match(*I, Req, &Incomplete), ElementsAre()); + EXPECT_TRUE(Incomplete) << "Short queries have different semantics"; + + Req.Query = "tt"; + EXPECT_THAT(match(*I, Req, &Incomplete), ElementsAre()); + EXPECT_TRUE(Incomplete) << "Short queries have different semantics"; + + Req.Query = "ttf"; + EXPECT_THAT(match(*I, Req, &Incomplete), ElementsAre("OneTwoThreeFour")); + EXPECT_FALSE(Incomplete) << "3-char string is not a short query"; +} + +TEST(DexTest, MatchQualifiedNamesWithoutSpecificScope) { + auto I = Dex::build(generateSymbols({"a::y1", "b::y2", "y3"}), RefSlab()); + FuzzyFindRequest Req; + Req.AnyScope = true; + Req.Query = "y"; + EXPECT_THAT(match(*I, Req), UnorderedElementsAre("a::y1", "b::y2", "y3")); +} + +TEST(DexTest, MatchQualifiedNamesWithGlobalScope) { + auto I = Dex::build(generateSymbols({"a::y1", "b::y2", "y3"}), RefSlab()); + FuzzyFindRequest Req; + Req.Query = "y"; + Req.Scopes = {""}; + EXPECT_THAT(match(*I, Req), UnorderedElementsAre("y3")); +} + +TEST(DexTest, MatchQualifiedNamesWithOneScope) { + auto I = Dex::build( + generateSymbols({"a::y1", "a::y2", "a::x", "b::y2", "y3"}), RefSlab()); + FuzzyFindRequest Req; + Req.Query = "y"; + Req.Scopes = {"a::"}; + EXPECT_THAT(match(*I, Req), UnorderedElementsAre("a::y1", "a::y2")); +} + +TEST(DexTest, MatchQualifiedNamesWithMultipleScopes) { + auto I = Dex::build( + generateSymbols({"a::y1", "a::y2", "a::x", "b::y3", "y3"}), RefSlab()); + FuzzyFindRequest Req; + Req.Query = "y"; + Req.Scopes = {"a::", "b::"}; + EXPECT_THAT(match(*I, Req), UnorderedElementsAre("a::y1", "a::y2", "b::y3")); +} + +TEST(DexTest, NoMatchNestedScopes) { + auto I = Dex::build(generateSymbols({"a::y1", "a::b::y2"}), RefSlab()); + FuzzyFindRequest Req; + Req.Query = "y"; + Req.Scopes = {"a::"}; + EXPECT_THAT(match(*I, Req), UnorderedElementsAre("a::y1")); +} + +TEST(DexTest, WildcardScope) { + auto I = + Dex::build(generateSymbols({"a::y1", "a::b::y2", "c::y3"}), RefSlab()); + FuzzyFindRequest Req; + Req.AnyScope = true; + Req.Query = "y"; + Req.Scopes = {"a::"}; + EXPECT_THAT(match(*I, Req), + UnorderedElementsAre("a::y1", "a::b::y2", "c::y3")); +} + +TEST(DexTest, IgnoreCases) { + auto I = Dex::build(generateSymbols({"ns::ABC", "ns::abc"}), RefSlab()); + FuzzyFindRequest Req; + Req.Query = "AB"; + Req.Scopes = {"ns::"}; + EXPECT_THAT(match(*I, Req), UnorderedElementsAre("ns::ABC", "ns::abc")); +} + +TEST(DexTest, UnknownPostingList) { + // Regression test: we used to ignore unknown scopes and accept any symbol. + auto I = Dex::build(generateSymbols({"ns::ABC", "ns::abc"}), RefSlab()); + FuzzyFindRequest Req; + Req.Scopes = {"ns2::"}; + EXPECT_THAT(match(*I, Req), UnorderedElementsAre()); +} + +TEST(DexTest, Lookup) { + auto I = Dex::build(generateSymbols({"ns::abc", "ns::xyz"}), RefSlab()); + EXPECT_THAT(lookup(*I, SymbolID("ns::abc")), UnorderedElementsAre("ns::abc")); + EXPECT_THAT(lookup(*I, {SymbolID("ns::abc"), SymbolID("ns::xyz")}), + UnorderedElementsAre("ns::abc", "ns::xyz")); + EXPECT_THAT(lookup(*I, {SymbolID("ns::nonono"), SymbolID("ns::xyz")}), + UnorderedElementsAre("ns::xyz")); + EXPECT_THAT(lookup(*I, SymbolID("ns::nonono")), UnorderedElementsAre()); +} + +TEST(DexTest, SymbolIndexOptionsFilter) { + auto CodeCompletionSymbol = symbol("Completion"); + auto NonCodeCompletionSymbol = symbol("NoCompletion"); + CodeCompletionSymbol.Flags = Symbol::SymbolFlag::IndexedForCodeCompletion; + NonCodeCompletionSymbol.Flags = Symbol::SymbolFlag::None; + std::vector Symbols{CodeCompletionSymbol, NonCodeCompletionSymbol}; + Dex I(Symbols, RefSlab()); + FuzzyFindRequest Req; + Req.AnyScope = true; + Req.RestrictForCodeCompletion = false; + EXPECT_THAT(match(I, Req), ElementsAre("Completion", "NoCompletion")); + Req.RestrictForCodeCompletion = true; + EXPECT_THAT(match(I, Req), ElementsAre("Completion")); +} + +TEST(DexTest, ProximityPathsBoosting) { + auto RootSymbol = symbol("root::abc"); + RootSymbol.CanonicalDeclaration.FileURI = "unittest:///file.h"; + auto CloseSymbol = symbol("close::abc"); + CloseSymbol.CanonicalDeclaration.FileURI = "unittest:///a/b/c/d/e/f/file.h"; + + std::vector Symbols{CloseSymbol, RootSymbol}; + Dex I(Symbols, RefSlab()); + + FuzzyFindRequest Req; + Req.AnyScope = true; + Req.Query = "abc"; + // The best candidate can change depending on the proximity paths. + Req.Limit = 1; + + // FuzzyFind request comes from the file which is far from the root: expect + // CloseSymbol to come out. + Req.ProximityPaths = {testPath("a/b/c/d/e/f/file.h")}; + EXPECT_THAT(match(I, Req), ElementsAre("close::abc")); + + // FuzzyFind request comes from the file which is close to the root: expect + // RootSymbol to come out. + Req.ProximityPaths = {testPath("file.h")}; + EXPECT_THAT(match(I, Req), ElementsAre("root::abc")); +} + +TEST(DexTests, Refs) { + llvm::DenseMap> Refs; + auto AddRef = [&](const Symbol &Sym, const char *Filename, RefKind Kind) { + auto &SymbolRefs = Refs[Sym.ID]; + SymbolRefs.emplace_back(); + SymbolRefs.back().Kind = Kind; + SymbolRefs.back().Location.FileURI = Filename; + }; + auto Foo = symbol("foo"); + auto Bar = symbol("bar"); + AddRef(Foo, "foo.h", RefKind::Declaration); + AddRef(Foo, "foo.cc", RefKind::Definition); + AddRef(Foo, "reffoo.h", RefKind::Reference); + AddRef(Bar, "bar.h", RefKind::Declaration); + + RefsRequest Req; + Req.IDs.insert(Foo.ID); + Req.Filter = RefKind::Declaration | RefKind::Definition; + + std::vector Files; + Dex(std::vector{Foo, Bar}, Refs).refs(Req, [&](const Ref &R) { + Files.push_back(R.Location.FileURI); + }); + EXPECT_THAT(Files, UnorderedElementsAre("foo.h", "foo.cc")); + + Req.Limit = 1; + Files.clear(); + Dex(std::vector{Foo, Bar}, Refs).refs(Req, [&](const Ref &R) { + Files.push_back(R.Location.FileURI); + }); + EXPECT_THAT(Files, ElementsAre(AnyOf("foo.h", "foo.cc"))); +} + +TEST(DexTest, PreferredTypesBoosting) { + auto Sym1 = symbol("t1"); + Sym1.Type = "T1"; + auto Sym2 = symbol("t2"); + Sym2.Type = "T2"; + + std::vector Symbols{Sym1, Sym2}; + Dex I(Symbols, RefSlab()); + + FuzzyFindRequest Req; + Req.AnyScope = true; + Req.Query = "t"; + // The best candidate can change depending on the preferred type. + Req.Limit = 1; + + Req.PreferredTypes = {Sym1.Type}; + EXPECT_THAT(match(I, Req), ElementsAre("t1")); + + Req.PreferredTypes = {Sym2.Type}; + EXPECT_THAT(match(I, Req), ElementsAre("t2")); +} + +TEST(DexTest, TemplateSpecialization) { + SymbolSlab::Builder B; + + Symbol S = symbol("TempSpec"); + S.ID = SymbolID("0"); + B.insert(S); + + S = symbol("TempSpec"); + S.ID = SymbolID("1"); + S.TemplateSpecializationArgs = ""; + S.SymInfo.Properties = static_cast( + index::SymbolProperty::TemplateSpecialization); + B.insert(S); + + S = symbol("TempSpec"); + S.ID = SymbolID("2"); + S.TemplateSpecializationArgs = ""; + S.SymInfo.Properties = static_cast( + index::SymbolProperty::TemplatePartialSpecialization); + B.insert(S); + + auto I = dex::Dex::build(std::move(B).build(), RefSlab()); + FuzzyFindRequest Req; + Req.AnyScope = true; + + Req.Query = "TempSpec"; + EXPECT_THAT(match(*I, Req), + UnorderedElementsAre("TempSpec", "TempSpec", + "TempSpec")); + + // FIXME: Add filtering for template argument list. + Req.Query = "TempSpec WithFix(testing::Matcher FixMatcher) { + return Field(&Diag::Fixes, ElementsAre(FixMatcher)); +} + +testing::Matcher WithFix(testing::Matcher FixMatcher1, + testing::Matcher FixMatcher2) { + return Field(&Diag::Fixes, UnorderedElementsAre(FixMatcher1, FixMatcher2)); +} + +testing::Matcher WithNote(testing::Matcher NoteMatcher) { + return Field(&Diag::Notes, ElementsAre(NoteMatcher)); +} + +MATCHER_P2(Diag, Range, Message, + "Diag at " + llvm::to_string(Range) + " = [" + Message + "]") { + return arg.Range == Range && arg.Message == Message; +} + +MATCHER_P3(Fix, Range, Replacement, Message, + "Fix " + llvm::to_string(Range) + " => " + + testing::PrintToString(Replacement) + " = [" + Message + "]") { + return arg.Message == Message && arg.Edits.size() == 1 && + arg.Edits[0].range == Range && arg.Edits[0].newText == Replacement; +} + +MATCHER_P(EqualToLSPDiag, LSPDiag, + "LSP diagnostic " + llvm::to_string(LSPDiag)) { + if (toJSON(arg) != toJSON(LSPDiag)) { + *result_listener << llvm::formatv("expected:\n{0:2}\ngot\n{1:2}", + toJSON(LSPDiag), toJSON(arg)).str(); + return false; + } + return true; +} + +MATCHER_P(DiagSource, S, "") { return arg.Source == S; } +MATCHER_P(DiagName, N, "") { return arg.Name == N; } + +MATCHER_P(EqualToFix, Fix, "LSP fix " + llvm::to_string(Fix)) { + if (arg.Message != Fix.Message) + return false; + if (arg.Edits.size() != Fix.Edits.size()) + return false; + for (std::size_t I = 0; I < arg.Edits.size(); ++I) { + if (arg.Edits[I].range != Fix.Edits[I].range || + arg.Edits[I].newText != Fix.Edits[I].newText) + return false; + } + return true; +} + + +// Helper function to make tests shorter. +Position pos(int line, int character) { + Position Res; + Res.line = line; + Res.character = character; + return Res; +} + +TEST(DiagnosticsTest, DiagnosticRanges) { + // Check we report correct ranges, including various edge-cases. + Annotations Test(R"cpp( + namespace test{}; + void $decl[[foo]](); + int main() { + $typo[[go\ +o]](); + foo()$semicolon[[]]//with comments + $unk[[unknown]](); + double $type[[bar]] = "foo"; + struct Foo { int x; }; Foo a; + a.$nomember[[y]]; + test::$nomembernamespace[[test]]; + } + )cpp"); + EXPECT_THAT( + TestTU::withCode(Test.code()).build().getDiagnostics(), + ElementsAre( + // This range spans lines. + AllOf(Diag(Test.range("typo"), + "use of undeclared identifier 'goo'; did you mean 'foo'?"), + DiagSource(Diag::Clang), + DiagName("undeclared_var_use_suggest"), + WithFix( + Fix(Test.range("typo"), "foo", "change 'go\\ o' to 'foo'")), + // This is a pretty normal range. + WithNote(Diag(Test.range("decl"), "'foo' declared here"))), + // This range is zero-width and insertion. Therefore make sure we are + // not expanding it into other tokens. Since we are not going to + // replace those. + AllOf(Diag(Test.range("semicolon"), "expected ';' after expression"), + WithFix(Fix(Test.range("semicolon"), ";", "insert ';'"))), + // This range isn't provided by clang, we expand to the token. + Diag(Test.range("unk"), "use of undeclared identifier 'unknown'"), + Diag(Test.range("type"), + "cannot initialize a variable of type 'double' with an lvalue " + "of type 'const char [4]'"), + Diag(Test.range("nomember"), "no member named 'y' in 'Foo'"), + Diag(Test.range("nomembernamespace"), + "no member named 'test' in namespace 'test'"))); +} + +TEST(DiagnosticsTest, FlagsMatter) { + Annotations Test("[[void]] main() {}"); + auto TU = TestTU::withCode(Test.code()); + EXPECT_THAT(TU.build().getDiagnostics(), + ElementsAre(AllOf(Diag(Test.range(), "'main' must return 'int'"), + WithFix(Fix(Test.range(), "int", + "change 'void' to 'int'"))))); + // Same code built as C gets different diagnostics. + TU.Filename = "Plain.c"; + EXPECT_THAT( + TU.build().getDiagnostics(), + ElementsAre(AllOf( + Diag(Test.range(), "return type of 'main' is not 'int'"), + WithFix(Fix(Test.range(), "int", "change return type to 'int'"))))); +} + +TEST(DiagnosticsTest, DiagnosticPreamble) { + Annotations Test(R"cpp( + #include $[["not-found.h"]] + )cpp"); + + auto TU = TestTU::withCode(Test.code()); + EXPECT_THAT(TU.build().getDiagnostics(), + ElementsAre(testing::AllOf( + Diag(Test.range(), "'not-found.h' file not found"), + DiagSource(Diag::Clang), DiagName("pp_file_not_found")))); +} + +TEST(DiagnosticsTest, ClangTidy) { + Annotations Test(R"cpp( + #include $deprecated[["assert.h"]] + + #define $macrodef[[SQUARE]](X) (X)*(X) + int main() { + return $doubled[[sizeof]](sizeof(int)); + int y = 4; + return SQUARE($macroarg[[++]]y); + } + )cpp"); + auto TU = TestTU::withCode(Test.code()); + TU.HeaderFilename = "assert.h"; // Suppress "not found" error. + TU.ClangTidyChecks = + "-*, bugprone-sizeof-expression, bugprone-macro-repeated-side-effects, " + "modernize-deprecated-headers"; + EXPECT_THAT( + TU.build().getDiagnostics(), + UnorderedElementsAre( + AllOf(Diag(Test.range("deprecated"), + "inclusion of deprecated C++ header 'assert.h'; consider " + "using 'cassert' instead"), + DiagSource(Diag::ClangTidy), + DiagName("modernize-deprecated-headers"), + WithFix(Fix(Test.range("deprecated"), "", + "change '\"assert.h\"' to ''"))), + Diag(Test.range("doubled"), + "suspicious usage of 'sizeof(sizeof(...))'"), + AllOf( + Diag(Test.range("macroarg"), + "side effects in the 1st macro argument 'X' are repeated in " + "macro expansion"), + DiagSource(Diag::ClangTidy), + DiagName("bugprone-macro-repeated-side-effects"), + WithNote( + Diag(Test.range("macrodef"), "macro 'SQUARE' defined here"))), + Diag(Test.range("macroarg"), + "multiple unsequenced modifications to 'y'"))); +} + +TEST(DiagnosticsTest, Preprocessor) { + // This looks like a preamble, but there's an #else in the middle! + // Check that: + // - the #else doesn't generate diagnostics (we had this bug) + // - we get diagnostics from the taken branch + // - we get no diagnostics from the not taken branch + Annotations Test(R"cpp( + #ifndef FOO + #define FOO + int a = [[b]]; + #else + int x = y; + #endif + )cpp"); + EXPECT_THAT( + TestTU::withCode(Test.code()).build().getDiagnostics(), + ElementsAre(Diag(Test.range(), "use of undeclared identifier 'b'"))); +} + +TEST(DiagnosticsTest, InsideMacros) { + Annotations Test(R"cpp( + #define TEN 10 + #define RET(x) return x + 10 + + int* foo() { + RET($foo[[0]]); + } + int* bar() { + return $bar[[TEN]]; + } + )cpp"); + EXPECT_THAT(TestTU::withCode(Test.code()).build().getDiagnostics(), + ElementsAre(Diag(Test.range("foo"), + "cannot initialize return object of type " + "'int *' with an rvalue of type 'int'"), + Diag(Test.range("bar"), + "cannot initialize return object of type " + "'int *' with an rvalue of type 'int'"))); +} + +TEST(DiagnosticsTest, NoFixItInMacro) { + Annotations Test(R"cpp( + #define Define(name) void name() {} + + [[Define]](main) + )cpp"); + auto TU = TestTU::withCode(Test.code()); + EXPECT_THAT(TU.build().getDiagnostics(), + ElementsAre(AllOf(Diag(Test.range(), "'main' must return 'int'"), + Not(WithFix(_))))); +} + +TEST(DiagnosticsTest, ToLSP) { + URIForFile MainFile = + URIForFile::canonicalize(testPath("foo/bar/main.cpp"), ""); + URIForFile HeaderFile = + URIForFile::canonicalize(testPath("foo/bar/header.h"), ""); + + clangd::Diag D; + D.ID = clang::diag::err_enum_class_reference; + D.Name = "enum_class_reference"; + D.Source = clangd::Diag::Clang; + D.Message = "something terrible happened"; + D.Range = {pos(1, 2), pos(3, 4)}; + D.InsideMainFile = true; + D.Severity = DiagnosticsEngine::Error; + D.File = "foo/bar/main.cpp"; + D.AbsFile = MainFile.file(); + + clangd::Note NoteInMain; + NoteInMain.Message = "declared somewhere in the main file"; + NoteInMain.Range = {pos(5, 6), pos(7, 8)}; + NoteInMain.Severity = DiagnosticsEngine::Remark; + NoteInMain.File = "../foo/bar/main.cpp"; + NoteInMain.InsideMainFile = true; + NoteInMain.AbsFile = MainFile.file(); + + D.Notes.push_back(NoteInMain); + + clangd::Note NoteInHeader; + NoteInHeader.Message = "declared somewhere in the header file"; + NoteInHeader.Range = {pos(9, 10), pos(11, 12)}; + NoteInHeader.Severity = DiagnosticsEngine::Note; + NoteInHeader.File = "../foo/baz/header.h"; + NoteInHeader.InsideMainFile = false; + NoteInHeader.AbsFile = HeaderFile.file(); + D.Notes.push_back(NoteInHeader); + + clangd::Fix F; + F.Message = "do something"; + D.Fixes.push_back(F); + + // Diagnostics should turn into these: + clangd::Diagnostic MainLSP; + MainLSP.range = D.Range; + MainLSP.severity = getSeverity(DiagnosticsEngine::Error); + MainLSP.code = "enum_class_reference"; + MainLSP.source = "clang"; + MainLSP.message = R"(Something terrible happened (fix available) + +main.cpp:6:7: remark: declared somewhere in the main file + +../foo/baz/header.h:10:11: +note: declared somewhere in the header file)"; + + clangd::Diagnostic NoteInMainLSP; + NoteInMainLSP.range = NoteInMain.Range; + NoteInMainLSP.severity = getSeverity(DiagnosticsEngine::Remark); + NoteInMainLSP.message = R"(Declared somewhere in the main file + +main.cpp:2:3: error: something terrible happened)"; + + ClangdDiagnosticOptions Opts; + // Transform diagnostics and check the results. + std::vector>> LSPDiags; + toLSPDiags(D, MainFile, Opts, + [&](clangd::Diagnostic LSPDiag, ArrayRef Fixes) { + LSPDiags.push_back( + {std::move(LSPDiag), + std::vector(Fixes.begin(), Fixes.end())}); + }); + + EXPECT_THAT( + LSPDiags, + ElementsAre(Pair(EqualToLSPDiag(MainLSP), ElementsAre(EqualToFix(F))), + Pair(EqualToLSPDiag(NoteInMainLSP), IsEmpty()))); + EXPECT_EQ(LSPDiags[0].first.code, "enum_class_reference"); + EXPECT_EQ(LSPDiags[0].first.source, "clang"); + EXPECT_EQ(LSPDiags[1].first.code, ""); + EXPECT_EQ(LSPDiags[1].first.source, ""); + + // Same thing, but don't flatten notes into the main list. + LSPDiags.clear(); + Opts.EmitRelatedLocations = true; + toLSPDiags(D, MainFile, Opts, + [&](clangd::Diagnostic LSPDiag, ArrayRef Fixes) { + LSPDiags.push_back( + {std::move(LSPDiag), + std::vector(Fixes.begin(), Fixes.end())}); + }); + MainLSP.message = "Something terrible happened (fix available)"; + DiagnosticRelatedInformation NoteInMainDRI; + NoteInMainDRI.message = "Declared somewhere in the main file"; + NoteInMainDRI.location.range = NoteInMain.Range; + NoteInMainDRI.location.uri = MainFile; + MainLSP.relatedInformation = {NoteInMainDRI}; + DiagnosticRelatedInformation NoteInHeaderDRI; + NoteInHeaderDRI.message = "Declared somewhere in the header file"; + NoteInHeaderDRI.location.range = NoteInHeader.Range; + NoteInHeaderDRI.location.uri = HeaderFile; + MainLSP.relatedInformation = {NoteInMainDRI, NoteInHeaderDRI}; + EXPECT_THAT( + LSPDiags, + ElementsAre(Pair(EqualToLSPDiag(MainLSP), ElementsAre(EqualToFix(F))))); +} + +struct SymbolWithHeader { + std::string QName; + std::string DeclaringFile; + std::string IncludeHeader; +}; + +std::unique_ptr +buildIndexWithSymbol(llvm::ArrayRef Syms) { + SymbolSlab::Builder Slab; + for (const auto &S : Syms) { + Symbol Sym = cls(S.QName); + Sym.Flags |= Symbol::IndexedForCodeCompletion; + Sym.CanonicalDeclaration.FileURI = S.DeclaringFile.c_str(); + Sym.Definition.FileURI = S.DeclaringFile.c_str(); + Sym.IncludeHeaders.emplace_back(S.IncludeHeader, 1); + Slab.insert(Sym); + } + return MemIndex::build(std::move(Slab).build(), RefSlab()); +} + +TEST(IncludeFixerTest, IncompleteType) { + Annotations Test(R"cpp( +$insert[[]]namespace ns { + class X; + $nested[[X::]]Nested n; +} +class Y : $base[[public ns::X]] {}; +int main() { + ns::X *x; + x$access[[->]]f(); +} + )cpp"); + auto TU = TestTU::withCode(Test.code()); + auto Index = buildIndexWithSymbol( + {SymbolWithHeader{"ns::X", "unittest:///x.h", "\"x.h\""}}); + TU.ExternalIndex = Index.get(); + + EXPECT_THAT( + TU.build().getDiagnostics(), + UnorderedElementsAre( + AllOf(Diag(Test.range("nested"), + "incomplete type 'ns::X' named in nested name specifier"), + WithFix(Fix(Test.range("insert"), "#include \"x.h\"\n", + "Add include \"x.h\" for symbol ns::X"))), + AllOf(Diag(Test.range("base"), "base class has incomplete type"), + WithFix(Fix(Test.range("insert"), "#include \"x.h\"\n", + "Add include \"x.h\" for symbol ns::X"))), + AllOf(Diag(Test.range("access"), + "member access into incomplete type 'ns::X'"), + WithFix(Fix(Test.range("insert"), "#include \"x.h\"\n", + "Add include \"x.h\" for symbol ns::X"))))); +} + +TEST(IncludeFixerTest, NoSuggestIncludeWhenNoDefinitionInHeader) { + Annotations Test(R"cpp( +$insert[[]]namespace ns { + class X; +} +class Y : $base[[public ns::X]] {}; +int main() { + ns::X *x; + x$access[[->]]f(); +} + )cpp"); + auto TU = TestTU::withCode(Test.code()); + Symbol Sym = cls("ns::X"); + Sym.Flags |= Symbol::IndexedForCodeCompletion; + Sym.CanonicalDeclaration.FileURI = "unittest:///x.h"; + Sym.Definition.FileURI = "unittest:///x.cc"; + Sym.IncludeHeaders.emplace_back("\"x.h\"", 1); + + SymbolSlab::Builder Slab; + Slab.insert(Sym); + auto Index = MemIndex::build(std::move(Slab).build(), RefSlab()); + TU.ExternalIndex = Index.get(); + + EXPECT_THAT(TU.build().getDiagnostics(), + UnorderedElementsAre( + Diag(Test.range("base"), "base class has incomplete type"), + Diag(Test.range("access"), + "member access into incomplete type 'ns::X'"))); +} + +TEST(IncludeFixerTest, Typo) { + Annotations Test(R"cpp( +$insert[[]]namespace ns { +void foo() { + $unqualified1[[X]] x; + // No fix if the unresolved type is used as specifier. (ns::)X::Nested will be + // considered the unresolved type. + $unqualified2[[X]]::Nested n; +} +} +void bar() { + ns::$qualified1[[X]] x; // ns:: is valid. + ns::$qualified2[[X]](); // Error: no member in namespace + + ::$global[[Global]] glob; +} + )cpp"); + auto TU = TestTU::withCode(Test.code()); + auto Index = buildIndexWithSymbol( + {SymbolWithHeader{"ns::X", "unittest:///x.h", "\"x.h\""}, + SymbolWithHeader{"Global", "unittest:///global.h", "\"global.h\""}}); + TU.ExternalIndex = Index.get(); + + EXPECT_THAT( + TU.build().getDiagnostics(), + UnorderedElementsAre( + AllOf(Diag(Test.range("unqualified1"), "unknown type name 'X'"), + WithFix(Fix(Test.range("insert"), "#include \"x.h\"\n", + "Add include \"x.h\" for symbol ns::X"))), + Diag(Test.range("unqualified2"), "use of undeclared identifier 'X'"), + AllOf(Diag(Test.range("qualified1"), + "no type named 'X' in namespace 'ns'"), + WithFix(Fix(Test.range("insert"), "#include \"x.h\"\n", + "Add include \"x.h\" for symbol ns::X"))), + AllOf(Diag(Test.range("qualified2"), + "no member named 'X' in namespace 'ns'"), + WithFix(Fix(Test.range("insert"), "#include \"x.h\"\n", + "Add include \"x.h\" for symbol ns::X"))), + AllOf(Diag(Test.range("global"), + "no type named 'Global' in the global namespace"), + WithFix(Fix(Test.range("insert"), "#include \"global.h\"\n", + "Add include \"global.h\" for symbol Global"))))); +} + +TEST(IncludeFixerTest, MultipleMatchedSymbols) { + Annotations Test(R"cpp( +$insert[[]]namespace na { +namespace nb { +void foo() { + $unqualified[[X]] x; +} +} +} + )cpp"); + auto TU = TestTU::withCode(Test.code()); + auto Index = buildIndexWithSymbol( + {SymbolWithHeader{"na::X", "unittest:///a.h", "\"a.h\""}, + SymbolWithHeader{"na::nb::X", "unittest:///b.h", "\"b.h\""}}); + TU.ExternalIndex = Index.get(); + + EXPECT_THAT(TU.build().getDiagnostics(), + UnorderedElementsAre(AllOf( + Diag(Test.range("unqualified"), "unknown type name 'X'"), + WithFix(Fix(Test.range("insert"), "#include \"a.h\"\n", + "Add include \"a.h\" for symbol na::X"), + Fix(Test.range("insert"), "#include \"b.h\"\n", + "Add include \"b.h\" for symbol na::nb::X"))))); +} + +TEST(IncludeFixerTest, NoCrashMemebrAccess) { + Annotations Test(R"cpp( + struct X { int xyz; }; + void g() { X x; x.$[[xy]] } + )cpp"); + auto TU = TestTU::withCode(Test.code()); + auto Index = buildIndexWithSymbol( + SymbolWithHeader{"na::X", "unittest:///a.h", "\"a.h\""}); + TU.ExternalIndex = Index.get(); + + EXPECT_THAT( + TU.build().getDiagnostics(), + UnorderedElementsAre(Diag(Test.range(), "no member named 'xy' in 'X'"))); +} + +TEST(IncludeFixerTest, UseCachedIndexResults) { + // As index results for the identical request are cached, more than 5 fixes + // are generated. + Annotations Test(R"cpp( +$insert[[]]void foo() { + $x1[[X]] x; + $x2[[X]] x; + $x3[[X]] x; + $x4[[X]] x; + $x5[[X]] x; + $x6[[X]] x; + $x7[[X]] x; +} + +class X; +void bar(X *x) { + x$a1[[->]]f(); + x$a2[[->]]f(); + x$a3[[->]]f(); + x$a4[[->]]f(); + x$a5[[->]]f(); + x$a6[[->]]f(); + x$a7[[->]]f(); +} + )cpp"); + auto TU = TestTU::withCode(Test.code()); + auto Index = + buildIndexWithSymbol(SymbolWithHeader{"X", "unittest:///a.h", "\"a.h\""}); + TU.ExternalIndex = Index.get(); + + auto Parsed = TU.build(); + for (const auto &D : Parsed.getDiagnostics()) { + EXPECT_EQ(D.Fixes.size(), 1u); + EXPECT_EQ(D.Fixes[0].Message, + std::string("Add include \"a.h\" for symbol X")); + } +} + +TEST(IncludeFixerTest, UnresolvedNameAsSpecifier) { + Annotations Test(R"cpp( +$insert[[]]namespace ns { +} +void g() { ns::$[[scope]]::X_Y(); } + )cpp"); + auto TU = TestTU::withCode(Test.code()); + auto Index = buildIndexWithSymbol( + SymbolWithHeader{"ns::scope::X_Y", "unittest:///x.h", "\"x.h\""}); + TU.ExternalIndex = Index.get(); + + EXPECT_THAT( + TU.build().getDiagnostics(), + UnorderedElementsAre(AllOf( + Diag(Test.range(), "no member named 'scope' in namespace 'ns'"), + WithFix(Fix(Test.range("insert"), "#include \"x.h\"\n", + "Add include \"x.h\" for symbol ns::scope::X_Y"))))); +} + +TEST(IncludeFixerTest, UnresolvedSpecifierWithSemaCorrection) { + Annotations Test(R"cpp( +$insert[[]]namespace clang { +void f() { + // "clangd::" will be corrected to "clang::" by Sema. + $q1[[clangd]]::$x[[X]] x; + $q2[[clangd]]::$ns[[ns]]::Y y; +} +} + )cpp"); + auto TU = TestTU::withCode(Test.code()); + auto Index = buildIndexWithSymbol( + {SymbolWithHeader{"clang::clangd::X", "unittest:///x.h", "\"x.h\""}, + SymbolWithHeader{"clang::clangd::ns::Y", "unittest:///y.h", "\"y.h\""}}); + TU.ExternalIndex = Index.get(); + + EXPECT_THAT( + TU.build().getDiagnostics(), + UnorderedElementsAre( + AllOf( + Diag(Test.range("q1"), "use of undeclared identifier 'clangd'; " + "did you mean 'clang'?"), + WithFix(_, // change clangd to clang + Fix(Test.range("insert"), "#include \"x.h\"\n", + "Add include \"x.h\" for symbol clang::clangd::X"))), + AllOf( + Diag(Test.range("x"), "no type named 'X' in namespace 'clang'"), + WithFix(Fix(Test.range("insert"), "#include \"x.h\"\n", + "Add include \"x.h\" for symbol clang::clangd::X"))), + AllOf( + Diag(Test.range("q2"), "use of undeclared identifier 'clangd'; " + "did you mean 'clang'?"), + WithFix( + _, // change clangd to clangd + Fix(Test.range("insert"), "#include \"y.h\"\n", + "Add include \"y.h\" for symbol clang::clangd::ns::Y"))), + AllOf(Diag(Test.range("ns"), + "no member named 'ns' in namespace 'clang'"), + WithFix(Fix( + Test.range("insert"), "#include \"y.h\"\n", + "Add include \"y.h\" for symbol clang::clangd::ns::Y"))))); +} + +TEST(IncludeFixerTest, SpecifiedScopeIsNamespaceAlias) { + Annotations Test(R"cpp( +$insert[[]]namespace a {} +namespace b = a; +namespace c { + b::$[[X]] x; +} + )cpp"); + auto TU = TestTU::withCode(Test.code()); + auto Index = buildIndexWithSymbol( + SymbolWithHeader{"a::X", "unittest:///x.h", "\"x.h\""}); + TU.ExternalIndex = Index.get(); + + EXPECT_THAT(TU.build().getDiagnostics(), + UnorderedElementsAre(AllOf( + Diag(Test.range(), "no type named 'X' in namespace 'a'"), + WithFix(Fix(Test.range("insert"), "#include \"x.h\"\n", + "Add include \"x.h\" for symbol a::X"))))); +} + +} // namespace +} // namespace clangd +} // namespace clang + Index: clang-tools-extra/trunk/clangd/unittests/DraftStoreTests.cpp =================================================================== --- clang-tools-extra/trunk/clangd/unittests/DraftStoreTests.cpp +++ clang-tools-extra/trunk/clangd/unittests/DraftStoreTests.cpp @@ -0,0 +1,347 @@ +//===-- DraftStoreTests.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 "DraftStore.h" +#include "SourceCode.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace clang { +namespace clangd { +namespace { + +struct IncrementalTestStep { + llvm::StringRef Src; + llvm::StringRef Contents; +}; + +int rangeLength(llvm::StringRef Code, const Range &Rng) { + llvm::Expected Start = positionToOffset(Code, Rng.start); + llvm::Expected End = positionToOffset(Code, Rng.end); + assert(Start); + assert(End); + return *End - *Start; +} + +/// Send the changes one by one to updateDraft, verify the intermediate results. +void stepByStep(llvm::ArrayRef Steps) { + DraftStore DS; + Annotations InitialSrc(Steps.front().Src); + constexpr llvm::StringLiteral Path("/hello.cpp"); + + // Set the initial content. + DS.addDraft(Path, InitialSrc.code()); + + for (size_t i = 1; i < Steps.size(); i++) { + Annotations SrcBefore(Steps[i - 1].Src); + Annotations SrcAfter(Steps[i].Src); + llvm::StringRef Contents = Steps[i - 1].Contents; + TextDocumentContentChangeEvent Event{ + SrcBefore.range(), + rangeLength(SrcBefore.code(), SrcBefore.range()), + Contents.str(), + }; + + llvm::Expected Result = DS.updateDraft(Path, {Event}); + ASSERT_TRUE(!!Result); + EXPECT_EQ(*Result, SrcAfter.code()); + EXPECT_EQ(*DS.getDraft(Path), SrcAfter.code()); + } +} + +/// Send all the changes at once to updateDraft, check only the final result. +void allAtOnce(llvm::ArrayRef Steps) { + DraftStore DS; + Annotations InitialSrc(Steps.front().Src); + Annotations FinalSrc(Steps.back().Src); + constexpr llvm::StringLiteral Path("/hello.cpp"); + std::vector Changes; + + for (size_t i = 0; i < Steps.size() - 1; i++) { + Annotations Src(Steps[i].Src); + llvm::StringRef Contents = Steps[i].Contents; + + Changes.push_back({ + Src.range(), + rangeLength(Src.code(), Src.range()), + Contents.str(), + }); + } + + // Set the initial content. + DS.addDraft(Path, InitialSrc.code()); + + llvm::Expected Result = DS.updateDraft(Path, Changes); + + ASSERT_TRUE(!!Result) << llvm::toString(Result.takeError()); + EXPECT_EQ(*Result, FinalSrc.code()); + EXPECT_EQ(*DS.getDraft(Path), FinalSrc.code()); +} + +TEST(DraftStoreIncrementalUpdateTest, Simple) { + // clang-format off + IncrementalTestStep Steps[] = + { + // Replace a range + { +R"cpp(static int +hello[[World]]() +{})cpp", + "Universe" + }, + // Delete a range + { +R"cpp(static int +hello[[Universe]]() +{})cpp", + "" + }, + // Add a range + { +R"cpp(static int +hello[[]]() +{})cpp", + "Monde" + }, + { +R"cpp(static int +helloMonde() +{})cpp", + "" + } + }; + // clang-format on + + stepByStep(Steps); + allAtOnce(Steps); +} + +TEST(DraftStoreIncrementalUpdateTest, MultiLine) { + // clang-format off + IncrementalTestStep Steps[] = + { + // Replace a range + { +R"cpp(static [[int +helloWorld]]() +{})cpp", +R"cpp(char +welcome)cpp" + }, + // Delete a range + { +R"cpp(static char[[ +welcome]]() +{})cpp", + "" + }, + // Add a range + { +R"cpp(static char[[]]() +{})cpp", + R"cpp( +cookies)cpp" + }, + // Replace the whole file + { +R"cpp([[static char +cookies() +{}]])cpp", + R"cpp(#include +)cpp" + }, + // Delete the whole file + { + R"cpp([[#include +]])cpp", + "", + }, + // Add something to an empty file + { + "[[]]", + R"cpp(int main() { +)cpp", + }, + { + R"cpp(int main() { +)cpp", + "" + } + }; + // clang-format on + + stepByStep(Steps); + allAtOnce(Steps); +} + +TEST(DraftStoreIncrementalUpdateTest, WrongRangeLength) { + DraftStore DS; + Path File = "foo.cpp"; + + DS.addDraft(File, "int main() {}\n"); + + TextDocumentContentChangeEvent Change; + Change.range.emplace(); + Change.range->start.line = 0; + Change.range->start.character = 0; + Change.range->end.line = 0; + Change.range->end.character = 2; + Change.rangeLength = 10; + + Expected Result = DS.updateDraft(File, {Change}); + + EXPECT_TRUE(!Result); + EXPECT_EQ( + toString(Result.takeError()), + "Change's rangeLength (10) doesn't match the computed range length (2)."); +} + +TEST(DraftStoreIncrementalUpdateTest, EndBeforeStart) { + DraftStore DS; + Path File = "foo.cpp"; + + DS.addDraft(File, "int main() {}\n"); + + TextDocumentContentChangeEvent Change; + Change.range.emplace(); + Change.range->start.line = 0; + Change.range->start.character = 5; + Change.range->end.line = 0; + Change.range->end.character = 3; + + Expected Result = DS.updateDraft(File, {Change}); + + EXPECT_TRUE(!Result); + EXPECT_EQ(toString(Result.takeError()), + "Range's end position (0:3) is before start position (0:5)"); +} + +TEST(DraftStoreIncrementalUpdateTest, StartCharOutOfRange) { + DraftStore DS; + Path File = "foo.cpp"; + + DS.addDraft(File, "int main() {}\n"); + + TextDocumentContentChangeEvent Change; + Change.range.emplace(); + Change.range->start.line = 0; + Change.range->start.character = 100; + Change.range->end.line = 0; + Change.range->end.character = 100; + Change.text = "foo"; + + Expected Result = DS.updateDraft(File, {Change}); + + EXPECT_TRUE(!Result); + EXPECT_EQ(toString(Result.takeError()), + "utf-16 offset 100 is invalid for line 0"); +} + +TEST(DraftStoreIncrementalUpdateTest, EndCharOutOfRange) { + DraftStore DS; + Path File = "foo.cpp"; + + DS.addDraft(File, "int main() {}\n"); + + TextDocumentContentChangeEvent Change; + Change.range.emplace(); + Change.range->start.line = 0; + Change.range->start.character = 0; + Change.range->end.line = 0; + Change.range->end.character = 100; + Change.text = "foo"; + + Expected Result = DS.updateDraft(File, {Change}); + + EXPECT_TRUE(!Result); + EXPECT_EQ(toString(Result.takeError()), + "utf-16 offset 100 is invalid for line 0"); +} + +TEST(DraftStoreIncrementalUpdateTest, StartLineOutOfRange) { + DraftStore DS; + Path File = "foo.cpp"; + + DS.addDraft(File, "int main() {}\n"); + + TextDocumentContentChangeEvent Change; + Change.range.emplace(); + Change.range->start.line = 100; + Change.range->start.character = 0; + Change.range->end.line = 100; + Change.range->end.character = 0; + Change.text = "foo"; + + Expected Result = DS.updateDraft(File, {Change}); + + EXPECT_TRUE(!Result); + EXPECT_EQ(toString(Result.takeError()), "Line value is out of range (100)"); +} + +TEST(DraftStoreIncrementalUpdateTest, EndLineOutOfRange) { + DraftStore DS; + Path File = "foo.cpp"; + + DS.addDraft(File, "int main() {}\n"); + + TextDocumentContentChangeEvent Change; + Change.range.emplace(); + Change.range->start.line = 0; + Change.range->start.character = 0; + Change.range->end.line = 100; + Change.range->end.character = 0; + Change.text = "foo"; + + Expected Result = DS.updateDraft(File, {Change}); + + EXPECT_TRUE(!Result); + EXPECT_EQ(toString(Result.takeError()), "Line value is out of range (100)"); +} + +/// Check that if a valid change is followed by an invalid change, the original +/// version of the document (prior to all changes) is kept. +TEST(DraftStoreIncrementalUpdateTest, InvalidRangeInASequence) { + DraftStore DS; + Path File = "foo.cpp"; + + StringRef OriginalContents = "int main() {}\n"; + DS.addDraft(File, OriginalContents); + + // The valid change + TextDocumentContentChangeEvent Change1; + Change1.range.emplace(); + Change1.range->start.line = 0; + Change1.range->start.character = 0; + Change1.range->end.line = 0; + Change1.range->end.character = 0; + Change1.text = "Hello "; + + // The invalid change + TextDocumentContentChangeEvent Change2; + Change2.range.emplace(); + Change2.range->start.line = 0; + Change2.range->start.character = 5; + Change2.range->end.line = 0; + Change2.range->end.character = 100; + Change2.text = "something"; + + Expected Result = DS.updateDraft(File, {Change1, Change2}); + + EXPECT_TRUE(!Result); + EXPECT_EQ(toString(Result.takeError()), + "utf-16 offset 100 is invalid for line 0"); + + Optional Contents = DS.getDraft(File); + EXPECT_TRUE(Contents); + EXPECT_EQ(*Contents, OriginalContents); +} + +} // namespace +} // namespace clangd +} // namespace clang Index: clang-tools-extra/trunk/clangd/unittests/ExpectedTypeTest.cpp =================================================================== --- clang-tools-extra/trunk/clangd/unittests/ExpectedTypeTest.cpp +++ clang-tools-extra/trunk/clangd/unittests/ExpectedTypeTest.cpp @@ -0,0 +1,153 @@ +//===-- ExpectedTypeTest.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 "ClangdUnit.h" +#include "ExpectedTypes.h" +#include "TestTU.h" +#include "clang/AST/ASTContext.h" +#include "clang/AST/Decl.h" +#include "llvm/ADT/StringRef.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace clang { +namespace clangd { +namespace { + +using ::testing::Field; +using ::testing::Matcher; +using ::testing::SizeIs; +using ::testing::UnorderedElementsAreArray; + +class ExpectedTypeConversionTest : public ::testing::Test { +protected: + void build(llvm::StringRef Code) { + assert(!AST && "AST built twice"); + AST = TestTU::withCode(Code).build(); + } + + const ValueDecl *decl(llvm::StringRef Name) { + return &cast(findDecl(*AST, Name)); + } + + QualType typeOf(llvm::StringRef Name) { + return decl(Name)->getType().getCanonicalType(); + } + + /// An overload for convenience. + llvm::Optional fromCompletionResult(const ValueDecl *D) { + return OpaqueType::fromCompletionResult( + ASTCtx(), CodeCompletionResult(D, CCP_Declaration)); + } + + /// A set of DeclNames whose type match each other computed by + /// OpaqueType::fromCompletionResult. + using EquivClass = std::set; + + Matcher> + ClassesAre(llvm::ArrayRef Classes) { + using MapEntry = std::map::value_type; + + std::vector> Elements; + Elements.reserve(Classes.size()); + for (auto &Cls : Classes) + Elements.push_back(Field(&MapEntry::second, Cls)); + return UnorderedElementsAreArray(Elements); + } + + // Groups \p Decls into equivalence classes based on the result of + // 'OpaqueType::fromCompletionResult'. + std::map + buildEquivClasses(llvm::ArrayRef DeclNames) { + std::map Classes; + for (llvm::StringRef Name : DeclNames) { + auto Type = OpaqueType::fromType(ASTCtx(), typeOf(Name)); + Classes[Type->raw()].insert(Name); + } + return Classes; + } + + ASTContext &ASTCtx() { return AST->getASTContext(); } + +private: + // Set after calling build(). + llvm::Optional AST; +}; + +TEST_F(ExpectedTypeConversionTest, BasicTypes) { + build(R"cpp( + // ints. + bool b; + int i; + unsigned int ui; + long long ll; + + // floats. + float f; + double d; + + // pointers + int* iptr; + bool* bptr; + + // user-defined types. + struct X {}; + X user_type; + )cpp"); + + EXPECT_THAT(buildEquivClasses({"b", "i", "ui", "ll", "f", "d", "iptr", "bptr", + "user_type"}), + ClassesAre({{"b"}, + {"i", "ui", "ll"}, + {"f", "d"}, + {"iptr"}, + {"bptr"}, + {"user_type"}})); +} + +TEST_F(ExpectedTypeConversionTest, ReferencesDontMatter) { + build(R"cpp( + int noref; + int & ref = noref; + const int & const_ref = noref; + int && rv_ref = 10; + )cpp"); + + EXPECT_THAT(buildEquivClasses({"noref", "ref", "const_ref", "rv_ref"}), + SizeIs(1)); +} + +TEST_F(ExpectedTypeConversionTest, ArraysDecay) { + build(R"cpp( + int arr[2]; + int (&arr_ref)[2] = arr; + int *ptr; + )cpp"); + + EXPECT_THAT(buildEquivClasses({"arr", "arr_ref", "ptr"}), SizeIs(1)); +} + +TEST_F(ExpectedTypeConversionTest, FunctionReturns) { + build(R"cpp( + int returns_int(); + int* returns_ptr(); + + int int_; + int* int_ptr; + )cpp"); + + OpaqueType IntTy = *OpaqueType::fromType(ASTCtx(), typeOf("int_")); + EXPECT_EQ(fromCompletionResult(decl("returns_int")), IntTy); + + OpaqueType IntPtrTy = *OpaqueType::fromType(ASTCtx(), typeOf("int_ptr")); + EXPECT_EQ(fromCompletionResult(decl("returns_ptr")), IntPtrTy); +} + +} // namespace +} // namespace clangd +} // namespace clang Index: clang-tools-extra/trunk/clangd/unittests/FSTests.cpp =================================================================== --- clang-tools-extra/trunk/clangd/unittests/FSTests.cpp +++ clang-tools-extra/trunk/clangd/unittests/FSTests.cpp @@ -0,0 +1,50 @@ +//===-- FSTests.cpp - File system related tests -----------------*- 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 "FS.h" +#include "TestFS.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace clang { +namespace clangd { +namespace { + +TEST(FSTests, PreambleStatusCache) { + llvm::StringMap Files; + Files["x"] = ""; + Files["y"] = ""; + Files["main"] = ""; + auto FS = buildTestFS(Files); + FS->setCurrentWorkingDirectory(testRoot()); + + PreambleFileStatusCache StatCache(testPath("main")); + auto ProduceFS = StatCache.getProducingFS(FS); + EXPECT_TRUE(ProduceFS->openFileForRead("x")); + EXPECT_TRUE(ProduceFS->status("y")); + EXPECT_TRUE(ProduceFS->status("main")); + + EXPECT_TRUE(StatCache.lookup(testPath("x")).hasValue()); + EXPECT_TRUE(StatCache.lookup(testPath("y")).hasValue()); + // Main file is not cached. + EXPECT_FALSE(StatCache.lookup(testPath("main")).hasValue()); + + llvm::vfs::Status S("fake", llvm::sys::fs::UniqueID(0, 0), + std::chrono::system_clock::now(), 0, 0, 1024, + llvm::sys::fs::file_type::regular_file, + llvm::sys::fs::all_all); + StatCache.update(*FS, S); + auto ConsumeFS = StatCache.getConsumingFS(FS); + auto Cached = ConsumeFS->status(testPath("fake")); + EXPECT_TRUE(Cached); + EXPECT_EQ(Cached->getName(), S.getName()); +} + +} // namespace +} // namespace clangd +} // namespace clang Index: clang-tools-extra/trunk/clangd/unittests/FileDistanceTests.cpp =================================================================== --- clang-tools-extra/trunk/clangd/unittests/FileDistanceTests.cpp +++ clang-tools-extra/trunk/clangd/unittests/FileDistanceTests.cpp @@ -0,0 +1,123 @@ +//===-- FileDistanceTests.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 "FileDistance.h" +#include "TestFS.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace clang { +namespace clangd { +namespace { + +TEST(FileDistanceTests, Distance) { + FileDistanceOptions Opts; + Opts.UpCost = 5; + Opts.DownCost = 3; + SourceParams CostTwo; + CostTwo.Cost = 2; + FileDistance D( + {{"tools/clang/lib/Format/FormatToken.cpp", SourceParams()}, + {"tools/clang/include/clang/Format/FormatToken.h", SourceParams()}, + {"include/llvm/ADT/StringRef.h", CostTwo}}, + Opts); + + // Source + EXPECT_EQ(D.distance("tools/clang/lib/Format/FormatToken.cpp"), 0u); + EXPECT_EQ(D.distance("include/llvm/ADT/StringRef.h"), 2u); + // Parent + EXPECT_EQ(D.distance("tools/clang/lib/Format/"), 5u); + // Child + EXPECT_EQ(D.distance("tools/clang/lib/Format/FormatToken.cpp/Oops"), 3u); + // Ancestor (up+up+up+up) + EXPECT_EQ(D.distance("/"), 22u); + // Sibling (up+down) + EXPECT_EQ(D.distance("tools/clang/lib/Format/AnotherFile.cpp"), 8u); + // Cousin (up+up+down+down) + EXPECT_EQ(D.distance("include/llvm/Support/Allocator.h"), 18u); + // First cousin, once removed (up+up+up+down+down) + EXPECT_EQ(D.distance("include/llvm-c/Core.h"), 23u); +} + +TEST(FileDistanceTests, BadSource) { + // We mustn't assume that paths above sources are best reached via them. + FileDistanceOptions Opts; + Opts.UpCost = 5; + Opts.DownCost = 3; + SourceParams CostLots; + CostLots.Cost = 100; + FileDistance D({{"a", SourceParams()}, {"b/b/b", CostLots}}, Opts); + EXPECT_EQ(D.distance("b"), 8u); // a+up+down, not b+up+up + EXPECT_EQ(D.distance("b/b/b"), 14u); // a+up+down+down+down, not b + EXPECT_EQ(D.distance("b/b/b/c"), 17u); // a+up+down+down+down+down, not b+down +} + +// Force the unittest URI scheme to be linked, +static int LLVM_ATTRIBUTE_UNUSED UseUnittestScheme = UnittestSchemeAnchorSource; + +TEST(FileDistanceTests, URI) { + FileDistanceOptions Opts; + Opts.UpCost = 5; + Opts.DownCost = 3; + SourceParams CostLots; + CostLots.Cost = 1000; + + URIDistance D({{testPath("foo"), CostLots}, + {"/not/a/testpath", SourceParams()}, + {"C:\\not\\a\\testpath", SourceParams()}}, + Opts); +#ifdef _WIN32 + EXPECT_EQ(D.distance("file:///C%3a/not/a/testpath/either"), 3u); +#else + EXPECT_EQ(D.distance("file:///not/a/testpath/either"), 3u); +#endif + EXPECT_EQ(D.distance("unittest:///foo"), 1000u); + EXPECT_EQ(D.distance("unittest:///bar"), 1008u); +} + +TEST(FileDistance, LimitUpTraversals) { + FileDistanceOptions Opts; + Opts.UpCost = Opts.DownCost = 1; + SourceParams CheapButLimited, CostLots; + CheapButLimited.MaxUpTraversals = 1; + CostLots.Cost = 100; + + FileDistance D({{"/", CostLots}, {"/a/b/c", CheapButLimited}}, Opts); + EXPECT_EQ(D.distance("/a"), 101u); + EXPECT_EQ(D.distance("/a/z"), 102u); + EXPECT_EQ(D.distance("/a/b"), 1u); + EXPECT_EQ(D.distance("/a/b/z"), 2u); +} + +TEST(FileDistance, DisallowDownTraversalsFromRoot) { + FileDistanceOptions Opts; + Opts.UpCost = Opts.DownCost = 1; + Opts.AllowDownTraversalFromRoot = false; + SourceParams CostLots; + CostLots.Cost = 100; + + FileDistance D({{"/", SourceParams()}, {"/a/b/c", CostLots}}, Opts); + EXPECT_EQ(D.distance("/"), 0u); + EXPECT_EQ(D.distance("/a"), 102u); + EXPECT_EQ(D.distance("/a/b"), 101u); + EXPECT_EQ(D.distance("/x"), FileDistance::Unreachable); +} + +TEST(ScopeDistance, Smoke) { + ScopeDistance D({"x::y::z", "x::", "", "a::"}); + EXPECT_EQ(D.distance("x::y::z::"), 0u); + EXPECT_GT(D.distance("x::y::"), D.distance("x::y::z::")); + EXPECT_GT(D.distance("x::"), D.distance("x::y::")); + EXPECT_GT(D.distance("x::y::z::down::"), D.distance("x::y::")); + EXPECT_GT(D.distance(""), D.distance("a::")); + EXPECT_GT(D.distance("x::"), D.distance("a::")); +} + +} // namespace +} // namespace clangd +} // namespace clang Index: clang-tools-extra/trunk/clangd/unittests/FileIndexTests.cpp =================================================================== --- clang-tools-extra/trunk/clangd/unittests/FileIndexTests.cpp +++ clang-tools-extra/trunk/clangd/unittests/FileIndexTests.cpp @@ -0,0 +1,371 @@ +//===-- FileIndexTests.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 "AST.h" +#include "Annotations.h" +#include "ClangdUnit.h" +#include "SyncAPI.h" +#include "TestFS.h" +#include "TestTU.h" +#include "index/CanonicalIncludes.h" +#include "index/FileIndex.h" +#include "index/Index.h" +#include "clang/Frontend/CompilerInvocation.h" +#include "clang/Frontend/Utils.h" +#include "clang/Index/IndexSymbol.h" +#include "clang/Lex/Preprocessor.h" +#include "clang/Tooling/CompilationDatabase.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using testing::_; +using testing::AllOf; +using testing::Contains; +using testing::ElementsAre; +using testing::IsEmpty; +using testing::Pair; +using testing::UnorderedElementsAre; + +MATCHER_P(RefRange, Range, "") { + return std::make_tuple(arg.Location.Start.line(), arg.Location.Start.column(), + arg.Location.End.line(), arg.Location.End.column()) == + std::make_tuple(Range.start.line, Range.start.character, + Range.end.line, Range.end.character); +} +MATCHER_P(FileURI, F, "") { return llvm::StringRef(arg.Location.FileURI) == F; } +MATCHER_P(DeclURI, U, "") { + return llvm::StringRef(arg.CanonicalDeclaration.FileURI) == U; +} +MATCHER_P(DefURI, U, "") { + return llvm::StringRef(arg.Definition.FileURI) == U; +} +MATCHER_P(QName, N, "") { return (arg.Scope + arg.Name).str() == N; } + +namespace clang { +namespace clangd { +namespace { +testing::Matcher +RefsAre(std::vector> Matchers) { + return ElementsAre(testing::Pair(_, UnorderedElementsAreArray(Matchers))); +} + +Symbol symbol(llvm::StringRef ID) { + Symbol Sym; + Sym.ID = SymbolID(ID); + Sym.Name = ID; + return Sym; +} + +std::unique_ptr numSlab(int Begin, int End) { + SymbolSlab::Builder Slab; + for (int i = Begin; i <= End; i++) + Slab.insert(symbol(std::to_string(i))); + return llvm::make_unique(std::move(Slab).build()); +} + +std::unique_ptr refSlab(const SymbolID &ID, const char *Path) { + RefSlab::Builder Slab; + Ref R; + R.Location.FileURI = Path; + R.Kind = RefKind::Reference; + Slab.insert(ID, R); + return llvm::make_unique(std::move(Slab).build()); +} + +TEST(FileSymbolsTest, UpdateAndGet) { + FileSymbols FS; + EXPECT_THAT(runFuzzyFind(*FS.buildIndex(IndexType::Light), ""), IsEmpty()); + + FS.update("f1", numSlab(1, 3), refSlab(SymbolID("1"), "f1.cc")); + EXPECT_THAT(runFuzzyFind(*FS.buildIndex(IndexType::Light), ""), + UnorderedElementsAre(QName("1"), QName("2"), QName("3"))); + EXPECT_THAT(getRefs(*FS.buildIndex(IndexType::Light), SymbolID("1")), + RefsAre({FileURI("f1.cc")})); +} + +TEST(FileSymbolsTest, Overlap) { + FileSymbols FS; + FS.update("f1", numSlab(1, 3), nullptr); + FS.update("f2", numSlab(3, 5), nullptr); + for (auto Type : {IndexType::Light, IndexType::Heavy}) + EXPECT_THAT(runFuzzyFind(*FS.buildIndex(Type), ""), + UnorderedElementsAre(QName("1"), QName("2"), QName("3"), + QName("4"), QName("5"))); +} + +TEST(FileSymbolsTest, MergeOverlap) { + FileSymbols FS; + auto OneSymboSlab = [](Symbol Sym) { + SymbolSlab::Builder S; + S.insert(Sym); + return llvm::make_unique(std::move(S).build()); + }; + auto X1 = symbol("x"); + X1.CanonicalDeclaration.FileURI = "file:///x1"; + auto X2 = symbol("x"); + X2.Definition.FileURI = "file:///x2"; + + FS.update("f1", OneSymboSlab(X1), nullptr); + FS.update("f2", OneSymboSlab(X2), nullptr); + for (auto Type : {IndexType::Light, IndexType::Heavy}) + EXPECT_THAT( + runFuzzyFind(*FS.buildIndex(Type, DuplicateHandling::Merge), "x"), + UnorderedElementsAre( + AllOf(QName("x"), DeclURI("file:///x1"), DefURI("file:///x2")))); +} + +TEST(FileSymbolsTest, SnapshotAliveAfterRemove) { + FileSymbols FS; + + SymbolID ID("1"); + FS.update("f1", numSlab(1, 3), refSlab(ID, "f1.cc")); + + auto Symbols = FS.buildIndex(IndexType::Light); + EXPECT_THAT(runFuzzyFind(*Symbols, ""), + UnorderedElementsAre(QName("1"), QName("2"), QName("3"))); + EXPECT_THAT(getRefs(*Symbols, ID), RefsAre({FileURI("f1.cc")})); + + FS.update("f1", nullptr, nullptr); + auto Empty = FS.buildIndex(IndexType::Light); + EXPECT_THAT(runFuzzyFind(*Empty, ""), IsEmpty()); + EXPECT_THAT(getRefs(*Empty, ID), ElementsAre()); + + EXPECT_THAT(runFuzzyFind(*Symbols, ""), + UnorderedElementsAre(QName("1"), QName("2"), QName("3"))); + EXPECT_THAT(getRefs(*Symbols, ID), RefsAre({FileURI("f1.cc")})); +} + +// Adds Basename.cpp, which includes Basename.h, which contains Code. +void update(FileIndex &M, llvm::StringRef Basename, llvm::StringRef Code) { + TestTU File; + File.Filename = (Basename + ".cpp").str(); + File.HeaderFilename = (Basename + ".h").str(); + File.HeaderCode = Code; + auto AST = File.build(); + M.updatePreamble(File.Filename, AST.getASTContext(), AST.getPreprocessorPtr(), + AST.getCanonicalIncludes()); +} + +TEST(FileIndexTest, CustomizedURIScheme) { + FileIndex M; + update(M, "f", "class string {};"); + + EXPECT_THAT(runFuzzyFind(M, ""), ElementsAre(DeclURI("unittest:///f.h"))); +} + +TEST(FileIndexTest, IndexAST) { + FileIndex M; + update(M, "f1", "namespace ns { void f() {} class X {}; }"); + + FuzzyFindRequest Req; + Req.Query = ""; + Req.Scopes = {"ns::"}; + EXPECT_THAT(runFuzzyFind(M, Req), + UnorderedElementsAre(QName("ns::f"), QName("ns::X"))); +} + +TEST(FileIndexTest, NoLocal) { + FileIndex M; + update(M, "f1", "namespace ns { void f() { int local = 0; } class X {}; }"); + + EXPECT_THAT( + runFuzzyFind(M, ""), + UnorderedElementsAre(QName("ns"), QName("ns::f"), QName("ns::X"))); +} + +TEST(FileIndexTest, IndexMultiASTAndDeduplicate) { + FileIndex M; + update(M, "f1", "namespace ns { void f() {} class X {}; }"); + update(M, "f2", "namespace ns { void ff() {} class X {}; }"); + + FuzzyFindRequest Req; + Req.Scopes = {"ns::"}; + EXPECT_THAT( + runFuzzyFind(M, Req), + UnorderedElementsAre(QName("ns::f"), QName("ns::X"), QName("ns::ff"))); +} + +TEST(FileIndexTest, ClassMembers) { + FileIndex M; + update(M, "f1", "class X { static int m1; int m2; static void f(); };"); + + EXPECT_THAT(runFuzzyFind(M, ""), + UnorderedElementsAre(QName("X"), QName("X::m1"), QName("X::m2"), + QName("X::f"))); +} + +TEST(FileIndexTest, IncludeCollected) { + FileIndex M; + update( + M, "f", + "// IWYU pragma: private, include \nclass string {};"); + + auto Symbols = runFuzzyFind(M, ""); + EXPECT_THAT(Symbols, ElementsAre(_)); + EXPECT_THAT(Symbols.begin()->IncludeHeaders.front().IncludeHeader, + ""); +} + +TEST(FileIndexTest, HasSystemHeaderMappingsInPreamble) { + TestTU TU; + TU.HeaderCode = "class Foo{};"; + TU.HeaderFilename = "algorithm"; + + auto Symbols = runFuzzyFind(*TU.index(), ""); + EXPECT_THAT(Symbols, ElementsAre(_)); + EXPECT_THAT(Symbols.begin()->IncludeHeaders.front().IncludeHeader, + ""); +} + +TEST(FileIndexTest, TemplateParamsInLabel) { + auto Source = R"cpp( +template +class vector { +}; + +template +vector make_vector(Arg A) {} +)cpp"; + + FileIndex M; + update(M, "f", Source); + + auto Symbols = runFuzzyFind(M, ""); + EXPECT_THAT(Symbols, + UnorderedElementsAre(QName("vector"), QName("make_vector"))); + auto It = Symbols.begin(); + Symbol Vector = *It++; + Symbol MakeVector = *It++; + if (MakeVector.Name == "vector") + std::swap(MakeVector, Vector); + + EXPECT_EQ(Vector.Signature, ""); + EXPECT_EQ(Vector.CompletionSnippetSuffix, "<${1:class Ty}>"); + + EXPECT_EQ(MakeVector.Signature, "(Arg A)"); + EXPECT_EQ(MakeVector.CompletionSnippetSuffix, "<${1:class Ty}>(${2:Arg A})"); +} + +TEST(FileIndexTest, RebuildWithPreamble) { + auto FooCpp = testPath("foo.cpp"); + auto FooH = testPath("foo.h"); + // Preparse ParseInputs. + ParseInputs PI; + PI.CompileCommand.Directory = testRoot(); + PI.CompileCommand.Filename = FooCpp; + PI.CompileCommand.CommandLine = {"clang", "-xc++", FooCpp}; + + llvm::StringMap Files; + Files[FooCpp] = ""; + Files[FooH] = R"cpp( + namespace ns_in_header { + int func_in_header(); + } + )cpp"; + PI.FS = buildTestFS(std::move(Files)); + + PI.Contents = R"cpp( + #include "foo.h" + namespace ns_in_source { + int func_in_source(); + } + )cpp"; + + // Rebuild the file. + auto CI = buildCompilerInvocation(PI); + + FileIndex Index; + bool IndexUpdated = false; + buildPreamble( + FooCpp, *CI, /*OldPreamble=*/nullptr, tooling::CompileCommand(), PI, + /*StoreInMemory=*/true, + [&](ASTContext &Ctx, std::shared_ptr PP, + const CanonicalIncludes &CanonIncludes) { + EXPECT_FALSE(IndexUpdated) << "Expected only a single index update"; + IndexUpdated = true; + Index.updatePreamble(FooCpp, Ctx, std::move(PP), CanonIncludes); + }); + ASSERT_TRUE(IndexUpdated); + + // Check the index contains symbols from the preamble, but not from the main + // file. + FuzzyFindRequest Req; + Req.Query = ""; + Req.Scopes = {"", "ns_in_header::"}; + + EXPECT_THAT(runFuzzyFind(Index, Req), + UnorderedElementsAre(QName("ns_in_header"), + QName("ns_in_header::func_in_header"))); +} + +TEST(FileIndexTest, Refs) { + const char *HeaderCode = "class Foo {};"; + Annotations MainCode(R"cpp( + void f() { + $foo[[Foo]] foo; + } + )cpp"); + + auto Foo = + findSymbol(TestTU::withHeaderCode(HeaderCode).headerSymbols(), "Foo"); + + RefsRequest Request; + Request.IDs = {Foo.ID}; + + FileIndex Index; + // Add test.cc + TestTU Test; + Test.HeaderCode = HeaderCode; + Test.Code = MainCode.code(); + Test.Filename = "test.cc"; + auto AST = Test.build(); + Index.updateMain(Test.Filename, AST); + // Add test2.cc + TestTU Test2; + Test2.HeaderCode = HeaderCode; + Test2.Code = MainCode.code(); + Test2.Filename = "test2.cc"; + AST = Test2.build(); + Index.updateMain(Test2.Filename, AST); + + EXPECT_THAT(getRefs(Index, Foo.ID), + RefsAre({AllOf(RefRange(MainCode.range("foo")), + FileURI("unittest:///test.cc")), + AllOf(RefRange(MainCode.range("foo")), + FileURI("unittest:///test2.cc"))})); +} + +TEST(FileIndexTest, CollectMacros) { + FileIndex M; + update(M, "f", "#define CLANGD 1"); + EXPECT_THAT(runFuzzyFind(M, ""), Contains(QName("CLANGD"))); +} + +TEST(FileIndexTest, ReferencesInMainFileWithPreamble) { + TestTU TU; + TU.HeaderCode = "class Foo{};"; + Annotations Main(R"cpp( + #include "foo.h" + void f() { + [[Foo]] foo; + } + )cpp"); + TU.Code = Main.code(); + auto AST = TU.build(); + FileIndex Index; + Index.updateMain(testPath(TU.Filename), AST); + + // Expect to see references in main file, references in headers are excluded + // because we only index main AST. + EXPECT_THAT(getRefs(Index, findSymbol(TU.headerSymbols(), "Foo").ID), + RefsAre({RefRange(Main.range())})); +} + +} // namespace +} // namespace clangd +} // namespace clang Index: clang-tools-extra/trunk/clangd/unittests/FindSymbolsTests.cpp =================================================================== --- clang-tools-extra/trunk/clangd/unittests/FindSymbolsTests.cpp +++ clang-tools-extra/trunk/clangd/unittests/FindSymbolsTests.cpp @@ -0,0 +1,688 @@ +//===-- FindSymbolsTests.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 "FindSymbols.h" +#include "SyncAPI.h" +#include "TestFS.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace clang { +namespace clangd { + +namespace { + +using ::testing::AllOf; +using ::testing::AnyOf; +using ::testing::ElementsAre; +using ::testing::ElementsAreArray; +using ::testing::Field; +using ::testing::IsEmpty; +using ::testing::UnorderedElementsAre; + +class IgnoreDiagnostics : public DiagnosticsConsumer { + void onDiagnosticsReady(PathRef File, + std::vector Diagnostics) override {} +}; + +// GMock helpers for matching SymbolInfos items. +MATCHER_P(QName, Name, "") { + if (arg.containerName.empty()) + return arg.name == Name; + return (arg.containerName + "::" + arg.name) == Name; +} +MATCHER_P(WithName, N, "") { return arg.name == N; } +MATCHER_P(WithKind, Kind, "") { return arg.kind == Kind; } +MATCHER_P(SymRange, Range, "") { return arg.location.range == Range; } + +// GMock helpers for matching DocumentSymbol. +MATCHER_P(SymNameRange, Range, "") { return arg.selectionRange == Range; } +template +testing::Matcher Children(ChildMatchers... ChildrenM) { + return Field(&DocumentSymbol::children, ElementsAre(ChildrenM...)); +} + +ClangdServer::Options optsForTests() { + auto ServerOpts = ClangdServer::optsForTest(); + ServerOpts.WorkspaceRoot = testRoot(); + ServerOpts.BuildDynamicSymbolIndex = true; + return ServerOpts; +} + +class WorkspaceSymbolsTest : public ::testing::Test { +public: + WorkspaceSymbolsTest() + : Server(CDB, FSProvider, DiagConsumer, optsForTests()) { + // Make sure the test root directory is created. + FSProvider.Files[testPath("unused")] = ""; + CDB.ExtraClangFlags = {"-xc++"}; + } + +protected: + MockFSProvider FSProvider; + MockCompilationDatabase CDB; + IgnoreDiagnostics DiagConsumer; + ClangdServer Server; + int Limit = 0; + + std::vector getSymbols(llvm::StringRef Query) { + EXPECT_TRUE(Server.blockUntilIdleForTest()) << "Waiting for preamble"; + auto SymbolInfos = runWorkspaceSymbols(Server, Query, Limit); + EXPECT_TRUE(bool(SymbolInfos)) << "workspaceSymbols returned an error"; + return *SymbolInfos; + } + + void addFile(llvm::StringRef FileName, llvm::StringRef Contents) { + auto Path = testPath(FileName); + FSProvider.Files[Path] = Contents; + Server.addDocument(Path, Contents); + } +}; + +} // namespace + +TEST_F(WorkspaceSymbolsTest, Macros) { + addFile("foo.cpp", R"cpp( + #define MACRO X + )cpp"); + + // LSP's SymbolKind doesn't have a "Macro" kind, and + // indexSymbolKindToSymbolKind() currently maps macros + // to SymbolKind::String. + EXPECT_THAT(getSymbols("macro"), + ElementsAre(AllOf(QName("MACRO"), WithKind(SymbolKind::String)))); +} + +TEST_F(WorkspaceSymbolsTest, NoLocals) { + addFile("foo.cpp", R"cpp( + void test(int FirstParam, int SecondParam) { + struct LocalClass {}; + int local_var; + })cpp"); + EXPECT_THAT(getSymbols("l"), IsEmpty()); + EXPECT_THAT(getSymbols("p"), IsEmpty()); +} + +TEST_F(WorkspaceSymbolsTest, Globals) { + addFile("foo.h", R"cpp( + int global_var; + + int global_func(); + + struct GlobalStruct {};)cpp"); + addFile("foo.cpp", R"cpp( + #include "foo.h" + )cpp"); + EXPECT_THAT(getSymbols("global"), + UnorderedElementsAre( + AllOf(QName("GlobalStruct"), WithKind(SymbolKind::Struct)), + AllOf(QName("global_func"), WithKind(SymbolKind::Function)), + AllOf(QName("global_var"), WithKind(SymbolKind::Variable)))); +} + +TEST_F(WorkspaceSymbolsTest, Unnamed) { + addFile("foo.h", R"cpp( + struct { + int InUnnamed; + } UnnamedStruct;)cpp"); + addFile("foo.cpp", R"cpp( + #include "foo.h" + )cpp"); + EXPECT_THAT(getSymbols("UnnamedStruct"), + ElementsAre(AllOf(QName("UnnamedStruct"), + WithKind(SymbolKind::Variable)))); + EXPECT_THAT(getSymbols("InUnnamed"), + ElementsAre(AllOf(QName("(anonymous struct)::InUnnamed"), + WithKind(SymbolKind::Field)))); +} + +TEST_F(WorkspaceSymbolsTest, InMainFile) { + addFile("foo.cpp", R"cpp( + int test() {} + static test2() {} + )cpp"); + EXPECT_THAT(getSymbols("test"), ElementsAre(QName("test"), QName("test2"))); +} + +TEST_F(WorkspaceSymbolsTest, Namespaces) { + addFile("foo.h", R"cpp( + namespace ans1 { + int ai1; + namespace ans2 { + int ai2; + } + } + )cpp"); + addFile("foo.cpp", R"cpp( + #include "foo.h" + )cpp"); + EXPECT_THAT(getSymbols("a"), + UnorderedElementsAre(QName("ans1"), QName("ans1::ai1"), + QName("ans1::ans2"), + QName("ans1::ans2::ai2"))); + EXPECT_THAT(getSymbols("::"), ElementsAre(QName("ans1"))); + EXPECT_THAT(getSymbols("::a"), ElementsAre(QName("ans1"))); + EXPECT_THAT(getSymbols("ans1::"), + UnorderedElementsAre(QName("ans1::ai1"), QName("ans1::ans2"))); + EXPECT_THAT(getSymbols("::ans1"), ElementsAre(QName("ans1"))); + EXPECT_THAT(getSymbols("::ans1::"), + UnorderedElementsAre(QName("ans1::ai1"), QName("ans1::ans2"))); + EXPECT_THAT(getSymbols("::ans1::ans2"), ElementsAre(QName("ans1::ans2"))); + EXPECT_THAT(getSymbols("::ans1::ans2::"), + ElementsAre(QName("ans1::ans2::ai2"))); +} + +TEST_F(WorkspaceSymbolsTest, AnonymousNamespace) { + addFile("foo.h", R"cpp( + namespace { + void test() {} + } + )cpp"); + addFile("foo.cpp", R"cpp( + #include "foo.h" + )cpp"); + EXPECT_THAT(getSymbols("test"), ElementsAre(QName("test"))); +} + +TEST_F(WorkspaceSymbolsTest, MultiFile) { + addFile("foo.h", R"cpp( + int foo() { + } + )cpp"); + addFile("foo2.h", R"cpp( + int foo2() { + } + )cpp"); + addFile("foo.cpp", R"cpp( + #include "foo.h" + #include "foo2.h" + )cpp"); + EXPECT_THAT(getSymbols("foo"), + UnorderedElementsAre(QName("foo"), QName("foo2"))); +} + +TEST_F(WorkspaceSymbolsTest, GlobalNamespaceQueries) { + addFile("foo.h", R"cpp( + int foo() { + } + class Foo { + int a; + }; + namespace ns { + int foo2() { + } + } + )cpp"); + addFile("foo.cpp", R"cpp( + #include "foo.h" + )cpp"); + EXPECT_THAT(getSymbols("::"), + UnorderedElementsAre( + AllOf(QName("Foo"), WithKind(SymbolKind::Class)), + AllOf(QName("foo"), WithKind(SymbolKind::Function)), + AllOf(QName("ns"), WithKind(SymbolKind::Namespace)))); + EXPECT_THAT(getSymbols(":"), IsEmpty()); + EXPECT_THAT(getSymbols(""), IsEmpty()); +} + +TEST_F(WorkspaceSymbolsTest, Enums) { + addFile("foo.h", R"cpp( + enum { + Red + }; + enum Color { + Green + }; + enum class Color2 { + Yellow + }; + namespace ns { + enum { + Black + }; + enum Color3 { + Blue + }; + enum class Color4 { + White + }; + } + )cpp"); + addFile("foo.cpp", R"cpp( + #include "foo.h" + )cpp"); + EXPECT_THAT(getSymbols("Red"), ElementsAre(QName("Red"))); + EXPECT_THAT(getSymbols("::Red"), ElementsAre(QName("Red"))); + EXPECT_THAT(getSymbols("Green"), ElementsAre(QName("Green"))); + EXPECT_THAT(getSymbols("Green"), ElementsAre(QName("Green"))); + EXPECT_THAT(getSymbols("Color2::Yellow"), + ElementsAre(QName("Color2::Yellow"))); + EXPECT_THAT(getSymbols("Yellow"), ElementsAre(QName("Color2::Yellow"))); + + EXPECT_THAT(getSymbols("ns::Black"), ElementsAre(QName("ns::Black"))); + EXPECT_THAT(getSymbols("ns::Blue"), ElementsAre(QName("ns::Blue"))); + EXPECT_THAT(getSymbols("ns::Color4::White"), + ElementsAre(QName("ns::Color4::White"))); +} + +TEST_F(WorkspaceSymbolsTest, Ranking) { + addFile("foo.h", R"cpp( + namespace ns{} + void func(); + )cpp"); + addFile("foo.cpp", R"cpp( + #include "foo.h" + )cpp"); + EXPECT_THAT(getSymbols("::"), ElementsAre(QName("func"), QName("ns"))); +} + +TEST_F(WorkspaceSymbolsTest, WithLimit) { + addFile("foo.h", R"cpp( + int foo; + int foo2; + )cpp"); + addFile("foo.cpp", R"cpp( + #include "foo.h" + )cpp"); + // Foo is higher ranked because of exact name match. + EXPECT_THAT(getSymbols("foo"), + UnorderedElementsAre( + AllOf(QName("foo"), WithKind(SymbolKind::Variable)), + AllOf(QName("foo2"), WithKind(SymbolKind::Variable)))); + + Limit = 1; + EXPECT_THAT(getSymbols("foo"), ElementsAre(QName("foo"))); +} + +TEST_F(WorkspaceSymbolsTest, TempSpecs) { + addFile("foo.h", R"cpp( + template class Foo {}; + template class Foo {}; + template <> class Foo {}; + template <> class Foo {}; + )cpp"); + // Foo is higher ranked because of exact name match. + EXPECT_THAT( + getSymbols("Foo"), + UnorderedElementsAre( + AllOf(QName("Foo"), WithKind(SymbolKind::Class)), + AllOf(QName("Foo"), WithKind(SymbolKind::Class)), + AllOf(QName("Foo"), WithKind(SymbolKind::Class)), + AllOf(QName("Foo"), WithKind(SymbolKind::Class)))); +} + +namespace { +class DocumentSymbolsTest : public ::testing::Test { +public: + DocumentSymbolsTest() + : Server(CDB, FSProvider, DiagConsumer, optsForTests()) {} + +protected: + MockFSProvider FSProvider; + MockCompilationDatabase CDB; + IgnoreDiagnostics DiagConsumer; + ClangdServer Server; + + std::vector getSymbols(PathRef File) { + EXPECT_TRUE(Server.blockUntilIdleForTest()) << "Waiting for preamble"; + auto SymbolInfos = runDocumentSymbols(Server, File); + EXPECT_TRUE(bool(SymbolInfos)) << "documentSymbols returned an error"; + return *SymbolInfos; + } + + void addFile(llvm::StringRef FilePath, llvm::StringRef Contents) { + FSProvider.Files[FilePath] = Contents; + Server.addDocument(FilePath, Contents); + } +}; +} // namespace + +TEST_F(DocumentSymbolsTest, BasicSymbols) { + std::string FilePath = testPath("foo.cpp"); + Annotations Main(R"( + class Foo; + class Foo { + Foo() {} + Foo(int a) {} + void $decl[[f]](); + friend void f1(); + friend class Friend; + Foo& operator=(const Foo&); + ~Foo(); + class Nested { + void f(); + }; + }; + class Friend { + }; + + void f1(); + inline void f2() {} + static const int KInt = 2; + const char* kStr = "123"; + + void f1() {} + + namespace foo { + // Type alias + typedef int int32; + using int32_t = int32; + + // Variable + int v1; + + // Namespace + namespace bar { + int v2; + } + // Namespace alias + namespace baz = bar; + + using bar::v2; + } // namespace foo + )"); + + addFile(FilePath, Main.code()); + EXPECT_THAT( + getSymbols(FilePath), + ElementsAreArray( + {AllOf(WithName("Foo"), WithKind(SymbolKind::Class), Children()), + AllOf(WithName("Foo"), WithKind(SymbolKind::Class), + Children(AllOf(WithName("Foo"), WithKind(SymbolKind::Method), + Children()), + AllOf(WithName("Foo"), WithKind(SymbolKind::Method), + Children()), + AllOf(WithName("f"), WithKind(SymbolKind::Method), + Children()), + AllOf(WithName("operator="), + WithKind(SymbolKind::Method), Children()), + AllOf(WithName("~Foo"), WithKind(SymbolKind::Method), + Children()), + AllOf(WithName("Nested"), WithKind(SymbolKind::Class), + Children(AllOf(WithName("f"), + WithKind(SymbolKind::Method), + Children()))))), + AllOf(WithName("Friend"), WithKind(SymbolKind::Class), Children()), + AllOf(WithName("f1"), WithKind(SymbolKind::Function), Children()), + AllOf(WithName("f2"), WithKind(SymbolKind::Function), Children()), + AllOf(WithName("KInt"), WithKind(SymbolKind::Variable), Children()), + AllOf(WithName("kStr"), WithKind(SymbolKind::Variable), Children()), + AllOf(WithName("f1"), WithKind(SymbolKind::Function), Children()), + AllOf(WithName("foo"), WithKind(SymbolKind::Namespace), + Children( + AllOf(WithName("int32"), WithKind(SymbolKind::Class), + Children()), + AllOf(WithName("int32_t"), WithKind(SymbolKind::Class), + Children()), + AllOf(WithName("v1"), WithKind(SymbolKind::Variable), + Children()), + AllOf(WithName("bar"), WithKind(SymbolKind::Namespace), + Children(AllOf(WithName("v2"), + WithKind(SymbolKind::Variable), + Children()))), + AllOf(WithName("baz"), WithKind(SymbolKind::Namespace), + Children()), + AllOf(WithName("v2"), WithKind(SymbolKind::Namespace))))})); +} + +TEST_F(DocumentSymbolsTest, DeclarationDefinition) { + std::string FilePath = testPath("foo.cpp"); + Annotations Main(R"( + class Foo { + void $decl[[f]](); + }; + void Foo::$def[[f]]() { + } + )"); + + addFile(FilePath, Main.code()); + EXPECT_THAT(getSymbols(FilePath), + ElementsAre(AllOf(WithName("Foo"), WithKind(SymbolKind::Class), + Children(AllOf( + WithName("f"), WithKind(SymbolKind::Method), + SymNameRange(Main.range("decl"))))), + AllOf(WithName("f"), WithKind(SymbolKind::Method), + SymNameRange(Main.range("def"))))); +} + +TEST_F(DocumentSymbolsTest, ExternSymbol) { + std::string FilePath = testPath("foo.cpp"); + addFile(testPath("foo.h"), R"cpp( + extern int var; + )cpp"); + addFile(FilePath, R"cpp( + #include "foo.h" + )cpp"); + + EXPECT_THAT(getSymbols(FilePath), IsEmpty()); +} + +TEST_F(DocumentSymbolsTest, NoLocals) { + std::string FilePath = testPath("foo.cpp"); + addFile(FilePath, + R"cpp( + void test(int FirstParam, int SecondParam) { + struct LocalClass {}; + int local_var; + })cpp"); + EXPECT_THAT(getSymbols(FilePath), ElementsAre(WithName("test"))); +} + +TEST_F(DocumentSymbolsTest, Unnamed) { + std::string FilePath = testPath("foo.h"); + addFile(FilePath, + R"cpp( + struct { + int InUnnamed; + } UnnamedStruct; + )cpp"); + EXPECT_THAT( + getSymbols(FilePath), + ElementsAre( + AllOf(WithName("(anonymous struct)"), WithKind(SymbolKind::Struct), + Children(AllOf(WithName("InUnnamed"), + WithKind(SymbolKind::Field), Children()))), + AllOf(WithName("UnnamedStruct"), WithKind(SymbolKind::Variable), + Children()))); +} + +TEST_F(DocumentSymbolsTest, InHeaderFile) { + addFile(testPath("bar.h"), R"cpp( + int foo() { + } + )cpp"); + std::string FilePath = testPath("foo.h"); + addFile(FilePath, R"cpp( + #include "bar.h" + int test() { + } + )cpp"); + addFile(testPath("foo.cpp"), R"cpp( + #include "foo.h" + )cpp"); + EXPECT_THAT(getSymbols(FilePath), ElementsAre(WithName("test"))); +} + +TEST_F(DocumentSymbolsTest, Template) { + std::string FilePath = testPath("foo.cpp"); + addFile(FilePath, R"( + template struct Tmpl {T x = 0;}; + template <> struct Tmpl { + int y = 0; + }; + extern template struct Tmpl; + template struct Tmpl; + + template + int funcTmpl(U a); + template <> + int funcTmpl(double a); + + template + int varTmpl = T(); + template <> + double varTmpl = 10.0; + )"); + EXPECT_THAT( + getSymbols(FilePath), + ElementsAre( + AllOf(WithName("Tmpl"), WithKind(SymbolKind::Struct), + Children(AllOf(WithName("x"), WithKind(SymbolKind::Field)))), + AllOf(WithName("Tmpl"), WithKind(SymbolKind::Struct), + Children(WithName("y"))), + AllOf(WithName("Tmpl"), WithKind(SymbolKind::Struct), + Children()), + AllOf(WithName("Tmpl"), WithKind(SymbolKind::Struct), + Children()), + AllOf(WithName("funcTmpl"), Children()), + AllOf(WithName("funcTmpl"), Children()), + AllOf(WithName("varTmpl"), Children()), + AllOf(WithName("varTmpl"), Children()))); +} + +TEST_F(DocumentSymbolsTest, Namespaces) { + std::string FilePath = testPath("foo.cpp"); + addFile(FilePath, R"cpp( + namespace ans1 { + int ai1; + namespace ans2 { + int ai2; + } + } + namespace { + void test() {} + } + + namespace na { + inline namespace nb { + class Foo {}; + } + } + namespace na { + // This is still inlined. + namespace nb { + class Bar {}; + } + } + )cpp"); + EXPECT_THAT( + getSymbols(FilePath), + ElementsAreArray>( + {AllOf(WithName("ans1"), + Children(AllOf(WithName("ai1"), Children()), + AllOf(WithName("ans2"), Children(WithName("ai2"))))), + AllOf(WithName("(anonymous namespace)"), Children(WithName("test"))), + AllOf(WithName("na"), + Children(AllOf(WithName("nb"), Children(WithName("Foo"))))), + AllOf(WithName("na"), + Children(AllOf(WithName("nb"), Children(WithName("Bar")))))})); +} + +TEST_F(DocumentSymbolsTest, Enums) { + std::string FilePath = testPath("foo.cpp"); + addFile(FilePath, R"( + enum { + Red + }; + enum Color { + Green + }; + enum class Color2 { + Yellow + }; + namespace ns { + enum { + Black + }; + } + )"); + EXPECT_THAT( + getSymbols(FilePath), + ElementsAre( + AllOf(WithName("(anonymous enum)"), Children(WithName("Red"))), + AllOf(WithName("Color"), Children(WithName("Green"))), + AllOf(WithName("Color2"), Children(WithName("Yellow"))), + AllOf(WithName("ns"), Children(AllOf(WithName("(anonymous enum)"), + Children(WithName("Black"))))))); +} + +TEST_F(DocumentSymbolsTest, FromMacro) { + std::string FilePath = testPath("foo.cpp"); + Annotations Main(R"( + #define FF(name) \ + class name##_Test {}; + + $expansion[[FF]](abc); + + #define FF2() \ + class $spelling[[Test]] {}; + + FF2(); + )"); + addFile(FilePath, Main.code()); + EXPECT_THAT( + getSymbols(FilePath), + ElementsAre( + AllOf(WithName("abc_Test"), SymNameRange(Main.range("expansion"))), + AllOf(WithName("Test"), SymNameRange(Main.range("spelling"))))); +} + +TEST_F(DocumentSymbolsTest, FuncTemplates) { + std::string FilePath = testPath("foo.cpp"); + Annotations Source(R"cpp( + template + T foo() {} + + auto x = foo(); + auto y = foo() + )cpp"); + addFile(FilePath, Source.code()); + // Make sure we only see the template declaration, not instantiations. + EXPECT_THAT(getSymbols(FilePath), + ElementsAre(WithName("foo"), WithName("x"), WithName("y"))); +} + +TEST_F(DocumentSymbolsTest, UsingDirectives) { + std::string FilePath = testPath("foo.cpp"); + Annotations Source(R"cpp( + namespace ns { + int foo; + } + + namespace ns_alias = ns; + + using namespace ::ns; // check we don't loose qualifiers. + using namespace ns_alias; // and namespace aliases. + )cpp"); + addFile(FilePath, Source.code()); + EXPECT_THAT(getSymbols(FilePath), + ElementsAre(WithName("ns"), WithName("ns_alias"), + WithName("using namespace ::ns"), + WithName("using namespace ns_alias"))); +} + +TEST_F(DocumentSymbolsTest, TempSpecs) { + addFile("foo.cpp", R"cpp( + template class Foo {}; + template class Foo {}; + template <> class Foo {}; + template <> class Foo {}; + )cpp"); + // Foo is higher ranked because of exact name match. + EXPECT_THAT( + getSymbols("foo.cpp"), + UnorderedElementsAre( + AllOf(WithName("Foo"), WithKind(SymbolKind::Class)), + AllOf(WithName("Foo"), WithKind(SymbolKind::Class)), + AllOf(WithName("Foo"), WithKind(SymbolKind::Class)), + AllOf(WithName("Foo"), WithKind(SymbolKind::Class)))); +} + +} // namespace clangd +} // namespace clang Index: clang-tools-extra/trunk/clangd/unittests/FunctionTests.cpp =================================================================== --- clang-tools-extra/trunk/clangd/unittests/FunctionTests.cpp +++ clang-tools-extra/trunk/clangd/unittests/FunctionTests.cpp @@ -0,0 +1,51 @@ +//===-- FunctionTests.cpp -------------------------------------------------===// +// +// 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 "Function.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace clang { +namespace clangd { +namespace { + +TEST(EventTest, Subscriptions) { + Event E; + int N = 0; + { + Event::Subscription SubA; + // No subscriptions are active. + E.broadcast(42); + EXPECT_EQ(0, N); + + Event::Subscription SubB = E.observe([&](int) { ++N; }); + // Now one is active. + E.broadcast(42); + EXPECT_EQ(1, N); + + SubA = E.observe([&](int) { ++N; }); + // Both are active. + EXPECT_EQ(1, N); + E.broadcast(42); + EXPECT_EQ(3, N); + + SubA = std::move(SubB); + // One is active. + EXPECT_EQ(3, N); + E.broadcast(42); + EXPECT_EQ(4, N); + } + // None are active. + EXPECT_EQ(4, N); + E.broadcast(42); + EXPECT_EQ(4, N); +} + +} // namespace +} // namespace clangd +} // namespace clang Index: clang-tools-extra/trunk/clangd/unittests/FuzzyMatchTests.cpp =================================================================== --- clang-tools-extra/trunk/clangd/unittests/FuzzyMatchTests.cpp +++ clang-tools-extra/trunk/clangd/unittests/FuzzyMatchTests.cpp @@ -0,0 +1,312 @@ +//===-- FuzzyMatchTests.cpp - String fuzzy matcher tests --------*- 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 "FuzzyMatch.h" + +#include "llvm/ADT/StringExtras.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace clang { +namespace clangd { +namespace { +using testing::Not; + +struct ExpectedMatch { + // Annotations are optional, and will not be asserted if absent. + ExpectedMatch(llvm::StringRef Match) : Word(Match), Annotated(Match) { + for (char C : "[]") + Word.erase(std::remove(Word.begin(), Word.end(), C), Word.end()); + if (Word.size() == Annotated->size()) + Annotated = llvm::None; + } + bool accepts(llvm::StringRef ActualAnnotated) const { + return !Annotated || ActualAnnotated == *Annotated; + } + + friend llvm::raw_ostream &operator<<(llvm::raw_ostream &OS, + const ExpectedMatch &M) { + OS << "'" << M.Word; + if (M.Annotated) + OS << "' as " << *M.Annotated; + return OS; + } + + std::string Word; + +private: + llvm::Optional Annotated; +}; + +struct MatchesMatcher : public testing::MatcherInterface { + ExpectedMatch Candidate; + llvm::Optional Score; + MatchesMatcher(ExpectedMatch Candidate, llvm::Optional Score) + : Candidate(std::move(Candidate)), Score(Score) {} + + void DescribeTo(::std::ostream *OS) const override { + llvm::raw_os_ostream(*OS) << "Matches " << Candidate; + if (Score) + *OS << " with score " << *Score; + } + + bool MatchAndExplain(llvm::StringRef Pattern, + testing::MatchResultListener *L) const override { + std::unique_ptr OS( + L->stream() + ? (llvm::raw_ostream *)(new llvm::raw_os_ostream(*L->stream())) + : new llvm::raw_null_ostream()); + FuzzyMatcher Matcher(Pattern); + auto Result = Matcher.match(Candidate.Word); + auto AnnotatedMatch = Matcher.dumpLast(*OS << "\n"); + return Result && Candidate.accepts(AnnotatedMatch) && + (!Score || testing::Value(*Result, testing::FloatEq(*Score))); + } +}; + +// Accepts patterns that match a given word, optionally requiring a score. +// Dumps the debug tables on match failure. +testing::Matcher matches(llvm::StringRef M, + llvm::Optional Score = {}) { + return testing::MakeMatcher(new MatchesMatcher(M, Score)); +} + +TEST(FuzzyMatch, Matches) { + EXPECT_THAT("", matches("unique_ptr")); + EXPECT_THAT("u_p", matches("[u]nique[_p]tr")); + EXPECT_THAT("up", matches("[u]nique_[p]tr")); + EXPECT_THAT("uq", Not(matches("unique_ptr"))); + EXPECT_THAT("qp", Not(matches("unique_ptr"))); + EXPECT_THAT("log", Not(matches("SVGFEMorphologyElement"))); + + EXPECT_THAT("tit", matches("win.[tit]")); + EXPECT_THAT("title", matches("win.[title]")); + EXPECT_THAT("WordCla", matches("[Word]Character[Cla]ssifier")); + EXPECT_THAT("WordCCla", matches("[WordC]haracter[Cla]ssifier")); + + EXPECT_THAT("dete", Not(matches("editor.quickSuggestionsDelay"))); + + EXPECT_THAT("highlight", matches("editorHover[Highlight]")); + EXPECT_THAT("hhighlight", matches("editor[H]over[Highlight]")); + EXPECT_THAT("dhhighlight", Not(matches("editorHoverHighlight"))); + + EXPECT_THAT("-moz", matches("[-moz]-foo")); + EXPECT_THAT("moz", matches("-[moz]-foo")); + EXPECT_THAT("moza", matches("-[moz]-[a]nimation")); + + EXPECT_THAT("ab", matches("[ab]A")); + EXPECT_THAT("ccm", Not(matches("cacmelCase"))); + EXPECT_THAT("bti", Not(matches("the_black_knight"))); + EXPECT_THAT("ccm", Not(matches("camelCase"))); + EXPECT_THAT("cmcm", Not(matches("camelCase"))); + EXPECT_THAT("BK", matches("the_[b]lack_[k]night")); + EXPECT_THAT("KeyboardLayout=", Not(matches("KeyboardLayout"))); + EXPECT_THAT("LLL", matches("SVisual[L]ogger[L]ogs[L]ist")); + EXPECT_THAT("LLLL", Not(matches("SVilLoLosLi"))); + EXPECT_THAT("LLLL", Not(matches("SVisualLoggerLogsList"))); + EXPECT_THAT("TEdit", matches("[T]ext[Edit]")); + EXPECT_THAT("TEdit", matches("[T]ext[Edit]or")); + EXPECT_THAT("TEdit", Not(matches("[T]ext[edit]"))); + EXPECT_THAT("TEdit", matches("[t]ext_[edit]")); + EXPECT_THAT("TEditDt", matches("[T]ext[Edit]or[D]ecoration[T]ype")); + EXPECT_THAT("TEdit", matches("[T]ext[Edit]orDecorationType")); + EXPECT_THAT("Tedit", matches("[T]ext[Edit]")); + EXPECT_THAT("ba", Not(matches("?AB?"))); + EXPECT_THAT("bkn", matches("the_[b]lack_[kn]ight")); + EXPECT_THAT("bt", Not(matches("the_[b]lack_knigh[t]"))); + EXPECT_THAT("ccm", Not(matches("[c]amelCase[cm]"))); + EXPECT_THAT("fdm", Not(matches("[f]in[dM]odel"))); + EXPECT_THAT("fob", Not(matches("[fo]o[b]ar"))); + EXPECT_THAT("fobz", Not(matches("foobar"))); + EXPECT_THAT("foobar", matches("[foobar]")); + EXPECT_THAT("form", matches("editor.[form]atOnSave")); + EXPECT_THAT("g p", matches("[G]it:[ P]ull")); + EXPECT_THAT("g p", matches("[G]it:[ P]ull")); + EXPECT_THAT("gip", matches("[Gi]t: [P]ull")); + EXPECT_THAT("gip", matches("[Gi]t: [P]ull")); + EXPECT_THAT("gp", matches("[G]it: [P]ull")); + EXPECT_THAT("gp", matches("[G]it_Git_[P]ull")); + EXPECT_THAT("is", matches("[I]mport[S]tatement")); + EXPECT_THAT("is", matches("[is]Valid")); + EXPECT_THAT("lowrd", Not(matches("[low]Wo[rd]"))); + EXPECT_THAT("myvable", Not(matches("[myva]ria[ble]"))); + EXPECT_THAT("no", Not(matches(""))); + EXPECT_THAT("no", Not(matches("match"))); + EXPECT_THAT("ob", Not(matches("foobar"))); + EXPECT_THAT("sl", matches("[S]Visual[L]oggerLogsList")); + EXPECT_THAT("sllll", matches("[S]Visua[L]ogger[Ll]ama[L]ist")); + EXPECT_THAT("THRE", matches("H[T]ML[HRE]lement")); + EXPECT_THAT("b", Not(matches("NDEBUG"))); + EXPECT_THAT("Three", matches("[Three]")); + EXPECT_THAT("fo", Not(matches("barfoo"))); + EXPECT_THAT("fo", matches("bar_[fo]o")); + EXPECT_THAT("fo", matches("bar_[Fo]o")); + EXPECT_THAT("fo", matches("bar [fo]o")); + EXPECT_THAT("fo", matches("bar.[fo]o")); + EXPECT_THAT("fo", matches("bar/[fo]o")); + EXPECT_THAT("fo", matches("bar\\[fo]o")); + + EXPECT_THAT( + "aaaaaa", + matches("[aaaaaa]aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")); + EXPECT_THAT("baba", Not(matches("ababababab"))); + EXPECT_THAT("fsfsfs", Not(matches("dsafdsafdsafdsafdsafdsafdsafasdfdsa"))); + EXPECT_THAT("fsfsfsfsfsfsfsf", + Not(matches("dsafdsafdsafdsafdsafdsafdsafasdfdsafdsafdsafdsafdsfd" + "safdsfdfdfasdnfdsajfndsjnafjndsajlknfdsa"))); + + EXPECT_THAT(" g", matches("[ g]roup")); + EXPECT_THAT("g", matches(" [g]roup")); + EXPECT_THAT("g g", Not(matches(" groupGroup"))); + EXPECT_THAT("g g", matches(" [g]roup[ G]roup")); + EXPECT_THAT(" g g", matches("[ ] [g]roup[ G]roup")); + EXPECT_THAT("zz", matches("[zz]Group")); + EXPECT_THAT("zzg", matches("[zzG]roup")); + EXPECT_THAT("g", matches("zz[G]roup")); + + EXPECT_THAT("aaaa", matches("_a_[aaaa]")); // Prefer consecutive. + // These would ideally match, but would need special segmentation rules. + EXPECT_THAT("printf", Not(matches("s[printf]"))); + EXPECT_THAT("str", Not(matches("o[str]eam"))); + EXPECT_THAT("strcpy", Not(matches("strncpy"))); + EXPECT_THAT("std", Not(matches("PTHREAD_MUTEX_STALLED"))); + EXPECT_THAT("std", Not(matches("pthread_condattr_setpshared"))); +} + +struct RankMatcher : public testing::MatcherInterface { + std::vector RankedStrings; + RankMatcher(std::initializer_list RankedStrings) + : RankedStrings(RankedStrings) {} + + void DescribeTo(::std::ostream *OS) const override { + llvm::raw_os_ostream O(*OS); + O << "Ranks strings in order: ["; + for (const auto &Str : RankedStrings) + O << "\n\t" << Str; + O << "\n]"; + } + + bool MatchAndExplain(llvm::StringRef Pattern, + testing::MatchResultListener *L) const override { + std::unique_ptr OS( + L->stream() + ? (llvm::raw_ostream *)(new llvm::raw_os_ostream(*L->stream())) + : new llvm::raw_null_ostream()); + FuzzyMatcher Matcher(Pattern); + const ExpectedMatch *LastMatch; + llvm::Optional LastScore; + bool Ok = true; + for (const auto &Str : RankedStrings) { + auto Score = Matcher.match(Str.Word); + if (!Score) { + *OS << "\nDoesn't match '" << Str.Word << "'"; + Matcher.dumpLast(*OS << "\n"); + Ok = false; + } else { + std::string Buf; + llvm::raw_string_ostream Info(Buf); + auto AnnotatedMatch = Matcher.dumpLast(Info); + + if (!Str.accepts(AnnotatedMatch)) { + *OS << "\nDoesn't match " << Str << ", but " << AnnotatedMatch << "\n" + << Info.str(); + Ok = false; + } else if (LastScore && *LastScore < *Score) { + *OS << "\nRanks '" << Str.Word << "'=" << *Score << " above '" + << LastMatch->Word << "'=" << *LastScore << "\n" + << Info.str(); + Matcher.match(LastMatch->Word); + Matcher.dumpLast(*OS << "\n"); + Ok = false; + } + } + LastMatch = &Str; + LastScore = Score; + } + return Ok; + } +}; + +// Accepts patterns that match all the strings and rank them in the given order. +// Dumps the debug tables on match failure. +template +testing::Matcher ranks(T... RankedStrings) { + return testing::MakeMatcher( + new RankMatcher{ExpectedMatch(RankedStrings)...}); +} + +TEST(FuzzyMatch, Ranking) { + EXPECT_THAT("cons", + ranks("[cons]ole", "[Cons]ole", "ArrayBuffer[Cons]tructor")); + EXPECT_THAT("foo", ranks("[foo]", "[Foo]")); + EXPECT_THAT("onMes", + ranks("[onMes]sage", "[onmes]sage", "[on]This[M]ega[Es]capes")); + EXPECT_THAT("onmes", + ranks("[onmes]sage", "[onMes]sage", "[on]This[M]ega[Es]capes")); + EXPECT_THAT("CC", ranks("[C]amel[C]ase", "[c]amel[C]ase")); + EXPECT_THAT("cC", ranks("[c]amel[C]ase", "[C]amel[C]ase")); + EXPECT_THAT("p", ranks("[p]", "[p]arse", "[p]osix", "[p]afdsa", "[p]ath")); + EXPECT_THAT("pa", ranks("[pa]rse", "[pa]th", "[pa]fdsa")); + EXPECT_THAT("log", ranks("[log]", "Scroll[Log]icalPosition")); + EXPECT_THAT("e", ranks("[e]lse", "Abstract[E]lement")); + EXPECT_THAT("workbench.sideb", + ranks("[workbench.sideB]ar.location", + "[workbench.]editor.default[SideB]ySideLayout")); + EXPECT_THAT("editor.r", ranks("[editor.r]enderControlCharacter", + "[editor.]overview[R]ulerlanes", + "diff[Editor.r]enderSideBySide")); + EXPECT_THAT("-mo", ranks("[-mo]z-columns", "[-]ms-ime-[mo]de")); + EXPECT_THAT("convertModelPosition", + ranks("[convertModelPosition]ToViewPosition", + "[convert]ViewTo[ModelPosition]")); + EXPECT_THAT("is", ranks("[is]ValidViewletId", "[i]mport [s]tatement")); + EXPECT_THAT("strcpy", ranks("[strcpy]", "[strcpy]_s")); +} + +// Verify some bounds so we know scores fall in the right range. +// Testing exact scores is fragile, so we prefer Ranking tests. +TEST(FuzzyMatch, Scoring) { + EXPECT_THAT("abs", matches("[a]w[B]xYz[S]", 7.f / 12.f)); + EXPECT_THAT("abs", matches("[abs]l", 1.f)); + EXPECT_THAT("abs", matches("[abs]", 2.f)); + EXPECT_THAT("Abs", matches("[abs]", 2.f)); +} + +TEST(FuzzyMatch, InitialismAndPrefix) { + // We want these scores to be roughly the same. + EXPECT_THAT("up", matches("[u]nique_[p]tr", 3.f / 4.f)); + EXPECT_THAT("up", matches("[up]per_bound", 1.f)); +} + +// Returns pretty-printed segmentation of Text. +// e.g. std::basic_string --> +-- +---- +----- +std::string segment(llvm::StringRef Text) { + std::vector Roles(Text.size()); + calculateRoles(Text, Roles); + std::string Printed; + for (unsigned I = 0; I < Text.size(); ++I) + Printed.push_back("?-+ "[static_cast(Roles[I])]); + return Printed; +} + +// this is a no-op hack so clang-format will vertically align our testcases. +llvm::StringRef returns(llvm::StringRef Text) { return Text; } + +TEST(FuzzyMatch, Segmentation) { + EXPECT_THAT(segment("std::basic_string"), // + returns("+-- +---- +-----")); + EXPECT_THAT(segment("XMLHttpRequest"), // + returns("+--+---+------")); + EXPECT_THAT(segment("t3h PeNgU1N oF d00m!!!!!!!!"), // + returns("+-- +-+-+-+ ++ +--- ")); +} + +} // namespace +} // namespace clangd +} // namespace clang Index: clang-tools-extra/trunk/clangd/unittests/GlobalCompilationDatabaseTests.cpp =================================================================== --- clang-tools-extra/trunk/clangd/unittests/GlobalCompilationDatabaseTests.cpp +++ clang-tools-extra/trunk/clangd/unittests/GlobalCompilationDatabaseTests.cpp @@ -0,0 +1,151 @@ +//===-- GlobalCompilationDatabaseTests.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 "GlobalCompilationDatabase.h" + +#include "TestFS.h" +#include "llvm/ADT/StringExtras.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace clang { +namespace clangd { +namespace { +using ::testing::AllOf; +using ::testing::Contains; +using ::testing::ElementsAre; +using ::testing::EndsWith; +using ::testing::Not; + +TEST(GlobalCompilationDatabaseTest, FallbackCommand) { + DirectoryBasedGlobalCompilationDatabase DB(None); + auto Cmd = DB.getFallbackCommand(testPath("foo/bar.cc")); + EXPECT_EQ(Cmd.Directory, testPath("foo")); + EXPECT_THAT(Cmd.CommandLine, + ElementsAre(EndsWith("clang"), testPath("foo/bar.cc"))); + EXPECT_EQ(Cmd.Output, ""); + + // .h files have unknown language, so they are parsed liberally as obj-c++. + Cmd = DB.getFallbackCommand(testPath("foo/bar.h")); + EXPECT_THAT(Cmd.CommandLine, + ElementsAre(EndsWith("clang"), "-xobjective-c++-header", + testPath("foo/bar.h"))); +} + +static tooling::CompileCommand cmd(llvm::StringRef File, llvm::StringRef Arg) { + return tooling::CompileCommand(testRoot(), File, {"clang", Arg, File}, ""); +} + +class OverlayCDBTest : public ::testing::Test { + class BaseCDB : public GlobalCompilationDatabase { + public: + llvm::Optional + getCompileCommand(llvm::StringRef File, + ProjectInfo *Project) const override { + if (File == testPath("foo.cc")) { + if (Project) + Project->SourceRoot = testRoot(); + return cmd(File, "-DA=1"); + } + return None; + } + + tooling::CompileCommand + getFallbackCommand(llvm::StringRef File) const override { + return cmd(File, "-DA=2"); + } + }; + +protected: + OverlayCDBTest() : Base(llvm::make_unique()) {} + std::unique_ptr Base; +}; + +TEST_F(OverlayCDBTest, GetCompileCommand) { + OverlayCDB CDB(Base.get(), {}, std::string("")); + EXPECT_THAT(CDB.getCompileCommand(testPath("foo.cc"))->CommandLine, + AllOf(Contains(testPath("foo.cc")), Contains("-DA=1"))); + EXPECT_EQ(CDB.getCompileCommand(testPath("missing.cc")), llvm::None); + + auto Override = cmd(testPath("foo.cc"), "-DA=3"); + CDB.setCompileCommand(testPath("foo.cc"), Override); + EXPECT_THAT(CDB.getCompileCommand(testPath("foo.cc"))->CommandLine, + Contains("-DA=3")); + EXPECT_EQ(CDB.getCompileCommand(testPath("missing.cc")), llvm::None); + CDB.setCompileCommand(testPath("missing.cc"), Override); + EXPECT_THAT(CDB.getCompileCommand(testPath("missing.cc"))->CommandLine, + Contains("-DA=3")); +} + +TEST_F(OverlayCDBTest, GetFallbackCommand) { + OverlayCDB CDB(Base.get(), {"-DA=4"}); + EXPECT_THAT(CDB.getFallbackCommand(testPath("bar.cc")).CommandLine, + ElementsAre("clang", "-DA=2", testPath("bar.cc"), "-DA=4")); +} + +TEST_F(OverlayCDBTest, NoBase) { + OverlayCDB CDB(nullptr, {"-DA=6"}, std::string("")); + EXPECT_EQ(CDB.getCompileCommand(testPath("bar.cc")), None); + auto Override = cmd(testPath("bar.cc"), "-DA=5"); + CDB.setCompileCommand(testPath("bar.cc"), Override); + EXPECT_THAT(CDB.getCompileCommand(testPath("bar.cc"))->CommandLine, + Contains("-DA=5")); + + EXPECT_THAT(CDB.getFallbackCommand(testPath("foo.cc")).CommandLine, + ElementsAre(EndsWith("clang"), testPath("foo.cc"), "-DA=6")); +} + +TEST_F(OverlayCDBTest, Watch) { + OverlayCDB Inner(nullptr); + OverlayCDB Outer(&Inner); + + std::vector> Changes; + auto Sub = Outer.watch([&](const std::vector &ChangedFiles) { + Changes.push_back(ChangedFiles); + }); + + Inner.setCompileCommand("A.cpp", tooling::CompileCommand()); + Outer.setCompileCommand("B.cpp", tooling::CompileCommand()); + Inner.setCompileCommand("A.cpp", llvm::None); + Outer.setCompileCommand("C.cpp", llvm::None); + EXPECT_THAT(Changes, ElementsAre(ElementsAre("A.cpp"), ElementsAre("B.cpp"), + ElementsAre("A.cpp"), ElementsAre("C.cpp"))); +} + +TEST_F(OverlayCDBTest, Adjustments) { + OverlayCDB CDB(Base.get(), {}, std::string("")); + auto Cmd = CDB.getCompileCommand(testPath("foo.cc")).getValue(); + // Delete the file name. + Cmd.CommandLine.pop_back(); + + // Check dependency file commands are dropped. + Cmd.CommandLine.push_back("-MF"); + Cmd.CommandLine.push_back("random-dependency"); + + // Check plugin-related commands are dropped. + Cmd.CommandLine.push_back("-Xclang"); + Cmd.CommandLine.push_back("-load"); + Cmd.CommandLine.push_back("-Xclang"); + Cmd.CommandLine.push_back("random-plugin"); + + Cmd.CommandLine.push_back("-DA=5"); + Cmd.CommandLine.push_back(Cmd.Filename); + + CDB.setCompileCommand(testPath("foo.cc"), Cmd); + + EXPECT_THAT(CDB.getCompileCommand(testPath("foo.cc"))->CommandLine, + AllOf(Contains("-fsyntax-only"), Contains("-DA=5"), + Contains(testPath("foo.cc")), Not(Contains("-MF")), + Not(Contains("random-dependency")), + Not(Contains("-Xclang")), Not(Contains("-load")), + Not(Contains("random-plugin")))); +} + +} // namespace +} // namespace clangd +} // namespace clang Index: clang-tools-extra/trunk/clangd/unittests/HeadersTests.cpp =================================================================== --- clang-tools-extra/trunk/clangd/unittests/HeadersTests.cpp +++ clang-tools-extra/trunk/clangd/unittests/HeadersTests.cpp @@ -0,0 +1,279 @@ +//===-- HeadersTests.cpp - Include headers unit tests -----------*- 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 "Headers.h" + +#include "Compiler.h" +#include "TestFS.h" +#include "TestTU.h" +#include "clang/Frontend/CompilerInstance.h" +#include "clang/Frontend/CompilerInvocation.h" +#include "clang/Frontend/FrontendActions.h" +#include "clang/Lex/PreprocessorOptions.h" +#include "llvm/Support/Path.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace clang { +namespace clangd { +namespace { + +using ::testing::AllOf; +using ::testing::ElementsAre; +using ::testing::UnorderedElementsAre; + +class HeadersTest : public ::testing::Test { +public: + HeadersTest() { + CDB.ExtraClangFlags = {SearchDirArg.c_str()}; + FS.Files[MainFile] = ""; + // Make sure directory sub/ exists. + FS.Files[testPath("sub/EMPTY")] = ""; + } + +private: + std::unique_ptr setupClang() { + auto Cmd = CDB.getCompileCommand(MainFile); + assert(static_cast(Cmd)); + auto VFS = FS.getFileSystem(); + VFS->setCurrentWorkingDirectory(Cmd->Directory); + + ParseInputs PI; + PI.CompileCommand = *Cmd; + PI.FS = VFS; + auto CI = buildCompilerInvocation(PI); + EXPECT_TRUE(static_cast(CI)); + // The diagnostic options must be set before creating a CompilerInstance. + CI->getDiagnosticOpts().IgnoreWarnings = true; + auto Clang = prepareCompilerInstance( + std::move(CI), /*Preamble=*/nullptr, + llvm::MemoryBuffer::getMemBuffer(FS.Files[MainFile], MainFile), VFS, + IgnoreDiags); + + EXPECT_FALSE(Clang->getFrontendOpts().Inputs.empty()); + return Clang; + } + +protected: + IncludeStructure collectIncludes() { + auto Clang = setupClang(); + PreprocessOnlyAction Action; + EXPECT_TRUE( + Action.BeginSourceFile(*Clang, Clang->getFrontendOpts().Inputs[0])); + IncludeStructure Includes; + Clang->getPreprocessor().addPPCallbacks( + collectIncludeStructureCallback(Clang->getSourceManager(), &Includes)); + EXPECT_TRUE(Action.Execute()); + Action.EndSourceFile(); + return Includes; + } + + // Calculates the include path, or returns "" on error or header should not be + // inserted. + std::string calculate(PathRef Original, PathRef Preferred = "", + const std::vector &Inclusions = {}) { + auto Clang = setupClang(); + PreprocessOnlyAction Action; + EXPECT_TRUE( + Action.BeginSourceFile(*Clang, Clang->getFrontendOpts().Inputs[0])); + + if (Preferred.empty()) + Preferred = Original; + auto ToHeaderFile = [](llvm::StringRef Header) { + return HeaderFile{Header, + /*Verbatim=*/!llvm::sys::path::is_absolute(Header)}; + }; + + IncludeInserter Inserter(MainFile, /*Code=*/"", format::getLLVMStyle(), + CDB.getCompileCommand(MainFile)->Directory, + &Clang->getPreprocessor().getHeaderSearchInfo()); + for (const auto &Inc : Inclusions) + Inserter.addExisting(Inc); + auto Inserted = ToHeaderFile(Preferred); + if (!Inserter.shouldInsertInclude(Original, Inserted)) + return ""; + std::string Path = Inserter.calculateIncludePath(Inserted); + Action.EndSourceFile(); + return Path; + } + + llvm::Optional insert(llvm::StringRef VerbatimHeader) { + auto Clang = setupClang(); + PreprocessOnlyAction Action; + EXPECT_TRUE( + Action.BeginSourceFile(*Clang, Clang->getFrontendOpts().Inputs[0])); + + IncludeInserter Inserter(MainFile, /*Code=*/"", format::getLLVMStyle(), + CDB.getCompileCommand(MainFile)->Directory, + &Clang->getPreprocessor().getHeaderSearchInfo()); + auto Edit = Inserter.insert(VerbatimHeader); + Action.EndSourceFile(); + return Edit; + } + + MockFSProvider FS; + MockCompilationDatabase CDB; + std::string MainFile = testPath("main.cpp"); + std::string Subdir = testPath("sub"); + std::string SearchDirArg = (llvm::Twine("-I") + Subdir).str(); + IgnoringDiagConsumer IgnoreDiags; +}; + +MATCHER_P(Written, Name, "") { return arg.Written == Name; } +MATCHER_P(Resolved, Name, "") { return arg.Resolved == Name; } +MATCHER_P(IncludeLine, N, "") { return arg.R.start.line == N; } + +MATCHER_P2(Distance, File, D, "") { + if (arg.getKey() != File) + *result_listener << "file =" << arg.getKey().str(); + if (arg.getValue() != D) + *result_listener << "distance =" << arg.getValue(); + return arg.getKey() == File && arg.getValue() == D; +} + +TEST_F(HeadersTest, CollectRewrittenAndResolved) { + FS.Files[MainFile] = R"cpp( +#include "sub/bar.h" // not shortest +)cpp"; + std::string BarHeader = testPath("sub/bar.h"); + FS.Files[BarHeader] = ""; + + EXPECT_THAT(collectIncludes().MainFileIncludes, + UnorderedElementsAre( + AllOf(Written("\"sub/bar.h\""), Resolved(BarHeader)))); + EXPECT_THAT(collectIncludes().includeDepth(MainFile), + UnorderedElementsAre(Distance(MainFile, 0u), + Distance(testPath("sub/bar.h"), 1u))); +} + +TEST_F(HeadersTest, OnlyCollectInclusionsInMain) { + std::string BazHeader = testPath("sub/baz.h"); + FS.Files[BazHeader] = ""; + std::string BarHeader = testPath("sub/bar.h"); + FS.Files[BarHeader] = R"cpp( +#include "baz.h" +)cpp"; + FS.Files[MainFile] = R"cpp( +#include "bar.h" +)cpp"; + EXPECT_THAT( + collectIncludes().MainFileIncludes, + UnorderedElementsAre(AllOf(Written("\"bar.h\""), Resolved(BarHeader)))); + EXPECT_THAT(collectIncludes().includeDepth(MainFile), + UnorderedElementsAre(Distance(MainFile, 0u), + Distance(testPath("sub/bar.h"), 1u), + Distance(testPath("sub/baz.h"), 2u))); + // includeDepth() also works for non-main files. + EXPECT_THAT(collectIncludes().includeDepth(testPath("sub/bar.h")), + UnorderedElementsAre(Distance(testPath("sub/bar.h"), 0u), + Distance(testPath("sub/baz.h"), 1u))); +} + +TEST_F(HeadersTest, PreambleIncludesPresentOnce) { + // We use TestTU here, to ensure we use the preamble replay logic. + // We're testing that the logic doesn't crash, and doesn't result in duplicate + // includes. (We'd test more directly, but it's pretty well encapsulated!) + auto TU = TestTU::withCode(R"cpp( + #include "a.h" + #include "a.h" + void foo(); + #include "a.h" + )cpp"); + TU.HeaderFilename = "a.h"; // suppress "not found". + EXPECT_THAT(TU.build().getIncludeStructure().MainFileIncludes, + ElementsAre(IncludeLine(1), IncludeLine(2), IncludeLine(4))); +} + +TEST_F(HeadersTest, UnResolvedInclusion) { + FS.Files[MainFile] = R"cpp( +#include "foo.h" +)cpp"; + + EXPECT_THAT(collectIncludes().MainFileIncludes, + UnorderedElementsAre(AllOf(Written("\"foo.h\""), Resolved("")))); + EXPECT_THAT(collectIncludes().includeDepth(MainFile), + UnorderedElementsAre(Distance(MainFile, 0u))); +} + +TEST_F(HeadersTest, InsertInclude) { + std::string Path = testPath("sub/bar.h"); + FS.Files[Path] = ""; + EXPECT_EQ(calculate(Path), "\"bar.h\""); +} + +TEST_F(HeadersTest, DoNotInsertIfInSameFile) { + MainFile = testPath("main.h"); + EXPECT_EQ(calculate(MainFile), ""); +} + +TEST_F(HeadersTest, ShortenedInclude) { + std::string BarHeader = testPath("sub/bar.h"); + EXPECT_EQ(calculate(BarHeader), "\"bar.h\""); + + SearchDirArg = (llvm::Twine("-I") + Subdir + "/..").str(); + CDB.ExtraClangFlags = {SearchDirArg.c_str()}; + BarHeader = testPath("sub/bar.h"); + EXPECT_EQ(calculate(BarHeader), "\"sub/bar.h\""); +} + +TEST_F(HeadersTest, NotShortenedInclude) { + std::string BarHeader = + llvm::sys::path::convert_to_slash(testPath("sub-2/bar.h")); + EXPECT_EQ(calculate(BarHeader, ""), "\"" + BarHeader + "\""); +} + +TEST_F(HeadersTest, PreferredHeader) { + std::string BarHeader = testPath("sub/bar.h"); + EXPECT_EQ(calculate(BarHeader, ""), ""); + + std::string BazHeader = testPath("sub/baz.h"); + EXPECT_EQ(calculate(BarHeader, BazHeader), "\"baz.h\""); +} + +TEST_F(HeadersTest, DontInsertDuplicatePreferred) { + Inclusion Inc; + Inc.Written = "\"bar.h\""; + Inc.Resolved = ""; + EXPECT_EQ(calculate(testPath("sub/bar.h"), "\"bar.h\"", {Inc}), ""); + EXPECT_EQ(calculate("\"x.h\"", "\"bar.h\"", {Inc}), ""); +} + +TEST_F(HeadersTest, DontInsertDuplicateResolved) { + Inclusion Inc; + Inc.Written = "fake-bar.h"; + Inc.Resolved = testPath("sub/bar.h"); + EXPECT_EQ(calculate(Inc.Resolved, "", {Inc}), ""); + // Do not insert preferred. + EXPECT_EQ(calculate(Inc.Resolved, "\"BAR.h\"", {Inc}), ""); +} + +TEST_F(HeadersTest, PreferInserted) { + auto Edit = insert(""); + EXPECT_TRUE(Edit.hasValue()); + EXPECT_TRUE(StringRef(Edit->newText).contains("")); +} + +TEST(Headers, NoHeaderSearchInfo) { + std::string MainFile = testPath("main.cpp"); + IncludeInserter Inserter(MainFile, /*Code=*/"", format::getLLVMStyle(), + /*BuildDir=*/"", /*HeaderSearchInfo=*/nullptr); + + auto HeaderPath = testPath("sub/bar.h"); + auto Inserting = HeaderFile{HeaderPath, /*Verbatim=*/false}; + auto Verbatim = HeaderFile{"", /*Verbatim=*/true}; + + EXPECT_EQ(Inserter.calculateIncludePath(Inserting), "\"" + HeaderPath + "\""); + EXPECT_EQ(Inserter.shouldInsertInclude(HeaderPath, Inserting), false); + + EXPECT_EQ(Inserter.calculateIncludePath(Verbatim), ""); + EXPECT_EQ(Inserter.shouldInsertInclude(HeaderPath, Verbatim), true); +} + +} // namespace +} // namespace clangd +} // namespace clang Index: clang-tools-extra/trunk/clangd/unittests/IndexActionTests.cpp =================================================================== --- clang-tools-extra/trunk/clangd/unittests/IndexActionTests.cpp +++ clang-tools-extra/trunk/clangd/unittests/IndexActionTests.cpp @@ -0,0 +1,253 @@ +//===------ IndexActionTests.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 "TestFS.h" +#include "index/IndexAction.h" +#include "clang/Tooling/Tooling.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace clang { +namespace clangd { +namespace { + +using ::testing::AllOf; +using ::testing::ElementsAre; +using ::testing::Not; +using ::testing::Pair; +using ::testing::UnorderedElementsAre; +using ::testing::UnorderedPointwise; + +std::string toUri(llvm::StringRef Path) { return URI::create(Path).toString(); } + +MATCHER(IsTU, "") { return arg.IsTU; } + +MATCHER_P(HasDigest, Digest, "") { return arg.Digest == Digest; } + +MATCHER_P(HasName, Name, "") { return arg.Name == Name; } + +MATCHER(HasSameURI, "") { + llvm::StringRef URI = testing::get<0>(arg); + const std::string &Path = testing::get<1>(arg); + return toUri(Path) == URI; +} + +testing::Matcher +IncludesAre(const std::vector &Includes) { + return ::testing::Field(&IncludeGraphNode::DirectIncludes, + UnorderedPointwise(HasSameURI(), Includes)); +} + +void checkNodesAreInitialized(const IndexFileIn &IndexFile, + const std::vector &Paths) { + ASSERT_TRUE(IndexFile.Sources); + EXPECT_THAT(Paths.size(), IndexFile.Sources->size()); + for (llvm::StringRef Path : Paths) { + auto URI = toUri(Path); + const auto &Node = IndexFile.Sources->lookup(URI); + // Uninitialized nodes will have an empty URI. + EXPECT_EQ(Node.URI.data(), IndexFile.Sources->find(URI)->getKeyData()); + } +} + +std::map toMap(const IncludeGraph &IG) { + std::map Nodes; + for (auto &I : IG) + Nodes.emplace(I.getKey(), I.getValue()); + return Nodes; +} + +class IndexActionTest : public ::testing::Test { +public: + IndexActionTest() : InMemoryFileSystem(new llvm::vfs::InMemoryFileSystem) {} + + IndexFileIn + runIndexingAction(llvm::StringRef MainFilePath, + const std::vector &ExtraArgs = {}) { + IndexFileIn IndexFile; + llvm::IntrusiveRefCntPtr Files( + new FileManager(FileSystemOptions(), InMemoryFileSystem)); + + auto Action = createStaticIndexingAction( + SymbolCollector::Options(), + [&](SymbolSlab S) { IndexFile.Symbols = std::move(S); }, + [&](RefSlab R) { IndexFile.Refs = std::move(R); }, + [&](IncludeGraph IG) { IndexFile.Sources = std::move(IG); }); + + std::vector Args = {"index_action", "-fsyntax-only", + "-xc++", "-std=c++11", + "-iquote", testRoot()}; + Args.insert(Args.end(), ExtraArgs.begin(), ExtraArgs.end()); + Args.push_back(MainFilePath); + + tooling::ToolInvocation Invocation( + Args, Action.release(), Files.get(), + std::make_shared()); + + Invocation.run(); + + checkNodesAreInitialized(IndexFile, FilePaths); + return IndexFile; + } + + void addFile(llvm::StringRef Path, llvm::StringRef Content) { + InMemoryFileSystem->addFile(Path, 0, + llvm::MemoryBuffer::getMemBuffer(Content)); + FilePaths.push_back(Path); + } + +protected: + std::vector FilePaths; + llvm::IntrusiveRefCntPtr InMemoryFileSystem; +}; + +TEST_F(IndexActionTest, CollectIncludeGraph) { + std::string MainFilePath = testPath("main.cpp"); + std::string MainCode = "#include \"level1.h\""; + std::string Level1HeaderPath = testPath("level1.h"); + std::string Level1HeaderCode = "#include \"level2.h\""; + std::string Level2HeaderPath = testPath("level2.h"); + std::string Level2HeaderCode = ""; + + addFile(MainFilePath, MainCode); + addFile(Level1HeaderPath, Level1HeaderCode); + addFile(Level2HeaderPath, Level2HeaderCode); + + IndexFileIn IndexFile = runIndexingAction(MainFilePath); + auto Nodes = toMap(*IndexFile.Sources); + + EXPECT_THAT(Nodes, + UnorderedElementsAre( + Pair(toUri(MainFilePath), + AllOf(IsTU(), IncludesAre({Level1HeaderPath}), + HasDigest(digest(MainCode)))), + Pair(toUri(Level1HeaderPath), + AllOf(Not(IsTU()), IncludesAre({Level2HeaderPath}), + HasDigest(digest(Level1HeaderCode)))), + Pair(toUri(Level2HeaderPath), + AllOf(Not(IsTU()), IncludesAre({}), + HasDigest(digest(Level2HeaderCode)))))); +} + +TEST_F(IndexActionTest, IncludeGraphSelfInclude) { + std::string MainFilePath = testPath("main.cpp"); + std::string MainCode = "#include \"header.h\""; + std::string HeaderPath = testPath("header.h"); + std::string HeaderCode = R"cpp( + #ifndef _GUARD_ + #define _GUARD_ + #include "header.h" + #endif)cpp"; + + addFile(MainFilePath, MainCode); + addFile(HeaderPath, HeaderCode); + + IndexFileIn IndexFile = runIndexingAction(MainFilePath); + auto Nodes = toMap(*IndexFile.Sources); + + EXPECT_THAT( + Nodes, + UnorderedElementsAre( + Pair(toUri(MainFilePath), AllOf(IsTU(), IncludesAre({HeaderPath}), + HasDigest(digest(MainCode)))), + Pair(toUri(HeaderPath), AllOf(Not(IsTU()), IncludesAre({HeaderPath}), + HasDigest(digest(HeaderCode)))))); +} + +TEST_F(IndexActionTest, IncludeGraphSkippedFile) { + std::string MainFilePath = testPath("main.cpp"); + std::string MainCode = R"cpp( + #include "common.h" + #include "header.h" + )cpp"; + + std::string CommonHeaderPath = testPath("common.h"); + std::string CommonHeaderCode = R"cpp( + #ifndef _GUARD_ + #define _GUARD_ + void f(); + #endif)cpp"; + + std::string HeaderPath = testPath("header.h"); + std::string HeaderCode = R"cpp( + #include "common.h" + void g();)cpp"; + + addFile(MainFilePath, MainCode); + addFile(HeaderPath, HeaderCode); + addFile(CommonHeaderPath, CommonHeaderCode); + + IndexFileIn IndexFile = runIndexingAction(MainFilePath); + auto Nodes = toMap(*IndexFile.Sources); + + EXPECT_THAT( + Nodes, UnorderedElementsAre( + Pair(toUri(MainFilePath), + AllOf(IsTU(), IncludesAre({HeaderPath, CommonHeaderPath}), + HasDigest(digest(MainCode)))), + Pair(toUri(HeaderPath), + AllOf(Not(IsTU()), IncludesAre({CommonHeaderPath}), + HasDigest(digest(HeaderCode)))), + Pair(toUri(CommonHeaderPath), + AllOf(Not(IsTU()), IncludesAre({}), + HasDigest(digest(CommonHeaderCode)))))); +} + +TEST_F(IndexActionTest, IncludeGraphDynamicInclude) { + std::string MainFilePath = testPath("main.cpp"); + std::string MainCode = R"cpp( + #ifndef FOO + #define FOO "main.cpp" + #else + #define FOO "header.h" + #endif + + #include FOO)cpp"; + std::string HeaderPath = testPath("header.h"); + std::string HeaderCode = ""; + + addFile(MainFilePath, MainCode); + addFile(HeaderPath, HeaderCode); + + IndexFileIn IndexFile = runIndexingAction(MainFilePath); + auto Nodes = toMap(*IndexFile.Sources); + + EXPECT_THAT( + Nodes, + UnorderedElementsAre( + Pair(toUri(MainFilePath), + AllOf(IsTU(), IncludesAre({MainFilePath, HeaderPath}), + HasDigest(digest(MainCode)))), + Pair(toUri(HeaderPath), AllOf(Not(IsTU()), IncludesAre({}), + HasDigest(digest(HeaderCode)))))); +} + +TEST_F(IndexActionTest, NoWarnings) { + std::string MainFilePath = testPath("main.cpp"); + std::string MainCode = R"cpp( + void foo(int x) { + if (x = 1) // -Wparentheses + return; + if (x = 1) // -Wparentheses + return; + } + void bar() {} + )cpp"; + addFile(MainFilePath, MainCode); + // We set -ferror-limit so the warning-promoted-to-error would be fatal. + // This would cause indexing to stop (if warnings weren't disabled). + IndexFileIn IndexFile = runIndexingAction( + MainFilePath, {"-ferror-limit=1", "-Wparentheses", "-Werror"}); + ASSERT_TRUE(IndexFile.Sources); + ASSERT_NE(0u, IndexFile.Sources->size()); + EXPECT_THAT(*IndexFile.Symbols, ElementsAre(HasName("foo"), HasName("bar"))); +} + +} // namespace +} // namespace clangd +} // namespace clang Index: clang-tools-extra/trunk/clangd/unittests/IndexTests.cpp =================================================================== --- clang-tools-extra/trunk/clangd/unittests/IndexTests.cpp +++ clang-tools-extra/trunk/clangd/unittests/IndexTests.cpp @@ -0,0 +1,408 @@ +//===-- IndexTests.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 "TestIndex.h" +#include "TestTU.h" +#include "index/FileIndex.h" +#include "index/Index.h" +#include "index/MemIndex.h" +#include "index/Merge.h" +#include "index/Symbol.h" +#include "clang/Index/IndexSymbol.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using testing::_; +using testing::AllOf; +using testing::AnyOf; +using testing::ElementsAre; +using testing::IsEmpty; +using testing::Pair; +using testing::Pointee; +using testing::UnorderedElementsAre; + +namespace clang { +namespace clangd { +namespace { + +MATCHER_P(Named, N, "") { return arg.Name == N; } +MATCHER_P(RefRange, Range, "") { + return std::make_tuple(arg.Location.Start.line(), arg.Location.Start.column(), + arg.Location.End.line(), arg.Location.End.column()) == + std::make_tuple(Range.start.line, Range.start.character, + Range.end.line, Range.end.character); +} +MATCHER_P(FileURI, F, "") { return StringRef(arg.Location.FileURI) == F; } + +TEST(SymbolLocation, Position) { + using Position = SymbolLocation::Position; + Position Pos; + + Pos.setLine(1); + EXPECT_EQ(1u, Pos.line()); + Pos.setColumn(2); + EXPECT_EQ(2u, Pos.column()); + EXPECT_FALSE(Pos.hasOverflow()); + + Pos.setLine(Position::MaxLine + 1); // overflow + EXPECT_TRUE(Pos.hasOverflow()); + EXPECT_EQ(Pos.line(), Position::MaxLine); + Pos.setLine(1); // reset the overflowed line. + + Pos.setColumn(Position::MaxColumn + 1); // overflow + EXPECT_TRUE(Pos.hasOverflow()); + EXPECT_EQ(Pos.column(), Position::MaxColumn); +} + +TEST(SymbolSlab, FindAndIterate) { + SymbolSlab::Builder B; + B.insert(symbol("Z")); + B.insert(symbol("Y")); + B.insert(symbol("X")); + EXPECT_EQ(nullptr, B.find(SymbolID("W"))); + for (const char *Sym : {"X", "Y", "Z"}) + EXPECT_THAT(B.find(SymbolID(Sym)), Pointee(Named(Sym))); + + SymbolSlab S = std::move(B).build(); + EXPECT_THAT(S, UnorderedElementsAre(Named("X"), Named("Y"), Named("Z"))); + EXPECT_EQ(S.end(), S.find(SymbolID("W"))); + for (const char *Sym : {"X", "Y", "Z"}) + EXPECT_THAT(*S.find(SymbolID(Sym)), Named(Sym)); +} + +TEST(SwapIndexTest, OldIndexRecycled) { + auto Token = std::make_shared(); + std::weak_ptr WeakToken = Token; + + SwapIndex S(llvm::make_unique( + SymbolSlab(), RefSlab(), std::move(Token), /*BackingDataSize=*/0)); + EXPECT_FALSE(WeakToken.expired()); // Current MemIndex keeps it alive. + S.reset(llvm::make_unique()); // Now the MemIndex is destroyed. + EXPECT_TRUE(WeakToken.expired()); // So the token is too. +} + +TEST(MemIndexTest, MemIndexDeduplicate) { + std::vector Symbols = {symbol("1"), symbol("2"), symbol("3"), + symbol("2") /* duplicate */}; + FuzzyFindRequest Req; + Req.Query = "2"; + Req.AnyScope = true; + MemIndex I(Symbols, RefSlab()); + EXPECT_THAT(match(I, Req), ElementsAre("2")); +} + +TEST(MemIndexTest, MemIndexLimitedNumMatches) { + auto I = MemIndex::build(generateNumSymbols(0, 100), RefSlab()); + FuzzyFindRequest Req; + Req.Query = "5"; + Req.AnyScope = true; + Req.Limit = 3; + bool Incomplete; + auto Matches = match(*I, Req, &Incomplete); + EXPECT_TRUE(Req.Limit); + EXPECT_EQ(Matches.size(), *Req.Limit); + EXPECT_TRUE(Incomplete); +} + +TEST(MemIndexTest, FuzzyMatch) { + auto I = MemIndex::build( + generateSymbols({"LaughingOutLoud", "LionPopulation", "LittleOldLady"}), + RefSlab()); + FuzzyFindRequest Req; + Req.Query = "lol"; + Req.AnyScope = true; + Req.Limit = 2; + EXPECT_THAT(match(*I, Req), + UnorderedElementsAre("LaughingOutLoud", "LittleOldLady")); +} + +TEST(MemIndexTest, MatchQualifiedNamesWithoutSpecificScope) { + auto I = + MemIndex::build(generateSymbols({"a::y1", "b::y2", "y3"}), RefSlab()); + FuzzyFindRequest Req; + Req.Query = "y"; + Req.AnyScope = true; + EXPECT_THAT(match(*I, Req), UnorderedElementsAre("a::y1", "b::y2", "y3")); +} + +TEST(MemIndexTest, MatchQualifiedNamesWithGlobalScope) { + auto I = + MemIndex::build(generateSymbols({"a::y1", "b::y2", "y3"}), RefSlab()); + FuzzyFindRequest Req; + Req.Query = "y"; + Req.Scopes = {""}; + EXPECT_THAT(match(*I, Req), UnorderedElementsAre("y3")); +} + +TEST(MemIndexTest, MatchQualifiedNamesWithOneScope) { + auto I = MemIndex::build( + generateSymbols({"a::y1", "a::y2", "a::x", "b::y2", "y3"}), RefSlab()); + FuzzyFindRequest Req; + Req.Query = "y"; + Req.Scopes = {"a::"}; + EXPECT_THAT(match(*I, Req), UnorderedElementsAre("a::y1", "a::y2")); +} + +TEST(MemIndexTest, MatchQualifiedNamesWithMultipleScopes) { + auto I = MemIndex::build( + generateSymbols({"a::y1", "a::y2", "a::x", "b::y3", "y3"}), RefSlab()); + FuzzyFindRequest Req; + Req.Query = "y"; + Req.Scopes = {"a::", "b::"}; + EXPECT_THAT(match(*I, Req), UnorderedElementsAre("a::y1", "a::y2", "b::y3")); +} + +TEST(MemIndexTest, NoMatchNestedScopes) { + auto I = MemIndex::build(generateSymbols({"a::y1", "a::b::y2"}), RefSlab()); + FuzzyFindRequest Req; + Req.Query = "y"; + Req.Scopes = {"a::"}; + EXPECT_THAT(match(*I, Req), UnorderedElementsAre("a::y1")); +} + +TEST(MemIndexTest, IgnoreCases) { + auto I = MemIndex::build(generateSymbols({"ns::ABC", "ns::abc"}), RefSlab()); + FuzzyFindRequest Req; + Req.Query = "AB"; + Req.Scopes = {"ns::"}; + EXPECT_THAT(match(*I, Req), UnorderedElementsAre("ns::ABC", "ns::abc")); +} + +TEST(MemIndexTest, Lookup) { + auto I = MemIndex::build(generateSymbols({"ns::abc", "ns::xyz"}), RefSlab()); + EXPECT_THAT(lookup(*I, SymbolID("ns::abc")), UnorderedElementsAre("ns::abc")); + EXPECT_THAT(lookup(*I, {SymbolID("ns::abc"), SymbolID("ns::xyz")}), + UnorderedElementsAre("ns::abc", "ns::xyz")); + EXPECT_THAT(lookup(*I, {SymbolID("ns::nonono"), SymbolID("ns::xyz")}), + UnorderedElementsAre("ns::xyz")); + EXPECT_THAT(lookup(*I, SymbolID("ns::nonono")), UnorderedElementsAre()); +} + +TEST(MemIndexTest, TemplateSpecialization) { + SymbolSlab::Builder B; + + Symbol S = symbol("TempSpec"); + S.ID = SymbolID("1"); + B.insert(S); + + S = symbol("TempSpec"); + S.ID = SymbolID("2"); + S.TemplateSpecializationArgs = ""; + S.SymInfo.Properties = static_cast( + index::SymbolProperty::TemplateSpecialization); + B.insert(S); + + S = symbol("TempSpec"); + S.ID = SymbolID("3"); + S.TemplateSpecializationArgs = ""; + S.SymInfo.Properties = static_cast( + index::SymbolProperty::TemplatePartialSpecialization); + B.insert(S); + + auto I = MemIndex::build(std::move(B).build(), RefSlab()); + FuzzyFindRequest Req; + Req.AnyScope = true; + + Req.Query = "TempSpec"; + EXPECT_THAT(match(*I, Req), + UnorderedElementsAre("TempSpec", "TempSpec", + "TempSpec")); + + // FIXME: Add filtering for template argument list. + Req.Query = "TempSpec + +namespace clang { +namespace clangd { +namespace { + +// No fmemopen on windows or on versions of MacOS X earlier than 10.13, so we +// can't easily run this test. +#if !(defined(_WIN32) || (defined(__MAC_OS_X_VERSION_MIN_REQUIRED) && \ + __MAC_OS_X_VERSION_MIN_REQUIRED < 101300)) + +// Fixture takes care of managing the input/output buffers for the transport. +class JSONTransportTest : public ::testing::Test { + std::string InBuf, OutBuf, MirrorBuf; + llvm::raw_string_ostream Out, Mirror; + std::unique_ptr In; + +protected: + JSONTransportTest() : Out(OutBuf), Mirror(MirrorBuf), In(nullptr, nullptr) {} + + template + std::unique_ptr transport(std::string InData, bool Pretty, + JSONStreamStyle Style) { + InBuf = std::move(InData); + In = {fmemopen(&InBuf[0], InBuf.size(), "r"), &fclose}; + return newJSONTransport(In.get(), Out, &Mirror, Pretty, Style); + } + + std::string input() const { return InBuf; } + std::string output() { return Out.str(); } + std::string input_mirror() { return Mirror.str(); } +}; + +// Echo is a simple server running on a transport: +// - logs each message it gets. +// - when it gets a call, replies to it +// - when it gets a notification for method "call", makes a call on Target +// Hangs up when it gets an exit notification. +class Echo : public Transport::MessageHandler { + Transport &Target; + std::string LogBuf; + llvm::raw_string_ostream Log; + +public: + Echo(Transport &Target) : Target(Target), Log(LogBuf) {} + + std::string log() { return Log.str(); } + + bool onNotify(llvm::StringRef Method, llvm::json::Value Params) override { + Log << "Notification " << Method << ": " << Params << "\n"; + if (Method == "call") + Target.call("echo call", std::move(Params), 42); + return Method != "exit"; + } + + bool onCall(llvm::StringRef Method, llvm::json::Value Params, + llvm::json::Value ID) override { + Log << "Call " << Method << "(" << ID << "): " << Params << "\n"; + if (Method == "err") + Target.reply( + ID, llvm::make_error("trouble at mill", ErrorCode(88))); + else + Target.reply(ID, std::move(Params)); + return true; + } + + bool onReply(llvm::json::Value ID, + llvm::Expected Params) override { + if (Params) + Log << "Reply(" << ID << "): " << *Params << "\n"; + else + Log << "Reply(" << ID + << "): error = " << llvm::toString(Params.takeError()) << "\n"; + return true; + } +}; + +std::string trim(llvm::StringRef S) { return S.trim().str(); } + +// Runs an Echo session using the standard JSON-RPC format we use in production. +TEST_F(JSONTransportTest, StandardDense) { + auto T = transport( + "Content-Length: 52\r\n\r\n" + R"({"jsonrpc": "2.0", "method": "call", "params": 1234})" + "Content-Length: 46\r\n\r\n" + R"({"jsonrpc": "2.0", "id": 1234, "result": 5678})" + "Content-Length: 67\r\n\r\n" + R"({"jsonrpc": "2.0", "method": "foo", "id": "abcd", "params": "efgh"})" + "Content-Length: 73\r\n\r\n" + R"({"jsonrpc": "2.0", "id": "xyz", "error": {"code": 99, "message": "bad!"}})" + "Content-Length: 68\r\n\r\n" + R"({"jsonrpc": "2.0", "method": "err", "id": "wxyz", "params": "boom!"})" + "Content-Length: 36\r\n\r\n" + R"({"jsonrpc": "2.0", "method": "exit"})", + /*Pretty=*/false, JSONStreamStyle::Standard); + Echo E(*T); + auto Err = T->loop(E); + EXPECT_FALSE(bool(Err)) << toString(std::move(Err)); + + const char *WantLog = R"( +Notification call: 1234 +Reply(1234): 5678 +Call foo("abcd"): "efgh" +Reply("xyz"): error = 99: bad! +Call err("wxyz"): "boom!" +Notification exit: null + )"; + EXPECT_EQ(trim(E.log()), trim(WantLog)); + const char *WantOutput = + "Content-Length: 60\r\n\r\n" + R"({"id":42,"jsonrpc":"2.0","method":"echo call","params":1234})" + "Content-Length: 45\r\n\r\n" + R"({"id":"abcd","jsonrpc":"2.0","result":"efgh"})" + "Content-Length: 77\r\n\r\n" + R"({"error":{"code":88,"message":"trouble at mill"},"id":"wxyz","jsonrpc":"2.0"})"; + EXPECT_EQ(output(), WantOutput); + EXPECT_EQ(trim(input_mirror()), trim(input())); +} + +// Runs an Echo session using the "delimited" input and pretty-printed output +// that we use in lit tests. +TEST_F(JSONTransportTest, DelimitedPretty) { + auto T = transport(R"jsonrpc( +{"jsonrpc": "2.0", "method": "call", "params": 1234} +--- +{"jsonrpc": "2.0", "id": 1234, "result": 5678} +--- +{"jsonrpc": "2.0", "method": "foo", "id": "abcd", "params": "efgh"} +--- +{"jsonrpc": "2.0", "id": "xyz", "error": {"code": 99, "message": "bad!"}} +--- +{"jsonrpc": "2.0", "method": "err", "id": "wxyz", "params": "boom!"} +--- +{"jsonrpc": "2.0", "method": "exit"} + )jsonrpc", + /*Pretty=*/true, JSONStreamStyle::Delimited); + Echo E(*T); + auto Err = T->loop(E); + EXPECT_FALSE(bool(Err)) << toString(std::move(Err)); + + const char *WantLog = R"( +Notification call: 1234 +Reply(1234): 5678 +Call foo("abcd"): "efgh" +Reply("xyz"): error = 99: bad! +Call err("wxyz"): "boom!" +Notification exit: null + )"; + EXPECT_EQ(trim(E.log()), trim(WantLog)); + const char *WantOutput = "Content-Length: 77\r\n\r\n" + R"({ + "id": 42, + "jsonrpc": "2.0", + "method": "echo call", + "params": 1234 +})" + "Content-Length: 58\r\n\r\n" + R"({ + "id": "abcd", + "jsonrpc": "2.0", + "result": "efgh" +})" + "Content-Length: 105\r\n\r\n" + R"({ + "error": { + "code": 88, + "message": "trouble at mill" + }, + "id": "wxyz", + "jsonrpc": "2.0" +})"; + EXPECT_EQ(output(), WantOutput); + EXPECT_EQ(trim(input_mirror()), trim(input())); +} + +// IO errors such as EOF ane reported. +// The only successful return from loop() is if a handler returned false. +TEST_F(JSONTransportTest, EndOfFile) { + auto T = transport("Content-Length: 52\r\n\r\n" + R"({"jsonrpc": "2.0", "method": "call", "params": 1234})", + /*Pretty=*/false, JSONStreamStyle::Standard); + Echo E(*T); + auto Err = T->loop(E); + EXPECT_EQ(trim(E.log()), "Notification call: 1234"); + EXPECT_TRUE(bool(Err)); // Ran into EOF with no handler signalling done. + consumeError(std::move(Err)); + EXPECT_EQ(trim(input_mirror()), trim(input())); +} + +#endif + +} // namespace +} // namespace clangd +} // namespace clang Index: clang-tools-extra/trunk/clangd/unittests/Matchers.h =================================================================== --- clang-tools-extra/trunk/clangd/unittests/Matchers.h +++ clang-tools-extra/trunk/clangd/unittests/Matchers.h @@ -0,0 +1,199 @@ +//===-- Matchers.h ----------------------------------------------*- 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 +// +//===----------------------------------------------------------------------===// +// +// GMock matchers that aren't specific to particular tests. +// +//===----------------------------------------------------------------------===// + +#ifndef LLVM_CLANG_TOOLS_EXTRA_UNITTESTS_CLANGD_MATCHERS_H +#define LLVM_CLANG_TOOLS_EXTRA_UNITTESTS_CLANGD_MATCHERS_H +#include "Protocol.h" +#include "gmock/gmock.h" + +namespace clang { +namespace clangd { +using ::testing::Matcher; + +// EXPECT_IFF expects matcher if condition is true, and Not(matcher) if false. +// This is hard to write as a function, because matchers may be polymorphic. +#define EXPECT_IFF(condition, value, matcher) \ + do { \ + if (condition) \ + EXPECT_THAT(value, matcher); \ + else \ + EXPECT_THAT(value, ::testing::Not(matcher)); \ + } while (0) + +// HasSubsequence(m1, m2, ...) matches a vector containing elements that match +// m1, m2 ... in that order. +// +// SubsequenceMatcher implements this once the type of vector is known. +template +class SubsequenceMatcher + : public ::testing::MatcherInterface &> { + std::vector> Matchers; + +public: + SubsequenceMatcher(std::vector> M) : Matchers(M) {} + + void DescribeTo(std::ostream *OS) const override { + *OS << "Contains the subsequence ["; + const char *Sep = ""; + for (const auto &M : Matchers) { + *OS << Sep; + M.DescribeTo(OS); + Sep = ", "; + } + *OS << "]"; + } + + bool MatchAndExplain(const std::vector &V, + ::testing::MatchResultListener *L) const override { + std::vector Matches(Matchers.size()); + size_t I = 0; + for (size_t J = 0; I < Matchers.size() && J < V.size(); ++J) + if (Matchers[I].Matches(V[J])) + Matches[I++] = J; + if (I == Matchers.size()) // We exhausted all matchers. + return true; + if (L->IsInterested()) { + *L << "\n Matched:"; + for (size_t K = 0; K < I; ++K) { + *L << "\n\t"; + Matchers[K].DescribeTo(L->stream()); + *L << " ==> " << ::testing::PrintToString(V[Matches[K]]); + } + *L << "\n\t"; + Matchers[I].DescribeTo(L->stream()); + *L << " ==> no subsequent match"; + } + return false; + } +}; + +// PolySubsequenceMatcher implements a "polymorphic" SubsequenceMatcher. +// It captures the types of the element matchers, and can be converted to +// Matcher> if each matcher can be converted to Matcher. +// This allows HasSubsequence() to accept polymorphic matchers like Not(). +template class PolySubsequenceMatcher { + std::tuple Matchers; + +public: + PolySubsequenceMatcher(M &&... Args) + : Matchers(std::make_tuple(std::forward(Args)...)) {} + + template operator Matcher &>() const { + return ::testing::MakeMatcher(new SubsequenceMatcher( + TypedMatchers(llvm::index_sequence_for{}))); + } + +private: + template + std::vector> TypedMatchers(llvm::index_sequence) const { + return {std::get(Matchers)...}; + } +}; + +// HasSubsequence(m1, m2, ...) matches a vector containing elements that match +// m1, m2 ... in that order. +// The real implementation is in SubsequenceMatcher. +template +PolySubsequenceMatcher HasSubsequence(Args &&... M) { + return PolySubsequenceMatcher(std::forward(M)...); +} + +// EXPECT_ERROR seems like a pretty generic name, make sure it's not defined +// already. +#ifdef EXPECT_ERROR +#error "Refusing to redefine EXPECT_ERROR" +#endif + +// Consumes llvm::Expected, checks it contains an error and marks it as +// handled. +#define EXPECT_ERROR(expectedValue) \ + do { \ + auto &&ComputedValue = (expectedValue); \ + if (ComputedValue) { \ + ADD_FAILURE() << "expected an error from " << #expectedValue \ + << " but got " \ + << ::testing::PrintToString(*ComputedValue); \ + break; \ + } \ + llvm::consumeError(ComputedValue.takeError()); \ + } while (false) + +// Implements the HasValue(m) matcher for matching an Optional whose +// value matches matcher m. +template class OptionalMatcher { +public: + explicit OptionalMatcher(const InnerMatcher &matcher) : matcher_(matcher) {} + + // This type conversion operator template allows Optional(m) to be + // used as a matcher for any Optional type whose value type is + // compatible with the inner matcher. + // + // The reason we do this instead of relying on + // MakePolymorphicMatcher() is that the latter is not flexible + // enough for implementing the DescribeTo() method of Optional(). + template operator Matcher() const { + return MakeMatcher(new Impl(matcher_)); + } + +private: + // The monomorphic implementation that works for a particular optional type. + template + class Impl : public ::testing::MatcherInterface { + public: + using Value = typename std::remove_const< + typename std::remove_reference::type>::type::value_type; + + explicit Impl(const InnerMatcher &matcher) + : matcher_(::testing::MatcherCast(matcher)) {} + + virtual void DescribeTo(::std::ostream *os) const { + *os << "has a value that "; + matcher_.DescribeTo(os); + } + + virtual void DescribeNegationTo(::std::ostream *os) const { + *os << "does not have a value that "; + matcher_.DescribeTo(os); + } + + virtual bool + MatchAndExplain(Optional optional, + ::testing::MatchResultListener *listener) const { + if (!optional.hasValue()) + return false; + + *listener << "which has a value "; + return MatchPrintAndExplain(*optional, matcher_, listener); + } + + private: + const Matcher matcher_; + + GTEST_DISALLOW_ASSIGN_(Impl); + }; + + const InnerMatcher matcher_; + + GTEST_DISALLOW_ASSIGN_(OptionalMatcher); +}; + +// Creates a matcher that matches an Optional that has a value +// that matches inner_matcher. +template +inline OptionalMatcher +HasValue(const InnerMatcher &inner_matcher) { + return OptionalMatcher(inner_matcher); +} + +} // namespace clangd +} // namespace clang +#endif Index: clang-tools-extra/trunk/clangd/unittests/PrintASTTests.cpp =================================================================== --- clang-tools-extra/trunk/clangd/unittests/PrintASTTests.cpp +++ clang-tools-extra/trunk/clangd/unittests/PrintASTTests.cpp @@ -0,0 +1,102 @@ +//===--- PrintASTTests.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 "AST.h" +#include "Annotations.h" +#include "Protocol.h" +#include "SourceCode.h" +#include "TestTU.h" +#include "clang/AST/RecursiveASTVisitor.h" +#include "gmock/gmock.h" +#include "gtest/gtest-param-test.h" +#include "gtest/gtest.h" +#include "gtest/internal/gtest-param-util-generated.h" + +namespace clang { +namespace clangd { +namespace { + +using testing::ElementsAreArray; + +struct Case { + const char *AnnotatedCode; + std::vector Expected; +}; +class ASTUtils : public testing::Test, + public ::testing::WithParamInterface {}; + +TEST_P(ASTUtils, PrintTemplateArgs) { + auto Pair = GetParam(); + Annotations Test(Pair.AnnotatedCode); + auto AST = TestTU::withCode(Test.code()).build(); + struct Visitor : RecursiveASTVisitor { + Visitor(std::vector Points) : Points(std::move(Points)) {} + bool VisitNamedDecl(const NamedDecl *ND) { + if (TemplateArgsAtPoints.size() == Points.size()) + return true; + auto Pos = sourceLocToPosition(ND->getASTContext().getSourceManager(), + ND->getLocation()); + if (Pos != Points[TemplateArgsAtPoints.size()]) + return true; + TemplateArgsAtPoints.push_back(printTemplateSpecializationArgs(*ND)); + return true; + } + std::vector TemplateArgsAtPoints; + const std::vector Points; + }; + Visitor V(Test.points()); + V.TraverseDecl(AST.getASTContext().getTranslationUnitDecl()); + EXPECT_THAT(V.TemplateArgsAtPoints, ElementsAreArray(Pair.Expected)); +} + +INSTANTIATE_TEST_CASE_P(ASTUtilsTests, ASTUtils, + testing::ValuesIn(std::vector({ + { + R"cpp( + template class Bar {}; + template <> class ^Bar {};)cpp", + {""}}, + { + R"cpp( + template class Bar {}; + template class Z, int Q> + struct Foo {}; + template struct ^Foo; + template + struct ^Foo {};)cpp", + {"", ""}}, + { + R"cpp( + template void Foz() {}; + template <> void ^Foz<3, 5, 8>() {};)cpp", + {"<3, 5, 8>"}}, + { + R"cpp( + template class Bar {}; + template