Paul King created GROOVY-12728:
----------------------------------

             Summary: groovy-contracts: a class literal given to @Requires, 
@Ensures or @Invariant is silently never checked, and crashes the compiler when 
the contract is inherited
                 Key: GROOVY-12728
                 URL: https://issues.apache.org/jira/browse/GROOVY-12728
             Project: Groovy
          Issue Type: Bug
          Components: groovy-contracts
    Affects Versions: 6.0.0
            Reporter: Paul King


The value of {{@Requires}}, {{@Ensures}} and {{@Invariant}} is declared as 
{{Class}}, so a class literal passes the annotation checks. The contract 
transforms then take it for the condition class they generate from a closure:
* With no inherited contract, the condition is never checked, and nothing 
reports that.
* When the method or class also inherits a contract, compilation fails with an 
internal error.

h3. Silently never checked

{code:groovy}
import groovy.contracts.*

class A {
    @Requires(Object) void m(int x) {}
}
new A().m(-1)   // returns normally
{code}
With {{@Requires(\{ x > 0 \})}} the same call throws {{PreconditionViolation}}.

The same happens with:
* {{@Requires(String)}} and {{@Requires(Object.class)}};
* {{@Ensures(Object) int m(int x) \{ x \}}}, where {{m(-1)}} returns -1 (with 
{{@Ensures(\{ result > 0 \})}} it throws {{PostconditionViolation}});
* {{@Invariant(Object)}} on a class whose state breaks the invariant (with 
{{@Invariant(\{ count >= 0 \})}} it throws {{ClassInvariantViolation}}).

h3. Compiler crash with an inherited contract

{code:groovy}
import groovy.contracts.*

class B { @Requires({ x > 0 }) void m(int x) {} }
class C extends B { @Requires(Object) void m(int x) {} }
{code}
{noformat}
BUG! exception in phase 'instruction selection' in source unit 'Script1.groovy' 
Index 0 out of bounds for length 0
{noformat}
Inheritance gives the same crash for:
* {{@Ensures(Object)}} overriding a method that has {{@Ensures(\{ result > 0 
\})}};
* {{@Invariant(Object)}} on a subclass of a class that has {{@Invariant(\{ 
count >= 0 \})}}.

h3. Cause

* The transforms replace each condition closure with a reference to a generated 
class. Later passes then treat any value that is not a closure as that 
replacement ({{BaseVisitor.getReplacedCondition}}). So a class literal written 
by the user goes down the "already generated" path.
* {{BaseVisitor.asConditionExecution}} builds {{new <value class>(this, 
this).doCall(...)}} and calls {{getMethods("doCall").get(0)}}. {{Object}} has 
no {{doCall}}, which is the IndexOutOfBounds above.
* Without an inherited contract, that code path is never reached, so the 
condition is silently skipped.

h3. Suggested fix

Reject any value that is not a closure when the annotation is first processed, 
before the closure is replaced. GROOVY-12723 now does this for {{@Modifies}}:
{noformat}
@Modifies requires a closure listing the modifiable targets; use @Modifies({}) 
to declare that nothing is modified; found a class java.lang.Object
{noformat}
Today a list or string value is already rejected by the generic annotation 
check ("Only classes, closures, and method references can be used for attribute 
'value'"). Only class literals get through.



--
This message was sent by Atlassian Jira
(v10.3.26#10030026)

Reply via email to