Scott Murphy Heiberg created GROOVY-12687:
---------------------------------------------

             Summary: groovyc: StackOverflowError when the error tolerance is 
reached inside a type-checking extension
                 Key: GROOVY-12687
                 URL: https://issues.apache.org/jira/browse/GROOVY-12687
             Project: Groovy
          Issue Type: Bug
    Affects Versions: 6.0.0
            Reporter: Scott Murphy Heiberg


When a type-checking extension reports the error that reaches the compiler's 
error tolerance (10 by default), {{groovyc}} (with or without {{--check}}) 
prints no diagnostics and ends with a {{StackOverflowError}}. Ten reports from 
NullChecker in one method are enough.

{{TenOnly.groovy}}:

{code:groovy}
import groovy.transform.TypeChecked

@interface Nullable {}

@TypeChecked(extensions = 'groovy.typecheckers.NullChecker')
class TenOnly {
    void touch(@Nullable String a) {
        a.length(); a.length(); a.length(); a.length(); a.length()
        a.length(); a.length(); a.length(); a.length(); a.length()
    }
}
{code}

{code:sh}
groovyc --check --error-format=short TenOnly.groovy
{code}

Actual: exit 1 and about 1,000 lines on stderr, no diagnostic:

{code:text}
java.lang.reflect.InvocationTargetException
        at 
org.codehaus.groovy.tools.GroovyStarter.rootLoader(GroovyStarter.java:150)
        at org.codehaus.groovy.tools.GroovyStarter.main(GroovyStarter.java:52)
Caused by: java.lang.StackOverflowError
        ...
        at 
org.codehaus.groovy.control.ErrorFormat.writeShort(ErrorFormat.java:116)
        at org.codehaus.groovy.control.ErrorFormat$2.write(ErrorFormat.java:71)
        at 
org.codehaus.groovy.control.ErrorCollector.write(ErrorCollector.java:349)
        at 
org.codehaus.groovy.control.MultipleCompilationErrorsException.getMessage(MultipleCompilationErrorsException.java:79)
        at 
org.codehaus.groovy.control.messages.ExceptionMessage.toDiagnostic(ExceptionMessage.java:68)
        at 
org.codehaus.groovy.control.ErrorFormat.writeShort(ErrorFormat.java:116)
        (the last five frames repeat; 203 times in the printed trace)
{code}

The same file without {{--error-format=short}} cycles through 
{{ErrorFormat.writeFull}}, {{ExceptionMessage.write}} and the same 
{{ErrorCollector.write}} / {{MultipleCompilationErrorsException.getMessage}} 
pair. Neither {{--check}} nor the annotation is required: the same class with 
eleven dereferences and no {{@TypeChecked}}, compiled with {{groovyc -d out 
--configscript nullchecker.groovy Many.groovy}} where {{nullchecker.groovy}} is 
{{withConfig(configuration) { ast(groovy.transform.TypeChecked, extensions: 
['groovy.typecheckers.NullChecker']) } }}, fails the same way.

Expected: the ten diagnostics and {{10 errors}}, as for ordinary type errors. 
Controls:

* The same class with nine dereferences prints nine {{Potential null 
dereference: 'a' is @Nullable}} lines and {{9 errors}}.
* Twelve {{int a = 'x'}} assignments under plain {{@TypeChecked}} (no 
extension) print the first ten {{Cannot assign value of type java.lang.String 
to variable of type int}} diagnostics and {{10 errors}}.
* 6.0.0-beta-1 on {{TenOnly.groovy}} prints the ten diagnostics and {{10 
errors}} (no crash), so the tolerance case is a regression between 6.0.0-beta-1 
and 6.0.0: GROOVY-12306 (fixed in 6.0.0-RC-1) made it reachable. The underlying 
defect is older: an extension that reports a fatal error overflows the same way 
on 6.0.0-beta-1 and 5.0.8. Current master is affected too.
* {{groovyc --check --error-format=short --tolerance=0 TenOnly.groovy}} 
(unlimited tolerance) prints all ten diagnostics and {{10 errors}}; this is a 
workaround.

Analysis. A script that compiles {{TenOnly.groovy}} through {{CompilationUnit}} 
and inspects the thrown {{MultipleCompilationErrorsException}} without 
rendering it shows 11 messages in the collector: ten {{SyntaxErrorMessage}} and 
one {{ExceptionMessage}} whose cause is a 
{{MultipleCompilationErrorsException}} carrying the same collector, raised from 
{{ErrorCollector.failIfErrors}} via 
{{StaticTypeCheckingVisitor.addStaticTypeError}} inside the extension's 
{{afterVisitMethod}}. So the tenth {{addStaticTypeError}} makes 
{{failIfErrors}} throw; {{AbstractTypeCheckingExtension.safeCall}} catches 
{{Exception}} and calls {{SourceUnit.addException}}, which records that 
exception as an {{ExceptionMessage}} in the same collector. Rendering the 
collector renders the exception's message, which renders the collector again, 
without end.

A fix that works: {{safeCall}} rethrows a 
{{MultipleCompilationErrorsException}} that carries the source unit's own error 
collector instead of recording it; other exceptions are recorded as before. 
With that change the file above reports its ten diagnostics and {{10 errors}}, 
{{--tolerance=3}} reports three, an extension reporting a fatal error reports 
one error, and {{--tolerance=0}} and plain {{@TypeChecked}} are unchanged. A 
pull request with a regression test ({{ErrorToleranceTest}}) follows.

Impact: any build or tool that compiles with an extension (the Groovy 6 
checkers, or a custom {{TypeCheckingDSL}}) and hits ten reports loses every 
diagnostic.

Environment: Groovy 6.0.0 on BellSoft JDK 21.0.12, macOS 26.5 (arm64); 
reproduced on master at 9a1a0d0ccb with JDK 21.



--
This message was sent by Atlassian Jira
(v10.3.26#10030026)

Reply via email to