• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pmd / pmd / 841

30 Sep 2026 06:23PM UTC coverage: 79.741% (-0.001%) from 79.742%
841

push

github

web-flow
[java] ExceptionAsFlowControl: a throw in a lambda invoked by the method it is passed to (#7118)

* [java] ExceptionAsFlowControl: a throw in a lambda invoked by the method it is passed to

#7105 stopped the walk at every lambda body. That fixed the false positive of
#4815, but it also stopped reporting a throw inside a lambda that the method it
is passed to calls itself. spring-framework's
LeakAwareNettyDataBufferFactory#checkForLeaks (v5.3.13, line 56) throws inside
Iterable.forEach and catches the exception right below.

The walk now continues through a lambda that is an argument of a call whose
method calls that argument: Iterable.forEach, Iterator.forEachRemaining,
Map.forEach, Optional.ifPresent/ifPresentOrElse, and the Stream methods that
take a lambda argument. A terminal operation calls the action while the call is
evaluated; an intermediate operation is evaluated by the terminal operation of
the same pipeline. Every other lambda stays opaque, so #4815 does not come back.

Signed-off-by: Eljees <3.14hell@gmail.com>

* Use compound invocation matcher and clarify release notes

* [java] Handle primitive streams in ExceptionAsFlowControl

---------

Signed-off-by: Eljees <3.14hell@gmail.com>

20278 of 26472 branches covered (76.6%)

Branch coverage included in aggregate %.

7 of 8 new or added lines in 1 file covered. (87.5%)

43610 of 53647 relevant lines covered (81.29%)

0.82 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.18
/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/rule/design/ExceptionAsFlowControlRule.java
1
/*
2
 * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
3
 */
4

5
package net.sourceforge.pmd.lang.java.rule.design;
6

7
import net.sourceforge.pmd.lang.java.ast.ASTArgumentList;
8
import net.sourceforge.pmd.lang.java.ast.ASTBodyDeclaration;
9
import net.sourceforge.pmd.lang.java.ast.ASTCatchClause;
10
import net.sourceforge.pmd.lang.java.ast.ASTLambdaExpression;
11
import net.sourceforge.pmd.lang.java.ast.ASTMethodCall;
12
import net.sourceforge.pmd.lang.java.ast.ASTThrowStatement;
13
import net.sourceforge.pmd.lang.java.ast.ASTTryStatement;
14
import net.sourceforge.pmd.lang.java.ast.JavaNode;
15
import net.sourceforge.pmd.lang.java.ast.internal.JavaAstUtils;
16
import net.sourceforge.pmd.lang.java.rule.AbstractJavaRulechainRule;
17
import net.sourceforge.pmd.lang.java.types.InvocationMatcher;
18
import net.sourceforge.pmd.lang.java.types.InvocationMatcher.CompoundInvocationMatcher;
19
import net.sourceforge.pmd.lang.java.types.JTypeMirror;
20

21
/**
22
 * Catches the use of exception statements as a flow control device.
23
 *
24
 * @author Will Sargent
25
 */
26
public class ExceptionAsFlowControlRule extends AbstractJavaRulechainRule {
27

28
    /**
29
     * Methods that call the action of a functional interface argument themselves,
30
     * so that the action runs while the call is evaluated and an exception thrown
31
     * by it is relayed to the caller. A throw statement in a lambda that is passed
32
     * to one of them therefore reaches a try statement around the call, exactly
33
     * like a throw statement outside a lambda. The stream methods that take a
34
     * lambda argument are in the list as well, since an intermediate operation is
35
     * evaluated by the terminal operation of the same pipeline.
36
     */
37
    private static final CompoundInvocationMatcher CALLS_THE_LAMBDA_ARGUMENT = InvocationMatcher.parseAll(
1 ✔
38
        "java.lang.Iterable#forEach(_*)",
39
        "java.util.Iterator#forEachRemaining(_*)",
40
        "java.util.Map#forEach(_*)",
41
        "java.util.Optional#ifPresent(_*)",
42
        "java.util.Optional#ifPresentOrElse(_*)",
43
        "java.util.stream.Stream#_(_*)",
44
        "java.util.stream.IntStream#_(_*)",
45
        "java.util.stream.LongStream#_(_*)",
46
        "java.util.stream.DoubleStream#_(_*)"
47
    );
48

49
    // TODO tests:
50
    //   - catch a supertype of the exception (unless this is unwanted)
51
    //   - throw statements with not just a new SomethingExpression, eg a method call returning an exception
52
    public ExceptionAsFlowControlRule() {
53
        super(ASTThrowStatement.class);
1 ✔
54
    }
1 ✔
55

56
    @Override
57
    public Object visit(ASTThrowStatement node, Object data) {
58
        JTypeMirror thrownType = node.getExpr().getTypeMirror();
1 ✔
59
        JavaNode parent = node.getParent();
1 ✔
60
        while (!(parent instanceof ASTBodyDeclaration)) {
1 ✔
61
            if (parent instanceof ASTLambdaExpression && !isLambdaArgumentCalled((ASTLambdaExpression) parent)) {
1 ✔
62
                // An exception thrown in a lambda body leaves the lambda, not
63
                // the enclosing method: it reaches whoever invokes the lambda,
64
                // which in general is not the try statement around it. See #4815.
65
                return null;
1 ✔
66
            }
67
            if (parent instanceof ASTCatchClause) {
1 ✔
68
                // if the exception is thrown in a catch block, then we
69
                // have to ignore the try stmt (jump past it).
70
                parent = parent.getParent().getParent();
1 ✔
71
                continue;
1 ✔
72
            }
73
            if (parent instanceof ASTTryStatement) {
1 ✔
74
                // maybe the exception is being caught here.
75
                for (ASTCatchClause catchClause : ((ASTTryStatement) parent).getCatchClauses()) {
1 ✔
76
                    if (catchClause.getParameter().getAllExceptionTypes().any(it -> thrownType.isSubtypeOf(it.getTypeMirror()))) {
1 ✔
77
                        if (!JavaAstUtils.isJustRethrowException(catchClause)) {
1 ✔
78
                            asCtx(data).addViolation(catchClause, node.getReportLocation().getStartLine());
1 ✔
79
                            return null;
1 ✔
80
                        } else {
81
                            break;
82
                        }
83
                    }
84
                }
1 ✔
85
            }
86
            parent = parent.getParent();
1 ✔
87
        }
88
        return null;
1 ✔
89
    }
90

91
    /**
92
     * Returns true if the lambda is an argument of a call that calls that argument
93
     * itself, so that an exception thrown by the lambda reaches a try statement
94
     * around the call. Every other lambda is opaque: it may be invoked later, on
95
     * another thread, or not at all.
96
     */
97
    private static boolean isLambdaArgumentCalled(ASTLambdaExpression lambda) {
98
        JavaNode argumentList = lambda.getParent();
1 ✔
99
        if (!(argumentList instanceof ASTArgumentList) || !(argumentList.getParent() instanceof ASTMethodCall)) {
1 !
NEW
100
            return false;
×
101
        }
102
        return CALLS_THE_LAMBDA_ARGUMENT.anyMatch(argumentList.getParent());
1 ✔
103
    }
104

105
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc