From a2bd3c5bb5abf488dfce9f3ad319386e71652aa2 Mon Sep 17 00:00:00 2001 From: Hamish Nicholson Date: Thu, 8 May 2025 20:56:06 +0200 Subject: [PATCH 1/5] feat: Add clangd integration tests and documentation This commit introduces integration tests for clangd, the C/C++ language server. Key changes include: - New test suite for clangd, covering definition and diagnostics. These tests require a compile_commands.json file, which is generated by the bear tool in CI, but needs to be done manually locally. - Addition of a C/C++ (clangd) section to the main README.md, with setup instructions. - Configuration of the GitHub Actions workflow to run clangd integration tests, including installation of clang, clangd and bear. - Adjustment to the test helper `normalizePaths` to correctly handle paths from clangd output. This is neccessary because the generated compile_commands.json file is generated in the workspaces/clangd directory and contains absolute paths to the source files. --- .github/workflows/go.yml | 36 ++++ .gitignore | 6 + README.md | 31 +++ .../snapshots/clangd/definition/class.snap | 11 ++ .../snapshots/clangd/definition/constant.snap | 8 + .../snapshots/clangd/definition/foobar.snap | 11 ++ .../clangd/definition/helperFunction.snap | 8 + .../snapshots/clangd/definition/method.snap | 11 ++ .../snapshots/clangd/definition/struct.snap | 10 + .../snapshots/clangd/definition/type.snap | 8 + .../snapshots/clangd/definition/variable.snap | 8 + .../snapshots/clangd/diagnostics/clean.snap | 1 + .../clangd/diagnostics/unreachable.snap | 10 + integrationtests/tests/clangd/README.md | 19 ++ .../clangd/definition/definition_test.go | 176 ++++++++++++++++++ .../clangd/diagnostics/diagnostics_test.go | 97 ++++++++++ .../tests/clangd/internal/helpers.go | 41 ++++ integrationtests/tests/common/helpers.go | 9 + integrationtests/workspaces/clangd/Makefile | 80 ++++++++ .../workspaces/clangd/include/helper.hpp | 1 + .../clangd/src/another_consumer.cpp | 4 + .../workspaces/clangd/src/clean.cpp | 7 + .../workspaces/clangd/src/consumer.cpp | 9 + .../workspaces/clangd/src/helper.cpp | 7 + .../workspaces/clangd/src/main.cpp | 16 ++ .../workspaces/clangd/src/types.cpp | 10 + 26 files changed, 635 insertions(+) create mode 100644 integrationtests/snapshots/clangd/definition/class.snap create mode 100644 integrationtests/snapshots/clangd/definition/constant.snap create mode 100644 integrationtests/snapshots/clangd/definition/foobar.snap create mode 100644 integrationtests/snapshots/clangd/definition/helperFunction.snap create mode 100644 integrationtests/snapshots/clangd/definition/method.snap create mode 100644 integrationtests/snapshots/clangd/definition/struct.snap create mode 100644 integrationtests/snapshots/clangd/definition/type.snap create mode 100644 integrationtests/snapshots/clangd/definition/variable.snap create mode 100644 integrationtests/snapshots/clangd/diagnostics/clean.snap create mode 100644 integrationtests/snapshots/clangd/diagnostics/unreachable.snap create mode 100644 integrationtests/tests/clangd/README.md create mode 100644 integrationtests/tests/clangd/definition/definition_test.go create mode 100644 integrationtests/tests/clangd/diagnostics/diagnostics_test.go create mode 100644 integrationtests/tests/clangd/internal/helpers.go create mode 100644 integrationtests/workspaces/clangd/Makefile create mode 100644 integrationtests/workspaces/clangd/include/helper.hpp create mode 100644 integrationtests/workspaces/clangd/src/another_consumer.cpp create mode 100644 integrationtests/workspaces/clangd/src/clean.cpp create mode 100644 integrationtests/workspaces/clangd/src/consumer.cpp create mode 100644 integrationtests/workspaces/clangd/src/helper.cpp create mode 100644 integrationtests/workspaces/clangd/src/main.cpp create mode 100644 integrationtests/workspaces/clangd/src/types.cpp diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 2a75b04..c3ca00b 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -131,3 +131,39 @@ jobs: - name: Run TypeScript integration tests run: go test ./integrationtests/tests/typescript/... + + clangd-integration-tests: + name: Clangd Integration Tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: "1.24" + check-latest: true + cache: true + + - name: Install LLVM, Clang and bear + run: | + sudo apt-get update + sudo apt-get install -y clang-16 llvm-16 clangd-16 bear + + - name: Verify Clangd Installation + run: | + sudo ln -s /usr/bin/clangd-16 /usr/bin/clangd + clangd-16 --version + clangd --version + + - name: Create compile commands + run: | + cd integrationtests/workspaces/clangd + bear -- make + cd ../../../.. + + - name: Run Clangd definition tests + run: go test ./integrationtests/tests/clangd/definition... + + - name: Run Clangd diagnostics tests + run: go test ./integrationtests/tests/clangd/diagnostics... diff --git a/.gitignore b/.gitignore index 2fdd64f..92d4648 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,12 @@ mcp-language-server # Test output test-output/ *.diff +integrationtests/workspaces/clangd/compile_commands.json +integrationtests/workspaces/clangd/src/*.o +integrationtests/workspaces/clangd/clean_program +integrationtests/workspaces/clangd/program +integrationtests/workspaces/clangd/.cache + # Temporary files *~ diff --git a/README.md b/README.md index bd9e9c6..0a79d0a 100644 --- a/README.md +++ b/README.md @@ -124,6 +124,37 @@ This is an [MCP](https://modelcontextprotocol.io/introduction) server that runs +
+ C/C++ (clangd) +
+

Install clangd: Download prebuilt binaries from the official LLVM releases page or install via your system's package manager (e.g., apt install clangd, brew install clangd).

+

Configure your MCP client: This will be different but similar for each client. For Claude Desktop, add the following to ~/Library/Application\\ Support/Claude/claude_desktop_config.json

+ +
+{
+  "mcpServers": {
+    "language-server": {
+      "command": "mcp-language-server",
+      "args": [
+        "--workspace",
+        "/Users/you/dev/yourproject/",
+        "--lsp",
+        "/path/to/your/clangd_binary",
+        "--",
+        "--compile-commands-dir=/path/to/yourproject/build_or_compile_commands_dir"
+      ]
+    }
+  }
+}
+
+

Note:

+
    +
  • Replace /path/to/your/clangd_binary with the actual path to your clangd executable.
  • +
  • --compile-commands-dir should point to the directory containing your compile_commands.json file (e.g., ./build, ./cmake-build-debug).
  • +
  • Ensure compile_commands.json is generated for your project for clangd to work effectively.
  • +
+
+
Other
diff --git a/integrationtests/snapshots/clangd/definition/class.snap b/integrationtests/snapshots/clangd/definition/class.snap new file mode 100644 index 0000000..5c0a706 --- /dev/null +++ b/integrationtests/snapshots/clangd/definition/class.snap @@ -0,0 +1,11 @@ +--- + +Symbol: TestClass +/TEST_OUTPUT/workspace/src/consumer.cpp +Range: L6:C1 - L9:C2 + + 6|class TestClass { + 7| public: + 8| void method() {} + 9|}; + diff --git a/integrationtests/snapshots/clangd/definition/constant.snap b/integrationtests/snapshots/clangd/definition/constant.snap new file mode 100644 index 0000000..c4c3a61 --- /dev/null +++ b/integrationtests/snapshots/clangd/definition/constant.snap @@ -0,0 +1,8 @@ +--- + +Symbol: TEST_CONSTANT +/TEST_OUTPUT/workspace/src/helper.cpp +Range: L4:C1 - L4:C29 + +4|const int TEST_CONSTANT = 42; + diff --git a/integrationtests/snapshots/clangd/definition/foobar.snap b/integrationtests/snapshots/clangd/definition/foobar.snap new file mode 100644 index 0000000..0ad3994 --- /dev/null +++ b/integrationtests/snapshots/clangd/definition/foobar.snap @@ -0,0 +1,11 @@ +--- + +Symbol: foo_bar +/TEST_OUTPUT/workspace/src/main.cpp +Range: L5:C1 - L8:C2 + +5|void foo_bar() { +6| std::cout << "Hello, World!" << std::endl; +7| return; +8|} + diff --git a/integrationtests/snapshots/clangd/definition/helperFunction.snap b/integrationtests/snapshots/clangd/definition/helperFunction.snap new file mode 100644 index 0000000..6be5f7b --- /dev/null +++ b/integrationtests/snapshots/clangd/definition/helperFunction.snap @@ -0,0 +1,8 @@ +--- + +Symbol: helperFunction +/TEST_OUTPUT/workspace/clangd/include/helper.hpp +Range: L1:C1 - L1:C22 + +1|void helperFunction(); + diff --git a/integrationtests/snapshots/clangd/definition/method.snap b/integrationtests/snapshots/clangd/definition/method.snap new file mode 100644 index 0000000..4c1a0b1 --- /dev/null +++ b/integrationtests/snapshots/clangd/definition/method.snap @@ -0,0 +1,11 @@ +--- + +Symbol: method +/TEST_OUTPUT/workspace/src/consumer.cpp +Range: L6:C1 - L9:C2 + + 6|class TestClass { + 7| public: + 8| void method() {} + 9|}; + diff --git a/integrationtests/snapshots/clangd/definition/struct.snap b/integrationtests/snapshots/clangd/definition/struct.snap new file mode 100644 index 0000000..3e35ab5 --- /dev/null +++ b/integrationtests/snapshots/clangd/definition/struct.snap @@ -0,0 +1,10 @@ +--- + +Symbol: TestStruct +/TEST_OUTPUT/workspace/src/types.cpp +Range: L6:C1 - L8:C2 + +6|struct TestStruct { +7| int value; +8|}; + diff --git a/integrationtests/snapshots/clangd/definition/type.snap b/integrationtests/snapshots/clangd/definition/type.snap new file mode 100644 index 0000000..983ffa0 --- /dev/null +++ b/integrationtests/snapshots/clangd/definition/type.snap @@ -0,0 +1,8 @@ +--- + +Symbol: TestType +/TEST_OUTPUT/workspace/src/types.cpp +Range: L10:C1 - L10:C21 + +10|using TestType = int; + diff --git a/integrationtests/snapshots/clangd/definition/variable.snap b/integrationtests/snapshots/clangd/definition/variable.snap new file mode 100644 index 0000000..1f160ab --- /dev/null +++ b/integrationtests/snapshots/clangd/definition/variable.snap @@ -0,0 +1,8 @@ +--- + +Symbol: TEST_VARIABLE +/TEST_OUTPUT/workspace/src/helper.cpp +Range: L5:C1 - L5:C24 + +5|int TEST_VARIABLE = 100; + diff --git a/integrationtests/snapshots/clangd/diagnostics/clean.snap b/integrationtests/snapshots/clangd/diagnostics/clean.snap new file mode 100644 index 0000000..a9dfdff --- /dev/null +++ b/integrationtests/snapshots/clangd/diagnostics/clean.snap @@ -0,0 +1 @@ +/TEST_OUTPUT/workspace/src/clean.cpp \ No newline at end of file diff --git a/integrationtests/snapshots/clangd/diagnostics/unreachable.snap b/integrationtests/snapshots/clangd/diagnostics/unreachable.snap new file mode 100644 index 0000000..b21242a --- /dev/null +++ b/integrationtests/snapshots/clangd/diagnostics/unreachable.snap @@ -0,0 +1,10 @@ +/TEST_OUTPUT/workspace/src/main.cpp +Diagnostics in File: 1 +WARNING at L15:C38: Code will never be executed (Source: clang, Code: -Wunreachable-code) + +10|int main() { +... +13| +14| // Intentional error: unreachable code +15| std::cout << "This is unreachable" << std::endl; +16|} diff --git a/integrationtests/tests/clangd/README.md b/integrationtests/tests/clangd/README.md new file mode 100644 index 0000000..30753bc --- /dev/null +++ b/integrationtests/tests/clangd/README.md @@ -0,0 +1,19 @@ +\ +# Clangd Integration Tests + +This directory contains integration tests for `clangd`, the C/C++ language server. + +## Prerequisites + +Before running these tests, you must generate the `compile_commands.json` file in the `integrationtests/workspaces/clangd` directory. This can typically be done by navigating to that directory and running a tool like `bear` with your build command (e.g., `bear -- make`). + +The GitHub Actions workflow for these tests uses the following command from the root of the repository: +```bash +cd integrationtests/workspaces/clangd +bear -- make +cd ../../../.. +``` + +## Clangd Version + +These tests are currently run against **clangd version 16**. While they may pass with other versions of clangd, compatibility is not guaranteed. diff --git a/integrationtests/tests/clangd/definition/definition_test.go b/integrationtests/tests/clangd/definition/definition_test.go new file mode 100644 index 0000000..e4f4dee --- /dev/null +++ b/integrationtests/tests/clangd/definition/definition_test.go @@ -0,0 +1,176 @@ +package definition_test + +import ( + "context" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/isaacphi/mcp-language-server/integrationtests/tests/clangd/internal" + "github.com/isaacphi/mcp-language-server/integrationtests/tests/common" + "github.com/isaacphi/mcp-language-server/internal/tools" +) + +// TestReadDefinition tests the ReadDefinition tool with various C++ type definitions +func TestReadDefinition(t *testing.T) { + // Helper function to open all files and wait for indexing + openAllFilesAndWait := func(suite *common.TestSuite, ctx context.Context) { + // Open all files to ensure clangd indexes everything + filesToOpen := []string{ + "src/main.cpp", + "src/types.cpp", + "src/helper.cpp", + "src/consumer.cpp", + "src/another_consumer.cpp", + "src/clean.cpp", + } + + for _, file := range filesToOpen { + filePath := filepath.Join(suite.WorkspaceDir, file) + err := suite.Client.OpenFile(ctx, filePath) + if err != nil { + // Don't fail the test, some files might not exist in certain tests + t.Logf("Note: Failed to open %s: %v", file, err) + } + } + // Wait for indexing to complete. clangd won't index files until they are opened. + time.Sleep(10 * time.Second) + } + + suite := internal.GetTestSuite(t) + + ctx, cancel := context.WithTimeout(suite.Context, 10*time.Second) + defer cancel() + + // Open all files and wait for clangd to index them + openAllFilesAndWait(suite, ctx) + + tests := []struct { + name string + symbolName string + expectedText string + snapshotName string + }{ + { + name: "Function", + symbolName: "foo_bar", + expectedText: "void foo_bar()", + snapshotName: "foobar", + }, + { + name: "Class", + symbolName: "TestClass", + expectedText: "class TestClass", + snapshotName: "class", + }, + { + name: "Method", + symbolName: "method", + expectedText: "void method()", + snapshotName: "method", + }, + { + name: "Struct", + symbolName: "TestStruct", + expectedText: "struct TestStruct", + snapshotName: "struct", + }, + { + name: "Type", + symbolName: "TestType", + expectedText: "using TestType", + snapshotName: "type", + }, + { + name: "Constant", + symbolName: "TEST_CONSTANT", + expectedText: "const int TEST_CONSTANT", + snapshotName: "constant", + }, + { + name: "Variable", + symbolName: "TEST_VARIABLE", + expectedText: "int TEST_VARIABLE", + snapshotName: "variable", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // Call the ReadDefinition tool + result, err := tools.ReadDefinition(ctx, suite.Client, tc.symbolName) + if err != nil { + t.Fatalf("Failed to read definition: %v", err) + } + + // Check that the result contains relevant information + if !strings.Contains(result, tc.expectedText) { + t.Errorf("Definition does not contain expected text: %s", tc.expectedText) + } + + // Use snapshot testing to verify exact output + common.SnapshotTest(t, "clangd", "definition", tc.snapshotName, result) + }) + } +} + +func TestReadDefinitionInAnotherFile(t *testing.T) { + // Helper function to open all files and wait for indexing + openAllFilesAndWait := func(suite *common.TestSuite, ctx context.Context) { + // Open all files to ensure clangd indexes everything + filesToOpen := []string{ + "src/main.cpp", + } + + for _, file := range filesToOpen { + filePath := filepath.Join(suite.WorkspaceDir, file) + err := suite.Client.OpenFile(ctx, filePath) + if err != nil { + // Don't fail the test, some files might not exist in certain tests + t.Logf("Note: Failed to open %s: %v", file, err) + } + } + time.Sleep(5 * time.Second) + } + + suite := internal.GetTestSuite(t) + + ctx, cancel := context.WithTimeout(suite.Context, 10*time.Second) + defer cancel() + + // Open all files and wait for clangd to index them + openAllFilesAndWait(suite, ctx) + + tests := []struct { + name string + symbolName string + expectedText string + snapshotName string + }{ + { + name: "Function", + symbolName: "helperFunction", + expectedText: "void helperFunction()", + snapshotName: "helperFunction", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // Call the ReadDefinition tool + result, err := tools.ReadDefinition(ctx, suite.Client, tc.symbolName) + if err != nil { + t.Fatalf("Failed to read definition: %v", err) + } + + // Check that the result contains relevant information + if !strings.Contains(result, tc.expectedText) { + t.Errorf("Definition does not contain expected text: %s", tc.expectedText) + } + + // Use snapshot testing to verify exact output + common.SnapshotTest(t, "clangd", "definition", tc.snapshotName, result) + }) + } +} diff --git a/integrationtests/tests/clangd/diagnostics/diagnostics_test.go b/integrationtests/tests/clangd/diagnostics/diagnostics_test.go new file mode 100644 index 0000000..02222b8 --- /dev/null +++ b/integrationtests/tests/clangd/diagnostics/diagnostics_test.go @@ -0,0 +1,97 @@ +package diagnostics_test + +// note: clangd doesn't support pull diagnostics (textdocument/diagnostic) +// see: https://github.com/clangd/clangd/issues/2108 +import ( + "context" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/isaacphi/mcp-language-server/integrationtests/tests/clangd/internal" + "github.com/isaacphi/mcp-language-server/integrationtests/tests/common" + "github.com/isaacphi/mcp-language-server/internal/tools" +) + +// TestDiagnostics tests diagnostics functionality with the Clangd language server +func TestDiagnostics(t *testing.T) { + // Helper function to open all files and wait for indexing + openAllFilesAndWait := func(suite *common.TestSuite, ctx context.Context) { + // Open all files to ensure clangd indexes everything + filesToOpen := []string{ + "src/main.cpp", + "src/types.cpp", + "src/helper.cpp", + "src/consumer.cpp", + "src/another_consumer.cpp", + "src/clean.cpp", + } + + for _, file := range filesToOpen { + filePath := filepath.Join(suite.WorkspaceDir, file) + err := suite.Client.OpenFile(ctx, filePath) + if err != nil { + // Don't fail the test, some files might not exist in certain tests + t.Logf("Note: Failed to open %s: %v", file, err) + } + } + // Wait for indexing to complete. clangd won't index files until they are opened. + time.Sleep(10 * time.Second) + + } + + // Test with a clean file + t.Run("CleanFile", func(t *testing.T) { + // Get a test suite with clean code + suite := internal.GetTestSuite(t) + + ctx, cancel := context.WithTimeout(suite.Context, 10*time.Second) + defer cancel() + + // Open all files and wait for clangd to index them + openAllFilesAndWait(suite, ctx) + + filePath := filepath.Join(suite.WorkspaceDir, "src/clean.cpp") + result, err := tools.GetDiagnosticsForFile(ctx, suite.Client, filePath, 2, true) + if err != nil { + t.Fatalf("GetDiagnosticsForFile failed: %v", err) + } + + // Verify we have no diagnostics + if !strings.Contains(result, "No diagnostics found") { + t.Errorf("Expected no diagnostics but got: %s", result) + } + + common.SnapshotTest(t, "clangd", "diagnostics", "clean", result) + }) + + // Test with a file containing an error + t.Run("FileWithError", func(t *testing.T) { + // Get a test suite with code that contains errors + suite := internal.GetTestSuite(t) + + ctx, cancel := context.WithTimeout(suite.Context, 10*time.Second) + defer cancel() + + // Open all files and wait for clangd to index them + openAllFilesAndWait(suite, ctx) + + filePath := filepath.Join(suite.WorkspaceDir, "src/main.cpp") + result, err := tools.GetDiagnosticsForFile(ctx, suite.Client, filePath, 2, true) + if err != nil { + t.Fatalf("GetDiagnosticsForFile failed: %v", err) + } + + // Verify we have diagnostics about unreachable code + if strings.Contains(result, "No diagnostics found") { + t.Errorf("Expected diagnostics but got none") + } + + if !strings.Contains(result, "unreachable") { + t.Errorf("Expected unreachable code error but got: %s", result) + } + + common.SnapshotTest(t, "clangd", "diagnostics", "unreachable", result) + }) +} diff --git a/integrationtests/tests/clangd/internal/helpers.go b/integrationtests/tests/clangd/internal/helpers.go new file mode 100644 index 0000000..fce3401 --- /dev/null +++ b/integrationtests/tests/clangd/internal/helpers.go @@ -0,0 +1,41 @@ +// Package internal contains shared helpers for Clangd tests +package internal + +import ( + "path/filepath" + "testing" + + "github.com/isaacphi/mcp-language-server/integrationtests/tests/common" +) + +// GetTestSuite returns a test suite for Clangd language server tests +func GetTestSuite(t *testing.T) *common.TestSuite { + // Configure Clangd LSP + repoRoot, err := filepath.Abs("../../../..") + if err != nil { + t.Fatalf("Failed to get repo root: %v", err) + } + + config := common.LSPTestConfig{ + Name: "clangd", + Command: "clangd", + Args: []string{}, + WorkspaceDir: filepath.Join(repoRoot, "integrationtests/workspaces/clangd"), + InitializeTimeMs: 2000, + } + + // Create a test suite + suite := common.NewTestSuite(t, config) + + // Set up the suite + if err := suite.Setup(); err != nil { + t.Fatalf("Failed to set up test suite: %v", err) + } + + // Register cleanup + t.Cleanup(func() { + suite.Cleanup() + }) + + return suite +} diff --git a/integrationtests/tests/common/helpers.go b/integrationtests/tests/common/helpers.go index 76ea59a..3d7243c 100644 --- a/integrationtests/tests/common/helpers.go +++ b/integrationtests/tests/common/helpers.go @@ -107,6 +107,15 @@ func normalizePaths(_ *testing.T, input string) string { lines[i] = "/TEST_OUTPUT/workspace/" + parts[1] } } + // Some tests, e.g. clangd, may include fully qualified paths to the base /workspaces/ directory + if strings.Contains(line, "/workspaces/") { + // Extract everything after /workspace/ + parts := strings.Split(line, "/workspaces/") + if len(parts) > 1 { + // Replace with a simple placeholder path + lines[i] = "/TEST_OUTPUT/workspace/" + parts[1] + } + } } return strings.Join(lines, "\n") diff --git a/integrationtests/workspaces/clangd/Makefile b/integrationtests/workspaces/clangd/Makefile new file mode 100644 index 0000000..dd96ef3 --- /dev/null +++ b/integrationtests/workspaces/clangd/Makefile @@ -0,0 +1,80 @@ +CXX = clang++ +LD = clang++ + +# --- Include Path Configuration (from your original Makefile) --- +# This section attempts to get all system include directories from clang++ +# and add them explicitly so clang tidy can find them. +CXX_INCLUDE_DIRS := $(shell clang++ -E -x c++ - -v < /dev/null 2>&1 | grep -A 20 '#include <...>' | grep '^ ' | sed 's/^ //' | grep -v '(framework directory)') +CXX_INCLUDE_FLAGS := $(foreach dir,$(CXX_INCLUDE_DIRS),-isystem $(dir)) +CXXFLAGS = -std=c++17 -I./include -Wunreachable-code -Wall -Wextra -Wno-error $(CXX_INCLUDE_FLAGS) + +# --- Source and Object File Definitions --- +SRCDIR = src +# Place object files in the same directory as sources, or a separate build/obj directory +OBJDIR = $(SRCDIR) + +# Automatically find all .cpp files in the source directory +SOURCES = $(wildcard $(SRCDIR)/*.cpp) + +# Create a list of object file names based on sources, placing them in OBJDIR +# Example: src/main.cpp -> src/main.o +OBJECTS = $(patsubst $(SRCDIR)/%.cpp,$(OBJDIR)/%.o,$(SOURCES)) + +# --- Target Executables --- +TARGET_PROGRAM = program +TARGET_CLEAN_PROGRAM = clean_program # Assuming this is another program to be built from clean.cpp + +# --- Specific Object Files (if needed for explicit dependencies) --- +# These should be correctly generated by the OBJECTS variable and pattern rule below. +# Listing them explicitly for clarity in target dependencies. +OBJ_MAIN = $(OBJDIR)/main.o +# Add other specific object files your 'program' executable depends on +OTHER_OBJS = $(OBJDIR)/helper.o $(OBJDIR)/types.o $(OBJDIR)/consumer.o $(OBJDIR)/another_consumer.o +OBJ_FOR_CLEAN_PROGRAM = $(OBJDIR)/clean.o + + +# --- Build Rules --- + +# Default target: build all specified programs +all: $(TARGET_PROGRAM) $(TARGET_CLEAN_PROGRAM) + +# Rule to link the main program +$(TARGET_PROGRAM): $(OBJ_MAIN) $(OTHER_OBJS) + @echo "Linking $@..." + $(LD) $^ -o $@ $(LDFLAGS) + +# Rule to build and link the 'clean_program' +# This assumes clean.cpp is one of the files in $(SRCDIR) +$(TARGET_CLEAN_PROGRAM): $(OBJ_FOR_CLEAN_PROGRAM) + @echo "Building and linking $@..." + $(LD) $^ -o $@ $(LDFLAGS) + +# --- Generic Pattern Rule for Compilation --- +# This rule tells Make how to build any .o file in OBJDIR from a .cpp file in SRCDIR. +# It crucially uses $(CXX) (clang++) and $(CXXFLAGS). +$(OBJDIR)/%.o: $(SRCDIR)/%.cpp + @echo "Compiling $< to $@..." + $(CXX) $(CXXFLAGS) -c $< -o $@ + +# --- Cleaning Rule --- +clean: + @echo "Cleaning up object files and executables..." + rm -f $(OBJDIR)/*.o $(TARGET_PROGRAM) $(TARGET_CLEAN_PROGRAM) + +# --- Debugging Rule (optional) --- +debug: + @echo "--- Debug Info ---" + @echo "CXX: $(CXX)" + @echo "CXXFLAGS: $(CXXFLAGS)" + @echo "LDFLAGS: $(LDFLAGS)" + @echo "SRCDIR: $(SRCDIR)" + @echo "OBJDIR: $(OBJDIR)" + @echo "SOURCES: $(SOURCES)" + @echo "OBJECTS: $(OBJECTS)" + @echo "TARGET_PROGRAM: $(TARGET_PROGRAM)" + @echo "TARGET_CLEAN_PROGRAM: $(TARGET_CLEAN_PROGRAM)" + @echo "--- End Debug Info ---" + +# --- Phony Targets --- +# Declare targets that are not actual files +.PHONY: all clean debug \ No newline at end of file diff --git a/integrationtests/workspaces/clangd/include/helper.hpp b/integrationtests/workspaces/clangd/include/helper.hpp new file mode 100644 index 0000000..48fc6fc --- /dev/null +++ b/integrationtests/workspaces/clangd/include/helper.hpp @@ -0,0 +1 @@ +void helperFunction(); \ No newline at end of file diff --git a/integrationtests/workspaces/clangd/src/another_consumer.cpp b/integrationtests/workspaces/clangd/src/another_consumer.cpp new file mode 100644 index 0000000..9013d61 --- /dev/null +++ b/integrationtests/workspaces/clangd/src/another_consumer.cpp @@ -0,0 +1,4 @@ +// Placeholder file for another_consumer.cpp +#include + +void anotherConsume() { std::cout << "Another consume function" << std::endl; } \ No newline at end of file diff --git a/integrationtests/workspaces/clangd/src/clean.cpp b/integrationtests/workspaces/clangd/src/clean.cpp new file mode 100644 index 0000000..bbb511f --- /dev/null +++ b/integrationtests/workspaces/clangd/src/clean.cpp @@ -0,0 +1,7 @@ +// Placeholder file for clean.cpp +#include + +int main() { + std::cout << "Clean file" << std::endl; + return 0; +} \ No newline at end of file diff --git a/integrationtests/workspaces/clangd/src/consumer.cpp b/integrationtests/workspaces/clangd/src/consumer.cpp new file mode 100644 index 0000000..025e5e9 --- /dev/null +++ b/integrationtests/workspaces/clangd/src/consumer.cpp @@ -0,0 +1,9 @@ +// Placeholder file for consumer.cpp +#include + +void consume() { std::cout << "Consume function" << std::endl; } + +class TestClass { + public: + void method() {} +}; \ No newline at end of file diff --git a/integrationtests/workspaces/clangd/src/helper.cpp b/integrationtests/workspaces/clangd/src/helper.cpp new file mode 100644 index 0000000..74b9fed --- /dev/null +++ b/integrationtests/workspaces/clangd/src/helper.cpp @@ -0,0 +1,7 @@ +// Placeholder file for helper.cpp +#include +#include "helper.hpp" +const int TEST_CONSTANT = 42; +int TEST_VARIABLE = 100; + +void helperFunction() { std::cout << "Helper function" << std::endl; } \ No newline at end of file diff --git a/integrationtests/workspaces/clangd/src/main.cpp b/integrationtests/workspaces/clangd/src/main.cpp new file mode 100644 index 0000000..39215fe --- /dev/null +++ b/integrationtests/workspaces/clangd/src/main.cpp @@ -0,0 +1,16 @@ +#include +#include "helper.hpp" + +// FooBar is a simple function for testing +void foo_bar() { + std::cout << "Hello, World!" << std::endl; + return; +} + +int main() { + helperFunction(); + return 0; + + // Intentional error: unreachable code + std::cout << "This is unreachable" << std::endl; +} \ No newline at end of file diff --git a/integrationtests/workspaces/clangd/src/types.cpp b/integrationtests/workspaces/clangd/src/types.cpp new file mode 100644 index 0000000..798ad7f --- /dev/null +++ b/integrationtests/workspaces/clangd/src/types.cpp @@ -0,0 +1,10 @@ +// Placeholder file for types.cpp +#include + +void printType() { std::cout << "Type function" << std::endl; } + +struct TestStruct { + int value; +}; + +using TestType = int; \ No newline at end of file From a89ceedd0d77e42b2a04abdc0bc00fdba9dcfdd0 Mon Sep 17 00:00:00 2001 From: Hamish Nicholson Date: Fri, 9 May 2025 11:18:32 +0200 Subject: [PATCH 2/5] Add clangd hover tests --- .../snapshots/clangd/definition/class.snap | 11 +- .../snapshots/clangd/definition/method.snap | 11 +- .../snapshots/clangd/definition/variable.snap | 2 +- .../clangd/diagnostics/unreachable.snap | 9 +- .../snapshots/clangd/hover/class-method.snap | 10 ++ .../snapshots/clangd/hover/class-type.snap | 5 + .../clangd/hover/function-definition-cpp.snap | 5 + .../snapshots/clangd/hover/function-main.snap | 6 + .../clangd/hover/no-hover-info-comment.snap | 2 + .../snapshots/clangd/hover/outside-file.snap | 1 + .../snapshots/clangd/hover/variable.snap | 7 + .../clangd/definition/definition_test.go | 2 +- .../tests/clangd/hover/hover_test.go | 156 ++++++++++++++++++ .../workspaces/clangd/src/consumer.cpp | 7 +- .../workspaces/clangd/src/helper.cpp | 2 +- .../workspaces/clangd/src/main.cpp | 2 + 16 files changed, 224 insertions(+), 14 deletions(-) create mode 100644 integrationtests/snapshots/clangd/hover/class-method.snap create mode 100644 integrationtests/snapshots/clangd/hover/class-type.snap create mode 100644 integrationtests/snapshots/clangd/hover/function-definition-cpp.snap create mode 100644 integrationtests/snapshots/clangd/hover/function-main.snap create mode 100644 integrationtests/snapshots/clangd/hover/no-hover-info-comment.snap create mode 100644 integrationtests/snapshots/clangd/hover/outside-file.snap create mode 100644 integrationtests/snapshots/clangd/hover/variable.snap create mode 100644 integrationtests/tests/clangd/hover/hover_test.go diff --git a/integrationtests/snapshots/clangd/definition/class.snap b/integrationtests/snapshots/clangd/definition/class.snap index 5c0a706..a17c8ea 100644 --- a/integrationtests/snapshots/clangd/definition/class.snap +++ b/integrationtests/snapshots/clangd/definition/class.snap @@ -2,10 +2,15 @@ Symbol: TestClass /TEST_OUTPUT/workspace/src/consumer.cpp -Range: L6:C1 - L9:C2 +Range: L6:C1 - L14:C2 6|class TestClass { 7| public: - 8| void method() {} - 9|}; + 8| /** + 9| * @brief A method that takes an integer parameter. +10| * +11| * @param param The integer parameter to be processed. +12| */ +13| void method(int param) {} +14|}; diff --git a/integrationtests/snapshots/clangd/definition/method.snap b/integrationtests/snapshots/clangd/definition/method.snap index 4c1a0b1..f81a92d 100644 --- a/integrationtests/snapshots/clangd/definition/method.snap +++ b/integrationtests/snapshots/clangd/definition/method.snap @@ -2,10 +2,15 @@ Symbol: method /TEST_OUTPUT/workspace/src/consumer.cpp -Range: L6:C1 - L9:C2 +Range: L6:C1 - L14:C2 6|class TestClass { 7| public: - 8| void method() {} - 9|}; + 8| /** + 9| * @brief A method that takes an integer parameter. +10| * +11| * @param param The integer parameter to be processed. +12| */ +13| void method(int param) {} +14|}; diff --git a/integrationtests/snapshots/clangd/definition/variable.snap b/integrationtests/snapshots/clangd/definition/variable.snap index 1f160ab..376cdfe 100644 --- a/integrationtests/snapshots/clangd/definition/variable.snap +++ b/integrationtests/snapshots/clangd/definition/variable.snap @@ -4,5 +4,5 @@ Symbol: TEST_VARIABLE /TEST_OUTPUT/workspace/src/helper.cpp Range: L5:C1 - L5:C24 -5|int TEST_VARIABLE = 100; +5|int TEST_VARIABLE = 100; /// A test variable used for integration testing purposes. diff --git a/integrationtests/snapshots/clangd/diagnostics/unreachable.snap b/integrationtests/snapshots/clangd/diagnostics/unreachable.snap index b21242a..5b8372b 100644 --- a/integrationtests/snapshots/clangd/diagnostics/unreachable.snap +++ b/integrationtests/snapshots/clangd/diagnostics/unreachable.snap @@ -1,10 +1,11 @@ /TEST_OUTPUT/workspace/src/main.cpp Diagnostics in File: 1 -WARNING at L15:C38: Code will never be executed (Source: clang, Code: -Wunreachable-code) +WARNING at L14:C3: Code will never be executed (Source: clang, Code: -Wunreachable-code) 10|int main() { ... +12| return 0; 13| -14| // Intentional error: unreachable code -15| std::cout << "This is unreachable" << std::endl; -16|} +14| foo_bar(); +15| +16| // Intentional error: unreachable code diff --git a/integrationtests/snapshots/clangd/hover/class-method.snap b/integrationtests/snapshots/clangd/hover/class-method.snap new file mode 100644 index 0000000..4b6f4ac --- /dev/null +++ b/integrationtests/snapshots/clangd/hover/class-method.snap @@ -0,0 +1,10 @@ +instance-method method + +→ void +Parameters: +- int param +@brief A method that takes an integer parameter. +@param param The integer parameter to be processed. + +// In TestClass +public: void method(int param) \ No newline at end of file diff --git a/integrationtests/snapshots/clangd/hover/class-type.snap b/integrationtests/snapshots/clangd/hover/class-type.snap new file mode 100644 index 0000000..da19143 --- /dev/null +++ b/integrationtests/snapshots/clangd/hover/class-type.snap @@ -0,0 +1,5 @@ +class TestClass + +Size: 1 byte + +class TestClass {} \ No newline at end of file diff --git a/integrationtests/snapshots/clangd/hover/function-definition-cpp.snap b/integrationtests/snapshots/clangd/hover/function-definition-cpp.snap new file mode 100644 index 0000000..11114a0 --- /dev/null +++ b/integrationtests/snapshots/clangd/hover/function-definition-cpp.snap @@ -0,0 +1,5 @@ +function helperFunction + +→ void + +void helperFunction() \ No newline at end of file diff --git a/integrationtests/snapshots/clangd/hover/function-main.snap b/integrationtests/snapshots/clangd/hover/function-main.snap new file mode 100644 index 0000000..ee683d4 --- /dev/null +++ b/integrationtests/snapshots/clangd/hover/function-main.snap @@ -0,0 +1,6 @@ +function foo_bar + +→ void +FooBar is a simple function for testing + +void foo_bar() \ No newline at end of file diff --git a/integrationtests/snapshots/clangd/hover/no-hover-info-comment.snap b/integrationtests/snapshots/clangd/hover/no-hover-info-comment.snap new file mode 100644 index 0000000..218ceb1 --- /dev/null +++ b/integrationtests/snapshots/clangd/hover/no-hover-info-comment.snap @@ -0,0 +1,2 @@ +No hover information available for this position on the following line: +// FooBar is a simple function for testing diff --git a/integrationtests/snapshots/clangd/hover/outside-file.snap b/integrationtests/snapshots/clangd/hover/outside-file.snap new file mode 100644 index 0000000..f175581 --- /dev/null +++ b/integrationtests/snapshots/clangd/hover/outside-file.snap @@ -0,0 +1 @@ +No hover information available for this position on the following line: diff --git a/integrationtests/snapshots/clangd/hover/variable.snap b/integrationtests/snapshots/clangd/hover/variable.snap new file mode 100644 index 0000000..cae3b23 --- /dev/null +++ b/integrationtests/snapshots/clangd/hover/variable.snap @@ -0,0 +1,7 @@ +variable TEST_VARIABLE + +Type: int +Value = 100 (0x64) +A test variable used for integration testing purposes. + +int TEST_VARIABLE = 100 \ No newline at end of file diff --git a/integrationtests/tests/clangd/definition/definition_test.go b/integrationtests/tests/clangd/definition/definition_test.go index e4f4dee..3f75116 100644 --- a/integrationtests/tests/clangd/definition/definition_test.go +++ b/integrationtests/tests/clangd/definition/definition_test.go @@ -67,7 +67,7 @@ func TestReadDefinition(t *testing.T) { { name: "Method", symbolName: "method", - expectedText: "void method()", + expectedText: "void method(int param)", snapshotName: "method", }, { diff --git a/integrationtests/tests/clangd/hover/hover_test.go b/integrationtests/tests/clangd/hover/hover_test.go new file mode 100644 index 0000000..efbb1fb --- /dev/null +++ b/integrationtests/tests/clangd/hover/hover_test.go @@ -0,0 +1,156 @@ +package hover_test + +import ( + "context" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/isaacphi/mcp-language-server/integrationtests/tests/clangd/internal" + "github.com/isaacphi/mcp-language-server/integrationtests/tests/common" + "github.com/isaacphi/mcp-language-server/internal/tools" +) + +// TestHover tests hover functionality with the Clangd language server +func TestHover(t *testing.T) { + // Helper function to open all files and wait for indexing + openAllFilesAndWait := func(suite *common.TestSuite, ctx context.Context) { + // Open all files to ensure clangd indexes everything + filesToOpen := []string{ + "src/main.cpp", + "src/types.cpp", + "src/helper.cpp", + "src/consumer.cpp", + "src/another_consumer.cpp", + "src/clean.cpp", + "include/helper.hpp", + } + + for _, file := range filesToOpen { + filePath := filepath.Join(suite.WorkspaceDir, file) + err := suite.Client.OpenFile(ctx, filePath) + if err != nil { + // Don't fail the test, some files might not exist in certain tests + t.Logf("Note: Failed to open %s: %v", file, err) + } + } + // Wait for indexing to complete. clangd won't index files until they are opened. + time.Sleep(10 * time.Second) + } + + tests := []struct { + name string + file string + line int + column int + expectedText string // Text that should be in the hover result + unexpectedText string // Text that should NOT be in the hover result (optional) + snapshotName string + }{ + // Tests using types.cpp file + { + name: "Class", + file: "src/consumer.cpp", + line: 6, // Assuming TestClass definition + column: 7, // "TestClass" + expectedText: "class TestClass", + snapshotName: "class-type", + }, + { + name: "Method in Class", + file: "src/consumer.cpp", + line: 13, // Assuming method definition within TestClass + column: 10, // "method" + expectedText: "public: void method(int param)", + snapshotName: "class-method", + }, + { + name: "Variable", // Global variable in helper.cpp with inline comment + file: "src/helper.cpp", + line: 5, // Assuming TEST_VARIABLE definition + column: 5, // "TEST_VARIABLE" + expectedText: "int TEST_VARIABLE", + snapshotName: "variable", + }, + { + name: "Function in main.cpp", + file: "src/main.cpp", + line: 14, // Assuming foo_bar use + column: 6, // "foo_bar" + expectedText: "function foo_bar", + snapshotName: "function-main", + }, + { + name: "Function definition in helper.cpp", + file: "src/main.cpp", + line: 11, // Assuming helperFunction use + column: 7, // "helperFunction" + expectedText: "function helperFunction", + snapshotName: "function-definition-cpp", + }, + // Test for a location without hover info (empty space or comment) + { + name: "NoHoverInfoComment", + file: "src/main.cpp", + line: 4, // Comment line + column: 1, + unexpectedText: "void", // Should not find any specific code hover + snapshotName: "no-hover-info-comment", + }, + // Test for a location outside the file - expect an error or no result + { + name: "OutsideFile", + file: "src/main.cpp", + line: 1000, // Line number beyond file length + column: 1, + unexpectedText: "void", // Should not find any specific code hover + snapshotName: "outside-file", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Get a test suite + suite := internal.GetTestSuite(t) + + ctx, cancel := context.WithTimeout(suite.Context, 10*time.Second) + defer cancel() + + // Open all files and wait for clangd to index them + openAllFilesAndWait(suite, ctx) + + filePath := filepath.Join(suite.WorkspaceDir, tt.file) + + // Get hover info + result, err := tools.GetHoverInfo(ctx, suite.Client, filePath, tt.line, tt.column) + if err != nil { + // For the "OutsideFile" test or "NoHoverInfo" we might expect an error or empty result + if tt.name == "OutsideFile" || strings.HasPrefix(tt.name, "NoHoverInfo") { + // Create a snapshot even for error case or empty result + snapshotContent := "No hover information expected or error occurred." + if err != nil { + snapshotContent = err.Error() + } else if result != "" { + snapshotContent = result + } + common.SnapshotTest(t, "clangd", "hover", tt.snapshotName, snapshotContent) + return + } + t.Fatalf("GetHoverInfo failed for %s: %v. Result: %s", tt.name, err, result) + } + + // Verify expected content + if tt.expectedText != "" && !strings.Contains(result, tt.expectedText) { + t.Errorf("Test %s: Expected hover info to contain %q but got: %s", tt.name, tt.expectedText, result) + } + + // Verify unexpected content is absent + if tt.unexpectedText != "" && strings.Contains(result, tt.unexpectedText) { + t.Errorf("Test %s: Expected hover info NOT to contain %q but it was found: %s", tt.name, tt.unexpectedText, result) + } + + common.SnapshotTest(t, "clangd", "hover", tt.snapshotName, result) + }) + } +} diff --git a/integrationtests/workspaces/clangd/src/consumer.cpp b/integrationtests/workspaces/clangd/src/consumer.cpp index 025e5e9..228101c 100644 --- a/integrationtests/workspaces/clangd/src/consumer.cpp +++ b/integrationtests/workspaces/clangd/src/consumer.cpp @@ -5,5 +5,10 @@ void consume() { std::cout << "Consume function" << std::endl; } class TestClass { public: - void method() {} + /** + * @brief A method that takes an integer parameter. + * + * @param param The integer parameter to be processed. + */ + void method(int param) {} }; \ No newline at end of file diff --git a/integrationtests/workspaces/clangd/src/helper.cpp b/integrationtests/workspaces/clangd/src/helper.cpp index 74b9fed..9397ea3 100644 --- a/integrationtests/workspaces/clangd/src/helper.cpp +++ b/integrationtests/workspaces/clangd/src/helper.cpp @@ -2,6 +2,6 @@ #include #include "helper.hpp" const int TEST_CONSTANT = 42; -int TEST_VARIABLE = 100; +int TEST_VARIABLE = 100; // A test variable used for integration testing purposes. void helperFunction() { std::cout << "Helper function" << std::endl; } \ No newline at end of file diff --git a/integrationtests/workspaces/clangd/src/main.cpp b/integrationtests/workspaces/clangd/src/main.cpp index 39215fe..050bff8 100644 --- a/integrationtests/workspaces/clangd/src/main.cpp +++ b/integrationtests/workspaces/clangd/src/main.cpp @@ -11,6 +11,8 @@ int main() { helperFunction(); return 0; + foo_bar(); + // Intentional error: unreachable code std::cout << "This is unreachable" << std::endl; } \ No newline at end of file From 977ecf8c4c0755d49a64fc93cc7476cc458fad93 Mon Sep 17 00:00:00 2001 From: Hamish Nicholson Date: Fri, 9 May 2025 12:29:43 +0200 Subject: [PATCH 3/5] Add clangd references test --- .../snapshots/clangd/definition/class.snap | 22 +-- .../snapshots/clangd/definition/constant.snap | 2 +- .../clangd/definition/helperFunction.snap | 6 +- .../snapshots/clangd/definition/method.snap | 22 +-- .../snapshots/clangd/definition/struct.snap | 2 +- .../snapshots/clangd/definition/type.snap | 2 +- .../snapshots/clangd/definition/variable.snap | 4 +- .../foobar-function-references.snap | 15 ++ .../helper-function-references.snap | 31 ++++ .../clangd/references/references_test.go | 170 ++++++++++++++++++ .../workspaces/clangd/src/consumer.cpp | 3 +- 11 files changed, 248 insertions(+), 31 deletions(-) create mode 100644 integrationtests/snapshots/clangd/references/foobar-function-references.snap create mode 100644 integrationtests/snapshots/clangd/references/helper-function-references.snap create mode 100644 integrationtests/tests/clangd/references/references_test.go diff --git a/integrationtests/snapshots/clangd/definition/class.snap b/integrationtests/snapshots/clangd/definition/class.snap index a17c8ea..8b1a97b 100644 --- a/integrationtests/snapshots/clangd/definition/class.snap +++ b/integrationtests/snapshots/clangd/definition/class.snap @@ -1,16 +1,16 @@ --- Symbol: TestClass -/TEST_OUTPUT/workspace/src/consumer.cpp -Range: L6:C1 - L14:C2 +/TEST_OUTPUT/workspace/clangd/src/consumer.cpp +Range: L7:C1 - L15:C2 - 6|class TestClass { - 7| public: - 8| /** - 9| * @brief A method that takes an integer parameter. -10| * -11| * @param param The integer parameter to be processed. -12| */ -13| void method(int param) {} -14|}; + 7|class TestClass { + 8| public: + 9| /** +10| * @brief A method that takes an integer parameter. +11| * +12| * @param param The integer parameter to be processed. +13| */ +14| void method(int param) { helperFunction(); } +15|}; diff --git a/integrationtests/snapshots/clangd/definition/constant.snap b/integrationtests/snapshots/clangd/definition/constant.snap index c4c3a61..90253d9 100644 --- a/integrationtests/snapshots/clangd/definition/constant.snap +++ b/integrationtests/snapshots/clangd/definition/constant.snap @@ -1,7 +1,7 @@ --- Symbol: TEST_CONSTANT -/TEST_OUTPUT/workspace/src/helper.cpp +/TEST_OUTPUT/workspace/clangd/src/helper.cpp Range: L4:C1 - L4:C29 4|const int TEST_CONSTANT = 42; diff --git a/integrationtests/snapshots/clangd/definition/helperFunction.snap b/integrationtests/snapshots/clangd/definition/helperFunction.snap index 6be5f7b..4b9d978 100644 --- a/integrationtests/snapshots/clangd/definition/helperFunction.snap +++ b/integrationtests/snapshots/clangd/definition/helperFunction.snap @@ -1,8 +1,8 @@ --- Symbol: helperFunction -/TEST_OUTPUT/workspace/clangd/include/helper.hpp -Range: L1:C1 - L1:C22 +/TEST_OUTPUT/workspace/clangd/src/helper.cpp +Range: L7:C1 - L7:C71 -1|void helperFunction(); +7|void helperFunction() { std::cout << "Helper function" << std::endl; } diff --git a/integrationtests/snapshots/clangd/definition/method.snap b/integrationtests/snapshots/clangd/definition/method.snap index f81a92d..1ec50b2 100644 --- a/integrationtests/snapshots/clangd/definition/method.snap +++ b/integrationtests/snapshots/clangd/definition/method.snap @@ -1,16 +1,16 @@ --- Symbol: method -/TEST_OUTPUT/workspace/src/consumer.cpp -Range: L6:C1 - L14:C2 +/TEST_OUTPUT/workspace/clangd/src/consumer.cpp +Range: L7:C1 - L15:C2 - 6|class TestClass { - 7| public: - 8| /** - 9| * @brief A method that takes an integer parameter. -10| * -11| * @param param The integer parameter to be processed. -12| */ -13| void method(int param) {} -14|}; + 7|class TestClass { + 8| public: + 9| /** +10| * @brief A method that takes an integer parameter. +11| * +12| * @param param The integer parameter to be processed. +13| */ +14| void method(int param) { helperFunction(); } +15|}; diff --git a/integrationtests/snapshots/clangd/definition/struct.snap b/integrationtests/snapshots/clangd/definition/struct.snap index 3e35ab5..ff5711d 100644 --- a/integrationtests/snapshots/clangd/definition/struct.snap +++ b/integrationtests/snapshots/clangd/definition/struct.snap @@ -1,7 +1,7 @@ --- Symbol: TestStruct -/TEST_OUTPUT/workspace/src/types.cpp +/TEST_OUTPUT/workspace/clangd/src/types.cpp Range: L6:C1 - L8:C2 6|struct TestStruct { diff --git a/integrationtests/snapshots/clangd/definition/type.snap b/integrationtests/snapshots/clangd/definition/type.snap index 983ffa0..c9bb0fc 100644 --- a/integrationtests/snapshots/clangd/definition/type.snap +++ b/integrationtests/snapshots/clangd/definition/type.snap @@ -1,7 +1,7 @@ --- Symbol: TestType -/TEST_OUTPUT/workspace/src/types.cpp +/TEST_OUTPUT/workspace/clangd/src/types.cpp Range: L10:C1 - L10:C21 10|using TestType = int; diff --git a/integrationtests/snapshots/clangd/definition/variable.snap b/integrationtests/snapshots/clangd/definition/variable.snap index 376cdfe..c9d1442 100644 --- a/integrationtests/snapshots/clangd/definition/variable.snap +++ b/integrationtests/snapshots/clangd/definition/variable.snap @@ -1,8 +1,8 @@ --- Symbol: TEST_VARIABLE -/TEST_OUTPUT/workspace/src/helper.cpp +/TEST_OUTPUT/workspace/clangd/src/helper.cpp Range: L5:C1 - L5:C24 -5|int TEST_VARIABLE = 100; /// A test variable used for integration testing purposes. +5|int TEST_VARIABLE = 100; // A test variable used for integration testing purposes. diff --git a/integrationtests/snapshots/clangd/references/foobar-function-references.snap b/integrationtests/snapshots/clangd/references/foobar-function-references.snap new file mode 100644 index 0000000..855a974 --- /dev/null +++ b/integrationtests/snapshots/clangd/references/foobar-function-references.snap @@ -0,0 +1,15 @@ +--- + +/TEST_OUTPUT/workspace/clangd/src/main.cpp +References in File: 1 +At: L14:C3 + +10|int main() { +11| helperFunction(); +12| return 0; +13| +14| foo_bar(); +15| +16| // Intentional error: unreachable code +17| std::cout << "This is unreachable" << std::endl; +18|} diff --git a/integrationtests/snapshots/clangd/references/helper-function-references.snap b/integrationtests/snapshots/clangd/references/helper-function-references.snap new file mode 100644 index 0000000..e678ff4 --- /dev/null +++ b/integrationtests/snapshots/clangd/references/helper-function-references.snap @@ -0,0 +1,31 @@ +--- + +/TEST_OUTPUT/workspace/clangd/src/consumer.cpp +References in File: 1 +At: L14:C28 + + 9| /** +10| * @brief A method that takes an integer parameter. +11| * +12| * @param param The integer parameter to be processed. +13| */ +14| void method(int param) { helperFunction(); } +15|}; + +--- + +/TEST_OUTPUT/workspace/clangd/src/main.cpp +References in File: 1 +At: L11:C3 + + 6| std::cout << "Hello, World!" << std::endl; + 7| return; + 8|} + 9| +10|int main() { +11| helperFunction(); +12| return 0; +13| +14| foo_bar(); +15| +16| // Intentional error: unreachable code diff --git a/integrationtests/tests/clangd/references/references_test.go b/integrationtests/tests/clangd/references/references_test.go new file mode 100644 index 0000000..19eeea0 --- /dev/null +++ b/integrationtests/tests/clangd/references/references_test.go @@ -0,0 +1,170 @@ +package references_test + +import ( + "context" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/isaacphi/mcp-language-server/integrationtests/tests/clangd/internal" + "github.com/isaacphi/mcp-language-server/integrationtests/tests/common" + "github.com/isaacphi/mcp-language-server/internal/tools" +) + +// TestFindReferences tests the FindReferences tool with C++ symbols +// that have references across different files +func TestFindReferences(t *testing.T) { + // Helper function to open all files and wait for indexing + openAllFilesAndWait := func(suite *common.TestSuite, ctx context.Context) { + // Open all files to ensure clangd indexes everything + filesToOpen := []string{ + // "src/main.cpp", + // "src/types.cpp", + // "src/helper.cpp", + // "src/consumer.cpp", + // "src/another_consumer.cpp", + "src/clean.cpp", + } + + for _, file := range filesToOpen { + filePath := filepath.Join(suite.WorkspaceDir, file) + err := suite.Client.OpenFile(ctx, filePath) + if err != nil { + // Don't fail the test, some files might not exist in certain tests + t.Logf("Note: Failed to open %s: %v", file, err) + } + } + // Wait for indexing to complete. clangd won't index files until they are opened. + time.Sleep(30 * time.Second) + } + + suite := internal.GetTestSuite(t) + + ctx, cancel := context.WithTimeout(suite.Context, 30*time.Second) // Increased timeout for clangd references + defer cancel() + + // Open all files and wait for clangd to index them + openAllFilesAndWait(suite, ctx) + + tests := []struct { + name string + symbolName string // The symbol to find references for. For methods, use Class::Method. + fileHint string // File where the symbol definition is likely located (optional, can speed up). + lineHint int // Line number where the symbol is used/defined (optional, for focusing search). + colHint int // Column number (optional). + expectedText string // Text expected in one of the reference locations. + expectedFiles int // Minimum number of files where references should be found. + snapshotName string + }{ + { + name: "Function with references across files", + symbolName: "helperFunction", // used in main.cpp, consumer.cpp. Clangd seems to treat definitations as declarations, so the definition in helper.cpp is not included. + fileHint: "src/helper.cpp", + lineHint: 7, // Definition line + colHint: 6, + expectedText: "main.cpp", // Expect a reference in main.cpp + expectedFiles: 2, // main.cpp, consumer.cpp + snapshotName: "helper-function-references", + }, + { + name: "Function with reference in same file", + symbolName: "foo_bar", // Defined and used in main.cpp + fileHint: "src/main.cpp", + lineHint: 5, // Definition line + colHint: 6, + expectedText: "main.cpp", + expectedFiles: 1, // main.cpp (definition and usage) + snapshotName: "foobar-function-references", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // Call the FindReferences tool + result, err := tools.FindReferences(ctx, suite.Client, tc.symbolName) + if err != nil { + t.Fatalf("Failed to find references for %s: %v. Result: %s", tc.symbolName, err, result) + } + + // Check that the result contains relevant information + if !strings.Contains(result, tc.expectedText) { + t.Errorf("References for %s do not contain expected text %q in result: %s", tc.symbolName, tc.expectedText, result) + } + + // Count how many different files are mentioned in the result + fileCount := countFilesInResult(result, suite.WorkspaceDir) + if fileCount < tc.expectedFiles { + t.Errorf("Expected references for %s in at least %d files, but found in %d files. Result:\n%s", + tc.symbolName, tc.expectedFiles, fileCount, result) + } + + // Use snapshot testing to verify exact output + common.SnapshotTest(t, "clangd", "references", tc.snapshotName, result) + }) + } +} + +// countFilesInResult counts the number of unique files mentioned in the result +// It now normalizes paths relative to the workspace for more robust counting. +func countFilesInResult(result string, workspaceDir string) int { + fileMap := make(map[string]bool) + + // Normalize workspaceDir path for string matching + normalizedWorkspaceDir := filepath.ToSlash(workspaceDir) + + lines := strings.Split(result, "\n") + for _, line := range lines { + // A line representing a file path typically contains ".cpp" or ".hpp" + // and is an absolute path or relative to the workspace view. + if strings.Contains(line, ".cpp") || strings.Contains(line, ".hpp") { + // Attempt to extract a clean, relative path or a consistent absolute path part + var pathKey string + if strings.Contains(line, normalizedWorkspaceDir) { + relPath, err := filepath.Rel(normalizedWorkspaceDir, strings.Fields(line)[0]) // Assuming path is the first part + if err == nil { + pathKey = filepath.ToSlash(relPath) + } else { + // Fallback if Rel fails, use a snippet after a known part + pathKey = extractPathSegment(line) + } + } else { + // If not a full workspace path, it might be a relative path already or just a filename + pathKey = extractPathSegment(line) // A more generic extraction + } + if pathKey != "" { + fileMap[pathKey] = true + } + } + } + return len(fileMap) +} + +// extractPathSegment tries to get a consistent file path identifier from a line of text. +func extractPathSegment(line string) string { + // Look for common C++ file extensions + var ext string + if strings.Contains(line, ".cpp") { + ext = ".cpp" + } else if strings.Contains(line, ".hpp") { + ext = ".hpp" + } else { + return "" // Not a C++ source/header file line we can easily parse + } + + fields := strings.Fields(line) + for _, field := range fields { + if strings.HasSuffix(field, ext) { + // Attempt to clean common prefixes like "uri: file://" or line numbers + cleanedPath := strings.TrimPrefix(field, "uri:") + cleanedPath = strings.TrimPrefix(cleanedPath, "file://") + // Remove trailing colons or line/char numbers like ":10:5" + parts := strings.Split(cleanedPath, ":") + if len(parts) > 0 { + return parts[0] // Return the part before the first colon, assuming it's the path + } + return cleanedPath + } + } + return "" +} diff --git a/integrationtests/workspaces/clangd/src/consumer.cpp b/integrationtests/workspaces/clangd/src/consumer.cpp index 228101c..5e162f2 100644 --- a/integrationtests/workspaces/clangd/src/consumer.cpp +++ b/integrationtests/workspaces/clangd/src/consumer.cpp @@ -1,5 +1,6 @@ // Placeholder file for consumer.cpp #include +#include "helper.hpp" void consume() { std::cout << "Consume function" << std::endl; } @@ -10,5 +11,5 @@ class TestClass { * * @param param The integer parameter to be processed. */ - void method(int param) {} + void method(int param) { helperFunction(); } }; \ No newline at end of file From 2e95b272ac41c7387ac673295b8201002907f3a2 Mon Sep 17 00:00:00 2001 From: Hamish Nicholson Date: Fri, 9 May 2025 12:30:36 +0200 Subject: [PATCH 4/5] Open file in FindReferences --- internal/tools/references.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/internal/tools/references.go b/internal/tools/references.go index 4af25da..cb424e5 100644 --- a/internal/tools/references.go +++ b/internal/tools/references.go @@ -66,7 +66,12 @@ func FindReferences(ctx context.Context, client *lsp.Client, symbolName string) IncludeDeclaration: false, }, } - + // File is likely to be opened already, but may not be. + err := client.OpenFile(ctx, loc.URI.Path()) + if err != nil { + toolsLogger.Error("Error opening file: %v", err) + continue + } refs, err := client.References(ctx, refsParams) if err != nil { return "", fmt.Errorf("failed to get references: %v", err) From 23d89bf8ce8dca6c6cb2710fe03785290f87dbf7 Mon Sep 17 00:00:00 2001 From: Hamish Nicholson Date: Fri, 9 May 2025 12:34:52 +0200 Subject: [PATCH 5/5] Explicitly pass --compile-commands-dir to clangd in integration tests --- .../tests/clangd/definition/definition_test.go | 7 +------ .../tests/clangd/diagnostics/diagnostics_test.go | 7 +------ integrationtests/tests/clangd/hover/hover_test.go | 14 ++++---------- integrationtests/tests/clangd/internal/helpers.go | 2 +- .../tests/clangd/references/references_test.go | 9 ++------- 5 files changed, 9 insertions(+), 30 deletions(-) diff --git a/integrationtests/tests/clangd/definition/definition_test.go b/integrationtests/tests/clangd/definition/definition_test.go index 3f75116..72d82d5 100644 --- a/integrationtests/tests/clangd/definition/definition_test.go +++ b/integrationtests/tests/clangd/definition/definition_test.go @@ -16,14 +16,9 @@ import ( func TestReadDefinition(t *testing.T) { // Helper function to open all files and wait for indexing openAllFilesAndWait := func(suite *common.TestSuite, ctx context.Context) { - // Open all files to ensure clangd indexes everything + // Open one file so that clangd loads compiles commands and begins indexing filesToOpen := []string{ "src/main.cpp", - "src/types.cpp", - "src/helper.cpp", - "src/consumer.cpp", - "src/another_consumer.cpp", - "src/clean.cpp", } for _, file := range filesToOpen { diff --git a/integrationtests/tests/clangd/diagnostics/diagnostics_test.go b/integrationtests/tests/clangd/diagnostics/diagnostics_test.go index 02222b8..29ea864 100644 --- a/integrationtests/tests/clangd/diagnostics/diagnostics_test.go +++ b/integrationtests/tests/clangd/diagnostics/diagnostics_test.go @@ -18,14 +18,9 @@ import ( func TestDiagnostics(t *testing.T) { // Helper function to open all files and wait for indexing openAllFilesAndWait := func(suite *common.TestSuite, ctx context.Context) { - // Open all files to ensure clangd indexes everything + // Open one file so that clangd loads compiles commands and begins indexing filesToOpen := []string{ "src/main.cpp", - "src/types.cpp", - "src/helper.cpp", - "src/consumer.cpp", - "src/another_consumer.cpp", - "src/clean.cpp", } for _, file := range filesToOpen { diff --git a/integrationtests/tests/clangd/hover/hover_test.go b/integrationtests/tests/clangd/hover/hover_test.go index efbb1fb..a0f14a9 100644 --- a/integrationtests/tests/clangd/hover/hover_test.go +++ b/integrationtests/tests/clangd/hover/hover_test.go @@ -16,15 +16,9 @@ import ( func TestHover(t *testing.T) { // Helper function to open all files and wait for indexing openAllFilesAndWait := func(suite *common.TestSuite, ctx context.Context) { - // Open all files to ensure clangd indexes everything + // Open one file so that clangd loads compiles commands and begins indexing filesToOpen := []string{ "src/main.cpp", - "src/types.cpp", - "src/helper.cpp", - "src/consumer.cpp", - "src/another_consumer.cpp", - "src/clean.cpp", - "include/helper.hpp", } for _, file := range filesToOpen { @@ -36,7 +30,7 @@ func TestHover(t *testing.T) { } } // Wait for indexing to complete. clangd won't index files until they are opened. - time.Sleep(10 * time.Second) + time.Sleep(5 * time.Second) } tests := []struct { @@ -52,7 +46,7 @@ func TestHover(t *testing.T) { { name: "Class", file: "src/consumer.cpp", - line: 6, // Assuming TestClass definition + line: 7, // Assuming TestClass definition column: 7, // "TestClass" expectedText: "class TestClass", snapshotName: "class-type", @@ -60,7 +54,7 @@ func TestHover(t *testing.T) { { name: "Method in Class", file: "src/consumer.cpp", - line: 13, // Assuming method definition within TestClass + line: 14, // Assuming method definition within TestClass column: 10, // "method" expectedText: "public: void method(int param)", snapshotName: "class-method", diff --git a/integrationtests/tests/clangd/internal/helpers.go b/integrationtests/tests/clangd/internal/helpers.go index fce3401..cccf4e6 100644 --- a/integrationtests/tests/clangd/internal/helpers.go +++ b/integrationtests/tests/clangd/internal/helpers.go @@ -19,7 +19,7 @@ func GetTestSuite(t *testing.T) *common.TestSuite { config := common.LSPTestConfig{ Name: "clangd", Command: "clangd", - Args: []string{}, + Args: []string{"--compile-commands-dir=" + filepath.Join(repoRoot, "integrationtests/workspaces/clangd")}, WorkspaceDir: filepath.Join(repoRoot, "integrationtests/workspaces/clangd"), InitializeTimeMs: 2000, } diff --git a/integrationtests/tests/clangd/references/references_test.go b/integrationtests/tests/clangd/references/references_test.go index 19eeea0..9d8049d 100644 --- a/integrationtests/tests/clangd/references/references_test.go +++ b/integrationtests/tests/clangd/references/references_test.go @@ -17,14 +17,9 @@ import ( func TestFindReferences(t *testing.T) { // Helper function to open all files and wait for indexing openAllFilesAndWait := func(suite *common.TestSuite, ctx context.Context) { - // Open all files to ensure clangd indexes everything + // Open one file so that clangd loads compiles commands and begins indexing filesToOpen := []string{ - // "src/main.cpp", - // "src/types.cpp", - // "src/helper.cpp", - // "src/consumer.cpp", - // "src/another_consumer.cpp", - "src/clean.cpp", + "src/main.cpp", } for _, file := range filesToOpen {