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

pmd / pmd / 757

30 Aug 2026 05:00PM UTC coverage: 79.409% (+0.001%) from 79.408%
757

push

github

web-flow
[java] Fix #5732: UnnecessaryCast false positive for package-private members (#6993)

* [java] Fix #5732: UnnecessaryCast false positive for package-private members

Package-private methods and fields are not inherited by subclasses in another package, so a cast to the declaring type is required to access them.

* [java] Fix #5732: Clarify package-private visibility in UnnecessaryCast

Package-private members are inherited; the cast is required because they
are not accessible on a subtype in a different package. Also drop
redundant unresolved/null checks already handled in visit().

* Put the issue title in release notes

---------

Co-authored-by: Sören Glimm <git@uncleowen.de>

19688 of 25790 branches covered (76.34%)

Branch coverage included in aggregate %.

19 of 22 new or added lines in 1 file covered. (86.36%)

42606 of 52657 relevant lines covered (80.91%)

0.82 hits per line

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

88.97
/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/rule/codestyle/UnnecessaryCastRule.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.codestyle;
6

7
import static net.sourceforge.pmd.lang.java.ast.BinaryOp.ADD;
8
import static net.sourceforge.pmd.lang.java.ast.BinaryOp.AND;
9
import static net.sourceforge.pmd.lang.java.ast.BinaryOp.DIV;
10
import static net.sourceforge.pmd.lang.java.ast.BinaryOp.GE;
11
import static net.sourceforge.pmd.lang.java.ast.BinaryOp.GT;
12
import static net.sourceforge.pmd.lang.java.ast.BinaryOp.LE;
13
import static net.sourceforge.pmd.lang.java.ast.BinaryOp.LT;
14
import static net.sourceforge.pmd.lang.java.ast.BinaryOp.MOD;
15
import static net.sourceforge.pmd.lang.java.ast.BinaryOp.MUL;
16
import static net.sourceforge.pmd.lang.java.ast.BinaryOp.OR;
17
import static net.sourceforge.pmd.lang.java.ast.BinaryOp.SHIFT_OPS;
18
import static net.sourceforge.pmd.lang.java.ast.BinaryOp.SUB;
19
import static net.sourceforge.pmd.lang.java.ast.BinaryOp.XOR;
20
import static net.sourceforge.pmd.lang.java.ast.internal.JavaAstUtils.isInfixExprWithOperator;
21

22
import java.lang.reflect.Modifier;
23
import java.util.EnumSet;
24
import java.util.Set;
25

26
import org.checkerframework.checker.nullness.qual.NonNull;
27
import org.checkerframework.checker.nullness.qual.Nullable;
28

29
import net.sourceforge.pmd.lang.java.ast.ASTCastExpression;
30
import net.sourceforge.pmd.lang.java.ast.ASTConditionalExpression;
31
import net.sourceforge.pmd.lang.java.ast.ASTConstructorCall;
32
import net.sourceforge.pmd.lang.java.ast.ASTExpression;
33
import net.sourceforge.pmd.lang.java.ast.ASTFieldAccess;
34
import net.sourceforge.pmd.lang.java.ast.ASTInfixExpression;
35
import net.sourceforge.pmd.lang.java.ast.ASTLambdaExpression;
36
import net.sourceforge.pmd.lang.java.ast.ASTMethodCall;
37
import net.sourceforge.pmd.lang.java.ast.ASTMethodReference;
38
import net.sourceforge.pmd.lang.java.ast.ASTReturnStatement;
39
import net.sourceforge.pmd.lang.java.ast.BinaryOp;
40
import net.sourceforge.pmd.lang.java.ast.JavaNode;
41
import net.sourceforge.pmd.lang.java.ast.internal.JavaAstUtils;
42
import net.sourceforge.pmd.lang.java.ast.internal.PrettyPrintingUtil;
43
import net.sourceforge.pmd.lang.java.rule.AbstractJavaRulechainRule;
44
import net.sourceforge.pmd.lang.java.symbols.JAccessibleElementSymbol;
45
import net.sourceforge.pmd.lang.java.symbols.JClassSymbol;
46
import net.sourceforge.pmd.lang.java.symbols.JFieldSymbol;
47
import net.sourceforge.pmd.lang.java.types.JClassType;
48
import net.sourceforge.pmd.lang.java.types.JMethodSig;
49
import net.sourceforge.pmd.lang.java.types.JTypeMirror;
50
import net.sourceforge.pmd.lang.java.types.JTypeVar;
51
import net.sourceforge.pmd.lang.java.types.Substitution;
52
import net.sourceforge.pmd.lang.java.types.TypeConversion;
53
import net.sourceforge.pmd.lang.java.types.TypeOps;
54
import net.sourceforge.pmd.lang.java.types.TypeOps.Convertibility;
55
import net.sourceforge.pmd.lang.java.types.TypeTestUtil;
56
import net.sourceforge.pmd.lang.java.types.ast.ExprContext;
57
import net.sourceforge.pmd.lang.java.types.ast.ExprContext.ExprContextKind;
58

59
/**
60
 * Detects casts where the operand is already a subtype of the context
61
 * type, or may be converted to it implicitly.
62
 */
63
public class UnnecessaryCastRule extends AbstractJavaRulechainRule {
64

65
    private static final Set<BinaryOp> BINARY_PROMOTED_OPS =
1✔
66
        EnumSet.of(LE, GE, GT, LT, ADD, SUB, MUL, DIV, MOD, AND, OR, XOR);
1✔
67

68
    public UnnecessaryCastRule() {
69
        super(ASTCastExpression.class);
1✔
70
    }
1✔
71

72
    @Override
73
    public Object visit(ASTCastExpression castExpr, Object data) {
74
        ASTExpression operand = castExpr.getOperand();
1✔
75

76
        // eg in
77
        // Object o = (Integer) 1;
78

79
        @Nullable ExprContext context = castExpr.getConversionContext();        // Object
1✔
80
        JTypeMirror coercionType = castExpr.getCastType().getTypeMirror();      // Integer
1✔
81
        JTypeMirror operandType = operand.getTypeMirror();                      // int
1✔
82

83
        if (TypeOps.isUnresolvedOrNull(operandType)
1✔
84
            || TypeOps.isUnresolvedOrNull(coercionType)) {
1✔
85
            return null;
1✔
86
        }
87

88
        // Note that we assume that coercionType is convertible to
89
        // contextType because the code must compile
90

91
        if (operand instanceof ASTLambdaExpression || operand instanceof ASTMethodReference) {
1✔
92
            // Then the cast provides a target type for the expression (always).
93
            // We need to check the enclosing context, as if it's invocation we give up for now
94
            if (context.isMissing() || context.hasKind(ExprContextKind.INVOCATION)) {
1!
95
                // Then the cast may be used to determine the overload.
96
                // We need to treat the casted lambda as a whole unit.
97
                // todo see below
98
                return null;
1✔
99
            }
100

101
            // Since the code is assumed to compile we'll just assume that coercionType
102
            // is a functional interface.
103
            if (coercionType.equals(context.getTargetType())) {
1✔
104
                // then we also know that the context is functional
105
                reportCast(castExpr, data);
1✔
106
            }
107
            // otherwise the cast is narrowing, and removing it would
108
            // change the runtime class of the produced lambda.
109
            // Eg `SuperItf obj = (SubItf) ()-> {};`
110
            // If we remove the cast, even if it might compile,
111
            // the object will not implement SubItf anymore.
112
        } else if (isCastRequiredForMemberAccess(castExpr, operandType)) {
1✔
113
            // Package-private members are not accessible on a subtype in
114
            // a different package, so the cast is required to select the member.
115
            return null;
1✔
116
        } else if (isCastUnnecessary(castExpr, context, coercionType, operandType)) {
1✔
117
            reportCast(castExpr, data);
1✔
118
        } else if (castExpr.getParent() instanceof ASTMethodCall
1✔
119
                    && castExpr.getIndexInParent() == 0) {
1!
120
            JMethodSig methodType = ((ASTMethodCall) castExpr.getParent()).getMethodType();
1✔
121
            handleMethodCall(castExpr, methodType, operandType, data);
1✔
122
        }
123
        return null;
1✔
124
    }
125

126
    private void handleMethodCall(ASTCastExpression castExpr, JMethodSig methodType,
127
            JTypeMirror operandType, Object data) {
128
        boolean generic = methodType.getSymbol().getFormalParameters().stream()
1✔
129
            .anyMatch(fp -> isTypeExpression(fp.getTypeMirror(Substitution.EMPTY)));
1✔
130
        if (!generic) {
1✔
131
            JTypeMirror declaringType = methodType.getDeclaringType();
1✔
132
            JTypeMirror returnType = methodType.getSymbol().getReturnType(Substitution.EMPTY);
1✔
133
            if (isTypeExpression(returnType)) {
1✔
134
                // A raw cast changes a generic return type (e.g. PropertySerializer<T>
135
                // becomes the raw type). That can be required for a chained call
136
                // such as serializer().toString(value) on PropertyDescriptor<?>.
137
                JTypeMirror coercionType = castExpr.getCastType().getTypeMirror();
1✔
138
                ExprContext methodCtx = ((ASTMethodCall) castExpr.getParent()).getConversionContext();
1✔
139
                if (coercionType.isRaw()
1✔
140
                    && (methodCtx.isMissing() || methodCtx.hasKind(ExprContextKind.INVOCATION))) {
1✔
141
                    return;
1✔
142
                }
143
            } else {
1✔
144
                // declaring type of List<T>::size is List<T>, but since the return type
145
                // is not generic, it's enough to check that operand is a List
146
                declaringType = declaringType.getErasure();
1✔
147
            }
148
            if (TypeTestUtil.isA(declaringType, operandType)) {
1✔
149
                reportCast(castExpr, data);
1✔
150
            }
151
        }
152
    }
1✔
153

154
    private boolean isTypeExpression(JTypeMirror type) {
155
        return type.isGeneric() || type instanceof JTypeVar;
1✔
156
    }
157

158
    private boolean isCastUnnecessary(ASTCastExpression castExpr, @NonNull ExprContext context, JTypeMirror coercionType, JTypeMirror operandType) {
159
        if (isCastNarrowingEnclosingType(castExpr, coercionType)) {
1✔
160
            // e.g. (Outer<S, ?>.Inner) outer.new Inner() when outer is Outer<? super S, ?>
161
            return false;
1✔
162
        } else if (operandType.equals(coercionType)) {
1✔
163
            return true;
1✔
164
        } else if (context.isMissing()) {
1✔
165
            // then we have fewer violation conditions
166

167
            return !operandType.isBottom() // casts on a null literal are necessary
1!
168
                   && operandType.isSubtypeOf(coercionType)
1✔
169
                   && !isCastToRawType(coercionType, operandType)
1✔
170
                   // If the context is missing when the parent is a lambda,
171
                   // that means the body of the lambda is determining the return
172
                   // type of the lambda
173
                   && getLambdaParent(castExpr) == null;
1✔
174
        }
175

176
        return !isCastDeterminingContext(castExpr, context, coercionType, operandType)
1✔
177
            && castIsUnnecessaryToMatchContext(context, coercionType, operandType);
1✔
178
    }
179

180
    /**
181
     * Whether this cast is casting a non-raw type to a raw type.
182
     * This is part of the {@link Convertibility#bySubtyping()} relation,
183
     * and needs to be singled out as operations on the raw type
184
     * behave differently than on the non-raw type. In that case the
185
     * cast may be necessary to avoid compile-errors, even though it
186
     * will be noop at runtime (an _unchecked_ cast).
187
     */
188
    private boolean isCastToRawType(JTypeMirror coercionType, JTypeMirror operandType) {
189
        return coercionType.isRaw() && !operandType.isRaw();
1!
190
    }
191

192
    /**
193
     * Whether this cast changes the enclosing type arguments of a
194
     * qualified inner-class instance creation. The type of
195
     * {@code outer.new Inner()} is determined by {@code outer}, so a
196
     * cast to {@code Outer<S>.Inner} is required when {@code outer} is
197
     * e.g. {@code Outer<? super S>} - even if type resolution reports
198
     * the same type for the operand and the cast.
199
     */
200
    private static boolean isCastNarrowingEnclosingType(ASTCastExpression castExpr, JTypeMirror coercionType) {
201
        ASTExpression operand = castExpr.getOperand();
1✔
202
        if (!(operand instanceof ASTConstructorCall) || !(coercionType instanceof JClassType)) {
1!
203
            return false;
1✔
204
        }
205
        ASTConstructorCall ctor = (ASTConstructorCall) operand;
1✔
206
        if (!ctor.isQualifiedInstanceCreation()) {
1✔
207
            return false;
1✔
208
        }
209
        ASTExpression qualifier = ctor.getQualifier();
1✔
210
        JClassType castEnclosing = ((JClassType) coercionType).getEnclosingType();
1✔
211
        if (qualifier == null || castEnclosing == null) {
1!
212
            return false;
×
213
        }
214
        JTypeMirror qualifierType = qualifier.getTypeMirror();
1✔
215
        return !TypeOps.isUnresolvedOrNull(qualifierType) && !qualifierType.isSubtypeOf(castEnclosing);
1!
216
    }
217

218
    private void reportCast(ASTCastExpression castExpr, Object data) {
219
        asCtx(data).addViolation(castExpr, PrettyPrintingUtil.prettyPrintType(castExpr.getCastType()));
1✔
220
    }
1✔
221

222
    private static boolean castIsUnnecessaryToMatchContext(ExprContext context,
223
                                                           JTypeMirror coercionType,
224
                                                           JTypeMirror operandType) {
225
        if (context.hasKind(ExprContextKind.INVOCATION)) {
1✔
226
            // todo unsupported for now, the cast may be disambiguating overloads
227
            return false;
1✔
228
        }
229

230
        JTypeMirror contextType = context.getTargetType();
1✔
231
        if (contextType == null) {
1!
232
            return false; // should not occur in valid code
×
233
        } else if (!TypeConversion.isConvertibleUsingBoxing(operandType, coercionType)) {
1✔
234
            // narrowing cast
235
            return false;
1✔
236
        } else if (!context.acceptsType(operandType)) {
1✔
237
            // then removing the cast would produce uncompilable code
238
            return false;
1✔
239
        }
240

241
        boolean isBoxingFollowingCast = contextType.isPrimitive() != coercionType.isPrimitive();
1✔
242
        // means boxing behavior is equivalent
243
        return !isBoxingFollowingCast || operandType.unbox().isSubtypeOf(contextType.unbox());
1✔
244
    }
245

246
    /**
247
     * Returns whether the context type actually depends on the cast.
248
     * This means our analysis as written above won't work, and usually
249
     * that the cast is necessary, because there's some primitive conversions
250
     * happening, or some other corner case.
251
     */
252
    private static boolean isCastDeterminingContext(ASTCastExpression castExpr, ExprContext context, @NonNull JTypeMirror coercionType, JTypeMirror operandType) {
253

254
        if (castExpr.getParent() instanceof ASTConditionalExpression && castExpr.getIndexInParent() != 0) {
1✔
255
            // a branch of a ternary
256
            return true;
1✔
257

258
        } else if (context.hasKind(ExprContextKind.STRING) && isInfixExprWithOperator(castExpr.getParent(), ADD)) {
1!
259

260
            // inside string concatenation
261
            return !TypeTestUtil.isA(String.class, JavaAstUtils.getOtherOperandIfInInfixExpr(castExpr))
1✔
262
                && !TypeTestUtil.isA(String.class, operandType);
1!
263

264
        } else if (context.hasKind(ExprContextKind.NUMERIC) && castExpr.getParent() instanceof ASTInfixExpression) {
1✔
265
            // numeric expr
266
            ASTInfixExpression parent = (ASTInfixExpression) castExpr.getParent();
1✔
267

268
            if (isInfixExprWithOperator(parent, SHIFT_OPS)) {
1✔
269
                // if so, then the cast is determining the width of expr
270
                // the right operand is always int
271
                return castExpr == parent.getLeftOperand()
1✔
272
                        && !TypeOps.isStrictSubtype(operandType.unbox(), operandType.getTypeSystem().INT);
1✔
273
            } else if (isInfixExprWithOperator(parent, BINARY_PROMOTED_OPS)) {
1!
274
                ASTExpression otherOperand = JavaAstUtils.getOtherOperandIfInInfixExpr(castExpr);
1✔
275
                JTypeMirror otherType = otherOperand.getTypeMirror();
1✔
276

277
                // Ie, the type that is taken by the binary promotion
278
                // is the type of the cast, not the type of the operand.
279
                // Eg in
280
                //     int i; ((double) i) * i
281
                // the only reason the mult expr has type double is because of the cast
282
                JTypeMirror promotedTypeWithoutCast = TypeConversion.binaryNumericPromotion(operandType, otherType);
1✔
283
                JTypeMirror promotedTypeWithCast = TypeConversion.binaryNumericPromotion(coercionType, otherType);
1✔
284
                return !promotedTypeWithoutCast.equals(promotedTypeWithCast);
1✔
285
            }
286

287
        }
288
        return false;
1✔
289
    }
290

291

292
    /**
293
     * Whether this cast is required because it selects a package-private
294
     * member that is not accessible on the operand type. When the operand
295
     * type is in a different package, {@code ((Super) sub).packagePrivate()}
296
     * cannot be rewritten as {@code sub.packagePrivate()}.
297
     *
298
     * @see <a href="https://github.com/pmd/pmd/issues/5732">#5732</a>
299
     */
300
    private boolean isCastRequiredForMemberAccess(ASTCastExpression castExpr, JTypeMirror operandType) {
301
        JavaNode parent = castExpr.getParent();
1✔
302
        if (parent instanceof ASTMethodCall) {
1✔
303
            ASTMethodCall call = (ASTMethodCall) parent;
1✔
304
            if (call.getQualifier() != castExpr || call.getOverloadSelectionInfo().isFailed()) {
1!
NEW
305
                return false;
×
306
            }
307
            return !isPackagePrivateMemberAccessibleOn(call.getMethodType().getSymbol(), operandType);
1✔
308
        }
309
        if (parent instanceof ASTFieldAccess) {
1✔
310
            ASTFieldAccess access = (ASTFieldAccess) parent;
1✔
311
            if (access.getQualifier() != castExpr) {
1!
NEW
312
                return false;
×
313
            }
314
            JFieldSymbol field = access.getReferencedSym();
1✔
315
            return field != null && !isPackagePrivateMemberAccessibleOn(field, operandType);
1!
316
        }
317
        return false;
1✔
318
    }
319

320
    /**
321
     * Public/protected members are accessible regardless of package;
322
     * package-private members are only accessible on types in the same
323
     * package as the declaration.
324
     */
325
    private static boolean isPackagePrivateMemberAccessibleOn(JAccessibleElementSymbol member,
326
                                                              JTypeMirror operandType) {
327
        int access = member.getModifiers() & (Modifier.PUBLIC | Modifier.PROTECTED | Modifier.PRIVATE);
1✔
328
        if (access != 0) {
1✔
329
            return true;
1✔
330
        }
331
        if (!(operandType instanceof JClassType)) {
1!
NEW
332
            return true;
×
333
        }
334
        JClassSymbol operandSym = ((JClassType) operandType).getSymbol();
1✔
335
        return member.getPackageName().equals(operandSym.getPackageName());
1✔
336
    }
337

338
    private static @Nullable ASTLambdaExpression getLambdaParent(ASTCastExpression castExpr) {
339
        if (castExpr.getParent() instanceof ASTLambdaExpression) {
1✔
340
            return (ASTLambdaExpression) castExpr.getParent();
1✔
341
        }
342
        if (castExpr.getParent() instanceof ASTReturnStatement) {
1!
343
            JavaNode returnTarget = JavaAstUtils.getReturnTarget((ASTReturnStatement) castExpr.getParent());
×
344

345
            if (returnTarget instanceof ASTLambdaExpression) {
×
346
                return (ASTLambdaExpression) returnTarget;
×
347
            }
348
        }
349
        return null;
1✔
350
    }
351

352
}
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