Scott Murphy Heiberg created GROOVY-12741:
---------------------------------------------

             Summary: groovy-contracts: a contract annotation on a trait or a 
trait method with a body crashes the compiler or fails at run time
                 Key: GROOVY-12741
                 URL: https://issues.apache.org/jira/browse/GROOVY-12741
             Project: Groovy
          Issue Type: Bug
          Components: groovy-contracts
    Affects Versions: 6.0.0
            Reporter: Scott Murphy Heiberg


h3. Problem

A {{@Requires}} on a trait method with a parameter crashes class generation:

{code:groovy}
import groovy.contracts.*

trait Bank {
    BigDecimal balance = 0
    @Requires({ amount > 0 })
    void deposit(BigDecimal amount) { balance += amount }
}
class Account implements Bank {}
new Account().deposit(1); println 'ok'
{code}

{noformat}
General error during instruction selection: Index 1 out of bounds for length 1
java.lang.ArrayIndexOutOfBoundsException: Index 1 out of bounds for length 1
        at 
org.codehaus.groovy.classgen.asm.InvocationWriter.loadArguments(InvocationWriter.java:379)
        at 
org.codehaus.groovy.classgen.asm.InvocationWriter.writeDirectMethodCall(InvocationWriter.java:292)
        at 
org.codehaus.groovy.classgen.asm.InvocationWriter.makeDirectCall(InvocationWriter.java:436)
        at 
org.codehaus.groovy.classgen.asm.InvocationWriter.makeCall(InvocationWriter.java:191)
        at 
org.codehaus.groovy.classgen.asm.InvocationWriter.writeInvokeMethod(InvocationWriter.java:622)
        at 
org.codehaus.groovy.classgen.AsmClassGenerator.visitMethodCallExpression(AsmClassGenerator.java:1244)
{noformat}

The same happens with {{@CompileStatic}} on the trait (in 
{{StaticInvocationWriter.loadArguments}}). The other contract annotations do 
not crash but do not work either:
* {{@Requires}} on a trait method without parameters, and {{@Ensures}} on any 
trait method: the class compiles, then every call fails with 
{{MissingPropertyException: No such property: $GCONTRACTS_ENABLED for class: 
Bank$Trait$Helper}}.
* {{@Invariant}} on a trait: inherited by the implementing class like an 
interface invariant, it fails at construction, while the trait's fields are 
still being initialised (the condition sees {{balance}} as null).

A contract on an abstract trait method works (it applies to the implementing 
class like an interface contract), and so does one on an interface method a 
trait implements.

Reproduced on Groovy 6.0.0 and on master (df1fe66ed1).

h3. Cause

The contracts transform runs in SEMANTIC_ANALYSIS before the trait transform 
and treats the trait as a class: it generates the condition class with the 
trait method's parameters and marks the trait {{@Contracted}}. The trait 
transform then moves each method with a body into the helper class as a static 
method with a leading {{$self}} parameter, keeping the annotations, and copies 
the class annotations to the helper. In INSTRUCTION_SELECTION the module weaves 
the helper (it carries {{@Contracted}}; 
{{CandidateChecks.isContractsCandidate}} is not consulted), building the 
{{doCall}} argument list from the helper method's parameters, one more than the 
condition class accepts; without parameters the extra argument is dropped and 
the woven {{$GCONTRACTS_ENABLED}} guard has no field to refer to in the helper.

h3. Fix

Supporting contracts on trait methods needs a design (where the check runs, 
what {{this}} is, how a trait invariant interacts with trait field 
initialisation, private and static trait methods). For now the module reports a 
compile error instead of crashing:

{noformat}
[groovy-contracts] @Requires is not supported on a trait method with a body 
(Bank.deposit); only an abstract trait method may carry a contract.
[groovy-contracts] @Invariant is not supported on a trait (Bank).
{noformat}

{{@ThrowsIf}} and {{@Decreases}} on a trait method with a body are reported the 
same way (without the abstract-method hint, since on an abstract method they 
stay informational). Their transforms run after the trait transform has moved 
the method into the helper class and rewritten the body to reach trait state 
through {{$self}}; the guard or measure woven there is not rewritten, so a 
condition that refers to trait state fails at run time with a missing property 
on the helper class, and only a condition that refers to parameters alone 
happened to work (once the separate core crash in {{TraitComposer}} for 
annotations with a closure member is fixed; that one is reported separately).

{noformat}
[groovy-contracts] @ThrowsIf is not supported on a trait method with a body 
(Bank.withdraw).
[groovy-contracts] @Decreases is not supported on a trait method with a body 
(Counting.sumUp).
{noformat}

A contract on an abstract trait method keeps working. Pull request to follow.




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

Reply via email to