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

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

Copilot commented on code in PR #2671:
URL: https://github.com/apache/groovy/pull/2671#discussion_r3532552109


##########
subprojects/groovy-contracts/src/main/java/org/apache/groovy/contracts/ast/ThrowsIfASTTransformation.java:
##########
@@ -0,0 +1,260 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.groovy.contracts.ast;
+
+import groovy.contracts.ThrowsIf;
+import org.apache.groovy.contracts.ThrowsIfSupport;
+import org.codehaus.groovy.ast.ASTNode;
+import org.codehaus.groovy.ast.AnnotationNode;
+import org.codehaus.groovy.ast.ClassHelper;
+import org.codehaus.groovy.ast.ClassNode;
+import org.codehaus.groovy.ast.MethodNode;
+import org.codehaus.groovy.ast.Parameter;
+import org.codehaus.groovy.ast.expr.ArrayExpression;
+import org.codehaus.groovy.ast.expr.ClassExpression;
+import org.codehaus.groovy.ast.expr.ClosureExpression;
+import org.codehaus.groovy.ast.expr.ConstantExpression;
+import org.codehaus.groovy.ast.expr.Expression;
+import org.codehaus.groovy.ast.expr.VariableExpression;
+import org.codehaus.groovy.ast.stmt.BlockStatement;
+import org.codehaus.groovy.ast.stmt.CatchStatement;
+import org.codehaus.groovy.ast.stmt.Statement;
+import org.codehaus.groovy.ast.stmt.TryCatchStatement;
+import org.codehaus.groovy.control.CompilePhase;
+import org.codehaus.groovy.control.SourceUnit;
+import org.codehaus.groovy.transform.ASTTransformation;
+import org.codehaus.groovy.transform.GroovyASTTransformation;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicLong;
+
+import static org.codehaus.groovy.ast.tools.GeneralUtils.args;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.assignS;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.block;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.boolX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.callX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.classX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.constX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.ctorX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.declS;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.equalsNullX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.ifS;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.localVarX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.nullX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.stmt;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.throwS;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.varX;
+
+/**
+ * Handles a method's {@link ThrowsIf} arm-set. Processed once per method (the
+ * transformation fires per annotation, so repeats are deduplicated via node
+ * metadata) because the semantics are per-<em>set</em>: guards are generated 
in
+ * declaration order, and the {@code checked} wrapper judges an escaping throw
+ * against <em>every</em> arm.
+ * <p>
+ * For each {@code WOVEN}-origin arm the guard-throw is generated at method
+ * entry, so that conceptually:
+ * <pre>
+ * &#64;ThrowsIf(value = { b == 0 }, exception = ArithmeticException)
+ * int divide(int a, int b) { a.intdiv(b) }
+ * </pre>
+ * compiles as:
+ * <pre>
+ * int divide(int a, int b) {
+ *     if (b == 0) throw new ArithmeticException('@ThrowsIf: b == 0')
+ *     a.intdiv(b)
+ * }
+ * </pre>
+ * — the general form of the pattern {@code groovy.transform.NullCheck} 
provides
+ * for the null-check special case. Arms with {@code woven = false} generate no
+ * guard — the throw already exists, in the body ({@code direct = true}, the
+ * default) or in invoked code ({@code direct = false}); identical bytecode, 
the
+ * {@code direct} distinction is information for tools and is not read here.
+ * <p>
+ * When any arm is {@code checked}, the conditions are additionally snapshot on
+ * entry and the body is wrapped so that, conceptually:
+ * <pre>
+ * boolean $c0 = &lt;cond0&gt;; ...                                 // entry 
snapshot, every arm
+ * if ($c0) throw new E0(...)                                  // woven arms, 
in declaration order
+ * Throwable $t = null
+ * try { &lt;original body&gt; }
+ * catch (Throwable caught) {
+ *     $t = caught
+ *     throw ThrowsIfSupport.onlyWhen(caught, [$c0, ...], [E0, ...], [texts], 
allExhaustive)
+ * } finally {
+ *     if ($t == null) ThrowsIfSupport.mustThrow([unwoven $ci ...], [texts])   
// normal return only
+ * }
+ * </pre>
+ * A justified throw propagates untouched (defined behaviour); a broken
+ * implementation raises {@link org.apache.groovy.contracts.ThrowsIfViolation},
+ * never the declared exception. Non-woven arms with no {@code checked} flag
+ * anywhere generate nothing — runtime-retained metadata for tools.
+ *
+ * @since 6.0.0
+ * @see ThrowsIf
+ * @see ThrowsIfSupport
+ */
+@GroovyASTTransformation(phase = CompilePhase.SEMANTIC_ANALYSIS)
+public class ThrowsIfASTTransformation implements ASTTransformation {
+
+    private static final String PROCESSED_KEY = 
"org.apache.groovy.contracts.THROWS_IF_PROCESSED";
+    private static final ClassNode THROWS_IF_TYPE = 
ClassHelper.make(ThrowsIf.class);
+    private static final ClassNode SUPPORT_TYPE = 
ClassHelper.make(ThrowsIfSupport.class);
+    private static final AtomicLong COUNTER = new AtomicLong();
+
+    /** One parsed arm of the method's {@code @ThrowsIf} set. */
+    private static final class Arm {
+        Expression condition;
+        String conditionText;
+        ClassNode exceptionType;
+        boolean woven;      // non-woven arms generate no guard (`direct` is 
tool metadata, unread here)
+        boolean checked;
+        boolean exhaustive;
+    }
+
+    @Override
+    public void visit(final ASTNode[] nodes, final SourceUnit source) {
+        if (nodes.length != 2) return;
+        if (!(nodes[0] instanceof AnnotationNode)) return;
+        if (!(nodes[1] instanceof MethodNode method)) return; // covers 
constructors (ConstructorNode)
+        if (method.isAbstract() || method.getCode() == null) return;
+        if (method.getNodeMetaData(PROCESSED_KEY) != null) return; // per-set 
semantics: fire once
+        method.putNodeMetaData(PROCESSED_KEY, Boolean.TRUE);
+
+        List<Arm> arms = collectArms(method);
+        if (arms.isEmpty()) return;
+
+        boolean anyChecked = arms.stream().anyMatch(a -> a.checked);
+        Statement originalBody = method.getCode();
+        List<Statement> prologue = new ArrayList<>();
+        String suffix = Long.toString(COUNTER.getAndIncrement());
+
+        if (!anyChecked) {
+            // Guards only, in declaration order.
+            for (Arm arm : arms) {
+                if (arm.woven) prologue.add(guardFor(arm, 
arm.condition.transformExpression(e -> e)));
+            }
+            if (prologue.isEmpty()) return;
+            prologue.add(originalBody);
+        } else {
+            // Snapshot every arm's condition on entry (the only-when 
judgement needs all of them),
+            // reuse the snapshots for the woven guards, then wrap the body 
with the checks.
+            List<Expression> heldVars = new ArrayList<>();
+            for (int i = 0; i < arms.size(); i++) {
+                VariableExpression held = localVarX("$_gc_ti_c" + i + "_" + 
suffix, ClassHelper.boolean_TYPE);
+                prologue.add(declS(held, 
boolX(arms.get(i).condition.transformExpression(e -> e))));
+                heldVars.add(varX(held));
+            }
+            for (int i = 0; i < arms.size(); i++) {
+                Arm arm = arms.get(i);
+                if (arm.woven) prologue.add(guardFor(arm, heldVars.get(i)));
+            }
+            boolean allExhaustive = arms.stream().allMatch(a -> a.exhaustive);
+            List<Expression> types = new ArrayList<>();
+            List<Expression> texts = new ArrayList<>();
+            List<Expression> unwovenHeld = new ArrayList<>();
+            List<Expression> unwovenTexts = new ArrayList<>();
+            for (int i = 0; i < arms.size(); i++) {
+                Arm arm = arms.get(i);
+                types.add(classX(arm.exceptionType));
+                texts.add(constX(arm.conditionText));
+                if (!arm.woven && arm.checked) {                  // woven 
arms hold must-throw by construction
+                    unwovenHeld.add(heldVars.get(i));
+                    unwovenTexts.add(constX(arm.conditionText));
+                }
+            }
+            VariableExpression thrown = localVarX("$_gc_ti_t_" + suffix, 
ClassHelper.make(Throwable.class));
+            prologue.add(declS(thrown, nullX()));
+            Parameter caught = new 
Parameter(ClassHelper.make(Throwable.class), "$_gc_ti_caught_" + suffix);
+            Statement catchBody = block(
+                    assignS(varX(thrown), varX(caught)),
+                    throwS(callX(SUPPORT_TYPE, "onlyWhen", args(
+                            varX(caught),
+                            arrayOf(ClassHelper.boolean_TYPE, heldVars),
+                            
arrayOf(ClassHelper.CLASS_Type.getPlainNodeReference(), types),
+                            arrayOf(ClassHelper.STRING_TYPE, texts),
+                            constX(allExhaustive, true)))));
+            Statement finallyBody = ifS(equalsNullX(varX(thrown)),
+                    stmt(callX(SUPPORT_TYPE, "mustThrow", args(
+                            arrayOf(ClassHelper.boolean_TYPE, unwovenHeld),
+                            arrayOf(ClassHelper.STRING_TYPE, unwovenTexts)))));
+            TryCatchStatement wrapper = new TryCatchStatement(originalBody, 
block(finallyBody));
+            wrapper.addCatch(new CatchStatement(caught, catchBody));
+            prologue.add(wrapper);
+        }
+
+        BlockStatement newBody = block(prologue.toArray(Statement[]::new));
+        newBody.setSourcePosition(originalBody);
+        method.setCode(newBody);

Review Comment:
   In constructors, the implicit/explicit `super(...)`/`this(...)` call must 
remain the first statement. As written, the woven guard/prologue statements 
will precede the constructor call (either directly, or by nesting the original 
body), which can break compilation and/or constructor semantics.



##########
subprojects/groovy-contracts/src/main/java/org/apache/groovy/contracts/ThrowsIfViolation.java:
##########
@@ -0,0 +1,51 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.groovy.contracts;
+
+/**
+ * Thrown whenever a {@code @ThrowsIf} exceptional-contract violation is 
detected
+ * by a {@code checked} arm-set: the method returned normally although a
+ * must-throw condition held, or threw a matching exception although no arm's
+ * condition held (the latter checked only for {@code exhaustive} arm-sets).
+ * <p>
+ * Deliberately <em>not</em> the arm's declared exception: the declared 
exception
+ * is defined behaviour delivered at method entry, whereas this violation 
reports
+ * a broken <em>implementation</em> of the contract — late-throwing the 
declared
+ * exception would masquerade the bug as defined behaviour.
+ *
+ * @see AssertionViolation
+ * @since 6.0.0
+ */
+public class ThrowsIfViolation extends AssertionViolation {
+
+    /**
+     * Creates a throws-if violation without an explicit detail message.
+     */
+    public ThrowsIfViolation() {
+    }
+
+    /**
+     * Creates a throws-if violation with an object-valued detail.
+     *
+     * @param o the detail object
+     */
+    public ThrowsIfViolation(Object o) {
+        super(o);
+    }

Review Comment:
   `ThrowsIfViolation` only exposes the no-arg and `Object` constructors, while 
other `AssertionViolation` subclasses in this module also expose the primitive 
overloads (boolean/char/int/long/float/double). Keeping the constructor surface 
consistent makes it easier to construct violations the same way across contract 
kinds.



##########
subprojects/groovy-contracts/src/main/java/org/apache/groovy/contracts/ast/ThrowsIfASTTransformation.java:
##########
@@ -0,0 +1,260 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.groovy.contracts.ast;
+
+import groovy.contracts.ThrowsIf;
+import org.apache.groovy.contracts.ThrowsIfSupport;
+import org.codehaus.groovy.ast.ASTNode;
+import org.codehaus.groovy.ast.AnnotationNode;
+import org.codehaus.groovy.ast.ClassHelper;
+import org.codehaus.groovy.ast.ClassNode;
+import org.codehaus.groovy.ast.MethodNode;
+import org.codehaus.groovy.ast.Parameter;
+import org.codehaus.groovy.ast.expr.ArrayExpression;
+import org.codehaus.groovy.ast.expr.ClassExpression;
+import org.codehaus.groovy.ast.expr.ClosureExpression;
+import org.codehaus.groovy.ast.expr.ConstantExpression;
+import org.codehaus.groovy.ast.expr.Expression;
+import org.codehaus.groovy.ast.expr.VariableExpression;
+import org.codehaus.groovy.ast.stmt.BlockStatement;
+import org.codehaus.groovy.ast.stmt.CatchStatement;
+import org.codehaus.groovy.ast.stmt.Statement;
+import org.codehaus.groovy.ast.stmt.TryCatchStatement;
+import org.codehaus.groovy.control.CompilePhase;
+import org.codehaus.groovy.control.SourceUnit;
+import org.codehaus.groovy.transform.ASTTransformation;
+import org.codehaus.groovy.transform.GroovyASTTransformation;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicLong;
+
+import static org.codehaus.groovy.ast.tools.GeneralUtils.args;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.assignS;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.block;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.boolX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.callX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.classX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.constX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.ctorX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.declS;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.equalsNullX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.ifS;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.localVarX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.nullX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.stmt;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.throwS;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.varX;
+
+/**
+ * Handles a method's {@link ThrowsIf} arm-set. Processed once per method (the
+ * transformation fires per annotation, so repeats are deduplicated via node
+ * metadata) because the semantics are per-<em>set</em>: guards are generated 
in
+ * declaration order, and the {@code checked} wrapper judges an escaping throw
+ * against <em>every</em> arm.
+ * <p>
+ * For each {@code WOVEN}-origin arm the guard-throw is generated at method
+ * entry, so that conceptually:
+ * <pre>
+ * &#64;ThrowsIf(value = { b == 0 }, exception = ArithmeticException)
+ * int divide(int a, int b) { a.intdiv(b) }
+ * </pre>
+ * compiles as:
+ * <pre>
+ * int divide(int a, int b) {
+ *     if (b == 0) throw new ArithmeticException('@ThrowsIf: b == 0')
+ *     a.intdiv(b)
+ * }
+ * </pre>
+ * — the general form of the pattern {@code groovy.transform.NullCheck} 
provides
+ * for the null-check special case. Arms with {@code woven = false} generate no
+ * guard — the throw already exists, in the body ({@code direct = true}, the
+ * default) or in invoked code ({@code direct = false}); identical bytecode, 
the
+ * {@code direct} distinction is information for tools and is not read here.
+ * <p>
+ * When any arm is {@code checked}, the conditions are additionally snapshot on
+ * entry and the body is wrapped so that, conceptually:
+ * <pre>
+ * boolean $c0 = &lt;cond0&gt;; ...                                 // entry 
snapshot, every arm
+ * if ($c0) throw new E0(...)                                  // woven arms, 
in declaration order
+ * Throwable $t = null
+ * try { &lt;original body&gt; }
+ * catch (Throwable caught) {
+ *     $t = caught
+ *     throw ThrowsIfSupport.onlyWhen(caught, [$c0, ...], [E0, ...], [texts], 
allExhaustive)
+ * } finally {
+ *     if ($t == null) ThrowsIfSupport.mustThrow([unwoven $ci ...], [texts])   
// normal return only
+ * }
+ * </pre>
+ * A justified throw propagates untouched (defined behaviour); a broken
+ * implementation raises {@link org.apache.groovy.contracts.ThrowsIfViolation},
+ * never the declared exception. Non-woven arms with no {@code checked} flag
+ * anywhere generate nothing — runtime-retained metadata for tools.
+ *
+ * @since 6.0.0
+ * @see ThrowsIf
+ * @see ThrowsIfSupport
+ */
+@GroovyASTTransformation(phase = CompilePhase.SEMANTIC_ANALYSIS)
+public class ThrowsIfASTTransformation implements ASTTransformation {
+
+    private static final String PROCESSED_KEY = 
"org.apache.groovy.contracts.THROWS_IF_PROCESSED";
+    private static final ClassNode THROWS_IF_TYPE = 
ClassHelper.make(ThrowsIf.class);
+    private static final ClassNode SUPPORT_TYPE = 
ClassHelper.make(ThrowsIfSupport.class);
+    private static final AtomicLong COUNTER = new AtomicLong();
+
+    /** One parsed arm of the method's {@code @ThrowsIf} set. */
+    private static final class Arm {
+        Expression condition;
+        String conditionText;
+        ClassNode exceptionType;
+        boolean woven;      // non-woven arms generate no guard (`direct` is 
tool metadata, unread here)
+        boolean checked;
+        boolean exhaustive;
+    }
+
+    @Override
+    public void visit(final ASTNode[] nodes, final SourceUnit source) {
+        if (nodes.length != 2) return;
+        if (!(nodes[0] instanceof AnnotationNode)) return;
+        if (!(nodes[1] instanceof MethodNode method)) return; // covers 
constructors (ConstructorNode)
+        if (method.isAbstract() || method.getCode() == null) return;
+        if (method.getNodeMetaData(PROCESSED_KEY) != null) return; // per-set 
semantics: fire once
+        method.putNodeMetaData(PROCESSED_KEY, Boolean.TRUE);
+
+        List<Arm> arms = collectArms(method);
+        if (arms.isEmpty()) return;
+
+        boolean anyChecked = arms.stream().anyMatch(a -> a.checked);
+        Statement originalBody = method.getCode();
+        List<Statement> prologue = new ArrayList<>();
+        String suffix = Long.toString(COUNTER.getAndIncrement());
+
+        if (!anyChecked) {
+            // Guards only, in declaration order.
+            for (Arm arm : arms) {
+                if (arm.woven) prologue.add(guardFor(arm, 
arm.condition.transformExpression(e -> e)));
+            }
+            if (prologue.isEmpty()) return;
+            prologue.add(originalBody);
+        } else {
+            // Snapshot every arm's condition on entry (the only-when 
judgement needs all of them),
+            // reuse the snapshots for the woven guards, then wrap the body 
with the checks.
+            List<Expression> heldVars = new ArrayList<>();
+            for (int i = 0; i < arms.size(); i++) {
+                VariableExpression held = localVarX("$_gc_ti_c" + i + "_" + 
suffix, ClassHelper.boolean_TYPE);
+                prologue.add(declS(held, 
boolX(arms.get(i).condition.transformExpression(e -> e))));
+                heldVars.add(varX(held));
+            }
+            for (int i = 0; i < arms.size(); i++) {
+                Arm arm = arms.get(i);
+                if (arm.woven) prologue.add(guardFor(arm, heldVars.get(i)));
+            }
+            boolean allExhaustive = arms.stream().allMatch(a -> a.exhaustive);
+            List<Expression> types = new ArrayList<>();
+            List<Expression> texts = new ArrayList<>();
+            List<Expression> unwovenHeld = new ArrayList<>();
+            List<Expression> unwovenTexts = new ArrayList<>();
+            for (int i = 0; i < arms.size(); i++) {
+                Arm arm = arms.get(i);
+                types.add(classX(arm.exceptionType));
+                texts.add(constX(arm.conditionText));
+                if (!arm.woven && arm.checked) {                  // woven 
arms hold must-throw by construction
+                    unwovenHeld.add(heldVars.get(i));
+                    unwovenTexts.add(constX(arm.conditionText));
+                }

Review Comment:
   When any arm is `checked`, the transformation snapshots *every* arm and 
enables runtime checking for the arm-set; the must-throw verification should 
therefore include all **unwoven** arms, not just those whose individual 
annotation instance has `checked=true`. Otherwise an unwoven arm can silently 
opt out of the must-throw check even though the arm-set is in checked mode.





> groovy-contracts: Add a ThrowsIf annotation
> -------------------------------------------
>
>                 Key: GROOVY-12135
>                 URL: https://issues.apache.org/jira/browse/GROOVY-12135
>             Project: Groovy
>          Issue Type: Improvement
>            Reporter: Paul King
>            Assignee: Paul King
>            Priority: Major
>
> h2. Summary
> groovy-contracts covers Eiffel's *normal-behaviour* contracts — 
> {{@Requires}}, {{@Ensures}}, {{@Invariant}} — and nothing exceptional. Yet 
> one of the most common contract-shaped code Groovy programmers write by hand 
> is the *guard clause*: test an argument, throw {{IllegalArgumentException}} / 
> {{NullPointerException}} / {{ArithmeticException}}. This proposes one 
> annotation to make that behaviour a first-class, machine-readable, optionally 
> *woven* contract:
> {code:groovy}
> @ThrowsIf(value = { b == 0 }, exception = ArithmeticException)
> int divide(int a, int b) { a.intdiv(b) }
> {code}
> read as an *iff*: the method throws {{ArithmeticException}} _exactly when_ 
> {{b == 0}} — it must throw when the condition holds, and may not throw that 
> exception otherwise. With the default {{woven = true}}, groovy-contracts 
> *generates* the guard at method entry — the general form of a pattern Groovy 
> core already ships: {{groovy.transform.NullCheck}} weaves exactly this kind 
> of guard for the null-check special case (as does Lombok's {{@NonNull}} for 
> Java). The annotation is the implementation, not a comment about one.
> h2. Motivation
> # *The missing quadrant.* Design-by-Contract in the Eiffel lineage specifies 
> the happy path; exceptional behaviour is left to javadoc prose. JML closed 
> this gap for Java with {{signals}} clauses; groovy-contracts has no analogue. 
> Guard clauses are today boilerplate ({{if (b == 0) throw ...}}, 
> {{Objects.requireNonNull\(y)}}, Guava {{Preconditions}}) — behaviour every 
> caller depends on, in a form no tool can consume.
> # *A {{@Requires}} is the wrong tool for it.* A precondition says _the caller 
> must not do this_ — violating it is the caller's bug. A guard throw says 
> _this input is handled, by throwing_ — it is *defined behaviour* callers may 
> rely on (and catch). The JDK's own culture is overwhelmingly defined-throws, 
> not preconditions: {{Math.floorDiv}} on a zero divisor _throws_, by 
> specification.
> # *Machine-readable exceptional behaviour — the AI-agent case.* To answer 
> "when does this method throw?" a tool or AI coding agent must today parse 
> javadoc prose, traverse the body for guarded throw sites (transitively), or 
> read checked-exception signatures (types only, unchecked exceptions 
> invisible). None is reliable; javadoc rots silently. An {{@ThrowsIf}} arm is 
> one structured line — condition, type, direction — and because it is 
> enforced, it cannot drift from the code without failing a build.
> # *It composes with verification but does not require it.* The design 
> originates in groovy-verify (an SMT-backed verifier on the {{@TypeChecked}} 
> extension SPI), which statically proves both directions of the iff — but 
> every feature proposed here has standalone runtime value.
> h2. Proposed annotation
> {code:groovy}
> @Repeatable(ThrowsIfConditions)
> @Retention(RetentionPolicy.RUNTIME)
> @Target([ElementType.METHOD, ElementType.CONSTRUCTOR])
> @interface ThrowsIf {
>     Class value()                        // the condition, a closure over the 
> parameters: { b == 0 }
>     Class exception() default Throwable  // the exception type thrown when 
> the condition holds
>     boolean woven() default true         // true: generate the guard-throw at 
> method entry;
>                                          // false: the throw already exists — 
> nothing is generated
>     boolean direct() default true        // information for tools, no effect 
> on bytecode; ignored
>                                          // (implicitly true) for woven arms. 
> true: a hand-written
>                                          // throw statement lives in this 
> body; false: the exception
>                                          // arises from code this method 
> executes (a call, possibly
>                                          // transitive — e.g. a third-party 
> library — or a runtime
>                                          // operation), so there is no throw 
> statement here to find
>     boolean exhaustive() default true    // no bytecode effect on its own; 
> under checked = true it
>                                          // gates the escaping-throw check — 
> false says "this method
>                                          // may throw the same exception for 
> other, unlisted reasons",
>                                          // so escaping throws are never 
> judged
>     boolean checked() default false      // true: verify the contract at 
> runtime in the assertion
>                                          // style of @Ensures — see Checking 
> below
> }
> {code}
> with {{ThrowsIfConditions}} the standard repeatable container. Conditions 
> accept the same closure conventions as {{@Requires}} (bare free variables 
> resolve to parameters).
> The first question a Groovy developer asks of an annotation is _what changes 
> in the generated code?_ — so, member by member:
> || member || effect on generated code ||
> | {{woven}} | {{true}} (default): the guard — {{if (cond) throw new 
> E('@ThrowsIf: cond')}} — is inserted at method entry. {{false}}: nothing is 
> generated. |
> | {{checked}} | {{true}}: the verification wrapper is generated — condition 
> snapshots at entry, the body wrapped in try/catch/finally (see Checking). 
> {{false}} (default): nothing. |
> | {{exception}} | the type the woven guard constructs, and the type the 
> checked wrapper matches escaping throws against. |
> | {{exhaustive}} | nothing on its own. Under {{checked = true}} it gates the 
> catch-side check: {{true}} (default) means an escaping matching exception 
> with no condition held is a violation; {{false}} means escaping throws are 
> never judged — the method may throw the same exception for unlisted reasons. 
> One {{false}} arm disclaims the check for the whole arm-set. |
> | {{direct}} | nothing, ever — pure information for readers and tools (see 
> below). |
> So exactly two members generate code ({{woven}}, {{checked}}); {{exhaustive}} 
> modulates what {{checked}} polices; {{direct}} and the unwoven/unchecked 
> cases are structured metadata.
> Two points about {{direct}} worth making explicit. First, it inverts the 
> usual annotation-attribute mental model: it has *no effect on bytecode* — 
> {{woven = false}} arms generate nothing either way — it is pure *information* 
> for readers and tools about where the existing throw is implemented, and 
> therefore where to look and who can verify it ({{direct = false}} tells an 
> analysis tool or AI agent not to traverse this body looking for a throw 
> statement, and is also why such arms are never woven: generating a guard from 
> a wrong claim about invoked code would silently change behaviour). Second, it 
> is designed for progressive disclosure: the defaults mean most users never 
> think about it — a developer writes {{woven = false}} for a hand-written 
> guard and {{direct = true}} is already right; the first contact with {{direct 
> = false}} is typically a verification tool reporting it cannot find the 
> promised throw in the body, at which moment the flag is meaningful. The 
> reference implementation's transform never reads {{direct}} at all — 
> "information only" is true by construction.
> *Semantics — the claims tools consume.* In specification terms an arm states 
> two directions: *must-throw* (condition on entry ⇒ the method throws, not 
> returns — implemented by the woven guard, or policed by the checked wrapper's 
> normal-return check) and *only-when* (a matching throw ⇒ some arm's condition 
> held — policed by the checked wrapper's catch-side check, and only when the 
> arm-set is {{exhaustive}}). The default is therefore an *iff*; {{exhaustive = 
> false}} is the one-directional (JML {{signals}}-style) form — the honest 
> choice when the full condition is unstatable: {{Integer.parseInt}}'s real 
> throw condition (malformed _or_ out of range) is beyond a parameter closure, 
> but {{s == null}} is a true sufficient half. Two deliberate non-claims: no 
> {{signals_only}} (an arm-set is exhaustive over the _conditions for the types 
> it mentions_, never over exception _types_ — an {{ArithmeticException}} arm 
> says nothing about whether the method can throw anything else), and multiple 
> arms are independent must-throws while only-when is a property of the whole 
> set.
> h2. The three modes, by example
> *Woven* (the default) — the Summary's {{divide}}: the annotation _is_ the 
> guard, generated at method entry.
> *Unwoven, direct* ({{woven = false}}) — the body already implements the 
> throw; the annotation is checkable documentation:
> {code:groovy}
> @ThrowsIf(value = { n < 0 }, exception = IllegalArgumentException, woven = 
> false)
> int fact(int n) {
>     if (n < 0) throw new IllegalArgumentException('negative')
>     ...
> }
> {code}
> *Unwoven, indirect* ({{woven = false, direct = false}}) — the throw 
> originates in code this method executes; the author documents behaviour they 
> do not implement, and there is no throw statement in this body to find:
> {code:groovy}
> @ThrowsIf(value = { y == null }, exception = NullPointerException, woven = 
> false, direct = false)
> Object myMethod(Object x, Object y) {
>     ...
>     Objects.requireNonNull(y)   // the library throws; the annotation records 
> the contract
>     ...
> }
> {code}
> The attributes are per-arm because real methods mix modes:
> {code:groovy}
> @ThrowsIf(value = { x == null }, exception = NullPointerException)            
>      // woven for me
> @ThrowsIf(value = { y == null }, exception = NullPointerException, woven = 
> false)  // I already guard y
> Object process(Object x, Object y) { ... }
> {code}
> h2. Checking — the runtime iff
> Weaving *implements* the contract; {{checked = true}} additionally *verifies* 
> it at runtime, in the assertion style of {{@Ensures}}: on a normal return, no 
> non-woven arm's condition may have held on entry (must-throw), and an 
> escaping exception matching some arm's type must be justified by a matching 
> arm's condition having held (only-when — checked only for {{exhaustive}} 
> arm-sets). Conceptually:
> {code:groovy}
> boolean $c0 = <cond0>; ...                                 // entry snapshot, 
> every arm
> if ($c0) throw new E0(...)                                 // woven arms, in 
> declaration order
> Throwable $t = null
> try { <original body> }
> catch (Throwable caught) {
>     $t = caught
>     throw ThrowsIfSupport.onlyWhen(caught, [$c0, ...], [E0, ...], [texts], 
> allExhaustive)
> } finally {
>     if ($t == null) ThrowsIfSupport.mustThrow([non-woven $ci ...], [texts])   
> // normal return only
> }
> {code}
> A broken implementation raises {{ThrowsIfViolation}} (an 
> {{AssertionViolation}} sibling of {{PostconditionViolation}}) — deliberately 
> *never* the declared exception, which is defined behaviour delivered at 
> entry; a justified throw always propagates to the caller untouched. Without 
> {{checked}}, the runtime enforces at most the must-throw half (by 
> construction, for woven arms) — the checked mode is what makes the iff real 
> at runtime, symmetric with {{@Requires}}/{{@Ensures}} being checked contracts 
> rather than documentation. A pleasant corollary: {{woven = false, direct = 
> false}} plus {{checked = true}} runtime-validates a claim about invoked 
> third-party code — a wrong specification is exposed, not silently believed. 
> If any arm is {{checked}}, the whole arm-set participates in the only-when 
> justification.
> h2. What users get with no verifier anywhere
> * *Declarative guard clauses* — the boilerplate every service method opens 
> with becomes one line, with a consistent auto-derived message. Groovy core 
> has already accepted this pattern once: {{groovy.transform.NullCheck}} is 
> precisely a woven guard-throw, specialised to null checks — {{@ThrowsIf}} 
> generalises it to arbitrary conditions and exception types (Lombok's 
> {{@NonNull}} is the same precedent on the Java side).
> * *A runtime-checked exceptional contract* — {{checked = true}} verifies both 
> directions of the iff in the same assertion style as {{@Ensures}}, including 
> validating {{direct = false}} claims about third-party callees.
> * *Exceptional behaviour as structured metadata* — RUNTIME-retained, 
> repeatable, reflectively consumable by doc generators, test generators, 
> static analysers, and AI coding agents. The {{direct}} member carries 
> information no other source provides reliably: whether a throw statement 
> exists in the body at all ({{direct = false}} tells an agent not to traverse 
> looking for one).
> * *A vocabulary distinction the ecosystem lacks* — {{@Requires}} = the 
> caller's obligation; {{@ThrowsIf}} = the method's defined exceptional 
> behaviour. Callers _may_ rely on a defined throw; they may _not_ rely on 
> precondition-violation behaviour.
> h2. Evidence
> The semantics are the stable survivor of several design rounds field-tested 
> in groovy-verify (observational vs generative weaving; a whole-method 
> {{@Trusted}} marker, rejected by the mixed-method case; iff-only, rejected by 
> {{parseInt}}; a {{trusted}} boolean whose name read as behavioural while its 
> meaning was informational, and a three-valued {{Origin}} enum that fixed that 
> at the cost of upfront cognitive load — both replaced by {{woven}}/{{direct}} 
> with progressive-disclosure defaults). A pinned corpus exercises every 
> direction — iff verification and both refutation directions, mixed-mode 
> methods, indirect arms including vacuity detection, {{exhaustive = false}} 
> tolerating unlisted throw reasons while must-throw stays enforced — and a 
> differential runtime rung cross-validates proved contracts against real 
> execution over an input grid. A reference implementation targeting 
> groovy-contracts directly accompanies this issue: the annotation pair, a 
> SEMANTIC_ANALYSIS local transform in the {{@Decreases}} style (guard weaving 
> in declaration order + the checked wrapper; {{direct}} deliberately unread), 
> {{ThrowsIfSupport}} / {{ThrowsIfViolation}}, and 18 tests including the 
> {{@Requires}} interaction cases and {{direct}}-ignored-for-woven-arms.
> h2. Compatibility
> Purely additive: a new annotation pair, no change to existing annotations or 
> weaving. The weaving-order question is settled empirically and pinned by 
> tests: the {{@Requires}} assertion weaves ahead of the {{@ThrowsIf}} 
> prologue, so an input violating both reports as {{PreconditionViolation}} (a 
> caller bug is judged first), and a precondition whose *evaluation* throws 
> surfaces raw — pre-body throws never reach the checked wrapper, so they 
> cannot be misjudged as contract violations ({{AssertionViolation}}s are 
> additionally passed straight through the checker as belt-and-braces). The one 
> deliberately tolerated redundancy: {{direct}} is ignored (implicitly true) 
> for woven arms, pinned by a test, rather than being an error — most users 
> should never have to think about it.
> h2. Open questions
> # *Inheritance* — does an override inherit arms, and what is the Liskov rule 
> for exceptional contracts? Deliberately undesigned; gc's existing 
> {{@Requires}}/{{@Ensures}} inheritance semantics should drive it.
> # *Default for {{checked}}* — {{false}} (opt-in overhead) as implemented, or 
> {{true}} for symmetry with {{@Requires}}/{{@Ensures}}, which check by default?
> # *Naming* — {{@ThrowsIf}} reads one-directional while the default is an iff. 
> {{@Signals}} would be actively misleading (JML's {{signals}} is the converse 
> direction); {{@ThrowsIff}} is accurate but unpronounceable. {{@ThrowsIf}} + 
> documented iff default is the least-bad. For the mode attributes, {{trusted}} 
> (behavioural-sounding), a three-valued {{Origin}} enum ({{WOVEN | BODY | 
> INVOKED}} — self-describing but upfront cognitive load), and 
> {{DIRECT/INDIRECT}} / {{EXPLICIT/IMPLICIT}} constant pairs were all 
> considered before landing on the {{woven}}/{{direct}} booleans with 
> progressive-disclosure defaults; better suggestions welcome.
> # *A possible follow-up, not part of this issue* — the same 
> {{woven}}/{{direct}} pair reads naturally on {{@Requires}} for preconditions 
> already enforced in-body ({{@Requires(value = \{ y != null }, woven = false, 
> direct = false)}} over an {{Objects.requireNonNull}}): the caller-obligation 
> claim, documented without double-weaving or changing the observed exception 
> type. That touches the classic precondition processor and its inheritance 
> machinery, so it deserves its own issue once this one settles.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to