Paul King created GROOVY-12747:
----------------------------------

             Summary: STC: assigning to a closure or lambda parameter typed 
from a functional interface converts the value to the inferred type
                 Key: GROOVY-12747
                 URL: https://issues.apache.org/jira/browse/GROOVY-12747
             Project: Groovy
          Issue Type: Bug
            Reporter: Paul King


Under {{@TypeChecked}} and {{@CompileStatic}}, an untyped closure or lambda 
parameter whose type is inferred from a functional-interface parameter behaves 
as a declared, typed parameter. Assigning a value of another type to the 
parameter therefore converts the value to the parameter's type, silently or 
with a {{GroovyCastException}}. Dynamic Groovy keeps the value.

{code:groovy}
@groovy.transform.TypeChecked // or @CompileStatic
def test() {
    List<Integer> numbers = [1, 2]
    numbers.stream().map { n -> n = n.toString(); n }.toList()
}
assert test() == [1, 2] // fails: [49, 50]
{code}

||Closure (for {{Stream.map}} on {{List<Integer>}} [1, 
2])||Dynamic||{{@TypeChecked}} / {{@CompileStatic}}||
|{{\{ n -> n = n.toString(); n \}}}|[1, 2]|*[49, 50]*, the char codes of "1" 
and "2"|
|{{\{ n -> n = n + 0.5; n \}}}|[1.5, 2.5]|*[1, 2]*, truncated|
|{{n -> \{ n = 'x'; n \}}} (lambda)|[x, x]|*[120, 120]*|
|{{Function<Integer, Object> f = \{ n -> n = 'x'; n \}}}|[x, x]|*[120, 120]*|
|{{\{ n -> n = 'ab'; n \}}}|[ab, ab]|{{GroovyCastException}}: Cannot cast 
object 'ab' with class 'java.lang.String' to class 'java.lang.Integer'|
|{{\{ it = 'x'; it \}}} (implicit parameter)|[x, x]|[x, x]|
|DGM {{numbers.collect \{ n -> n = n.toString(); n \}}} 
({{@ClosureParams}})|[1, 2]|[1, 2]|

So the same closure body behaves differently depending on how its parameter 
type was inferred. A parameter typed through {{@ClosureParams}}, or an implicit 
{{it}}, keeps the assigned value, as dynamic Groovy does. A parameter typed 
from a functional interface converts it.

h3. Cause

{{StaticTypeCheckingVisitor.inferParameterAndReturnTypesOfClosureOnRHS}} sets 
the inferred type on an untyped parameter ({{parameter.setType(type)}}, 
commented "GROOVY-11083, GROOVY-11085, et al."). The parameter is then declared 
with that type, so an assignment to it goes through a cast to the parameter 
type: a one-character String becomes its char code, a {{BigDecimal}} is 
narrowed to {{Integer}}, and other Strings fail. The {{@ClosureParams}} path 
({{processClosureParams}}) records the inferred types without changing the 
parameters' declared type, so flow typing applies.

h3. Expected

The assignment must not silently change the value. Either:
* treat the parameter as the {{@ClosureParams}} path does, so it keeps the 
assigned value as in dynamic Groovy; or
* report a static type error for an assignment that is incompatible with the 
inferred type, as Java does for a lambda parameter.

The first is consistent with the rest of Groovy's closure handling. Whichever 
is chosen, the {{@ClosureParams}} and functional-interface paths should agree.

h3. Notes

* Seen on master. {{setType}} dates from GROOVY-11083 and GROOVY-11085 (3.0.18 
/ 4.0.13); earlier versions were not checked.
* GROOVY-12746 types the parameters of closures passed to extension methods 
with functional-interface parameters, such as {{collectParallel}}, from the 
receiver. Those closures now have this behaviour too: {{numbers.collectParallel 
\{ n -> n = n.toString(); n \}}} gives [49, 50]. Before GROOVY-12746 their 
parameters fell back to {{Object}}.



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

Reply via email to