diff --git a/src/services/tree-sitter/__tests__/fixtures/sample-java-comprehensive.ts b/src/services/tree-sitter/__tests__/fixtures/sample-java-comprehensive.ts new file mode 100644 index 0000000000..34b7c8a926 --- /dev/null +++ b/src/services/tree-sitter/__tests__/fixtures/sample-java-comprehensive.ts @@ -0,0 +1,351 @@ +export default String.raw` +// Package declaration +package com.example.comprehensive; + +// Import statements +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; +import java.io.IOException; +import java.lang.annotation.*; +import static java.lang.Math.PI; +import static java.util.Collections.*; + +// Single-line comment +/* Multi-line comment + spanning multiple lines */ + +/** + * JavaDoc comment for annotation + * @since 1.0 + */ +@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +public @interface CustomAnnotation { + String value() default ""; + int priority() default 0; + Class[] types() default {}; + String[] tags() default {}; +} + +/** + * Interface with various method types + */ +public interface GenericInterface, U> { + // Abstract method + void abstractMethod(T param); + + // Default method with implementation + default U defaultMethod(T input) { + return processInput(input); + } + + // Static method in interface + static V staticInterfaceMethod(V value) { + return value; + } + + // Private method in interface (Java 9+) + private U processInput(T input) { + return null; + } +} + +/** + * Abstract class with various members + */ +public abstract class AbstractBase implements GenericInterface { + // Protected field + protected T data; + + // Static field + private static final String CONSTANT = "CONST_VALUE"; + + // Constructor + protected AbstractBase(T data) { + this.data = data; + } + + // Abstract method + public abstract T process(T input); + + // Concrete method + public final String getName() { + return this.getClass().getSimpleName(); + } +} + +/** + * Enum with constructor and methods + */ +public enum Status { + PENDING(0, "Pending"), + ACTIVE(1, "Active") { + @Override + public String getDescription() { + return "Currently active: " + description; + } + }, + COMPLETED(2, "Completed"), + FAILED(-1, "Failed"); + + private final int code; + protected final String description; + + Status(int code, String description) { + this.code = code; + this.description = description; + } + + public String getDescription() { + return description; + } +} + +/** + * Main class with comprehensive Java features + */ +@CustomAnnotation(value = "MainClass", priority = 1, types = {String.class, Integer.class}) +@SuppressWarnings("unchecked") +public class ComprehensiveExample> + extends AbstractBase + implements Serializable, Cloneable { + + // Serial version UID + private static final long serialVersionUID = 1L; + + // Various field types + private volatile int counter; + private transient String tempData; + public static final double PI_VALUE = 3.14159; + private final List items = new ArrayList<>(); + + // Static initializer block + static { + System.out.println("Static initializer"); + } + + // Instance initializer block + { + counter = 0; + tempData = "temp"; + } + + // Constructor with annotations + @SuppressWarnings("deprecation") + public ComprehensiveExample(@NonNull T initialData) { + super(initialData); + } + + // Overloaded constructor + public ComprehensiveExample(T data, int counter) { + this(data); + this.counter = counter; + } + + // Method with generic return type and throws clause + @Override + public T process(T input) throws IllegalArgumentException { + if (input == null) { + throw new IllegalArgumentException("Input cannot be null"); + } + return input; + } + + // Synchronized method + public synchronized void incrementCounter() { + counter++; + } + + // Method with varargs + public void processMultiple(T... items) { + for (T item : items) { + this.items.add(item); + } + } + + // Generic method with bounds + public > U genericMethod(U value) { + return value; + } + + // Method with array parameter + public static void arrayMethod(String[] args, int[][] matrix) { + System.out.println(Arrays.toString(args)); + } + + // Inner class + public class InnerClass { + private String innerField; + + public InnerClass(String field) { + this.innerField = field; + } + + public void accessOuter() { + System.out.println(ComprehensiveExample.this.counter); + } + } + + // Static nested class + public static class StaticNestedClass { + private static int nestedCounter; + + public StaticNestedClass() { + nestedCounter++; + } + + public static int getCounter() { + return nestedCounter; + } + } + + // Local class inside method + public void methodWithLocalClass() { + class LocalClass { + private String localField; + + public LocalClass(String field) { + this.localField = field; + } + + public void printLocal() { + System.out.println(localField); + } + } + + LocalClass local = new LocalClass("local"); + local.printLocal(); + } + + // Anonymous class + public Runnable createRunnable() { + return new Runnable() { + @Override + public void run() { + System.out.println("Anonymous class"); + } + }; + } + + // Lambda expressions + public void lambdaExamples() { + // Simple lambda + Runnable r1 = () -> System.out.println("Lambda"); + + // Lambda with parameters + Function f1 = s -> s.length(); + + // Lambda with block + Function f2 = (Integer i) -> { + String result = "Number: " + i; + return result; + }; + + // Method reference + Function f3 = String::length; + } + + // Try-with-resources + public void tryWithResources() throws IOException { + try (var resource = new AutoCloseable() { + @Override + public void close() throws Exception { + System.out.println("Closing"); + } + }) { + // Use resource + } catch (Exception e) { + e.printStackTrace(); + } finally { + System.out.println("Finally"); + } + } + + // Switch expression (Java 14+) + public String switchExpression(Status status) { + return switch (status) { + case PENDING -> "Waiting"; + case ACTIVE -> "Running"; + case COMPLETED -> "Done"; + case FAILED -> { + System.out.println("Failed status"); + yield "Error"; + } + }; + } +} + +/** + * Record class (Java 14+) + */ +public record PersonRecord( + String name, + int age, + List hobbies +) { + // Compact constructor + public PersonRecord { + Objects.requireNonNull(name); + if (age < 0) { + throw new IllegalArgumentException("Age cannot be negative"); + } + } + + // Additional method + public String getInfo() { + return String.format("%s (%d years)", name, age); + } + + // Static factory method + public static PersonRecord of(String name, int age) { + return new PersonRecord(name, age, new ArrayList<>()); + } +} + +/** + * Sealed class (Java 17+) + */ +public sealed class Shape + permits Circle, Rectangle, Triangle { + + protected final double area; + + protected Shape(double area) { + this.area = area; + } + + public double getArea() { + return area; + } +} + +// Permitted subclasses +final class Circle extends Shape { + private final double radius; + + public Circle(double radius) { + super(Math.PI * radius * radius); + this.radius = radius; + } +} + +final class Rectangle extends Shape { + private final double width; + private final double height; + + public Rectangle(double width, double height) { + super(width * height); + this.width = width; + this.height = height; + } +} + +non-sealed class Triangle extends Shape { + public Triangle(double base, double height) { + super(0.5 * base * height); + } +} +` 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__/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/__tests__/parseSourceCodeDefinitions.java-comprehensive.spec.ts b/src/services/tree-sitter/__tests__/parseSourceCodeDefinitions.java-comprehensive.spec.ts new file mode 100644 index 0000000000..1537b7efce --- /dev/null +++ b/src/services/tree-sitter/__tests__/parseSourceCodeDefinitions.java-comprehensive.spec.ts @@ -0,0 +1,254 @@ +import { describe, it, expect, beforeAll } from "vitest" +import { testParseSourceCodeDefinitions } from "./helpers" +import { javaQuery } from "../queries" +import sampleJavaComprehensiveContent from "./fixtures/sample-java-comprehensive" + +describe("Java parsing - comprehensive grammar test", () => { + let parseResult: string = "" + let lines: string[] = [] + + beforeAll(async () => { + const testOptions = { + language: "java", + wasmFile: "tree-sitter-java.wasm", + queryString: javaQuery, + extKey: "java", + } + + const result = await testParseSourceCodeDefinitions( + "/test/ComprehensiveExample.java", + sampleJavaComprehensiveContent, + testOptions, + ) + if (!result) { + throw new Error("Failed to parse Java source code") + } + parseResult = result + lines = parseResult.split("\n").filter((line) => line.trim()) + + // Debug output + console.log("\n=== COMPREHENSIVE PARSE RESULT ===") + console.log(parseResult) + console.log("==================================\n") + }) + + describe("No duplications", () => { + it("should not have duplicate class declarations", () => { + const classDeclarations = lines.filter( + (line) => + line.includes("class ComprehensiveExample") || + line.includes("class AbstractBase") || + line.includes("class Shape"), + ) + + // Check each class appears only once + const comprehensiveLines = classDeclarations.filter((line) => line.includes("ComprehensiveExample")) + const abstractLines = classDeclarations.filter((line) => line.includes("AbstractBase")) + const shapeLines = classDeclarations.filter((line) => line.includes("Shape")) + + expect(comprehensiveLines.length).toBeLessThanOrEqual(1) + expect(abstractLines.length).toBeLessThanOrEqual(1) + expect(shapeLines.length).toBeLessThanOrEqual(1) + }) + + it("should not have duplicate interface declarations", () => { + const interfaceLines = lines.filter((line) => line.includes("interface GenericInterface")) + expect(interfaceLines.length).toBeLessThanOrEqual(1) + }) + + it("should not have duplicate method declarations", () => { + // Check specific methods don't appear multiple times + const processLines = lines.filter((line) => line.includes("process(")) + const incrementLines = lines.filter((line) => line.includes("incrementCounter")) + const genericMethodLines = lines.filter((line) => line.includes("genericMethod")) + + // Each method should appear at most once + expect(processLines.length).toBeLessThanOrEqual(1) + expect(incrementLines.length).toBeLessThanOrEqual(1) + expect(genericMethodLines.length).toBeLessThanOrEqual(1) + }) + + it("should not show @Override as standalone definition", () => { + const overrideOnlyLines = lines.filter((line) => { + const content = line.split("|")[1]?.trim() || "" + return content === "@Override" + }) + expect(overrideOnlyLines.length).toBe(0) + }) + }) + + describe("Package and imports", () => { + it("should parse package declaration", () => { + const packageLine = lines.find((line) => line.includes("package com.example.comprehensive")) + expect(packageLine).toBeDefined() + }) + }) + + describe("Annotations", () => { + it("should parse annotation declarations", () => { + const annotationLine = lines.find((line) => line.includes("@interface CustomAnnotation")) + expect(annotationLine).toBeDefined() + }) + + it("should show annotated class with class declaration, not annotation", () => { + const classLine = lines.find((line) => line.includes("class ComprehensiveExample")) + expect(classLine).toBeDefined() + expect(classLine).toContain("class ComprehensiveExample") + expect(classLine).not.toContain("@CustomAnnotation") + }) + }) + + describe("Interfaces", () => { + it("should parse interface declaration", () => { + const interfaceLine = lines.find((line) => line.includes("interface GenericInterface")) + expect(interfaceLine).toBeDefined() + }) + + it("should not duplicate interface methods", () => { + // Interface methods should be part of interface declaration, not separate + const abstractMethodLines = lines.filter((line) => line.includes("void abstractMethod")) + const defaultMethodLines = lines.filter((line) => line.includes("defaultMethod")) + + expect(abstractMethodLines.length).toBeLessThanOrEqual(1) + expect(defaultMethodLines.length).toBeLessThanOrEqual(1) + }) + }) + + describe("Classes", () => { + it("should parse abstract class", () => { + const abstractLine = lines.find((line) => line.includes("abstract class AbstractBase")) + expect(abstractLine).toBeDefined() + }) + + it("should parse main class", () => { + const mainClassLine = lines.find((line) => line.includes("class ComprehensiveExample")) + expect(mainClassLine).toBeDefined() + }) + + it("should parse sealed class", () => { + const sealedLine = lines.find((line) => line.includes("sealed class Shape")) + expect(sealedLine).toBeDefined() + }) + + it("should parse final classes", () => { + const circleLine = lines.find((line) => line.includes("class Circle")) + const rectangleLine = lines.find((line) => line.includes("class Rectangle")) + expect(circleLine).toBeDefined() + expect(rectangleLine).toBeDefined() + }) + }) + + describe("Enums", () => { + it("should parse enum declaration", () => { + const enumLine = lines.find((line) => line.includes("enum Status")) + expect(enumLine).toBeDefined() + }) + }) + + describe("Records", () => { + it("should parse record declaration", () => { + const recordLine = lines.find((line) => line.includes("record PersonRecord")) + expect(recordLine).toBeDefined() + }) + }) + + describe("Inner classes", () => { + it("should parse inner class", () => { + const innerLine = lines.find((line) => line.includes("class InnerClass")) + expect(innerLine).toBeDefined() + }) + + it("should parse static nested class", () => { + const nestedLine = lines.find((line) => line.includes("class StaticNestedClass")) + expect(nestedLine).toBeDefined() + }) + }) + + describe("Methods", () => { + it("should parse overridden methods with correct signature", () => { + const processMethod = lines.find((line) => line.includes("process(T input)")) + expect(processMethod).toBeDefined() + if (processMethod) { + expect(processMethod).toContain("process") + expect(processMethod).not.toContain("@Override") + } + }) + + it("should parse synchronized methods", () => { + const syncMethod = lines.find((line) => line.includes("incrementCounter")) + expect(syncMethod).toBeDefined() + }) + + it("should parse generic methods", () => { + const genericMethod = lines.find((line) => line.includes("genericMethod")) + expect(genericMethod).toBeDefined() + }) + + it("should parse varargs methods", () => { + const varargMethod = lines.find((line) => line.includes("processMultiple")) + expect(varargMethod).toBeDefined() + }) + + it("should parse static methods", () => { + const staticMethod = lines.find((line) => line.includes("arrayMethod")) + expect(staticMethod).toBeDefined() + }) + }) + + describe("Constructors", () => { + it("should parse constructors", () => { + const constructorLines = lines.filter( + (line) => + line.includes("ComprehensiveExample(") || + line.includes("AbstractBase(") || + line.includes("Circle(") || + line.includes("Rectangle("), + ) + expect(constructorLines.length).toBeGreaterThan(0) + }) + }) + + describe("Lambda expressions", () => { + it("should parse lambda expressions", () => { + const lambdaLines = lines.filter((line) => line.includes("->")) + // Should find at least some lambda expressions + expect(lambdaLines.length).toBeGreaterThan(0) + }) + }) + + describe("Line ranges", () => { + it("should have correct line ranges for multi-line definitions", () => { + lines.forEach((line) => { + const match = line.match(/(\d+)--(\d+)/) + if (match) { + const startLine = parseInt(match[1]) + const endLine = parseInt(match[2]) + + // Multi-line definitions should have different start and end + if (endLine - startLine >= 3) { + // This is a multi-line definition (4+ lines) + expect(endLine).toBeGreaterThan(startLine) + } + } + }) + }) + }) + + describe("Output format", () => { + it("should format output correctly", () => { + lines.forEach((line) => { + // Each line should have the format: "startLine--endLine | content" + expect(line).toMatch(/^\d+--\d+ \| .+/) + }) + }) + + it("should not include comment-only lines as definitions", () => { + const commentOnlyLines = lines.filter((line) => { + const content = line.split("|")[1]?.trim() || "" + return content.startsWith("//") || content.startsWith("/*") || content.startsWith("*") + }) + // Comments should not be standalone definitions + expect(commentOnlyLines.length).toBe(0) + }) + }) +}) 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/__tests__/simple-java-override.test.ts b/src/services/tree-sitter/__tests__/simple-java-override.test.ts new file mode 100644 index 0000000000..b37fa7f304 --- /dev/null +++ b/src/services/tree-sitter/__tests__/simple-java-override.test.ts @@ -0,0 +1,52 @@ +import { describe, it, expect } from "vitest" +import { testParseSourceCodeDefinitions } from "./helpers" +import { javaQuery } from "../queries" + +describe("Simple Java @Override test", () => { + it("should show what gets captured for @Override methods", async () => { + const overrideTestContent = `class TestClass { + @Override + public void testMethod() { + // Implementation goes here + } +}` + + const testOptions = { + language: "java", + wasmFile: "tree-sitter-java.wasm", + queryString: javaQuery, + extKey: "java", + } + + const parseResult = await testParseSourceCodeDefinitions("/test/file.java", overrideTestContent, testOptions) + + console.log("\n=== PARSE RESULT ===") + console.log(parseResult) + console.log("====================\n") + + if (parseResult) { + 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") + + // Check for the issue + const hasOverrideLine = lines.some((line) => line.includes("@Override") && !line.includes("testMethod")) + + if (hasOverrideLine) { + console.log("❌ BUG CONFIRMED: @Override is shown without the method name") + const problematicLines = lines.filter( + (line) => line.includes("@Override") && !line.includes("testMethod"), + ) + console.log("Problematic lines:", problematicLines) + } else { + console.log("✅ No issue found - @Override appears with method name") + } + + // This test will fail if the bug exists + expect(hasOverrideLine).toBe(false) + } + }) +}) diff --git a/src/services/tree-sitter/index.ts b/src/services/tree-sitter/index.ts index 145ba84730..bc13dae84a 100644 --- a/src/services/tree-sitter/index.ts +++ b/src/services/tree-sitter/index.ts @@ -288,10 +288,14 @@ 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() - - // First pass - categorize captures by type + // Track already processed definitions to avoid duplicates + // Use a more comprehensive key that includes the actual content to better detect duplicates + const processedDefinitions = new Map< + string, + { startLine: number; endLine: number; displayLine: number; priority: number } + >() + + // Process captures and group by definition type and location captures.forEach((capture) => { const { node, name } = capture @@ -300,8 +304,74 @@ function processCaptures(captures: QueryCapture[], lines: string[], language: st return } + // For Java, skip certain captures to avoid duplication + if (language === "java") { + // 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 + const parent = node.parent + const grandParent = parent?.parent + const greatGrandParent = grandParent?.parent + + // Check if this is a method inside an interface body + if (name.includes("method")) { + if ( + parent?.type === "interface_body" || + grandParent?.type === "interface_body" || + greatGrandParent?.type === "interface_body" + ) { + // Skip interface methods as they're part of the interface declaration + return + } + } + + // Handle overlapping class captures + // Skip general class definitions if we have more specific inner/nested class captures + if (name === "definition.class" || name === "name.definition.class") { + const nodeStartRow = node.startPosition.row + + // Check if this class is inside another class body (making it an inner/nested class) + let currentParent = parent + while (currentParent) { + if (currentParent.type === "class_body") { + // This is a nested class, check if we have a more specific capture + const hasSpecificCapture = captures.some( + (c) => + (c.name === "definition.inner_class" || + c.name === "name.definition.inner_class" || + c.name === "definition.static_nested_class" || + c.name === "name.definition.static_nested_class") && + Math.abs(c.node.startPosition.row - nodeStartRow) <= 1, + ) + if (hasSpecificCapture) { + return // Skip this general capture in favor of the specific one + } + break + } + currentParent = currentParent.parent + } + } + + // Skip duplicate inner/static nested class captures + // Keep only the most specific one + if (name === "definition.inner_class" || name === "definition.static_nested_class") { + const nodeStartRow = node.startPosition.row + // Check if we already have a class definition at this location + const hasGeneralClass = captures.some( + (c) => + (c.name === "definition.class" || c.name === "name.definition.class") && + Math.abs(c.node.startPosition.row - nodeStartRow) <= 1, + ) + // If we have both, we'll keep this specific one and the general one will be skipped above + } + } + // Get the parent node that contains the full definition - const definitionNode = name.includes("name") ? node.parent : node + const definitionNode = name.includes("name") && node.parent ? node.parent : node if (!definitionNode) return // Get the start and end lines of the full definition @@ -314,52 +384,72 @@ 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}` - - // Skip already processed lines - if (processedLines.has(lineKey)) { - return + // 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 + 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 + } + } } // 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 + const displayLineContent = lines[displayLine]?.trim() || "" + if (!isNotHtmlElement(displayLineContent)) { + return + } - // 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) - } + // Create a unique key for this definition based on location and content + // This helps prevent duplicates when the same definition is captured multiple times + const defKey = `${startLine}-${endLine}-${displayLineContent.substring(0, 50)}` + + // Assign priority based on capture type (more specific captures have higher priority) + let priority = 0 + if (name.includes("inner_class") || name.includes("static_nested_class")) { + priority = 3 + } else if (name.includes("method") || name.includes("constructor")) { + priority = 2 + } else if ( + name.includes("class") || + name.includes("interface") || + name.includes("enum") || + name.includes("record") + ) { + priority = 1 } - // For other component definitions - else if (isNotHtmlElement(startLineContent)) { - 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) - } - } + + // Check if we've already processed a definition at this location + const existing = processedDefinitions.get(defKey) + if (existing) { + // Keep the capture with higher priority (more specific) + if (priority > existing.priority) { + processedDefinitions.set(defKey, { startLine, endLine, displayLine, priority }) } + return } + + // Store this definition + processedDefinitions.set(defKey, { startLine, endLine, displayLine, priority }) }) + // 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/services/tree-sitter/queries/java.ts b/src/services/tree-sitter/queries/java.ts index 63cb663e88..cba88a7eb7 100644 --- a/src/services/tree-sitter/queries/java.ts +++ b/src/services/tree-sitter/queries/java.ts @@ -29,7 +29,8 @@ export default ` (record_declaration name: (identifier) @name.definition.record) @definition.record -; Annotation declarations +; Annotation type declarations (e.g., @interface MyAnnotation) +; Note: This captures annotation type declarations, not annotation usages like @Override (annotation_type_declaration name: (identifier) @name.definition.annotation) @definition.annotation @@ -41,17 +42,16 @@ export default ` (method_declaration name: (identifier) @name.definition.method) @definition.method -; Inner class declarations -(class_declaration - (class_body - (class_declaration - name: (identifier) @name.definition.inner_class))) @definition.inner_class +; Inner class declarations (inside class body) +(class_body + (class_declaration + name: (identifier) @name.definition.inner_class)) @definition.inner_class -; Static nested class declarations -(class_declaration - (class_body - (class_declaration - name: (identifier) @name.definition.static_nested_class))) @definition.static_nested_class +; Static nested class declarations (with static modifier) +(class_body + (class_declaration + (modifiers "static") + name: (identifier) @name.definition.static_nested_class)) @definition.static_nested_class ; Lambda expressions (lambda_expression) @definition.lambda