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)