[
https://issues.apache.org/jira/browse/GROOVY-12636?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Paul King updated GROOVY-12636:
-------------------------------
Summary: TC/CS: an element taken from a raw List by subscript, or by GDK
methods such as first(), pop() and tail(), has an unresolved type (was: 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)
> TC/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
> Priority: Major
>
> 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)