Paul King created GROOVY-12636:
----------------------------------
Summary: Under {{@TTC/CS: an element taken from a raw List by
subscript, or by GDK methods such as first(), pop() and tail(), has an
unresolved type
Key: GROOVY-12636
URL: https://issues.apache.org/jira/browse/GROOVY-12636
Project: Groovy
Issue Type: Bug
Reporter: Paul King
h2. Description
Under {{@TypeChecked}} and {{@CompileStatic}}, an element taken from a raw
{{List}} by subscript, or by GDK methods such as {{first()}}, {{pop()}} and
{{tail()}}, has an unresolved type. Assignment and argument checks then accept
it for any target. The same element read through {{List.get(int)}} is typed
{{Object}} and is checked as such:
{code:groovy}
@groovy.transform.CompileStatic
def f(List l) {
int a = l.get(0) // [Static type checking] - Cannot assign value of type
java.lang.Object to variable of type int
int b = l[0] // compiles; becomes a run-time cast
int c = l.first() // compiles
}
{code}
Under {{@CompileStatic}}, the accepted forms become run-time casts. A wrong
element then fails with {{GroovyCastException}}, or converts unexpectedly. For
example, {{g(l[0])}} with {{g(Integer)}} and an element {{"x"}} passes {{120}}.
Raw collections are not lenient everywhere. The type checker already rejects
these on master:
* {{Integer a = m['age']}}, {{m.age}} and {{m.get('age')}} on a raw {{Map}}
* {{Integer n = l.find \{ ... \}}}, {{l.max()}} and {{l.sum()}} on a raw
{{List}}
* {{l.each \{ Integer i -> ... \}}} and {{for (Integer i in l)}} on a raw
{{List}}
So the leniency is an exception that only some raw-receiver calls get.
Seen on 4.0.32, 6.0.0 and master.
h2. Cause
{{l[0]}} on a raw {{List}} resolves the GDK's {{getAt(List<T> self, int idx)}}.
The receiver is passed as the {{self}} argument, and a raw {{List}} gives no
witness for {{T}}.
{{StaticTypeCheckingVisitor.extractGenericsConnectionsFromArguments}} then maps
{{T}} to its "no type witness" placeholder {{#T}} (GROOVY-8409, GROOVY-10067).
That placeholder exists for calls such as {{<T> T make()}}, where {{T}} really
is free and should follow the target type. Assignment and argument checks
therefore accept it for any target. Method lookup on it uses its bound
{{Object}}, so {{l[0]++}} and {{l[0].foo()}} are rejected. {{l.get(0)}} is
{{List}}'s own method, and the raw type erases its result to {{Object}}.
h2. Proposed change
Treat a call with a raw argument as an unchecked call, as Java does. When an
argument is raw while its parameter is generic, the type variables that appear
in that parameter take their declared bound, or {{Object}}, unless another
argument gives a real witness. The {{#T}} placeholder stays for a type variable
that nothing constrains, so {{<T> T make()}} keeps following the target type.
A closure, a lambda, a method reference and a {{Closure}}-typed argument are
left out. A closure literal is typed raw {{Closure}}, but Groovy infers its
type argument from the closure itself. Without that exclusion, {{inject}} and
similar methods regress.
A spike of about 40 lines in {{StaticTypeCheckingVisitor}} implements this.
h2. Impact (measured with the spike)
* *Groovy's own code:* no new errors. All modules' main and test sources (75
Groovy compile tasks) compiled with the spike compiler.
* *Core test suite:* 19,188 tests and no failures. No existing test pins the
lenient behaviour.
* *User code:* each of these stops compiling under {{@TypeChecked}} and
{{@CompileStatic}}, with {{List l}} raw:
|| Pattern || Fix ||
| {{int n = l[1]}} | {{l[1] as int}}, or a typed list |
| {{g(l[0], l[1])}} with typed parameters | a cast, or a typed list |
| {{int f(List l) \{ l[0] \}}} | a cast, or a typed list |
| {{int n = l.first()}}, {{l.pop()}} | a cast, or a typed list |
| {{List<String> t = l.tail()}} | a cast, or a typed list |
| {{Integer n = items[0]}} where {{items}} came from {{(List) m.items}} | a
cast, or {{List<Integer>}} |
* *Unchanged:* {{String s = l[0]}}, {{def x = l[0]}}, {{l[0] as int}}, {{(int)
l[0]}}, typed lists such as the result of {{tokenize}}, and dynamic code.
Not yet measured: the subproject test suites, and external {{@CompileStatic}}
code bases.
h2. Compatibility
This is a breaking change for statically checked code, so it is proposed for
Groovy 7 with a COMPATIBILITY entry. The entry would show the cast fix, and
would note that a raw {{List}} now behaves as a raw {{Map}} already does. Every
affected line can be fixed with a cast, {{as}}, or a typed collection.
h2. Related
The error message for a missing method on such an element named the internal
placeholder ({{Cannot find matching method #T#next()}}). That is fixed on
master in d57c388c74, which names the bound instead.
--
This message was sent by Atlassian Jira
(v10.3.26#10030026)