Skip to content

JaCoCo instrumentation of CCJSqlParser is approaching the constant-pool limit #2866

Open

Description

@minleejae

Problem

On current master (c84ad0ebc), JaCoCo instrumentation of the generated CCJSqlParser.class leaves only 808 entries before the class-file limit of 65,535 constant-pool entries.

When the limit is exceeded, JaCoCo fails with ClassTooLargeException and loads the parser without instrumentation. Tests still pass, but JaCoCo counts the parser as uncovered. Then jacocoTestCoverageVerification fails, and with it ./gradlew check and the gradle_check CI job:

Caused by: org.jacoco.agent.rt.internal_29a6edd.asm.ClassTooLargeException: Class too large: net/sf/jsqlparser/parser/CCJSqlParser
Rule violated for bundle JSQLParser: instructions covered ratio is 0.21, but expected minimum is 0.50

This happened on 2026-10-23 while combining grammar changes from #2847–#2849. Moving helpers into nested classes avoided the limit, but the remaining capacity is still small. Current master passes.

Root cause

The generated parser contains 5,591 methods, including 3,515 jj_3* lookahead routines. JaCoCo adds probes to measure coverage, and probe IDs of 32,768 or more require an additional constant-pool entry each.

Measured on c84ad0ebc:

Metric Value
Constant pool before instrumentation 46,304
JaCoCo probes 51,180
Constant pool after instrumentation 64,727
Remaining entries 808

Grammar additions can increase both the generated class size and the probe count.

Proposed short-term fix

Exclude only CCJSqlParser.class from:

  1. JaCoCo agent instrumentation.
  2. jacocoTestReport.
  3. jacocoTestCoverageVerification.
 test {
     jacoco {
-        excludes = ['net/sf/jsqlparser/parser/CCJSqlParserTokenManager']
+        excludes = ['net/sf/jsqlparser/parser/CCJSqlParserTokenManager',
+                    'net/sf/jsqlparser/parser/CCJSqlParser']
     }
 }

 jacocoTestReport {
     ...
+    afterEvaluate {
+        classDirectories.setFrom(files(classDirectories.files.collect {
+            fileTree(dir: it, exclude: [
+                    "**/parser/CCJSqlParserTokenManager.class",
+                    "**/parser/CCJSqlParser.class"
+            ])
+        }))
+    }
 }

 jacocoTestCoverageVerification {
     // Jacoco can't handle the TokenManager class
     afterEvaluate {
         classDirectories.setFrom(files(classDirectories.files.collect {
             fileTree(dir: it, exclude: [
-                    "**CCJSqlParserTokenManager**"
+                    "**/parser/CCJSqlParserTokenManager.class",
+                    "**/parser/CCJSqlParser.class"
             ])
         }))
     }

The exact class exclusion keeps nested parser helpers, such as CCJSqlParser$ObjectNames, instrumented.

I also found that the existing CCJSqlParserTokenManager exclusion does not work in coverage verification: its **CCJSqlParserTokenManager** pattern does not match the class-file path. The report also currently includes the uninstrumented TokenManager. The proposed change fixes both exclusions.

Verified on current master with all 12,262 tests:

Coverage metric Current Proposed
Instruction coverage 75.37% 82.74%
Missed lines 14,892 7,336
Tests Pass Pass

These numbers change because two classes leave the calculation, not because coverage improves:

  • The uninstrumented TokenManager (0%, 815 lines) is no longer counted.
  • The parser (82.19%, 6,741 missed lines) is no longer counted.

Both configurations pass the existing coverage thresholds.

Trade-off: We lose direct coverage measurements for the generated parser, including its grammar branches. However, all parser tests still run, and other classes, including nested helpers, remain covered. If the parser is split later (see below), the exclusion can be reverted.

Longer-term solution: split the generated parser

Excluding the parser from JaCoCo addresses the immediate issue. The parser itself, however, already uses 46,304 constant-pool entries without instrumentation, and javac fails with "too many constants" beyond 65,535. For comparison, it used 12,266 in 5.3 and 34,231 in 5.4.

Two contributors to its size are:

  • Lookahead routines: 3,515 jj_3* methods generated into one class.
  • Grammar locations: since 5.4, jj_consume_token and jj_scan_token calls pass grammar locations such as "18424:7", which are used in parse error messages. There are 7,353 distinct location strings, taking about 14,700 constant-pool entries (32% of the pool).

I tested a small proof of concept that moves the jj_3* routines into a non-static inner class while preserving their method bodies.

Metric Master PoC
Parser constant pool (uninstrumented) 46,304 37,834
Remaining entries after JaCoCo 808 23,451
Tests 12,262 passed 12,262 passed

The new inner class uses 17,060 entries and has 48,454 left after instrumentation. The parser remains instrumented, and preliminary performance measurements showed no clear difference (not a JMH benchmark).

This would require a change in the JavaCC 8 generator (manticore-projects/javacc-8-java) rather than JSQLParser's build configuration. It would substantially increase headroom, but it does not stop growth: applied to the 2026-10-14 parser, the parser class still used 5,637 entries over the following two weeks. Continued grammar growth and the location strings would still need attention.

If this direction is interesting, I can open a separate issue in manticore-projects/javacc-8-java with the PoC and measurements.

Additional notes

  • Maven: mvn verify does not enforce JaCoCo coverage thresholds, but its agent can encounter the same instrumentation failure. Consistent exclusions would require updating both prepare-agent and report.
  • Verification rules: The existing rule-level excludes have no effect, because the rules use the default BUNDLE element and the lists are matched against the bundle name. The proposed classDirectories exclusions remove only the two parser classes; the rule-level lists are left unchanged.
  • Measurement: The remaining capacity was measured using JaCoCo's Instrumenter. JaCoCo 0.8.14 and 0.8.15 produced the same result.
Measurement code
import java.nio.file.Files;
import java.nio.file.Path;
import org.jacoco.core.instr.Instrumenter;
import org.jacoco.core.runtime.OfflineInstrumentationAccessGenerator;

public class JacocoHeadroom {
    public static void main(String[] args) throws Exception {
        byte[] original = Files.readAllBytes(Path.of(args[0]));
        byte[] instrumented = new Instrumenter(new OfflineInstrumentationAccessGenerator())
                .instrument(original, "CCJSqlParser");
        int count = ((instrumented[8] & 0xff) << 8) | (instrumented[9] & 0xff);
        System.out.println("constant_pool_count=" + count + " left=" + (65535 - count));
    }
}

The agent's runtime access code differs slightly from the offline one, so treat the number as approximate.

I'm happy to prepare a PR for the short-term fix if this approach is acceptable.

AI assistance: prepared with Claude Code.

Activity

  1. manticore-projects commented on Oct 28, 2026

    @manticore-projects
    Contributor

    Thank you much, this makes a lot of sense. Please provide a PR for merging.

  2. manticore-projects commented on Oct 28, 2026

    @manticore-projects
    Contributor

    This would require a change in the JavaCC 8 generator (manticore-projects/javacc-8-java) rather than JSQLParser's build configuration.

    Yes please, we did that in the past already and JavaCC.org is not responding anymore.

  3. minleejae commented on Oct 28, 2026

    @minleejae
    ContributorAuthor

    @manticore-projects Thanks! I'll open a PR for the short-term exclusion.

    For the generator change, issues are disabled on manticore-projects/javacc-8-java, so I'll send it there directly as a PR with the PoC and measurements.

    A side question, mostly out of curiosity: since JavaCC.org is no longer responding and JSQLParser's build depends on the javacc-8-java fork, have you considered hosting the fork under the JSQLParser organization? It might make it easier for JSQLParser contributors to find and contribute to. I understand it may also serve your other projects, so feel free to ignore this if it doesn't fit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions