| = GEP-8: Static type checking |
| |
| :icons: font |
| |
| .Metadata |
| **** |
| [horizontal,options="compact"] |
| *Number*:: GEP-8 |
| *Title*:: Static type checking |
| *Version*:: 9 |
| *Type*:: Feature |
| *Status*:: Final |
| *Comment*:: Delivered in Groovy 2.0 and enhanced in later versions |
| *Leader*:: Cédric Champeau |
| *Created*:: 2011-10-08 |
| *Last modification* :: 2018-10-12 |
| **** |
| |
| == Abstract: Static Type Checking |
| |
| This GEP introduces a new feature in the language known as static type checking. |
| It is often disturbing for developers coming from a statically typed language (say Java) |
| to discover that the Groovy compiler will not complain at compile time: |
| |
| * when assignments are made on different types |
| * when a method doesn't exist |
| * when a property or variable doesn't exist |
| * when returned object type doesn't match the method signature |
| * ... |
| |
| All those are silent because the dynamic nature of the Groovy language makes such code perfectly valid. |
| However, in some situations, a developer may want Groovy to behave like a statically typed language |
| and have the compiler give hints about such "errors". To do this, Groovy must introduce static type checking. |
| |
| == Rationale: Static Type Checking vs Static compilation |
| |
| It is important to make the difference between static type checking and static compilation. |
| The goal of this GEP is to have an option to turn static type checking (STC) on. |
| If STC is activated, the compiler will be more verbose (you will also see the term "grumpy"), |
| but in the end, the generated bytecode and runtime behaviour will be exactly the same as if you |
| did not activate this mode. This is a major difference from an alternate compiler like Groovy++ |
| which will perform STC then produce a different bytecode and therefore produce different runtime semantics. |
| The scope of this GEP is only a static type checker, and therefore should only be considered as a |
| feature which allows developers to write statically checked code, so is an elegant way for example |
| to leverage the Groovy syntax to reduce verbosity of Java code while still getting strongly checked code. |
| Eventually, IDE could support the STC mode and provide information to the developer. |
| |
| === Implementation details |
| |
| ==== Development branch |
| |
| Since Groovy 2.0-beta-2, code has been merged into master branch. However, if heavy developments are |
| done on the type checker, it is advisable to work on the grumpy branch. It adds an AST transformation |
| named TypeChecked. If set, then the AST transformation will perform type inference and store type |
| information in AST nodes metadata. Eventually, if errors are found, it will add errors to the compiler |
| through a dedicated addStaticTypeError method which basically does the same as the traditional |
| addError method but prefixes the messages with a "Static type checking" message. |
| This is done to help the developer determine whether the error he is seeing is a "plain Groovy" error, |
| or an error thrown by the STC mode. |
| |
| ==== The StaticTypeCheckingTestCase class |
| |
| Static type checking behaviour must be tested. As there are tons of possible checks to be done, |
| a base test class provides a framework for testing this mode. |
| Unit tests for static type checking should override this class. |
| |
| === Decisions made |
| |
| ==== About this section |
| |
| The goal of this section is to provide code samples which demonstrates in what case the STC transformation |
| will actually complain and what is the expected error message, and serves as a basis to future STC documentation. |
| This section may not be up-to-date, and one should always take a look at the STC unit tests found in the |
| `src/test/groovy/transform/stc` directory. |
| |
| [options="header"] |
| |=== |
| | Feature | Example | Behavior | Status |
| | Method does not exist a| |
| ---- |
| def method() { |
| ... |
| } |
| methode() // typo |
| ---- |
| | Complains about undefined method | Implemented |
| | Property does not exist a| |
| ---- |
| class A { |
| int x |
| } |
| A obj = new A() |
| a.y = 2 |
| ---- |
| | Complains about undefined property "y" | Implemented |
| | Assignment type checking a| |
| ---- |
| int x = 2 |
| x = 'String' |
| ---- |
| | Assigning a String to an int is forbidden | Implemented |
| | Incompatible binary expressions a| |
| ---- |
| 1 + 'string' |
| ---- |
| | Checks that arguments of a binary expression are compatible (here, no 'plus' method is available | Implemented |
| | Possible loss of precision (1/2) a| |
| ---- |
| long myLong = ... |
| int myInt = myLong |
| ---- |
| | Complains about possible loss of precision | Implemented |
| | Possible loss of precision (2/2) a| |
| ---- |
| int myInt = 2L |
| ---- |
| | Will not complain because '2' can be represented as an int | Implemented |
| | Arrays components a| |
| ---- |
| String[] arr = { '1', '2', '3' } |
| arr[2] = 200 |
| ---- |
| | Cannot assign an int value in an array of type String[] | Implemented |
| | Method return type check a| |
| ---- |
| String method() { 'Hello' } |
| int x = method() // return types don't match |
| ---- |
| | Ensures that assignments are compatible with method return type | Implemented |
| | Explicit return type checking a| |
| ---- |
| int method() { |
| return 'String' // return type is not compatible |
| } |
| ---- |
| | Ensures that returned value is compatible with declared return type | Implemented |
| | Implicit return type checking a| |
| ---- |
| int method() { |
| 'String' // return type is not compatible |
| } |
| ---- |
| | Ensures that returned value is compatible with declared return type | Implemented |
| | Implicit toString() a| |
| ---- |
| String method(String name) { |
| StringBuilder sb = new StringBuilder() |
| sb 'Hi ' << name << '!' |
| } |
| ---- |
| | Implicit call to toString() | Implemented |
| | Basic type inference a| |
| ---- |
| def str = 'My string' |
| str.toUpperCase() // type of 'str' is inferred |
| ---- |
| | Method calls as well as property access are checked against inferred type | Implemented |
| | Basic flow analysis a| |
| ---- |
| def o |
| ... |
| if (o instanceof String) { |
| o.toUpperCase() // no explicit cast required |
| } |
| ---- |
| | Casts should not be necessary when type can be inferred from a previous instanceof check | Implemented |
| | DefaultGroovyMethods support a| |
| ---- |
| '123'.toInteger() // toInteger() is a Groovy extension method |
| ---- |
| | Method calls can be resolved against Groovy extension methods | Implemented |
| | with a| |
| ---- |
| class A { |
| int x |
| } |
| def a = new A() |
| a.with { |
| x = 1 |
| } |
| ---- |
| | Method calls can be resolved against Groovy extension methods | Implemented |
| | Categories a| |
| ---- |
| use (MyStringCategory) { |
| 'string'.methodInStringCategory() |
| } |
| ---- |
| | Compiler should be aware that extension method is found in a category | *N/A* (support will be limited as category support is inherently dynamic) |
| | Groovy list constructor a| |
| ---- |
| Dimension d = [100, 200] |
| ---- |
| | Type checks the arguments and the number of arguments | Implemented |
| | Groovy map constructor a| |
| ---- |
| Bean myBean = [x: 100, y: 200] |
| ---- |
| | Type checks the properties and checks for incorrect property names | Implemented |
| | Closure parameter types a| |
| ---- |
| def closure = { int x, int y -> x + y } |
| closure(1, 2) |
| closure('1', '2') // complains |
| ---- |
| | Type checking the arguments when calling a closure | Implemented |
| | Closure return type inference a| |
| ---- |
| def closure = { int x, int y -> x + y } |
| int sum = closure(1, 2) |
| ---- |
| | Closure return type can be inferred from block | Implemented |
| | Method return type inference a| |
| ---- |
| def method(int x, int y) { x + y } |
| int sum = method(1, 2) |
| ---- |
| | Return type can be inferred from a method if the method is itself annotated with @TypeChecked (or class is annotated with @TypeChecked) | Implemented |
| | Multiple assignments a| |
| ---- |
| def (x, y) = [1, 2] |
| ---- |
| | In case of inline declaration, type check arguments | Implemented |
| | Multiple assignments from a variable a| |
| ---- |
| def (x, y) = list |
| ---- |
| | In case of inline declaration, type check arguments | Implemented |
| | Generics a| |
| ---- |
| List<String> list = [] |
| List<String> list = ['a', 'b', 'c'] |
| List<String> list = [1, 2, 3] // should throw error |
| ---- |
| | Type checking of generic parameters | Implemented |
| | Spread operator a| |
| ---- |
| def list = ['a', 'b', 'c'] |
| list*.toUpperCase() |
| ---- |
| | Type checking against component type | Implemented |
| | Closure shared variables a| |
| ---- |
| def x = new Date() |
| def cl = { x = 'hello' } |
| cl() |
| x.toUpperCase() // should throw an error because the toUpperCase() method doesn't belong to both Date and String classes |
| ---- |
| | Type check assignments of closure shared variables. The type checker is required to perform a two-pass verification, |
| in order to check that method calls on a closure shared variables belong to the lowest upper bound of all assignment types. | Implemented |
| |=== |
| |
| === Open discussions |
| |
| ==== Closure parameter type inference |
| |
| With the current version of the checker, idiomatic constructs like : |
| |
| ``` |
| ['a','b','c'].collect { it.toUpperCase() } |
| ``` |
| |
| Are not properly recognized. You have to explicitly set the type of the "it" parameter inside the closure. |
| It is because the expected parameter types of closures are unknown at compile time. |
| There is a discussion about how to add this type information to source code so that the inference |
| engine can deal with them properly. The implementation of closure parameter type inference requires |
| a change to the method signatures. It will probably not belong to the initial release of the type checker. |
| |
| ==== Unification Types |
| |
| In cases of for example `x instanceof A || x instanceof B` with A and B being unrelated we could |
| still make an artificial union kind of type, that contains everything present in A and B, |
| to allow those kinds of method calls. The alternative to this is to allow only methods from Object here, |
| which is less interesting. This typing can also be used for multicatch, ensuring that a method call is |
| only valid if it exists on each of the exceptions for the multicatch. |
| In the current implementation (2011-10-14) the multicatch is already expanded at the point @TypeChecked will check. |
| Meaning effectively this already represents a kind of union type, as the same code is in each catch block |
| and thus the method call would fail, if the method is not available on each type. |
| The proposed behaviour is therefore to align the instanceof case with multicatch. |
| |
| == References and useful links |
| |
| * https://web.archive.org/web/20150508041021/http://docs.codehaus.org/display/GroovyJSR/GEP+8+-+Static+type+checking[GEP-8: Static type checking] (web archive link with comments) |
| * http://blackdragsview.blogspot.com/2011/10/flow-sensitive-typing.html[Flow Sensitive Typing?] |
| * https://web.archive.org/web/20150508040745/http://www.jroller.com/melix/entry/groovy_static_type_checker_status[Groovy static type checker: status update] (web archive) |
| |
| === Mailing-list discussions |
| |
| * https://markmail.org/thread/reou7z35nk64cai5[groovy-user: What to do on assignment?] Discussion about the expected behaviour when STC detects a potential error on assignment (for example, possible loose of precision on implicit number casts) |
| |
| === JIRA issues |
| |
| * https://issues.apache.org/jira/browse/GROOVY-5073[GROOVY-5073: GEP-8 - Static type checking] |
| * https://issues.apache.org/jira/browse/GROOVY-3014[GROOVY-3014: add an annotation that forces the compiler to check methods for their existence at compile time] |
| * https://issues.apache.org/jira/browse/GROOVY-5081[GROOVY-5081: Handle explicit and implicit returns] |
| |
| == Update history |
| |
| 8 (2012-02-21):: Version as extracted from Codehaus wiki |
| 9 (2018-10-16):: Numerous minor tweaks |