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

pmd / pmd / 110

07 Aug 2025 01:45PM UTC coverage: 78.461% (+0.04%) from 78.421%
110

push

github

adangel
Fix #4500: [java] Fix AvoidReassigningLoopVariablesRule to allow only simple assignments in the forReassign=skip case (#5930)

Merge pull request #5930 from UncleOwen:issue-4500

17854 of 23593 branches covered (75.67%)

Branch coverage included in aggregate %.

29 of 33 new or added lines in 1 file covered. (87.88%)

5 existing lines in 2 files now uncovered.

39140 of 49047 relevant lines covered (79.8%)

0.81 hits per line

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

94.74
/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/rule/internal/JavaRuleUtil.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.internal;
6

7
import static net.sourceforge.pmd.lang.java.types.JPrimitiveType.PrimitiveTypeKind.LONG;
8
import static net.sourceforge.pmd.util.CollectionUtil.immutableSetOf;
9

10
import java.io.InvalidObjectException;
11
import java.io.ObjectInputStream;
12
import java.io.ObjectStreamField;
13
import java.util.Set;
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;
19
import net.sourceforge.pmd.lang.java.ast.ASTAssignableExpr.ASTNamedReferenceExpr;
20
import net.sourceforge.pmd.lang.java.ast.ASTAssignmentExpression;
21
import net.sourceforge.pmd.lang.java.ast.ASTBodyDeclaration;
22
import net.sourceforge.pmd.lang.java.ast.ASTClassDeclaration;
23
import net.sourceforge.pmd.lang.java.ast.ASTConstructorCall;
24
import net.sourceforge.pmd.lang.java.ast.ASTExpression;
25
import net.sourceforge.pmd.lang.java.ast.ASTFieldDeclaration;
26
import net.sourceforge.pmd.lang.java.ast.ASTInfixExpression;
27
import net.sourceforge.pmd.lang.java.ast.ASTInitializer;
28
import net.sourceforge.pmd.lang.java.ast.ASTMethodCall;
29
import net.sourceforge.pmd.lang.java.ast.ASTMethodDeclaration;
30
import net.sourceforge.pmd.lang.java.ast.ASTNullLiteral;
31
import net.sourceforge.pmd.lang.java.ast.ASTThrowStatement;
32
import net.sourceforge.pmd.lang.java.ast.ASTTypeDeclaration;
33
import net.sourceforge.pmd.lang.java.ast.ASTUnaryExpression;
34
import net.sourceforge.pmd.lang.java.ast.ASTVariableId;
35
import net.sourceforge.pmd.lang.java.ast.Annotatable;
36
import net.sourceforge.pmd.lang.java.ast.BinaryOp;
37
import net.sourceforge.pmd.lang.java.ast.JModifier;
38
import net.sourceforge.pmd.lang.java.ast.JavaNode;
39
import net.sourceforge.pmd.lang.java.ast.ModifierOwner;
40
import net.sourceforge.pmd.lang.java.ast.ModifierOwner.Visibility;
41
import net.sourceforge.pmd.lang.java.ast.TypeNode;
42
import net.sourceforge.pmd.lang.java.ast.internal.JavaAstUtils;
43
import net.sourceforge.pmd.lang.java.symbols.JTypeDeclSymbol;
44
import net.sourceforge.pmd.lang.java.symbols.JVariableSymbol;
45
import net.sourceforge.pmd.lang.java.types.InvocationMatcher;
46
import net.sourceforge.pmd.lang.java.types.InvocationMatcher.CompoundInvocationMatcher;
47
import net.sourceforge.pmd.lang.java.types.JTypeMirror;
48
import net.sourceforge.pmd.lang.java.types.TypeTestUtil;
49

50
/**
51
 * Utilities shared between rules.
52
 */
53
public final class JavaRuleUtil {
54

55
    // this is a hacky way to do it, but let's see where this goes
56
    private static final CompoundInvocationMatcher KNOWN_PURE_METHODS = InvocationMatcher.parseAll(
1✔
57
        "_#toString()",
58
        "_#hashCode()",
59
        "_#equals(java.lang.Object)",
60
        "java.lang.String#_(_*)",
61
        // actually not all of them, probs only stream of some type
62
        // arg which doesn't implement Closeable...
63
        "java.util.stream.Stream#_(_*)",
64
        "java.util.Collection#contains(_)",
65
        "java.util.Collection#size()",
66
        "java.util.List#get(int)",
67
        "java.util.Map#get(_)",
68
        "java.lang.Iterable#iterator()",
69
        "java.lang.Comparable#compareTo(_)",
70
        "java.math.BigDecimal#_(_*)",
71
        "java.math.BigInteger#_(_*)",
72
        "java.time.temporal.Temporal#_(_*)",
73
        "java.time.Duration#_(_*)",
74
        "java.time.Period#_(_*)"
75
    );
76

77
    public static final Set<String> LOMBOK_ANNOTATIONS = immutableSetOf(
1✔
78
        "lombok.Data",
79
        "lombok.Getter",
80
        "lombok.Setter",
81
        "lombok.Value",
82
        "lombok.RequiredArgsConstructor",
83
        "lombok.AllArgsConstructor",
84
        "lombok.NoArgsConstructor",
85
        "lombok.Builder",
86
        "lombok.EqualsAndHashCode",
87
        "lombok.experimental.Delegate"
88
    );
89

90
    private JavaRuleUtil() {
91
        // utility class
92
    }
93

94

95
    /**
96
     * Return true if the given expression is enclosed in a zero check.
97
     * The expression must evaluate to a natural number (ie >= 0), so that
98
     * {@code e < 1} actually means {@code e == 0}.
99
     *
100
     * @param e Expression
101
     */
102
    public static boolean isZeroChecked(ASTExpression e) {
103
        JavaNode parent = e.getParent();
1✔
104
        if (parent instanceof ASTInfixExpression) {
1✔
105
            BinaryOp op = ((ASTInfixExpression) parent).getOperator();
1✔
106
            int checkLiteralAtIdx = 1 - e.getIndexInParent();
1✔
107
            JavaNode comparand = parent.getChild(checkLiteralAtIdx);
1✔
108
            int expectedValue;
109
            if (op == BinaryOp.NE || op == BinaryOp.EQ) {
1✔
110
                // e == 0, e != 0, symmetric
111
                expectedValue = 0;
1✔
112
            } else if (op == BinaryOp.LT || op == BinaryOp.GE) {
1✔
113
                // e < 1
114
                // 0 < e
115
                // e >= 1     (e != 0)
116
                // 1 >= e     (e == 0 || e == 1)
117
                // 0 >= e     (e == 0)
118
                // e >= 0     (true)
119
                expectedValue = checkLiteralAtIdx;
1✔
120
            } else if (op == BinaryOp.GT || op == BinaryOp.LE) {
1✔
121
                // 1 > e
122
                // e > 0
123

124
                // 1 <= e     (e != 0)
125
                // e <= 1     (e == 0 || e == 1)
126
                // e <= 0     (e == 0)
127
                // 0 <= e     (true)
128
                expectedValue = 1 - checkLiteralAtIdx;
1✔
129
            } else {
130
                return false;
1✔
131
            }
132

133
            return JavaAstUtils.isLiteralInt(comparand, expectedValue);
1✔
134
        }
135
        return false;
1✔
136
    }
137

138

139
    /**
140
     * Returns true if the expression is a stringbuilder (or stringbuffer)
141
     * append call, or a constructor call for one of these classes.
142
     *
143
     * <p>If it is a constructor call, returns false if this is a call to
144
     * the constructor with a capacity parameter.
145
     */
146
    public static boolean isStringBuilderCtorOrAppend(@Nullable ASTExpression e) {
147
        if (e instanceof ASTMethodCall) {
1✔
148
            ASTMethodCall call = (ASTMethodCall) e;
1✔
149
            if ("append".equals(call.getMethodName())) {
1✔
150
                ASTExpression qual = ((ASTMethodCall) e).getQualifier();
1✔
151
                return qual != null && isStringBufferOrBuilder(qual);
1✔
152
            }
153
        } else if (e instanceof ASTConstructorCall) {
1!
154
            return isStringBufferOrBuilder(((ASTConstructorCall) e).getTypeNode());
1✔
155
        }
156
        return false;
1✔
157
    }
158

159
    private static boolean isStringBufferOrBuilder(TypeNode node) {
160
        return TypeTestUtil.isExactlyA(StringBuilder.class, node)
1✔
161
            || TypeTestUtil.isExactlyA(StringBuffer.class, node);
1✔
162
    }
163

164
    /**
165
     * Returns true if the node is a utility class, according to this
166
     * custom definition.
167
     */
168
    public static boolean isUtilityClass(ASTTypeDeclaration node) {
169
        if (!node.isRegularClass()) {
1✔
170
            return false;
1✔
171
        }
172

173
        ASTClassDeclaration classNode = (ASTClassDeclaration) node;
1✔
174

175
        // A class with a superclass or interfaces should not be considered
176
        if (classNode.getSuperClassTypeNode() != null
1✔
177
            || !classNode.getSuperInterfaceTypeNodes().isEmpty()) {
1✔
178
            return false;
1✔
179
        }
180

181
        // A class without declarations shouldn't be reported
182
        boolean hasAny = false;
1✔
183

184
        for (ASTBodyDeclaration declNode : classNode.getDeclarations()) {
1✔
185
            if (declNode instanceof ASTFieldDeclaration
1✔
186
                || declNode instanceof ASTMethodDeclaration) {
187

188
                hasAny = isNonPrivate(declNode) && !JavaAstUtils.isMainMethod(declNode);
1✔
189
                if (!((ModifierOwner) declNode).hasModifiers(JModifier.STATIC)) {
1✔
190
                    return false;
1✔
191
                }
192

193
            } else if (declNode instanceof ASTInitializer) {
1✔
194
                if (!((ASTInitializer) declNode).isStatic()) {
1✔
195
                    return false;
1✔
196
                }
197
            }
198
        }
1✔
199

200
        return hasAny;
1✔
201
    }
202

203
    private static boolean isNonPrivate(ASTBodyDeclaration decl) {
204
        return ((ModifierOwner) decl).getVisibility() != Visibility.V_PRIVATE;
1✔
205
    }
206

207
    /**
208
     * Whether the name may be ignored by unused rules like UnusedAssignment.
209
     */
210
    public static boolean isExplicitUnusedVarName(String name) {
211
        return name.startsWith("ignored")
1✔
212
            || name.startsWith("unused")
1✔
213
            // before java 9 it's ok, after that, "_" is a reserved keyword
214
            // with Java 21 Preview (JEP 443), "_" means explicitly unused
215
            || "_".equals(name);
1✔
216
    }
217

218
    /**
219
     * Returns true if the string has the given word as a strict prefix.
220
     * There needs to be a camelcase word boundary after the prefix.
221
     *
222
     * <code>
223
     * startsWithCamelCaseWord("getter", "get") == false
224
     * startsWithCamelCaseWord("get", "get")    == false
225
     * startsWithCamelCaseWord("getX", "get")   == true
226
     * </code>
227
     *
228
     * @param camelCaseString A string
229
     * @param prefixWord      A prefix
230
     */
231
    public static boolean startsWithCamelCaseWord(String camelCaseString, String prefixWord) {
232
        return camelCaseString.startsWith(prefixWord)
1✔
233
            && camelCaseString.length() > prefixWord.length()
1✔
234
            && Character.isUpperCase(camelCaseString.charAt(prefixWord.length()));
1✔
235
    }
236

237

238
    /**
239
     * Returns true if the string has the given word as a word, not at the start.
240
     * There needs to be a camelcase word boundary after the prefix.
241
     *
242
     * <code>
243
     * containsCamelCaseWord("isABoolean", "Bool") == false
244
     * containsCamelCaseWord("isABoolean", "A")    == true
245
     * containsCamelCaseWord("isABoolean", "is")   == error (not capitalized)
246
     * </code>
247
     *
248
     * @param camelCaseString A string
249
     * @param capitalizedWord A word, non-empty, capitalized
250
     *
251
     * @throws AssertionError If the word is empty or not capitalized
252
     */
253
    public static boolean containsCamelCaseWord(String camelCaseString, String capitalizedWord) {
254
        assert capitalizedWord.length() > 0 && Character.isUpperCase(capitalizedWord.charAt(0))
1✔
255
            : "Not a capitalized string \"" + capitalizedWord + "\"";
256

257
        int index = camelCaseString.indexOf(capitalizedWord);
1✔
258
        if (index >= 0 && camelCaseString.length() > index + capitalizedWord.length()) {
1✔
259
            return Character.isUpperCase(camelCaseString.charAt(index + capitalizedWord.length()));
1✔
260
        }
261
        return index >= 0 && camelCaseString.length() == index + capitalizedWord.length();
1!
262
    }
263

264
    public static boolean isGetterOrSetterCall(ASTMethodCall call) {
265
        return isGetterCall(call) || isSetterCall(call);
1✔
266
    }
267

268
    private static boolean isSetterCall(ASTMethodCall call) {
269
        return call.getArguments().size() > 0 && startsWithCamelCaseWord(call.getMethodName(), "set");
1✔
270
    }
271

272
    public static boolean isGetterCall(ASTMethodCall call) {
273
        return call.getArguments().isEmpty()
1✔
274
            && (startsWithCamelCaseWord(call.getMethodName(), "get")
1✔
275
            || startsWithCamelCaseWord(call.getMethodName(), "is"))
1✔
276
            && !call.getMethodType().getReturnType().isVoid();
1✔
277
    }
278

279
    public static boolean isGetterOrSetter(ASTMethodDeclaration node) {
280
        return isGetter(node) || isSetter(node);
1✔
281
    }
282

283
    /** Attempts to determine if the method is a getter. */
284
    private static boolean isGetter(ASTMethodDeclaration node) {
285

286
        if (node.getArity() != 0 || node.isVoid()) {
1✔
287
            return false;
1✔
288
        }
289

290
        ASTTypeDeclaration enclosing = node.getEnclosingType();
1✔
291
        if (startsWithCamelCaseWord(node.getName(), "get")) {
1✔
292
            return JavaAstUtils.hasField(enclosing, node.getName().substring(3));
1✔
293
        } else if (startsWithCamelCaseWord(node.getName(), "is")
1✔
294
                && TypeTestUtil.isA(boolean.class, node.getResultTypeNode())) {
1✔
295
            return JavaAstUtils.hasField(enclosing, node.getName().substring(2));
1✔
296
        }
297

298
        return JavaAstUtils.hasField(enclosing, node.getName());
1✔
299
    }
300

301
    /** Attempts to determine if the method is a setter. */
302
    private static boolean isSetter(ASTMethodDeclaration node) {
303

304
        if (node.getArity() != 1 || !node.isVoid()) {
1✔
305
            return false;
1✔
306
        }
307

308
        ASTTypeDeclaration enclosing = node.getEnclosingType();
1✔
309

310
        if (startsWithCamelCaseWord(node.getName(), "set")) {
1✔
311
            return JavaAstUtils.hasField(enclosing, node.getName().substring(3));
1✔
312
        }
313

314
        return JavaAstUtils.hasField(enclosing, node.getName());
1✔
315
    }
316

317
    // TODO at least UnusedPrivateMethod has some serialization-related logic.
318

319
    /**
320
     * Whether some variable declared by the given node is a serialPersistentFields
321
     * (serialization-specific field).
322
     */
323
    public static boolean isSerialPersistentFields(final ASTFieldDeclaration field) {
324
        return field.hasModifiers(JModifier.FINAL, JModifier.STATIC, JModifier.PRIVATE)
1✔
325
            && field.getVarIds().any(it -> "serialPersistentFields".equals(it.getName()) && TypeTestUtil.isA(ObjectStreamField[].class, it));
1✔
326
    }
327

328
    /**
329
     * Whether some variable declared by the given node is a serialVersionUID
330
     * (serialization-specific field).
331
     */
332
    public static boolean isSerialVersionUID(ASTFieldDeclaration field) {
333
        return field.hasModifiers(JModifier.FINAL, JModifier.STATIC)
1✔
334
            && field.getVarIds().any(it -> "serialVersionUID".equals(it.getName()) && it.getTypeMirror().isPrimitive(LONG));
1✔
335
    }
336

337
    /**
338
     * True if the method is a {@code readObject} method defined for serialization.
339
     */
340
    public static boolean isSerializationReadObject(ASTMethodDeclaration node) {
341
        return node.getVisibility() == Visibility.V_PRIVATE
1✔
342
            && "readObject".equals(node.getName())
1✔
343
            && JavaAstUtils.hasExceptionList(node, InvalidObjectException.class)
1✔
344
            && JavaAstUtils.hasParameters(node, ObjectInputStream.class);
1✔
345
    }
346

347

348
    /**
349
     * Whether the node or one of its descendants is an expression with
350
     * side effects. Conservatively, any method call is a potential side-effect,
351
     * as well as assignments to fields or array elements. We could relax
352
     * this assumption with (much) more data-flow logic, including a memory model.
353
     *
354
     * <p>By default assignments to locals are not counted as side-effects,
355
     * unless the lhs is in the given set of symbols.
356
     *
357
     * @param node             A node
358
     * @param localVarsToTrack Local variables to track
359
     */
360
    public static boolean hasSideEffect(@Nullable JavaNode node, Set<? extends JVariableSymbol> localVarsToTrack) {
361
        return node != null && node.descendantsOrSelf()
1✔
362
                                   .filterIs(ASTExpression.class)
1✔
363
                                   .any(e -> hasSideEffectNonRecursive(e, localVarsToTrack));
1✔
364
    }
365

366
    /**
367
     * Returns true if the expression has side effects we don't track.
368
     * Does not recurse into sub-expressions.
369
     */
370
    private static boolean hasSideEffectNonRecursive(ASTExpression e, Set<? extends JVariableSymbol> localVarsToTrack) {
371
        if (e instanceof ASTAssignmentExpression) {
1✔
372
            ASTAssignableExpr lhs = ((ASTAssignmentExpression) e).getLeftOperand();
1✔
373
            return isNonLocalLhs(lhs) || JavaAstUtils.isReferenceToVar(lhs, localVarsToTrack);
1✔
374
        } else if (e instanceof ASTUnaryExpression) {
1✔
375
            ASTUnaryExpression unary = (ASTUnaryExpression) e;
1✔
376
            ASTExpression lhs = unary.getOperand();
1✔
377
            return !unary.getOperator().isPure()
1!
378
                && (isNonLocalLhs(lhs) || JavaAstUtils.isReferenceToVar(lhs, localVarsToTrack));
1!
379
        }
380

381
        // when there are throw statements,
382
        // then this side effect can never be observed in containing code,
383
        // because control flow jumps out of the method
384
        return e.ancestors(ASTThrowStatement.class).isEmpty()
1✔
385
                && (e instanceof ASTMethodCall && !isPure((ASTMethodCall) e)
1✔
386
                        || e instanceof ASTConstructorCall);
387
    }
388

389
    private static boolean isNonLocalLhs(ASTExpression lhs) {
390
        return lhs instanceof ASTArrayAccess || !JavaAstUtils.isReferenceToLocal(lhs);
1!
391
    }
392

393
    /**
394
     * Whether the invocation has no side-effects. Very conservative.
395
     */
396
    private static boolean isPure(ASTMethodCall call) {
397
        return isGetterCall(call) || KNOWN_PURE_METHODS.anyMatch(call);
1✔
398
    }
399

400
    /**
401
     * Whether the invocation has no side effects. Even more conservative than {@code isPure}.
402
     * Only checks methods in java.* packages because frameworks may define getter-like methods
403
     * with side effects (such as isEqualTo matcher in AssertJ).
404
     */
405
    public static boolean isKnownPure(ASTMethodCall call) {
406
        return isGetterCall(call)
1✔
407
                    && hasPureGetters(call.getMethodType().getDeclaringType())
1✔
408
            || KNOWN_PURE_METHODS.anyMatch(call) && !call.getMethodType().getReturnType().isVoid();
1✔
409
    }
410

411
    private static boolean hasPureGetters(JTypeMirror type) {
412
        JTypeDeclSymbol symbol = type.getSymbol();
1✔
413
        if (symbol == null) {
1!
UNCOV
414
            return false;
×
415
        }
416
        String pkg = symbol.getPackageName();
1✔
417
        return pkg.startsWith("java.")
1✔
418
            && !pkg.startsWith("java.nio") && !pkg.startsWith("java.net");
1!
419
    }
420

421
    public static @Nullable ASTVariableId getReferencedNode(ASTNamedReferenceExpr expr) {
UNCOV
422
        JVariableSymbol referencedSym = expr.getReferencedSym();
×
UNCOV
423
        return referencedSym == null ? null : referencedSym.tryGetNode();
×
424
    }
425

426
    /**
427
     * Checks whether the given node is annotated with any lombok annotation.
428
     * The node should be annotateable.
429
     *
430
     * @param node
431
     *            the Annotatable node to check
432
     * @return <code>true</code> if a lombok annotation has been found
433
     */
434
    public static boolean hasLombokAnnotation(Annotatable node) {
UNCOV
435
        return LOMBOK_ANNOTATIONS.stream().anyMatch(node::isAnnotationPresent);
×
436
    }
437

438
    /**
439
     * Returns true if the expression is a null check on the given variable.
440
     */
441
    public static boolean isNullCheck(ASTExpression expr, JVariableSymbol var) {
442
        return isNullCheck(expr, StablePathMatcher.matching(var));
1✔
443
    }
444

445
    public static boolean isNullCheck(ASTExpression expr, StablePathMatcher matcher) {
446
        if (expr instanceof ASTInfixExpression) {
1✔
447
            ASTInfixExpression condition = (ASTInfixExpression) expr;
1✔
448
            if (condition.getOperator().hasSamePrecedenceAs(BinaryOp.EQ)) {
1✔
449
                ASTNullLiteral nullLit = condition.firstChild(ASTNullLiteral.class);
1✔
450
                if (nullLit != null) {
1✔
451
                    return matcher.matches(JavaAstUtils.getOtherOperandIfInInfixExpr(nullLit));
1✔
452
                }
453
            }
454
        }
455
        return false;
1✔
456
    }
457

458
    /**
459
     * Returns true if the expr is in a null check (its parent is a null check).
460
     */
461
    public static boolean isNullChecked(ASTExpression expr) {
462
        if (expr.getParent() instanceof ASTInfixExpression) {
1✔
463
            ASTInfixExpression infx = (ASTInfixExpression) expr.getParent();
1✔
464
            if (infx.getOperator().hasSamePrecedenceAs(BinaryOp.EQ)) {
1✔
465
                return JavaAstUtils.getOtherOperandIfInInfixExpr(expr) instanceof ASTNullLiteral;
1✔
466
            }
467
        }
468
        return false;
1✔
469
    }
470

471

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

© 2026 Coveralls, Inc