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)

Reply via email to