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

pmd / pmd / 727

14 Aug 2026 06:44PM UTC coverage: 79.31% (+0.005%) from 79.305%
727

push

github

web-flow
[java] Fix #1287: GuardLogStatement false positive with a guard clause (#6920)

* [java] Fix #1287: GuardLogStatement false positive with a guard clause

hasGuard only looked for the guard among the ancestors of the log call, so the early-return style guard - a preceding sibling that leaves the block - was not recognized.

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

* [java] GuardLogStatement: let a guard clause cover nested log statements

Addresses the review on #6920.

- hasEarlyExitGuard now walks up the statement ancestors instead of looking
  only at the immediate siblings of the log statement, so an early-exit guard
  also covers log statements nested deeper than the guard itself - for example
  inside a loop that follows it. This is the case reported in review.
- alwaysExits uses children(...).last() and lets the recursive instanceof
  handle the null case, as suggested.

Test cases added:

- log inside a loop after the guard (the reported case)
- log nested several blocks deep after the guard
- a continue guard inside a loop does not guard a log after the loop
- a guard in a sibling if-block does not guard a later log statement

The last two are negative cases: they pin that walking up the ancestors does
not start swallowing real violations. Both already passed before this change,
so they fail if the walk is ever made too permissive.

* review

* [doc] Update release notes (#1287)

---------

Signed-off-by: Eljees <3.14hell@gmail.com>
Co-authored-by: Lukas Gräf <48957581+lukasgraef@users.noreply.github.com>
Co-authored-by: Lukas Gräf <lukas-graef@web.de>

19543 of 25614 branches covered (76.3%)

Branch coverage included in aggregate %.

20 of 21 new or added lines in 1 file covered. (95.24%)

42308 of 52372 relevant lines covered (80.78%)

0.82 hits per line

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

92.83
/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/rule/bestpractices/GuardLogStatementRule.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.bestpractices;
6

7
import static net.sourceforge.pmd.properties.PropertyFactory.stringListProperty;
8

9
import java.util.ArrayList;
10
import java.util.HashMap;
11
import java.util.List;
12
import java.util.Locale;
13
import java.util.Map;
14

15
import org.checkerframework.checker.nullness.qual.Nullable;
16

17
import net.sourceforge.pmd.lang.java.ast.ASTArrayAccess;
18
import net.sourceforge.pmd.lang.java.ast.ASTAssignableExpr.ASTNamedReferenceExpr;
19
import net.sourceforge.pmd.lang.java.ast.ASTBlock;
20
import net.sourceforge.pmd.lang.java.ast.ASTBreakStatement;
21
import net.sourceforge.pmd.lang.java.ast.ASTContinueStatement;
22
import net.sourceforge.pmd.lang.java.ast.ASTExpression;
23
import net.sourceforge.pmd.lang.java.ast.ASTExpressionStatement;
24
import net.sourceforge.pmd.lang.java.ast.ASTFieldAccess;
25
import net.sourceforge.pmd.lang.java.ast.ASTIfStatement;
26
import net.sourceforge.pmd.lang.java.ast.ASTLambdaExpression;
27
import net.sourceforge.pmd.lang.java.ast.ASTLiteral;
28
import net.sourceforge.pmd.lang.java.ast.ASTMethodCall;
29
import net.sourceforge.pmd.lang.java.ast.ASTMethodReference;
30
import net.sourceforge.pmd.lang.java.ast.ASTReturnStatement;
31
import net.sourceforge.pmd.lang.java.ast.ASTStatement;
32
import net.sourceforge.pmd.lang.java.ast.ASTThisExpression;
33
import net.sourceforge.pmd.lang.java.ast.ASTThrowStatement;
34
import net.sourceforge.pmd.lang.java.ast.ASTTypeExpression;
35
import net.sourceforge.pmd.lang.java.ast.ASTVariableAccess;
36
import net.sourceforge.pmd.lang.java.ast.QualifiableExpression;
37
import net.sourceforge.pmd.lang.java.ast.internal.JavaAstUtils;
38
import net.sourceforge.pmd.lang.java.rule.AbstractJavaRulechainRule;
39
import net.sourceforge.pmd.lang.java.types.TypeTestUtil;
40
import net.sourceforge.pmd.properties.PropertyDescriptor;
41
import net.sourceforge.pmd.reporting.RuleContext;
42

43
/**
44
 * Check that log.debug, log.trace, log.error, etc... statements are guarded by
45
 * some test expression on log.isDebugEnabled() or log.isTraceEnabled().
46
 *
47
 * @author Romain Pelisse - &lt;belaran@gmail.com&gt;
48
 * @author Heiko Rupp - &lt;hwr@pilhuhn.de&gt;
49
 * @author Tammo van Lessen - provided original XPath expression
50
 *
51
 */
52
public class GuardLogStatementRule extends AbstractJavaRulechainRule {
53
    /*
54
     * guard methods and log levels:
55
     *
56
     * log4j + apache commons logging (jakarta):
57
     * trace -> isTraceEnabled
58
     * debug -> isDebugEnabled
59
     * info  -> isInfoEnabled
60
     * warn  -> isWarnEnabled
61
     * error -> isErrorEnabled
62
     *
63
     *
64
     * java util:
65
     * log(Level.FINE) ->  isLoggable
66
     * finest ->  isLoggable
67
     * finer  ->  isLoggable
68
     * fine   ->  isLoggable
69
     * info   ->  isLoggable
70
     * warning -> isLoggable
71
     * severe  -> isLoggable
72
     */
73
    private static final PropertyDescriptor<List<String>> LOG_LEVELS =
1✔
74
            stringListProperty("logLevels")
1✔
75
                    .desc("LogLevels to guard")
1✔
76
                    .defaultValues("trace", "debug", "info", "warn", "error",
1✔
77
                                   "log", "finest", "finer", "fine", "info", "warning", "severe")
78
                    .build();
1✔
79

80
    private static final PropertyDescriptor<List<String>> GUARD_METHODS =
1✔
81
            stringListProperty("guardsMethods")
1✔
82
                    .desc("Method use to guard the log statement")
1✔
83
                    .defaultValues("isTraceEnabled", "isDebugEnabled", "isInfoEnabled", "isWarnEnabled", "isErrorEnabled", "isLoggable")
1✔
84
                    .build();
1✔
85

86
    private final Map<String, String> guardStmtByLogLevel = new HashMap<>(12);
1✔
87

88
    /*
89
     * java util methods, that need special handling, e.g. they require an argument, which
90
     * determines the log level
91
     */
92
    private static final String JAVA_UTIL_LOG_METHOD = "log";
93
    private static final String JAVA_UTIL_LOG_GUARD_METHOD = "isLoggable";
94

95
    public GuardLogStatementRule() {
96
        super(ASTExpressionStatement.class);
1✔
97
        definePropertyDescriptor(LOG_LEVELS);
1✔
98
        definePropertyDescriptor(GUARD_METHODS);
1✔
99
    }
1✔
100

101
    @Override
102
    public void start(RuleContext ctx) {
103
        extractProperties();
1✔
104
    }
1✔
105

106
    @Override
107
    public Object visit(ASTExpressionStatement node, Object data) {
108
        RuleContext ctx = (RuleContext) data;
1✔
109

110
        ASTExpression expr = node.getExpr();
1✔
111
        if (!(expr instanceof ASTMethodCall)) {
1✔
112
            return null;
1✔
113
        }
114

115
        ASTMethodCall methodCall = (ASTMethodCall) expr;
1✔
116
        String logLevel = getLogLevelName(methodCall);
1✔
117
        if (logLevel != null && guardStmtByLogLevel.containsKey(logLevel)) {
1✔
118
            if (needsGuard(methodCall) && !hasGuard(methodCall, logLevel)) {
1✔
119
                ctx.addViolation(node);
1✔
120
            }
121
        }
122
        return null;
1✔
123
    }
124

125
    @SuppressWarnings("PMD.SimplifyBooleanReturns")
126
    private boolean needsGuard(ASTMethodCall node) {
127
        if (node.getArguments().isEmpty()) {
1✔
128
            return false;
1✔
129
        }
130

131
        // get the message expression
132
        // it must either be a direct access (var / param access, lambda, method ref, etc.)
133
        // or a compile-time constant string to not require a guard
134
        int messageArg = getMessageArgIndex(node);
1✔
135
        ASTExpression messageExpr = node.getArguments().get(messageArg);
1✔
136
        if (!isDirectAccess(messageExpr) && !messageExpr.getConstFoldingResult().hasValue()) {
1✔
137
            return true;
1✔
138
        }
139

140
        // if any additional params are not a direct access or constant foldable, we need a guard
141
        return !areAdditionalParamsLowOverhead(node, messageArg + 1);
1✔
142
    }
143

144
    private boolean hasGuard(ASTMethodCall node, String logLevel) {
145
        for (ASTIfStatement ifStatement: node.ancestors(ASTIfStatement.class).take(2)) {
1✔
146
            if (ifStatement == null) {
1!
147
                return false;
×
148
            }
149
            if (containsGuardMethod(ifStatement, logLevel)) {
1✔
150
                return true;
1✔
151
            }
152
        }
1✔
153
        return hasEarlyExitGuard(node, logLevel);
1✔
154
    }
155

156
    /**
157
     * Recognizes the guard clause style, where the guard is not an ancestor of the log
158
     * statement, but an earlier statement of an enclosing block that exits that block.
159
     * Such a guard covers everything following it, however deeply nested.
160
     */
161
    private boolean hasEarlyExitGuard(ASTMethodCall node, String logLevel) {
162
        for (ASTStatement statement : node.ancestors(ASTStatement.class)) {
1✔
163
            if (statement.getParent() instanceof ASTBlock
1✔
164
                    && isGuardedBefore(statement, logLevel)) {
1✔
165
                return true;
1✔
166
            }
167
        }
1✔
168
        return false;
1✔
169
    }
170

171
    /**
172
     * Whether a statement preceding {@code statement} in its enclosing block is a
173
     * guard clause for {@code logLevel}.
174
     */
175
    private boolean isGuardedBefore(ASTStatement statement, String logLevel) {
176
        for (ASTStatement sibling : statement.getParent().children(ASTStatement.class)) {
1!
177
            if (sibling == statement) {
1✔
178
                return false;
1✔
179
            }
180
            if (sibling instanceof ASTIfStatement) {
1✔
181
                ASTIfStatement ifStatement = (ASTIfStatement) sibling;
1✔
182
                if (ifStatement.getElseBranch() == null
1!
183
                        && JavaAstUtils.isBooleanNegation(ifStatement.getCondition())
1✔
184
                        && containsGuardMethod(ifStatement, logLevel)
1!
185
                        && alwaysExits(ifStatement.getThenBranch())) {
1✔
186
                    return true;
1✔
187
                }
188
            }
189
        }
1✔
NEW
190
        return false;
×
191
    }
192

193
    private boolean alwaysExits(@Nullable ASTStatement statement) {
194
        if (statement instanceof ASTBlock) {
1✔
195
            return alwaysExits(statement.children(ASTStatement.class).last());
1✔
196
        }
197
        return statement instanceof ASTReturnStatement
1!
198
                || statement instanceof ASTThrowStatement
199
                || statement instanceof ASTContinueStatement
200
                || statement instanceof ASTBreakStatement;
201
    }
202

203
    /**
204
     * Determines the log level, that is used. It is either the called method name
205
     * itself or - in case java util logging is used, then it is the first argument of
206
     * the method call (if it exists).
207
     *
208
     * @param methodCall the method call
209
     *
210
     * @return the log level or <code>null</code> if it could not be determined
211
     */
212
    private @Nullable String getLogLevelName(ASTMethodCall methodCall) {
213
        String methodName = methodCall.getMethodName();
1✔
214
        if (!JAVA_UTIL_LOG_METHOD.equals(methodName)) {
1✔
215
            return methodName; // probably logger.warn(...)
1✔
216
        }
217

218
        return getJutilLogLevelInFirstArg(methodCall);
1✔
219
    }
220

221
    private int getMessageArgIndex(ASTMethodCall methodCall) {
222
        String methodName = methodCall.getMethodName();
1✔
223
        if (JAVA_UTIL_LOG_METHOD.equals(methodName)) {
1✔
224
            // LOGGER.log(Level.FINE, "m")
225
            return 1;
1✔
226
        }
227

228
        return 0;
1✔
229
    }
230

231
    private @Nullable String getJutilLogLevelInFirstArg(ASTMethodCall methodCall) {
232
        ASTExpression firstArg = methodCall.getArguments().toStream().get(0);
1✔
233
        if (TypeTestUtil.isA("java.util.logging.Level", firstArg) && firstArg instanceof ASTNamedReferenceExpr) {
1!
234
            return ((ASTNamedReferenceExpr) firstArg).getName().toLowerCase(Locale.ROOT);
1✔
235
        }
236
        return null;
1✔
237
    }
238

239
    private boolean areAdditionalParamsLowOverhead(ASTMethodCall call, int messageArgIndex) {
240
        // return true if the statement has limited overhead even if unguarded,
241
        // so that we can ignore it
242
        return call.getArguments().toStream()
1✔
243
                   .drop(messageArgIndex) // remove the level argument if needed
1✔
244
                   .all(it -> isDirectAccess(it) || it.getConstFoldingResult().hasValue());
1✔
245
    }
246

247
    private static boolean isDirectAccess(ASTExpression it) {
248
        final boolean isPermittedType = it instanceof ASTLiteral || it instanceof ASTLambdaExpression
1✔
249
                || it instanceof ASTVariableAccess || it instanceof ASTThisExpression
250
                || it instanceof ASTMethodReference || it instanceof ASTFieldAccess
251
                || it instanceof ASTArrayAccess;
252

253
        if (!isPermittedType) {
1✔
254
            return false;
1✔
255
        }
256

257
        if (it instanceof QualifiableExpression) {
1✔
258
            final ASTExpression qualifier = ((QualifiableExpression) it).getQualifier();
1✔
259

260
            // for array access, we also care about the index expression
261
            if (it instanceof ASTArrayAccess && !isDirectAccess(((ASTArrayAccess) it).getIndexExpression())) {
1✔
262
                return false;
1✔
263
            }
264

265
            return qualifier == null || qualifier instanceof ASTTypeExpression || isDirectAccess(qualifier);
1!
266
        }
267

268
        return true;
1✔
269
    }
270

271
    private void extractProperties() {
272
        if (guardStmtByLogLevel.isEmpty()) {
1✔
273

274
            List<String> logLevels = new ArrayList<>(super.getProperty(LOG_LEVELS));
1✔
275
            List<String> guardMethods = new ArrayList<>(super.getProperty(GUARD_METHODS));
1✔
276

277
            if (guardMethods.isEmpty() && !logLevels.isEmpty()) {
1!
278
                throw new IllegalArgumentException("Can't specify logLevels without specifying guardMethods.");
×
279
            }
280
            if (logLevels.size() > guardMethods.size()) {
1✔
281
                // reuse the last guardMethod for the remaining log levels
282
                int needed = logLevels.size() - guardMethods.size();
1✔
283
                String lastGuard = guardMethods.get(guardMethods.size() - 1);
1✔
284
                for (int i = 0; i < needed; i++) {
1✔
285
                    guardMethods.add(lastGuard);
1✔
286
                }
287
            }
288
            if (logLevels.size() != guardMethods.size()) {
1!
289
                throw new IllegalArgumentException("For each logLevel a guardMethod must be specified.");
×
290
            }
291

292
            buildGuardStatementMap(logLevels, guardMethods);
1✔
293
        }
294
    }
1✔
295

296
    private void buildGuardStatementMap(List<String> logLevels, List<String> guardMethods) {
297
        for (int i = 0; i < logLevels.size(); i++) {
1✔
298
            String logLevel = logLevels.get(i);
1✔
299
            if (guardStmtByLogLevel.containsKey(logLevel)) {
1✔
300
                String combinedGuard = guardStmtByLogLevel.get(logLevel);
1✔
301
                combinedGuard += "|" + guardMethods.get(i);
1✔
302
                guardStmtByLogLevel.put(logLevel, combinedGuard);
1✔
303
            } else {
1✔
304
                guardStmtByLogLevel.put(logLevel, guardMethods.get(i));
1✔
305
            }
306
        }
307
    }
1✔
308

309
    private boolean containsGuardMethod(ASTIfStatement ifStatement, String logLevel) {
310
        for (ASTMethodCall maybeAGuardCall : ifStatement.getCondition().descendantsOrSelf().filterIs(ASTMethodCall.class)) {
1✔
311
            String guardMethodName = maybeAGuardCall.getMethodName();
1✔
312
            // the guard is adapted to the actual log statement
313

314
            if (!guardStmtByLogLevel.get(logLevel).contains(guardMethodName)) {
1✔
315
                continue;
1✔
316
            }
317

318
            if (JAVA_UTIL_LOG_GUARD_METHOD.equals(guardMethodName)) {
1✔
319
                // java.util.logging: guard method with argument. Verify the log level
320
                if (logLevel.equals(getJutilLogLevelInFirstArg(maybeAGuardCall))) {
1✔
321
                    return true;
1✔
322
                }
323
            } else {
324
                return true;
1✔
325
            }
326

327
        }
1✔
328
        return false;
1✔
329
    }
330
}
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