Scott Murphy Heiberg created GROOVY-12689:
---------------------------------------------

             Summary: FormatStringChecker fails inside the compiler when an 
argument's type is declared in the same source
                 Key: GROOVY-12689
                 URL: https://issues.apache.org/jira/browse/GROOVY-12689
             Project: Groovy
          Issue Type: Bug
    Affects Versions: 6.0.0
            Reporter: Scott Murphy Heiberg


With {{groovy.typecheckers.FormatStringChecker}} enabled, a format call with a 
non-constant argument whose static type is declared in the same compilation 
unit aborts the compilation with a {{GroovyBugError}}. A plain class is enough, 
and so is an enum, a record, an anonymous class, an array of such a type, or 
{{this}} inside the checked class. The conversion does not matter: {{%s}} fails 
too.

{{Show.groovy}} and {{ShowColor.groovy}}:

{code:groovy}
import groovy.transform.TypeChecked

class Foo {}

@TypeChecked(extensions = 'groovy.typecheckers.FormatStringChecker')
String show() {
    String.format('%s', new Foo())
}
{code}

{code:groovy}
import groovy.transform.TypeChecked

enum Color { RED, GREEN }

@TypeChecked(extensions = 'groovy.typecheckers.FormatStringChecker')
String show() {
    String.format('%s', Color.RED)
}
{code}

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

Actual, for {{Show.groovy}}: exit 1 and

{code:text}
>>> a serious error occurred: BUG! exception in phase 'instruction selection' 
>>> in source unit 'Show.groovy' ClassNode#getTypeClass for Foo called before 
>>> the type class is set
>>> stacktrace:
BUG! exception in phase 'instruction selection' in source unit 'Show.groovy' 
ClassNode#getTypeClass for Foo called before the type class is set
        at org.codehaus.groovy.ast.ClassNode.getTypeClass(ClassNode.java:1987)
        at 
groovy.typecheckers.FormatStringChecker$1$_checkFormatStringTypes_closure5.doCall(FormatStringChecker.groovy:184)
        ...
        at 
groovy.typecheckers.FormatStringChecker$1.checkFormatStringTypes(FormatStringChecker.groovy:163)
        at 
groovy.typecheckers.FormatStringChecker$1.checkFormatterMethod(FormatStringChecker.groovy:156)
        at 
groovy.typecheckers.FormatStringChecker$1.visitMethodCallExpression(FormatStringChecker.groovy:122)
        ...
        at 
org.codehaus.groovy.transform.stc.GroovyTypeCheckingExtensionSupport.afterVisitMethod(GroovyTypeCheckingExtensionSupport.java:427)
        ...
{code}

{{ShowColor.groovy}} fails the same way with {{ClassNode#getTypeClass for Color 
called before the type class is set}}. The smallest form needs no second type:

{code:groovy}
@groovy.transform.TypeChecked(extensions = 
'groovy.typecheckers.FormatStringChecker')
class Main {
    String show() { String.format('%s', this) }
}
{code}

It fails with {{ClassNode#getTypeClass for Main called before the type class is 
set}}. The same happens when the checker is applied through 
{{CompilerConfiguration}}:

{code:groovy}
import groovy.transform.TypeChecked
import org.codehaus.groovy.control.CompilerConfiguration
import org.codehaus.groovy.control.customizers.ASTTransformationCustomizer

def config = new CompilerConfiguration()
config.addCompilationCustomizers(new ASTTransformationCustomizer(
        [extensions: 'groovy.typecheckers.FormatStringChecker'], TypeChecked))
new GroovyShell(config).evaluate('''
    class Foo {}
    String.format('%s', new Foo())
''')
{code}

{code:text}
org.codehaus.groovy.GroovyBugError: BUG! exception in phase 'instruction 
selection' in source unit 'Script1.groovy' ClassNode#getTypeClass for Foo 
called before the type class is set
{code}

Expected: these sources compile. They do when {{Foo}} is compiled first and put 
on the class path: {{groovyc --check}} of the same checked method then exits 0. 
A real mismatch should still be reported, for example 
{{IllegalFormatConversion: d != Foo}} for {{String.format('%d', new Foo())}}. 
That is what the checker reports for a precompiled {{Foo}} or for 
{{java.util.UUID}}.

When the checked code also declares an enum or a record, this error hides the 
one described in GROOVY-12688. The two have separate causes.

Affected versions: 6.0.0, 6.0.0-beta-1, 5.0.8 and 5.0.0 fail the same way, and 
so does current master (7.0.0-SNAPSHOT at 5e9212ce1e).

Cause: when a format call's arguments are not all constants, 
{{checkFormatStringTypes}} takes the runtime class of each argument's static 
type ({{getWrapper(getType(arg)).typeClass}}, line 184) and compares it with 
{{Class}} objects. A type compiled in the same compilation unit has no runtime 
class yet, so {{ClassNode#getTypeClass}} throws. The class is taken before the 
conversion is looked at, which is why {{%s}} fails as well. Because 
{{GroovyBugError}} is an {{Error}}, it escapes the extension's error handling 
and ends the compilation.

A fix that works: compare the argument's {{ClassNode}} with the {{ClassHelper}} 
types, and use {{StaticTypeCheckingSupport.implementsInterfaceOrIsSubclassOf}} 
for the date/time check, so that types from the same source are checked like 
precompiled ones. A pull request with regression tests follows.

Impact: FormatStringChecker was graduated to stable for 6.0.0 (GROOVY-12039). 
Checked code that passes an instance of one of its own types to a format 
method, including {{this}} or one of its own enum constants, cannot be compiled 
with it. Workaround: compile those types separately, or leave 
FormatStringChecker off for such sources.

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



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

Reply via email to