Paul King created GROOVY-12463:
----------------------------------
Summary: groovy-contracts: postcondition and class invariant not
checked on a trailing implicit return when the method also has an explicit
early return
Key: GROOVY-12463
URL: https://issues.apache.org/jira/browse/GROOVY-12463
Project: Groovy
Issue Type: Bug
Components: groovy-contracts
Affects Versions: 6.0.0
Reporter: Paul King
h2. Problem
When a non-void method has an explicit {{return}} on one path and produces its
value through an implicit return on another, the implicit path skips the
contract checks. Neither the {{@Ensures}} postcondition nor the class
{{@Invariant}} is checked there, and the method returns an unchecked value.
{code:groovy}
import groovy.contracts.*
class H {
@Ensures({ result > 0 })
int implicitAfterEarly(boolean b) { if (b) return 1; -1 }
@Ensures({ result > 0 })
int explicitBoth(boolean b) { if (b) return 1; return -1 }
@Ensures({ result > 0 })
int ifElseImplicit(boolean b) { if (b) { 1 } else { -1 } }
}
def h = new H()
h.implicitAfterEarly(false) // returns -1, no PostconditionViolation
h.explicitBoth(false) // PostconditionViolation
h.ifElseImplicit(false) // PostconditionViolation
{code}
The same happens with {{@CompileStatic}} and with class invariants:
{code:groovy}
@Invariant({ count >= 0 })
class K {
int count = 0
int dec(boolean b) { if (b) return count; count -= 5; count }
}
new K().dec(false) // returns -5, no ClassInvariantViolation
{code}
This is easy to hit with guard clauses, a common style: return early for bad
input, then let the last expression be the result. Found while writing a
contract for a date parser of that shape, where a 31 February result was not
caught.
h2. Cause
{{AssertStatementCreationUtility.getReturnStatements}} collects the method's
explicit {{ReturnStatement}} nodes. It runs {{ReturnAdder}}, which turns
trailing expressions and value-producing if/else, switch and try statements
into returns, only when that list is empty. GROOVY-12129 introduced the
{{ReturnAdder}} delegation, but its tests do not cover a method that mixes the
two. With any explicit return present, the trailing expression stays an
expression statement, no assertion is woven before it, and the compiler adds
the return later, unchecked.
All three callers are affected:
* {{PostconditionGenerator}}: {{@Ensures}}.
* {{ClassInvariantGenerator}}: invariant checks on non-void methods.
* {{AnnotationClosureVisitor.recordNonNullReturnViolations}}: the {{return
null}} paths handed to NullChecker.
h2. Fix
Always run {{ReturnAdder}} before collecting the returns. {{ReturnAdder}}
performs the conversion the compiler applies later anyway and leaves existing
return statements unchanged. Applying it unconditionally does not change what
the method computes; it only makes every exit path visible to the weaving.
h2. Tests
Added to {{ImplicitReturnPostconditionTests}} and {{SimpleClassInvariantTests}}:
* trailing implicit return after an early return is checked (violation
detected);
* the returned values are unchanged by weaving (vacuous postcondition);
* trailing implicit if/else after an early return is checked;
* the same under {{@CompileStatic}}, with an {{Optional}} result;
* the class invariant is checked on the trailing implicit path.
Four of the five fail before the fix. The values test passes either way,
because the bug skipped the check without changing the value. With the fix, the
full groovy-contracts suite (364 tests) and groovy-typecheckers suite (356
tests) pass.
--
This message was sent by Atlassian Jira
(v8.20.10#820010)