StackzOfZtuff
StackzOfZtuff

Reputation: 3102

Why does the Groovy @TypeChecked annotation catch me putting a String into an int variable but not the other way around?

I'm having trouble understanding Groovy types and type promotion. And the exact promises of Groovy's @TypeChecked annotation.

-- Or maybe I'm having trouble understanding some Groovy design philosophy.

I was playing around with the @TypeChecked annotation and it did not behave as expected. I made two example scripts and I expected both of them to fail because of type mismatches. But only ONE of the scripts fails.

The scripts are very similar. So I thought that they'd also behave in a similar way. The main difference is near the top: I either declare x as int or as String. And then I try to assign a different type to x.

Diff of scripts:

$ diff TypeChecked-fail-int-x.groovy TypeChecked-pass-String-x.groovy -y --width 70
@groovy.transform.TypeChecked           @groovy.transform.TypeChecked
void m(){                               void m(){
    int x                         |         String x

    x = 123                       |         x = "abc"
    println(x)                              println(x)
    println(x.getClass())                   println(x.getClass())

    println()                               println()

    x = "abc"                     |         x = 123
    println(x)                              println(x)
    println(x.getClass())                   println(x.getClass())
}                                       }

m()                                     m()

When I declare a variable as int but then try to assign a String I will get the expected error:

Script TypeChecked-fail-int-x.groovy: (Groovy web console here.)

@groovy.transform.TypeChecked
void m(){
    int x

    x = 123
    println(x)
    println(x.getClass())

    println()

    x = "abc"
    println(x)
    println(x.getClass())
}

m()

Output:

$ groovy --version
Groovy Version: 3.0.10 JVM: 11.0.17 Vendor: Ubuntu OS: Linux

$ groovy TypeChecked-fail-int-x.groovy
org.codehaus.groovy.control.MultipleCompilationErrorsException: startup failed:
/home/myuser/TypeChecked-fail-int-x.groovy: 11: [Static type checking] - Cannot assign value of type java.lang.String to variable of type int
 @ line 11, column 9.
       x = "abc"
           ^

1 error

However: if I do it THE OTHER WAY AROUND then it runs fine. I would have expected the type checker to ALSO catch this.

Script TypeChecked-pass-String-x.groovy: (Groovy web console here.)

@groovy.transform.TypeChecked
void m(){
    String x

    x = "abc"
    println(x)
    println(x.getClass())

    println()

    x = 123
    println(x)
    println(x.getClass())
}

m()

Output:

$ groovy TypeChecked-pass-String-x.groovy
abc
class java.lang.String

123
class java.lang.String

And not only does it run but suddenly int 123 has become String "123"!

I expected BOTH scripts to fail.

I also tried the @CompileStatic annotation and the results were the same.

Questions:

Update 2022-12-01: Fails even WITHOUT @TypeChecked

I found out something: The failing @TypeChecked script will fail even if you remove @TypeChecked. -- But now it fails with a different error message and AT RUNTIME (instead of at compile time).

I'm not sure if this all makes more or less sense to me now.

$ cat TypeChecked-fail-int-x.groovy | grep -v TypeChecked > no-typechecked.groovy

$ groovy no-typechecked.groovy
123
class java.lang.Integer

Caught: org.codehaus.groovy.runtime.typehandling.GroovyCastException: Cannot cast object 'abc' with class 'java.lang.String' to class 'int'
org.codehaus.groovy.runtime.typehandling.GroovyCastException: Cannot cast object 'abc' with class 'java.lang.String' to class 'int'
        at no-typechecked.m(no-typechecked.groovy:11)
        at no-typechecked.run(no-typechecked.groovy:16)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)

$ groovy --version
Groovy Version: 3.0.10 JVM: 11.0.17 Vendor: Ubuntu OS: Linux

But really I wasn't that interested in the cases that @TypeChecked stopped from running. I was more interested in why did NOT stop the other case from running. And this new nugget of knowledge changes nothing about that.

Upvotes: 0

Views: 420

Answers (1)

Dmitry Khamitov
Dmitry Khamitov

Reputation: 3276

Well... you came across two concepts in Groovy, Static Type Checking (TypeChecked) and Flow typing. Both of them might seem peculiar at first.

TypeChecked

TypeChecked has so-called "Type checking assignments" rules. Here is a snippet from that referred page:

An object o of type A can be assigned to a variable of type T if and only if:

  • T equals A
  • or T is one of String, boolean, Boolean or Class (this one is the most relevant for the question)
  • ...

For example, if you change the initial String type to Boolean you will also be surprised but that will be in line with the Groovy spec and the output will be:

true
class java.lang.Boolean

true
class java.lang.Boolean

If you are curious why the output has two true values you might want to read about The Groovy Truth.

Flow typing

Flow typing is an important concept of Groovy in type checked mode and an extension of type inference. The idea is that the compiler is capable of inferring the type of variables in the flow of the code, not just at initialization.

We are also interested in another statement from that link:

It is important to understand that it is not the fact of declaring a variable with def that triggers type inference. Flow typing works for any variable of any type. Declaring a variable with an explicit type only constrains what you can assign to the variable.

So, if you change the initial variable type from String to def you will see a different result:

abc
class java.lang.String

123
class java.lang.Integer

When the initial type is String it will always be a String variable and you can assign an object of any type to a String variable according to the "Type checking assignments" rules.

Summary

So, answering your questions:

Is this expected behavior or a bug? Sources?

Yes, this is expected. Please refer to the links and explanation above.

Why is 123 a String now? Is there some autoboxing/casting/type-promotion going on? Can I stop this?

Again, please refer to the links and explanation above. If you want the variable to change its type then define that variable through the def keyword. You can't stop this because (stating the doc again):

Flow typing has been introduced to reduce the difference in semantics between classic and static Groovy.

Upvotes: 1

Related Questions