From 0b1b51c226f3668121cda093c7c785983027c3af Mon Sep 17 00:00:00 2001 From: Roo Code Date: Fri, 22 Aug 2025 18:37:24 +0000 Subject: [PATCH] fix: improve Java annotation handling in listCodeDefinitionNames - Made annotation detection more generic (not hard-coded for specific annotations) - Skip lines starting with @ when looking for method signatures - Filter out standalone annotation definitions - Handles any number of annotations on methods/fields dynamically Fixes #7330 --- .../java-interface-and-annotations.test.ts | 133 ++++++++++++++++++ src/services/tree-sitter/index.ts | 57 ++++---- 2 files changed, 164 insertions(+), 26 deletions(-) create mode 100644 src/services/tree-sitter/__tests__/java-interface-and-annotations.test.ts diff --git a/src/services/tree-sitter/__tests__/java-interface-and-annotations.test.ts b/src/services/tree-sitter/__tests__/java-interface-and-annotations.test.ts new file mode 100644 index 0000000000..a86e3cda37 --- /dev/null +++ b/src/services/tree-sitter/__tests__/java-interface-and-annotations.test.ts @@ -0,0 +1,133 @@ +import { describe, it, expect } from "vitest" +import { testParseSourceCodeDefinitions } from "./helpers" +import { javaQuery } from "../queries" + +describe("Java interface methods and annotations", () => { + it("should correctly parse interface methods", async () => { + const interfaceContent = `interface TestInterface { + /** + * This is a test method + */ + void testMethod(); + + String getName(); + + int calculate(int a, int b); +}` + + const testOptions = { + language: "java", + wasmFile: "tree-sitter-java.wasm", + queryString: javaQuery, + extKey: "java", + } + + const parseResult = await testParseSourceCodeDefinitions( + "/test/TestInterface.java", + interfaceContent, + testOptions, + ) + + console.log("\n=== INTERFACE PARSE RESULT ===") + console.log(parseResult) + console.log("==============================\n") + + // Interface methods should be detected + expect(parseResult).toBeTruthy() + + // Force test to fail to see output + if (!parseResult) { + throw new Error("No parse result for interface") + } + if (!parseResult.includes("testMethod")) { + throw new Error(`Interface methods not detected. Result:\n${parseResult}`) + } + expect(parseResult).toContain("testMethod") + expect(parseResult).toContain("getName") + expect(parseResult).toContain("calculate") + }) + + it("should correctly handle multiple annotations on methods", async () => { + const classContent = `class TestClass implements TestInterface { + + @Override + @Test + public void testMethod() { + // Implementation goes here + } + + @Override + public String getName() { + return "TestClass"; + } + + @Override + @Deprecated + public int calculate(int a, int b) { + return a + b; + } + + @SuppressWarnings("unchecked") + private void helperMethod() { + // Helper implementation + } +}` + + const testOptions = { + language: "java", + wasmFile: "tree-sitter-java.wasm", + queryString: javaQuery, + extKey: "java", + } + + const parseResult = await testParseSourceCodeDefinitions("/test/TestClass.java", classContent, testOptions) + + console.log("\n=== CLASS PARSE RESULT ===") + console.log(parseResult) + console.log("==========================\n") + + if (parseResult) { + const lines = parseResult.split("\n").filter((line) => line.trim()) + + // Check that method names are shown, not annotations + const hasMethodNames = lines.some((line) => line.includes("testMethod")) + const hasGetName = lines.some((line) => line.includes("getName")) + const hasCalculate = lines.some((line) => line.includes("calculate")) + const hasHelper = lines.some((line) => line.includes("helperMethod")) + + // Check for the bug: annotations shown as method names + const hasStandaloneOverride = lines.some( + (line) => + line.includes("@Override") && + !line.includes("testMethod") && + !line.includes("getName") && + !line.includes("calculate"), + ) + const hasStandaloneTest = lines.some((line) => line.includes("@Test") && !line.includes("testMethod")) + const hasStandaloneDeprecated = lines.some( + (line) => line.includes("@Deprecated") && !line.includes("calculate"), + ) + + console.log("Method detection:") + console.log(" testMethod:", hasMethodNames) + console.log(" getName:", hasGetName) + console.log(" calculate:", hasCalculate) + console.log(" helperMethod:", hasHelper) + console.log("\nAnnotation issues:") + console.log(" Standalone @Override:", hasStandaloneOverride) + console.log(" Standalone @Test:", hasStandaloneTest) + console.log(" Standalone @Deprecated:", hasStandaloneDeprecated) + + // All methods should be detected + expect(hasMethodNames).toBe(true) + expect(hasGetName).toBe(true) + expect(hasCalculate).toBe(true) + expect(hasHelper).toBe(true) + + // Annotations should not appear as standalone method names + expect(hasStandaloneOverride).toBe(false) + expect(hasStandaloneTest).toBe(false) + expect(hasStandaloneDeprecated).toBe(false) + } + }) +}) diff --git a/src/services/tree-sitter/index.ts b/src/services/tree-sitter/index.ts index 12fdf84a36..478e2909a1 100644 --- a/src/services/tree-sitter/index.ts +++ b/src/services/tree-sitter/index.ts @@ -339,40 +339,45 @@ function processCaptures(captures: QueryCapture[], lines: string[], language: st } // For other component definitions else if (isNotHtmlElement(startLineContent)) { - // For Java, special handling to avoid showing @Override as a separate line - // when it's part of a method declaration + // For Java, special handling for methods with annotations if (language === "java" && name === "definition.method") { - // Check if the method has an annotation like @Override - const methodText = definitionNode.text - if (methodText.includes("@Override")) { - // Find the actual method declaration line (not the annotation line) - let methodDeclarationLine = startLine - for (let i = startLine; i <= endLine; i++) { - if ( - lines[i].includes("public") || - lines[i].includes("private") || - lines[i].includes("protected") || - lines[i].includes("void") || - lines[i].includes("static") - ) { - methodDeclarationLine = i - break - } + // Find the actual method declaration line (skip annotation lines) + let methodDeclarationLine = startLine + for (let i = startLine; i <= endLine; i++) { + const line = lines[i].trim() + // Skip empty lines and annotation lines (lines starting with @) + if (line && !line.startsWith("@") && !line.startsWith("//") && !line.startsWith("/*")) { + methodDeclarationLine = i + break } - // Output the method with its proper line range, but show the method declaration line - formattedOutput += `${startLine + 1}--${endLine + 1} | ${lines[methodDeclarationLine]}\n` - processedLines.add(lineKey) - } else { - // Normal method without annotations - formattedOutput += `${startLine + 1}--${endLine + 1} | ${lines[startLine]}\n` - processedLines.add(lineKey) } + // Output the method with its proper line range, showing the method declaration line + formattedOutput += `${startLine + 1}--${endLine + 1} | ${lines[methodDeclarationLine]}\n` + processedLines.add(lineKey) } else if (language === "java" && name === "definition.class") { // For Java classes, skip the entire class definition to avoid duplication // The class name will be handled by name.definition.class return + } else if (language === "java" && name.includes("definition.annotation")) { + // Skip standalone annotation definitions - they're not useful for code structure overview + // Annotations will be shown as part of the methods/fields they annotate + return } else { - formattedOutput += `${startLine + 1}--${endLine + 1} | ${lines[startLine]}\n` + // For Java, check if this line is just an annotation + if (language === "java" && lines[startLine].trim().startsWith("@")) { + // Find the next non-annotation line + let actualDefinitionLine = startLine + for (let i = startLine + 1; i <= endLine; i++) { + const line = lines[i].trim() + if (line && !line.startsWith("@")) { + actualDefinitionLine = i + break + } + } + formattedOutput += `${startLine + 1}--${endLine + 1} | ${lines[actualDefinitionLine]}\n` + } else { + formattedOutput += `${startLine + 1}--${endLine + 1} | ${lines[startLine]}\n` + } processedLines.add(lineKey) }