Scott Murphy Heiberg created GROOVY-12744:
---------------------------------------------
Summary: Node metadata snapshots and copies miss changes made
after the first one
Key: GROOVY-12744
URL: https://issues.apache.org/jira/browse/GROOVY-12744
Project: Groovy
Issue Type: Bug
Components: Compiler
Affects Versions: 6.0.0
Reporter: Scott Murphy Heiberg
Since GROOVY-11966, {{NodeMetaDataHandler.newMetaDataMap()}} wraps a
{{ListHashMap}} in {{Collections.synchronizedMap}}. The wrapper creates its
{{entrySet()}} view on the first call and returns that same view afterwards.
{{ListHashMap.entrySet()}} does not return a view backed by the map, as the
{{Map}} contract requires: with up to three entries it returns the entries of a
copy. So once a node's metadata has been read through {{getNodeMetaData()}} or
copied with {{copyNodeMetaData}}, every later read or copy of that node returns
the same old entries. Entries added since are missing, removed entries are
still there, and replaced values keep their old value. {{keySet()}} and
{{values()}} go stale in the same way.
{code:groovy}
import org.codehaus.groovy.ast.expr.ConstantExpression
def a = new ConstantExpression(1)
a.putNodeMetaData('k1', 1)
println a.nodeMetaData // [k1:1]
a.putNodeMetaData('k2', 2)
println a.nodeMetaData // [k1:1], expected [k1:1, k2:2]
def c = new ConstantExpression(3)
c.copyNodeMetaData(a)
println c.nodeMetaData // [k1:1], expected [k1:1, k2:2]
a.removeNodeMetaData('k1')
println a.nodeMetaData // [k1:1], expected [k2:2]
{code}
{{getNodeMetaData(key)}} is not affected, because it reads the map directly.
Compilations run into this. In the core test suite, static compilation
transforms of {{ConstructorCallExpression}} (map-style constructor calls) and
{{PropertyExpression}} copy a stale {{DIRECT_METHOD_CALL_TARGET}}. In every
case observed, the old and new values have the same signature, so no wrong
bytecode was found. Any code that reads all of a node's metadata or copies it
(AST transforms, tooling) can get old data.
On 5.0.x and 4.0.x, {{getNodeMetaData()}} and {{copyNodeMetaData()}} read the
{{ListHashMap}} directly, so each call sees the current entries. The
{{ListHashMap}} views break the {{Map}} contract there too, though: if code
keeps the map returned by {{getNodeMetaData()}} and reads it after the node
changes, it gets the old entries.
--
This message was sent by Atlassian Jira
(v10.3.26#10030026)