diff --git a/src/debug-full-java-output.txt b/src/debug-full-java-output.txt new file mode 100644 index 0000000000..a3c6c1f776 --- /dev/null +++ b/src/debug-full-java-output.txt @@ -0,0 +1,49 @@ +# file.java +2--193 | module test.module.definition { +3--7 | module test.module.definition { +8--8 | package test.package.definition; +10--10 | import java.util.List; +11--11 | import java.util.Map; +12--12 | import java.util.function.Function; +13--13 | import java.time.LocalDateTime; +30--44 | public interface TestInterfaceDefinition> { +30--30 | public interface TestInterfaceDefinition> { +47--63 | public enum TestEnumDefinition { +53--53 | private final int level; +53--53 | private final int level; +54--54 | private final String description; +54--54 | private final String description; +56--62 | TestEnumDefinition( +66--119 | value = "test", +71--71 | public class TestClassDefinition> +75--79 | value = "field", +79--79 | private final String prefix; +80--80 | private static int instanceCount = 0; +80--80 | private static int instanceCount = 0; +83--90 | public TestClassDefinition( +93--99 | public void testInterfaceMethod( +102--108 | public > R testGenericMethodDefinition( +102--102 | public > R testGenericMethodDefinition( +111--118 | private final Function testLambdaDefinition = ( +111--118 | private final Function testLambdaDefinition = ( +111--118 | private final Function testLambdaDefinition = ( +122--143 | public record TestRecordDefinition( +135--142 | public String formatMessage() { +146--160 | public abstract class TestAbstractClassDefinition { +146--146 | public abstract class TestAbstractClassDefinition { +147--147 | protected final T data; +147--147 | protected final T data; +149--153 | protected TestAbstractClassDefinition( +156--159 | public abstract String testAbstractMethod( +163--192 | public class TestOuterClassDefinition { +164--164 | private int value; +164--164 | private int value; +166--180 | public class TestInnerClassDefinition { +167--167 | private String innerField; +167--167 | private String innerField; +169--173 | public TestInnerClassDefinition( +175--179 | public void testInnerMethod() { +183--191 | public static class TestStaticNestedClassDefinition { +184--184 | private final String nestedField; +184--184 | private final String nestedField; +186--190 | public TestStaticNestedClassDefinition( diff --git a/src/debug-interface-output.txt b/src/debug-interface-output.txt new file mode 100644 index 0000000000..f5768a1018 --- /dev/null +++ b/src/debug-interface-output.txt @@ -0,0 +1,29 @@ + +=== FULL PARSE RESULT === +# TestFile.java +2--6 | interface TestInterface { +9--29 | class TestClass implements TestInterface { +11--14 | public void testMethod() { +16--19 | public String getName() { +21--24 | public int calculate(int a, int b) { +26--28 | private void helperMethod() { + +======================== + +=== INDIVIDUAL LINES === +Line 0: # TestFile.java +Line 1: 2--6 | interface TestInterface { +Line 2: 9--29 | class TestClass implements TestInterface { +Line 3: 11--14 | public void testMethod() { +Line 4: 16--19 | public String getName() { +Line 5: 21--24 | public int calculate(int a, int b) { +Line 6: 26--28 | private void helperMethod() { +======================== + +=== INTERFACE LINES === +2--6 | interface TestInterface { +======================== + +=== testMethod LINES === +11--14 | public void testMethod() { +======================== diff --git a/src/debug-output.txt b/src/debug-output.txt new file mode 100644 index 0000000000..08c12dccab --- /dev/null +++ b/src/debug-output.txt @@ -0,0 +1,24 @@ + +=== FULL PARSE RESULT === +# TestClass.java +1--16 | class TestClass implements TestInterface { +3--6 | public void testMethod() { +8--11 | public String getName() { +13--15 | private void helperMethod() { + +======================== + +=== INDIVIDUAL LINES === +Line 0: # TestClass.java +Line 1: 1--16 | class TestClass implements TestInterface { +Line 2: 3--6 | public void testMethod() { +Line 3: 8--11 | public String getName() { +Line 4: 13--15 | private void helperMethod() { +======================== + +=== testMethod LINES === +3--6 | public void testMethod() { +======================== + +=== @Override ONLY LINES === +============================ diff --git a/src/services/tree-sitter/__tests__/debug-full-java.test.ts b/src/services/tree-sitter/__tests__/debug-full-java.test.ts new file mode 100644 index 0000000000..39bd98a49b --- /dev/null +++ b/src/services/tree-sitter/__tests__/debug-full-java.test.ts @@ -0,0 +1,49 @@ +import { describe, it, expect } from "vitest" +import { testParseSourceCodeDefinitions } from "./helpers" +import { javaQuery } from "../queries" +import sampleJavaContent from "./fixtures/sample-java" +import * as fs from "fs" + +describe("Debug full Java parsing", () => { + it("should show what's being captured for full sample", async () => { + const testOptions = { + language: "java", + wasmFile: "tree-sitter-java.wasm", + queryString: javaQuery, + extKey: "java", + } + + const parseResult = await testParseSourceCodeDefinitions("/test/file.java", sampleJavaContent, testOptions) + + if (parseResult) { + // Write to file + fs.writeFileSync("debug-full-java-output.txt", parseResult) + console.log("Debug output written to debug-full-java-output.txt") + + // Check for specific patterns + const lines = parseResult.split("\n").filter((line) => line.trim()) + + // Check for class with annotations + const classLines = lines.filter((line) => line.includes("TestClassDefinition")) + console.log("\n=== CLASS LINES ===") + classLines.forEach((line) => console.log(line)) + + // Check for annotation declarations + const annotationLines = lines.filter((line) => line.includes("@Target") || line.includes("@TestAnnotation")) + console.log("\n=== ANNOTATION LINES ===") + annotationLines.forEach((line) => console.log(line)) + + // Check for interface methods + const interfaceMethodLines = lines.filter( + (line) => + line.includes("void testInterfaceMethod") || + line.includes("default String testInterfaceDefaultMethod"), + ) + console.log("\n=== INTERFACE METHOD LINES ===") + interfaceMethodLines.forEach((line) => console.log(line)) + } + + // This test is just for debugging, always pass + expect(true).toBe(true) + }) +}) diff --git a/src/services/tree-sitter/__tests__/debug-interface.test.ts b/src/services/tree-sitter/__tests__/debug-interface.test.ts new file mode 100644 index 0000000000..aeb2b3607a --- /dev/null +++ b/src/services/tree-sitter/__tests__/debug-interface.test.ts @@ -0,0 +1,81 @@ +import { describe, it, expect } from "vitest" +import { testParseSourceCodeDefinitions } from "./helpers" +import { javaQuery } from "../queries" +import * as fs from "fs" + +describe("Debug Java interface parsing", () => { + it("should show what's being captured for interface", async () => { + const javaContent = `// Test interface with methods +interface TestInterface { + void testMethod(); + String getName(); + int calculate(int a, int b); +} + +// Test class implementing interface with annotations +class TestClass implements TestInterface { + + @Override + public void testMethod() { + // Implementation goes here + } + + @Override + public String getName() { + return "TestClass"; + } + + @Override + public int calculate(int a, int b) { + return a + b; + } + + private void helperMethod() { + // Helper implementation + } +}` + + const testOptions = { + language: "java", + wasmFile: "tree-sitter-java.wasm", + queryString: javaQuery, + extKey: "java", + } + + const parseResult = await testParseSourceCodeDefinitions("/test/TestFile.java", javaContent, testOptions) + + let debugOutput = "" + + if (parseResult) { + debugOutput += "\n=== FULL PARSE RESULT ===\n" + debugOutput += parseResult + "\n" + debugOutput += "========================\n" + + const lines = parseResult.split("\n").filter((line) => line.trim()) + debugOutput += "\n=== INDIVIDUAL LINES ===\n" + lines.forEach((line, i) => { + debugOutput += `Line ${i}: ${line}\n` + }) + debugOutput += "========================\n" + + // Check for interface + const interfaceLines = lines.filter((line) => line.includes("interface TestInterface")) + debugOutput += "\n=== INTERFACE LINES ===\n" + interfaceLines.forEach((line) => (debugOutput += line + "\n")) + debugOutput += "========================\n" + + // Check for testMethod + const methodLines = lines.filter((line) => line.includes("testMethod")) + debugOutput += "\n=== testMethod LINES ===\n" + methodLines.forEach((line) => (debugOutput += line + "\n")) + debugOutput += "========================\n" + + // Write to file + fs.writeFileSync("debug-interface-output.txt", debugOutput) + console.log("Debug output written to debug-interface-output.txt") + } + + // This test is just for debugging, always pass + expect(true).toBe(true) + }) +}) diff --git a/src/services/tree-sitter/__tests__/debug-java.test.ts b/src/services/tree-sitter/__tests__/debug-java.test.ts new file mode 100644 index 0000000000..b0dcb6bbad --- /dev/null +++ b/src/services/tree-sitter/__tests__/debug-java.test.ts @@ -0,0 +1,71 @@ +import { describe, it, expect } from "vitest" +import { testParseSourceCodeDefinitions } from "./helpers" +import { javaQuery } from "../queries" +import * as fs from "fs" + +describe("Debug Java parsing", () => { + it("should show what's being captured", async () => { + const javaContent = `class TestClass implements TestInterface { + + @Override + public void testMethod() { + // Implementation goes here + } + + @Override + public String getName() { + return "TestClass"; + } + + 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", javaContent, testOptions) + + let debugOutput = "" + + if (parseResult) { + debugOutput += "\n=== FULL PARSE RESULT ===\n" + debugOutput += parseResult + "\n" + debugOutput += "========================\n" + + const lines = parseResult.split("\n").filter((line) => line.trim()) + debugOutput += "\n=== INDIVIDUAL LINES ===\n" + lines.forEach((line, i) => { + debugOutput += `Line ${i}: ${line}\n` + }) + debugOutput += "========================\n" + + // Check for duplicates + const methodLines = lines.filter((line) => line.includes("testMethod")) + debugOutput += "\n=== testMethod LINES ===\n" + methodLines.forEach((line) => (debugOutput += line + "\n")) + debugOutput += "========================\n" + + // Check for @Override lines + const overrideLines = lines.filter((line) => { + const content = line.split("|")[1]?.trim() || "" + return content === "@Override" + }) + debugOutput += "\n=== @Override ONLY LINES ===\n" + overrideLines.forEach((line) => (debugOutput += line + "\n")) + debugOutput += "============================\n" + + // Write to file + fs.writeFileSync("debug-output.txt", debugOutput) + console.log("Debug output written to debug-output.txt") + } + + // This test is just for debugging, always pass + expect(true).toBe(true) + }) +}) diff --git a/src/services/tree-sitter/__tests__/fixtures/sample-java-simple.ts b/src/services/tree-sitter/__tests__/fixtures/sample-java-simple.ts new file mode 100644 index 0000000000..ca8b5b0e7b --- /dev/null +++ b/src/services/tree-sitter/__tests__/fixtures/sample-java-simple.ts @@ -0,0 +1,31 @@ +export default String.raw` +// Test interface with methods +interface TestInterface { + void testMethod(); + String getName(); + int calculate(int a, int b); +} + +// Test class implementing interface with annotations +class TestClass implements TestInterface { + + @Override + public void testMethod() { + // Implementation goes here + } + + @Override + public String getName() { + return "TestClass"; + } + + @Override + public int calculate(int a, int b) { + return a + b; + } + + private void helperMethod() { + // Helper implementation + } +} +` diff --git a/src/services/tree-sitter/__tests__/parseSourceCodeDefinitions.java-simple.spec.ts b/src/services/tree-sitter/__tests__/parseSourceCodeDefinitions.java-simple.spec.ts new file mode 100644 index 0000000000..c3678bd798 --- /dev/null +++ b/src/services/tree-sitter/__tests__/parseSourceCodeDefinitions.java-simple.spec.ts @@ -0,0 +1,123 @@ +import { describe, it, expect, beforeAll } from "vitest" +import { testParseSourceCodeDefinitions } from "./helpers" +import { javaQuery } from "../queries" +import sampleJavaSimpleContent from "./fixtures/sample-java-simple" + +describe("Java parsing - duplication issue", () => { + let parseResult: string = "" + + beforeAll(async () => { + const testOptions = { + language: "java", + wasmFile: "tree-sitter-java.wasm", + queryString: javaQuery, + extKey: "java", + } + + const result = await testParseSourceCodeDefinitions( + "/test/TestClass.java", + sampleJavaSimpleContent, + testOptions, + ) + if (!result) { + throw new Error("Failed to parse Java source code") + } + parseResult = result + console.log("\n=== PARSE RESULT ===") + console.log(parseResult) + console.log("====================\n") + + // Show individual lines for debugging + const lines = parseResult.split("\n").filter((line) => line.trim()) + console.log("\n=== INDIVIDUAL LINES ===") + lines.forEach((line, i) => { + console.log(`Line ${i}: ${line}`) + }) + console.log("========================\n") + }) + + it("should parse interface declaration without duplication", () => { + const lines = parseResult.split("\n").filter((line) => line.trim()) + + // Count occurrences of interface declaration + const interfaceLines = lines.filter((line) => line.includes("interface TestInterface")) + console.log("Interface lines found:", interfaceLines) + + // Should appear exactly once + expect(interfaceLines.length).toBe(1) + }) + + it("should parse class declaration without duplication", () => { + const lines = parseResult.split("\n").filter((line) => line.trim()) + + // Count occurrences of class declaration + const classLines = lines.filter((line) => line.includes("class TestClass")) + console.log("Class lines found:", classLines) + + // Should appear exactly once + expect(classLines.length).toBe(1) + }) + + it("should parse each method without duplication", () => { + const lines = parseResult.split("\n").filter((line) => line.trim()) + + // Check testMethod + const testMethodLines = lines.filter((line) => line.includes("testMethod")) + console.log("testMethod lines found:", testMethodLines) + expect(testMethodLines.length).toBe(1) + + // Check getName + const getNameLines = lines.filter((line) => line.includes("getName")) + console.log("getName lines found:", getNameLines) + expect(getNameLines.length).toBe(1) + + // Check calculate + const calculateLines = lines.filter((line) => line.includes("calculate")) + console.log("calculate lines found:", calculateLines) + expect(calculateLines.length).toBe(1) + + // Check helperMethod + const helperLines = lines.filter((line) => line.includes("helperMethod")) + console.log("helperMethod lines found:", helperLines) + expect(helperLines.length).toBe(1) + }) + + it("should show method signatures, not annotations", () => { + const lines = parseResult.split("\n").filter((line) => line.trim()) + + // Check that @Override doesn't appear as a standalone line + const overrideOnlyLines = lines.filter((line) => { + const trimmed = line.split("|")[1]?.trim() || "" + return trimmed === "@Override" + }) + console.log("Lines with only @Override:", overrideOnlyLines) + + // Should not have any lines with just @Override + expect(overrideOnlyLines.length).toBe(0) + }) + + it("should show correct line ranges for methods with annotations", () => { + const lines = parseResult.split("\n").filter((line) => line.trim()) + + // For methods with @Override, the line range should include the annotation + // but the displayed text should be the method signature + const methodWithOverride = lines.find((line) => line.includes("public void testMethod")) + console.log("Method with @Override:", methodWithOverride) + + if (methodWithOverride) { + // Extract line range + const match = methodWithOverride.match(/(\d+)--(\d+)/) + if (match) { + const startLine = parseInt(match[1]) + const endLine = parseInt(match[2]) + + // The range should span multiple lines (including @Override) + expect(endLine - startLine).toBeGreaterThanOrEqual(1) + } + + // The displayed text should be the method signature, not @Override + expect(methodWithOverride).toContain("public void testMethod") + expect(methodWithOverride).not.toContain("@Override") + } + }) +}) diff --git a/src/services/tree-sitter/index.ts b/src/services/tree-sitter/index.ts index 478e2909a1..9aa51ec496 100644 --- a/src/services/tree-sitter/index.ts +++ b/src/services/tree-sitter/index.ts @@ -288,10 +288,10 @@ function processCaptures(captures: QueryCapture[], lines: string[], language: st // Sort captures by their start position captures.sort((a, b) => a.node.startPosition.row - b.node.startPosition.row) - // Track already processed lines to avoid duplicates - const processedLines = new Set() + // Track already processed definitions to avoid duplicates + const processedDefinitions = new Map() - // First pass - categorize captures by type + // Process captures and group by definition type and location captures.forEach((capture) => { const { node, name } = capture @@ -300,6 +300,40 @@ function processCaptures(captures: QueryCapture[], lines: string[], language: st return } + // For Java, skip certain captures to avoid duplication + if (language === "java") { + // Skip class body definitions as they duplicate the class declaration + if (name === "definition.class" && node.parent?.type === "class_body") { + return + } + // Skip standalone annotation type declarations + if (name.includes("definition.annotation")) { + return + } + // Skip comment definitions + if (name === "definition.comment") { + return + } + // Skip individual interface method definitions to avoid duplication + // The interface declaration already shows the interface with its methods + // Check various parent levels as the structure might vary + const parent = node.parent + const grandParent = parent?.parent + const greatGrandParent = grandParent?.parent + + if (name === "definition.method" || name === "name.definition.method") { + // Check if this method is inside an interface + if ( + parent?.type === "interface_body" || + grandParent?.type === "interface_body" || + greatGrandParent?.type === "interface_body" || + (parent?.type === "method_declaration" && grandParent?.type === "interface_body") + ) { + return + } + } + } + // Get the parent node that contains the full definition const definitionNode = name.includes("name") ? node.parent : node if (!definitionNode) return @@ -314,91 +348,59 @@ function processCaptures(captures: QueryCapture[], lines: string[], language: st return } - // Create unique key for this definition based on line range - // This ensures we don't output the same line range multiple times - const lineKey = `${startLine}-${endLine}` + // Determine the line to display (for Java definitions with annotations, find the actual declaration line) + let displayLine = startLine + if (language === "java") { + // For methods, classes, interfaces, etc. with annotations, find the actual declaration line + if (name.includes("definition")) { + for (let i = startLine; i <= endLine; i++) { + const line = lines[i].trim() + // Skip empty lines, annotations, and comments + if ( + line && + !line.startsWith("@") && + !line.startsWith("//") && + !line.startsWith("/*") && + !line.startsWith("*") + ) { + displayLine = i + break + } + } + } + } - // Skip already processed lines - if (processedLines.has(lineKey)) { + // Create a unique key for this definition + const defKey = `${definitionNode.type}-${startLine}-${endLine}` + + // Check if we've already processed a definition at this location + const existing = processedDefinitions.get(defKey) + if (existing) { + // For Java, prefer method definitions over other types at the same location + if (language === "java" && name === "definition.method" && !existing.displayLine) { + // Update with the method definition which has the correct display line + processedDefinitions.set(defKey, { startLine, endLine, displayLine }) + } return } // Check if this is a valid component definition (not an HTML element) - const startLineContent = lines[startLine].trim() - - // Special handling for component name definitions - if (name.includes("name.definition")) { - // Extract component name - const componentName = node.text - - // Add component name to output regardless of HTML filtering - if (!processedLines.has(lineKey) && componentName) { - formattedOutput += `${startLine + 1}--${endLine + 1} | ${lines[startLine]}\n` - processedLines.add(lineKey) - } + const displayLineContent = lines[displayLine].trim() + if (!isNotHtmlElement(displayLineContent)) { + return } - // For other component definitions - else if (isNotHtmlElement(startLineContent)) { - // For Java, special handling for methods with annotations - if (language === "java" && name === "definition.method") { - // 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, 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 { - // 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) - } - // If this is part of a larger definition, include its non-HTML context - if (node.parent && node.parent.lastChild) { - const contextEnd = node.parent.lastChild.endPosition.row - const contextSpan = contextEnd - node.parent.startPosition.row + 1 - - // Only include context if it spans multiple lines - if (contextSpan >= getMinComponentLines()) { - // Add the full range first - const rangeKey = `${node.parent.startPosition.row}-${contextEnd}` - if (!processedLines.has(rangeKey)) { - formattedOutput += `${node.parent.startPosition.row + 1}--${contextEnd + 1} | ${lines[node.parent.startPosition.row]}\n` - processedLines.add(rangeKey) - } - } - } - } + // Store this definition + processedDefinitions.set(defKey, { startLine, endLine, displayLine }) }) + // Generate output from processed definitions + const sortedDefinitions = Array.from(processedDefinitions.values()).sort((a, b) => a.startLine - b.startLine) + + for (const def of sortedDefinitions) { + formattedOutput += `${def.startLine + 1}--${def.endLine + 1} | ${lines[def.displayLine]}\n` + } + if (formattedOutput.length > 0) { return formattedOutput } diff --git a/src/test-output.txt b/src/test-output.txt new file mode 100644 index 0000000000..eec4cf4e3a --- /dev/null +++ b/src/test-output.txt @@ -0,0 +1,51 @@ + + RUN v3.2.4 /data/repos/Roo-Code/src + +··xx· + +⎯⎯⎯⎯⎯⎯⎯ Failed Tests 2 ⎯⎯⎯⎯⎯⎯⎯ + + FAIL services/tree-sitter/__tests__/parseSourceCodeDefinitions.java-simple.spec.ts > Java parsing - duplication issue > should parse each method without duplication +AssertionError: expected 2 to be 1 // Object.is equality + +- Expected ++ Received + +- 1 ++ 2 + + ❯ services/tree-sitter/__tests__/parseSourceCodeDefinitions.java-simple.spec.ts:63:34 + 61| const testMethodLines = lines.filter(line => line.includes("testMeth… + 62| console.log("testMethod lines found:", testMethodLines) + 63| expect(testMethodLines.length).toBe(1) + | ^ + 64| + 65| // Check getName + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/2]⎯ + + FAIL services/tree-sitter/__tests__/parseSourceCodeDefinitions.java-simple.spec.ts > Java parsing - duplication issue > should show method signatures, not annotations +AssertionError: expected 3 to be +0 // Object.is equality + +- Expected ++ Received + +- 0 ++ 3 + + ❯ services/tree-sitter/__tests__/parseSourceCodeDefinitions.java-simple.spec.ts:92:36 + 90| + 91| // Should not have any lines with just @Override + 92| expect(overrideOnlyLines.length).toBe(0) + | ^ + 93| }) + 94| + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[2/2]⎯ + + + Test Files 1 failed (1) + Tests 2 failed | 3 passed (5) + Start at 17:39:51 + Duration 870ms (transform 285ms, setup 98ms, collect 333ms, tests 83ms, environment 0ms, prepare 92ms) +