From 09b497291f0f3750717d4b56d513a0736a9c2f08 Mon Sep 17 00:00:00 2001 From: Paul King Date: Thu, 27 Aug 2026 11:30:17 +1000 Subject: [PATCH 1/5] GROOVY-12306: apply error tolerance to type checking and visitor errors The configured error tolerance was only enforced for errors reported through SourceUnit#addError. Errors reported through ClassCodeVisitorSupport#addError went straight to ErrorCollector#addErrorAndContinue and so were unbounded, which meant `groovyc -t 1` had no effect on the most common error class in @CompileStatic code: type checking errors reported in full however low the tolerance was set. Route the base ClassCodeVisitorSupport#addError through the tolerance-aware ErrorCollector#addError. StaticTypeCheckingVisitor overrides addError for its own error de-duplication and needs the same treatment, but only for the source unit's own collector: the temporary collectors it pushes for speculative checks must continue to collect without bailing out, since their errors are routinely discarded once a candidate is ruled in or out. ClassCompletionVerifierTest counts every error the verifier reports, so it now asks for the unlimited tolerance it always relied on rather than the fail-fast tolerance of 1 that SourceUnit#create(String,String) selects. Assisted-by: Claude Opus 5 (1M context) via Claude Code --- .../groovy/ast/ClassCodeVisitorSupport.java | 11 +- .../stc/StaticTypeCheckingVisitor.java | 14 ++- .../classgen/ClassCompletionVerifierTest.java | 4 +- .../groovy/control/ErrorToleranceTest.groovy | 108 ++++++++++++++++++ 4 files changed, 134 insertions(+), 3 deletions(-) create mode 100644 src/test/groovy/org/codehaus/groovy/control/ErrorToleranceTest.groovy diff --git a/src/main/java/org/codehaus/groovy/ast/ClassCodeVisitorSupport.java b/src/main/java/org/codehaus/groovy/ast/ClassCodeVisitorSupport.java index 82c0ccdde4a..7b17bec9318 100644 --- a/src/main/java/org/codehaus/groovy/ast/ClassCodeVisitorSupport.java +++ b/src/main/java/org/codehaus/groovy/ast/ClassCodeVisitorSupport.java @@ -40,6 +40,7 @@ import org.codehaus.groovy.ast.stmt.WhileStatement; import org.codehaus.groovy.ast.stmt.YieldStatement; import org.codehaus.groovy.control.SourceUnit; +import org.codehaus.groovy.control.messages.Message; import org.codehaus.groovy.syntax.SyntaxException; import org.codehaus.groovy.transform.ErrorCollecting; @@ -495,6 +496,12 @@ protected void visitStatementAnnotations(Statement statement) { /** * Adds an error message associated with an AST node to the source unit. * Errors are accumulated and reported after visitation completes. + *

+ * The error counts towards the configured + * {@link org.codehaus.groovy.control.CompilerConfiguration#getTolerance() error tolerance}, + * so visitation may be cut short once that many errors have been collected (GROOVY-12306). + * Use {@link org.codehaus.groovy.control.ErrorCollector#addErrorAndContinue(Message)} + * directly to report an error which must never bail out. * * @param error the error message to report * @param node the AST node associated with the error location @@ -502,6 +509,8 @@ protected void visitStatementAnnotations(Statement statement) { */ @Override public void addError(final String error, final ASTNode node) { - getSourceUnit().addErrorAndContinue(new SyntaxException(error + '\n', node)); + SourceUnit source = getSourceUnit(); + source.getErrorCollector().addError( + Message.create(new SyntaxException(error + '\n', node), source)); } } diff --git a/src/main/java/org/codehaus/groovy/transform/stc/StaticTypeCheckingVisitor.java b/src/main/java/org/codehaus/groovy/transform/stc/StaticTypeCheckingVisitor.java index ac48070d093..6bbfd265d66 100644 --- a/src/main/java/org/codehaus/groovy/transform/stc/StaticTypeCheckingVisitor.java +++ b/src/main/java/org/codehaus/groovy/transform/stc/StaticTypeCheckingVisitor.java @@ -117,8 +117,10 @@ import org.codehaus.groovy.control.ErrorCollector; import org.codehaus.groovy.control.ResolveVisitor; import org.codehaus.groovy.control.SourceUnit; +import org.codehaus.groovy.control.messages.Message; import org.codehaus.groovy.control.messages.WarningMessage; import org.codehaus.groovy.runtime.DefaultGroovyMethods; +import org.codehaus.groovy.syntax.SyntaxException; import org.codehaus.groovy.syntax.Token; import org.codehaus.groovy.syntax.TokenUtil; import org.codehaus.groovy.transform.RecordTypeASTTransformation; @@ -7123,7 +7125,17 @@ private static SetterInfo removeSetterInfo(final Expression exp) { public void addError(final String msg, final ASTNode node) { Long err = ((long) node.getLineNumber()) << 16 + node.getColumnNumber(); if ((DEBUG_GENERATED_CODE && node.getLineNumber() < 0) || !typeCheckingContext.reportedErrors.contains(err)) { - typeCheckingContext.getErrorCollector().addErrorAndContinue(msg + '\n', node, getSourceUnit()); + SourceUnit source = getSourceUnit(); + ErrorCollector collector = typeCheckingContext.getErrorCollector(); + Message message = Message.create(new SyntaxException(msg + '\n', node), source); + // GROOVY-12306: only the source unit's own collector enforces the error tolerance. + // The temporary collectors pushed for speculative checks must never bail out, as + // their errors are routinely discarded once a candidate is ruled in or out. + if (collector == source.getErrorCollector()) { + collector.addError(message); + } else { + collector.addErrorAndContinue(message); + } typeCheckingContext.reportedErrors.add(err); } } diff --git a/src/test/groovy/org/codehaus/groovy/classgen/ClassCompletionVerifierTest.java b/src/test/groovy/org/codehaus/groovy/classgen/ClassCompletionVerifierTest.java index 989c726736e..d17a30a5249 100644 --- a/src/test/groovy/org/codehaus/groovy/classgen/ClassCompletionVerifierTest.java +++ b/src/test/groovy/org/codehaus/groovy/classgen/ClassCompletionVerifierTest.java @@ -48,7 +48,9 @@ final class ClassCompletionVerifierTest { - private final SourceUnit sourceUnit = SourceUnit.create("dummy.groovy", ""); + // GROOVY-12306: these tests collect and count every error the verifier reports, so they need + // unlimited tolerance; the two-argument SourceUnit.create factory asks for fail-fast (1) + private final SourceUnit sourceUnit = SourceUnit.create("dummy.groovy", "", 0); private final ClassCompletionVerifier verifier = new ClassCompletionVerifier(sourceUnit); @Test diff --git a/src/test/groovy/org/codehaus/groovy/control/ErrorToleranceTest.groovy b/src/test/groovy/org/codehaus/groovy/control/ErrorToleranceTest.groovy new file mode 100644 index 00000000000..ceb0cde9ac4 --- /dev/null +++ b/src/test/groovy/org/codehaus/groovy/control/ErrorToleranceTest.groovy @@ -0,0 +1,108 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.codehaus.groovy.control + +import org.junit.jupiter.api.Test + +import static org.junit.jupiter.api.Assertions.assertEquals + +/** + * Tests {@link CompilerConfiguration#setTolerance(int)}, the number of non-fatal + * errors collected before compilation is abandoned. + */ +final class ErrorToleranceTest { + + private static final int MANY = 14 + + /** + * Static type-checking errors, one per method. These are raised via + * {@code StaticTypeCheckingVisitor#addError}, which overrides the base + * {@code ClassCodeVisitorSupport} reporting used below. + */ + private static String typeCheckingErrors(int count) { + def methods = (0.. Date: Thu, 27 Aug 2026 11:30:25 +1000 Subject: [PATCH 2/5] GROOVY-12306: treat a tolerance of zero as unlimited There was no way to ask the compiler to report every error. `groovyc -t 0` was silently discarded, because CompilationOptions could not tell a supplied zero from the option being absent, leaving the default of 10 in place; and even when set, a tolerance of zero would have bailed out on the first error rather than collecting them all. Treat a tolerance of zero or less as unlimited in ErrorCollector, and hold the command-line option in a boxed Integer so that an explicit zero reaches the configuration. Name the default as CompilerConfiguration.DEFAULT_TOLERANCE rather than repeating the literal, and state it in the option help, which previously gave no hint that the option was bounded by default. The option itself has been undocumented since it was added in GROOVY-11194, so add it to the groovyc option table too. Assisted-by: Claude Opus 5 (1M context) via Claude Code --- .../groovy/control/CompilerConfiguration.java | 24 +++++++++++++++---- .../groovy/control/ErrorCollector.java | 6 ++++- .../groovy/tools/FileSystemCompiler.java | 7 +++--- src/spec/doc/tools-groovyc.adoc | 1 + .../groovy/tools/FileSystemCompilerTest.java | 18 ++++++++++++++ 5 files changed, 47 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/codehaus/groovy/control/CompilerConfiguration.java b/src/main/java/org/codehaus/groovy/control/CompilerConfiguration.java index 31f89f89303..ca8977e671b 100644 --- a/src/main/java/org/codehaus/groovy/control/CompilerConfiguration.java +++ b/src/main/java/org/codehaus/groovy/control/CompilerConfiguration.java @@ -188,6 +188,14 @@ public class CompilerConfiguration { */ public static final String DEFAULT_SOURCE_ENCODING = "UTF-8"; + /** + * The default number of non-fatal errors tolerated before compilation is aborted. + * + * @see #setTolerance(int) + * @since 6.0.0 + */ + public static final int DEFAULT_TOLERANCE = 10; + /** * A convenience for getting a default configuration. Do not modify it! * See {@link #CompilerConfiguration(Properties)} for an example on how to @@ -561,7 +569,7 @@ public void setLogClassgenStackTraceMaxDepth(int logClassgenStackTraceMaxDepth) public CompilerConfiguration() { classpath = new LinkedList<>(); - tolerance = 10; + tolerance = DEFAULT_TOLERANCE; minimumRecompilationInterval = 100; warningLevel = WarningMessage.LIKELY_ERRORS; parameters = getBooleanSafe("groovy.parameters"); @@ -935,8 +943,8 @@ public void configure(final Properties configuration) throws ConfigurationExcept text = configuration.getProperty("groovy.output.debug"); if (text != null) setDebug("true".equalsIgnoreCase(text)); - numeric = 10; - text = configuration.getProperty("groovy.errors.tolerance", "10"); + numeric = DEFAULT_TOLERANCE; + text = configuration.getProperty("groovy.errors.tolerance", Integer.toString(DEFAULT_TOLERANCE)); try { numeric = Integer.parseInt(text); } catch (NumberFormatException e) { @@ -1122,7 +1130,9 @@ public void setParameters(final boolean parameters) { } /** - * Returns the requested error tolerance. + * Returns the requested error tolerance. Zero or less means unlimited. + * + * @see #setTolerance(int) */ public int getTolerance() { return this.tolerance; @@ -1131,7 +1141,11 @@ public int getTolerance() { /** * Sets the error tolerance, which is the number of * non-fatal errors (per unit) that should be tolerated before - * compilation is aborted. + * compilation is aborted. Defaults to {@value #DEFAULT_TOLERANCE}. + *

+ * A value of zero or less means unlimited: every error is collected and + * reported, and compilation is never cut short by the error count alone + * (GROOVY-12306). */ public void setTolerance(final int tolerance) { this.tolerance = tolerance; diff --git a/src/main/java/org/codehaus/groovy/control/ErrorCollector.java b/src/main/java/org/codehaus/groovy/control/ErrorCollector.java index 8229da6cdf1..74e74162af1 100644 --- a/src/main/java/org/codehaus/groovy/control/ErrorCollector.java +++ b/src/main/java/org/codehaus/groovy/control/ErrorCollector.java @@ -122,11 +122,15 @@ public void addErrorAndContinue(final Message message) { * Adds a non-fatal error to the message set, which may cause a failure if the error threshold is exceeded. * The message is not required to have a source line and column specified, but it is best practice to try * and include that information. + *

+ * A {@link CompilerConfiguration#getTolerance() tolerance} of zero or less means unlimited: + * every error is collected and the threshold never triggers a failure (GROOVY-12306). */ public void addError(final Message message) throws CompilationFailedException { addErrorAndContinue(message); - if (errors != null && errors.size() >= configuration.getTolerance()) { + int tolerance = configuration.getTolerance(); + if (tolerance > 0 && errors != null && errors.size() >= tolerance) { failIfErrors(); } } diff --git a/src/main/java/org/codehaus/groovy/tools/FileSystemCompiler.java b/src/main/java/org/codehaus/groovy/tools/FileSystemCompiler.java index ba6da860a15..161e3459a7d 100644 --- a/src/main/java/org/codehaus/groovy/tools/FileSystemCompiler.java +++ b/src/main/java/org/codehaus/groovy/tools/FileSystemCompiler.java @@ -538,8 +538,9 @@ public static class CompilationOptions { @Option(names = {"-cf", "--configscript"}, paramLabel = "