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:
- Is this expected behavior or a bug? Sources?
- Why is 123 a String now? Is there some autoboxing/casting/type-promotion going on? Can I stop this?
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.