[ 
https://issues.apache.org/jira/browse/GROOVY-12744?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18140814#comment-18140814
 ] 

ASF GitHub Bot commented on GROOVY-12744:
-----------------------------------------

codeconsole opened a new pull request, #3279:
URL: https://github.com/apache/groovy/pull/3279

   https://issues.apache.org/jira/browse/GROOVY-12744
   
   ## Problem
   
   Node metadata lives in a `ListHashMap` wrapped in 
`Collections.synchronizedMap` (GROOVY-11966). The wrapper creates its 
`entrySet()`, `keySet()` and `values()` views on first use and keeps returning 
them. With up to three entries, `ListHashMap` returned those views from a copy 
of its arrays, not from the map itself. So after the first `getNodeMetaData()` 
or `copyNodeMetaData(node)`, every later read or copy of that node returned the 
old entries. Entries added since were missing, removed ones were still there, 
and replaced values kept their old value.
   
   In the core test suite this happens during static compilation. Copies of 
map-style `ConstructorCallExpression`s and of a `PropertyExpression` took a 
`DIRECT_METHOD_CALL_TARGET` from an earlier type-checking pass. In every case 
found, the old and new targets have the same signature, so no wrong bytecode 
was found.
   
   ## Fix
   
   `ListHashMap.entrySet()`, `keySet()` and `values()` now return views backed 
by the map, as the `Map` contract requires: `size()`, `contains` and each new 
iterator see the map's current entries. Each iterator still works on a copy of 
the entries while the map keeps them in its arrays (up to three by default), 
and on a read-only view of the backing map otherwise, as before. The views 
still can't change the map.
   
   ## Tests
   
   - `ListHashMapTest`: the views and a `synchronizedMap` wrapper see later 
puts, replacements and removals, across both storage switches.
   - `ASTNodeTest`: `getNodeMetaData()` and `copyNodeMetaData` see entries 
added, replaced and removed after an earlier snapshot.
   
   Both fail without the fix. The full core `:test` suite, 
`NodeMetaDataHandlerStressTest` and the `groovy-ginq`, `groovy-contracts` and 
`groovy-macro` tests pass. With a temporary check that compared each copy 
against the live map, the 34 stale copies seen in 
`DefaultGroovyMethodsSTCTest`, `StaticCompileDGMTest`, `GenericsSTCTest` and 
`GenericsStaticCompileTest` drop to none.
   




> 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
>            Priority: Major
>
> 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)

Reply via email to