Skip to content

ADR-20: Java 21+ Implicit Class Detection ​

🇰🇷 한국어 버전

DateAuthorRepos
2026-01-04@specvitalcore

Context ​

Problem Statement ​

SpecVital Core's JUnit5 parser only traversed class_declaration nodes to find @Test annotated methods. Java 21 introduced implicit classes (JEP 445, finalized as JEP 463 in Java 22) where methods can exist at file level without explicit class wrappers.

Background ​

Java 21+ Implicitly Declared Classes (JEP 445/JEP 463) allow:

  • Source files without explicit class declarations
  • Methods declared directly at file level
  • Compiler auto-wraps as unnamed top-level class at compile time

This feature targets beginner-friendly Java programs but also enables cleaner test file organization:

Traditional Java (supported):

java
public class HelloTests {
    @Test
    void testHello() {
        // ...
    }
}

Java 21+ Implicit Class (previously unsupported):

java
// HelloTests.java - no class declaration
import org.junit.jupiter.api.Test;

@Test
void testHello() {
    // ...
}
PatternAST StructureParser Status
Traditional classprogram → class_declaration → method_declarationSupported
Implicit classprogram → method_declarationPreviously unsupported

Technical Analysis ​

The tree-sitter Java grammar correctly parses implicit classes, producing method_declaration nodes directly under program node. The limitation was purely in parser traversal logic, not grammar support.

Traditional:
program
└── class_declaration ("HelloTests")
    └── class_body
        └── method_declaration ("testHello")
            └── modifiers
                └── marker_annotation ("@Test")

Implicit Class:
program
└── method_declaration ("testHello")
    └── modifiers
        └── marker_annotation ("@Test")

Requirements ​

  1. Detect @Test methods under program node (file-level methods)
  2. Create synthetic TestSuite with filename-derived name
  3. Handle mixed scenarios (explicit class + file-level methods)
  4. Maintain backward compatibility with traditional class patterns
  5. No performance regression for traditional files

Decision ​

Extend JUnit5 parser to traverse method_declaration nodes under program node.

The parseTestClasses() function is extended to:

  1. Check for method_declaration children directly under program root node
  2. Create synthetic TestSuite using filename (e.g., HelloTests.java → HelloTests)
  3. Process annotations and extract test methods using existing infrastructure

Implementation ​

go
// pkg/parser/strategies/junit5/definition.go

func parseTestClasses(root *sitter.Node, source []byte, filename string) []domain.TestSuite {
    var suites []domain.TestSuite
    var implicitClassTests []domain.Test

    parser.WalkTree(root, func(node *sitter.Node) bool {
        switch node.Type() {
        case javaast.NodeClassDeclaration:
            // Traditional: class_declaration nodes
            if suite := parseTestClassWithDepth(node, source, filename, 0); suite != nil {
                suites = append(suites, *suite)
            }
            return false

        case javaast.NodeMethodDeclaration:
            // NEW: Handle Java 21+ implicit classes
            if node.Parent() != nil && node.Parent().Type() == "program" {
                if test := parseTestMethod(node, source, filename, domain.TestStatusActive, ""); test != nil {
                    implicitClassTests = append(implicitClassTests, *test)
                }
            }
        }
        return true
    })

    // Create synthetic suite for implicit class tests
    if len(implicitClassTests) > 0 {
        suites = append(suites, domain.TestSuite{
            Name:     getImplicitClassName(filename),
            Status:   domain.TestStatusActive,
            Location: parser.GetLocation(root, filename),
            Tests:    implicitClassTests,
        })
    }

    return suites
}

Suite Naming Strategy ​

FilenameSynthetic Suite Name
HelloTests.javaHelloTests
UserServiceTest.javaUserServiceTest
src/test/java/IntegrationTests.javaIntegrationTests

This matches the compiler's implicit class naming behavior.

Options Considered ​

Option A: Extend Existing Parser (Selected) ​

Add program-level method traversal to existing parseTestClasses() function.

Pros:

  • Minimal code change (~50 lines in single file)
  • Reuses existing annotation parsing and method extraction
  • No performance impact for traditional files
  • Consistent behavior across all JUnit5 patterns
  • Follows Shared Parser Modules pattern (ADR-08)

Cons:

  • Synthetic suite naming may differ from user expectations
  • Mixed file handling (explicit + implicit) adds edge case complexity

Option B: Separate Implicit Class Parser ​

Create independent implicit_class_parser.go module.

Pros:

  • Clean separation of concerns
  • Independent evolution of implicit class handling
  • No regression risk to traditional parsing

Cons:

  • ~80% code duplication with existing parser
  • Violates DRY principle
  • Bug fixes must be applied to both code paths
  • Increases maintenance burden
  • Against established patterns (ADR-08)

Option C: No Support (Require Explicit Classes) ​

Document limitation and require users to wrap tests in explicit classes.

Pros:

  • Zero implementation effort
  • No code changes

Cons:

  • Ignores valid Java 21+ language feature
  • User friction for modern codebases
  • Competitive disadvantage vs tools supporting Java 21+
  • Parser appears outdated

Option D: Two-Pass File Detection ​

First pass detects file type, second pass applies specialized parsing.

Pros:

  • Explicit file type determination
  • Could enable type-specific optimizations

Cons:

  • 2x parsing overhead
  • Unnecessary complexity
  • Over-engineering for straightforward problem

Consequences ​

Positive ​

  1. Java 21+ Compatibility

    • Full support for implicit class patterns
    • Parser stays current with language evolution
    • No user friction for modern Java codebases
  2. Minimal Implementation Risk

    • Single function modification
    • Existing test infrastructure reused
    • Traditional file handling unchanged
  3. Architectural Consistency

    • Follows Shared Parser Modules pattern (ADR-08)
    • Single code path for JUnit5 test extraction
    • Bug fixes automatically apply to both patterns
  4. Intuitive Behavior

    • Filename-based suite naming matches compiler behavior
    • Users can predict output from file name
    • Consistent with mental model of "file = test suite"

Negative ​

  1. Synthetic Suite Naming

    • Suite name derived from filename, not explicit declaration
    • Mitigation: Matches Java compiler's implicit class naming; intuitive for users
  2. Mixed File Edge Case

    • Files with both explicit class and file-level methods require careful handling
    • Mitigation: Explicit classes processed first; file-level methods grouped separately; rare pattern in practice
  3. Java Version Detection Absence

    • Parser does not verify Java version compatibility
    • Mitigation: Tree-sitter parses syntax regardless of version; runtime validation is user's responsibility

References ​

Open-source test coverage insights