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

pmd / pmd / 318

21 Dec 2025 06:01PM UTC coverage: 78.966% (-0.009%) from 78.975%
318

push

github

adangel
[java] Fix #6237: UnnecessaryCast error with switch expr returning lambdas (#6295)

18514 of 24322 branches covered (76.12%)

Branch coverage included in aggregate %.

72 of 82 new or added lines in 7 files covered. (87.8%)

5 existing lines in 2 files now uncovered.

40308 of 50168 relevant lines covered (80.35%)

0.81 hits per line

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

91.48
/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.DIV;
9
import static net.sourceforge.pmd.lang.java.ast.BinaryOp.GE;
10
import static net.sourceforge.pmd.lang.java.ast.BinaryOp.GT;
11
import static net.sourceforge.pmd.lang.java.ast.BinaryOp.LE;
12
import static net.sourceforge.pmd.lang.java.ast.BinaryOp.LT;
13
import static net.sourceforge.pmd.lang.java.ast.BinaryOp.MOD;
14
import static net.sourceforge.pmd.lang.java.ast.BinaryOp.MUL;
15
import static net.sourceforge.pmd.lang.java.ast.BinaryOp.SHIFT_OPS;
16
import static net.sourceforge.pmd.lang.java.ast.BinaryOp.SUB;
17
import static net.sourceforge.pmd.lang.java.ast.internal.JavaAstUtils.isInfixExprWithOperator;
18

19
import java.util.EnumSet;
20
import java.util.Set;
21

22
import org.checkerframework.checker.nullness.qual.NonNull;
23
import org.checkerframework.checker.nullness.qual.Nullable;
24

25
import net.sourceforge.pmd.lang.java.ast.ASTCastExpression;
26
import net.sourceforge.pmd.lang.java.ast.ASTConditionalExpression;
27
import net.sourceforge.pmd.lang.java.ast.ASTExpression;
28
import net.sourceforge.pmd.lang.java.ast.ASTInfixExpression;
29
import net.sourceforge.pmd.lang.java.ast.ASTLambdaExpression;
30
import net.sourceforge.pmd.lang.java.ast.ASTMethodCall;
31
import net.sourceforge.pmd.lang.java.ast.ASTMethodReference;
32
import net.sourceforge.pmd.lang.java.ast.ASTReturnStatement;
33
import net.sourceforge.pmd.lang.java.ast.BinaryOp;
34
import net.sourceforge.pmd.lang.java.ast.JavaNode;
35
import net.sourceforge.pmd.lang.java.ast.internal.JavaAstUtils;
36
import net.sourceforge.pmd.lang.java.ast.internal.PrettyPrintingUtil;
37
import net.sourceforge.pmd.lang.java.rule.AbstractJavaRulechainRule;
38
import net.sourceforge.pmd.lang.java.types.JMethodSig;
39
import net.sourceforge.pmd.lang.java.types.JTypeMirror;
40
import net.sourceforge.pmd.lang.java.types.JTypeVar;
41
import net.sourceforge.pmd.lang.java.types.Substitution;
42
import net.sourceforge.pmd.lang.java.types.TypeConversion;
43
import net.sourceforge.pmd.lang.java.types.TypeOps;
44
import net.sourceforge.pmd.lang.java.types.TypeOps.Convertibility;
45
import net.sourceforge.pmd.lang.java.types.TypeTestUtil;
46
import net.sourceforge.pmd.lang.java.types.ast.ExprContext;
47
import net.sourceforge.pmd.lang.java.types.ast.ExprContext.ExprContextKind;
48

49
/**
50
 * Detects casts where the operand is already a subtype of the context
51
 * type, or may be converted to it implicitly.
52
 */
53
public class UnnecessaryCastRule extends AbstractJavaRulechainRule {
54

55
    private static final Set<BinaryOp> BINARY_PROMOTED_OPS =
1✔
56
        EnumSet.of(LE, GE, GT, LT, ADD, SUB, MUL, DIV, MOD);
1✔
57

58
    public UnnecessaryCastRule() {
59
        super(ASTCastExpression.class);
1✔
60
    }
1✔
61

62
    @Override
63
    public Object visit(ASTCastExpression castExpr, Object data) {
64
        ASTExpression operand = castExpr.getOperand();
1✔
65

66
        // eg in
67
        // Object o = (Integer) 1;
68

69
        @Nullable ExprContext context = castExpr.getConversionContext();        // Object
1✔
70
        JTypeMirror coercionType = castExpr.getCastType().getTypeMirror();      // Integer
1✔
71
        JTypeMirror operandType = operand.getTypeMirror();                      // int
1✔
72

73
        if (TypeOps.isUnresolvedOrNull(operandType)
1✔
74
            || TypeOps.isUnresolvedOrNull(coercionType)) {
1✔
75
            return null;
1✔
76
        }
77

78
        // Note that we assume that coercionType is convertible to
79
        // contextType because the code must compile
80

81
        if (operand instanceof ASTLambdaExpression || operand instanceof ASTMethodReference) {
1✔
82
            // Then the cast provides a target type for the expression (always).
83
            // We need to check the enclosing context, as if it's invocation we give up for now
84
            if (context.isMissing() || context.hasKind(ExprContextKind.INVOCATION)) {
1!
85
                // Then the cast may be used to determine the overload.
86
                // We need to treat the casted lambda as a whole unit.
87
                // todo see below
88
                return null;
1✔
89
            }
90

91
            // Since the code is assumed to compile we'll just assume that coercionType
92
            // is a functional interface.
93
            if (coercionType.equals(context.getTargetType())) {
1✔
94
                // then we also know that the context is functional
95
                reportCast(castExpr, data);
1✔
96
            }
97
            // otherwise the cast is narrowing, and removing it would
98
            // change the runtime class of the produced lambda.
99
            // Eg `SuperItf obj = (SubItf) ()-> {};`
100
            // If we remove the cast, even if it might compile,
101
            // the object will not implement SubItf anymore.
102
        } else if (isCastUnnecessary(castExpr, context, coercionType, operandType)) {
1✔
103
            reportCast(castExpr, data);
1✔
104
        } else if (castExpr.getParent() instanceof ASTMethodCall
1✔
105
                    && castExpr.getIndexInParent() == 0) {
1!
106
            JMethodSig methodType = ((ASTMethodCall) castExpr.getParent()).getMethodType();
1✔
107
            handleMethodCall(castExpr, methodType, operandType, data);
1✔
108
        }
109
        return null;
1✔
110
    }
111

112
    private void handleMethodCall(ASTCastExpression castExpr, JMethodSig methodType,
113
            JTypeMirror operandType, Object data) {
114
        boolean generic = methodType.getSymbol().getFormalParameters().stream()
1✔
115
            .anyMatch(fp -> isTypeExpression(fp.getTypeMirror(Substitution.EMPTY)));
1✔
116
        if (!generic) {
1✔
117
            JTypeMirror declaringType = methodType.getDeclaringType();
1✔
118
            if (!isTypeExpression(methodType.getSymbol().getReturnType(Substitution.EMPTY))) {
1✔
119
                // declaring type of List<T>::size is List<T>, but since the return type
120
                // is not generic, it's enough to check that operand is a List
121
                declaringType = declaringType.getErasure();
1✔
122
            }
123
            if (TypeTestUtil.isA(declaringType, operandType)) {
1✔
124
                reportCast(castExpr, data);
1✔
125
            }
126
        }
127
    }
1✔
128

129
    private boolean isTypeExpression(JTypeMirror type) {
130
        return type.isGeneric() || type instanceof JTypeVar;
1✔
131
    }
132

133
    private boolean isCastUnnecessary(ASTCastExpression castExpr, @NonNull ExprContext context, JTypeMirror coercionType, JTypeMirror operandType) {
134
        if (operandType.equals(coercionType)) {
1✔
135
            return true;
1✔
136
        } else if (context.isMissing()) {
1✔
137
            // then we have fewer violation conditions
138

139
            return !operandType.isBottom() // casts on a null literal are necessary
1!
140
                   && operandType.isSubtypeOf(coercionType)
1✔
141
                   && !isCastToRawType(coercionType, operandType)
1✔
142
                   // If the context is missing when the parent is a lambda,
143
                   // that means the body of the lambda is determining the return
144
                   // type of the lambda
145
                   && getLambdaParent(castExpr) == null;
1✔
146
        }
147

148
        return !isCastDeterminingContext(castExpr, context, coercionType, operandType)
1✔
149
            && castIsUnnecessaryToMatchContext(context, coercionType, operandType);
1✔
150
    }
151

152
    /**
153
     * Whether this cast is casting a non-raw type to a raw type.
154
     * This is part of the {@link Convertibility#bySubtyping()} relation,
155
     * and needs to be singled out as operations on the raw type
156
     * behave differently than on the non-raw type. In that case the
157
     * cast may be necessary to avoid compile-errors, even though it
158
     * will be noop at runtime (an _unchecked_ cast).
159
     */
160
    private boolean isCastToRawType(JTypeMirror coercionType, JTypeMirror operandType) {
161
        return coercionType.isRaw() && !operandType.isRaw();
1!
162
    }
163

164
    private void reportCast(ASTCastExpression castExpr, Object data) {
165
        asCtx(data).addViolation(castExpr, PrettyPrintingUtil.prettyPrintType(castExpr.getCastType()));
1✔
166
    }
1✔
167

168
    private static boolean castIsUnnecessaryToMatchContext(ExprContext context,
169
                                                           JTypeMirror coercionType,
170
                                                           JTypeMirror operandType) {
171
        if (context.hasKind(ExprContextKind.INVOCATION)) {
1✔
172
            // todo unsupported for now, the cast may be disambiguating overloads
173
            return false;
1✔
174
        }
175

176
        JTypeMirror contextType = context.getTargetType();
1✔
177
        if (contextType == null) {
1!
178
            return false; // should not occur in valid code
×
179
        } else if (!TypeConversion.isConvertibleUsingBoxing(operandType, coercionType)) {
1✔
180
            // narrowing cast
181
            return false;
1✔
182
        } else if (!context.acceptsType(operandType)) {
1✔
183
            // then removing the cast would produce uncompilable code
184
            return false;
1✔
185
        }
186

187
        boolean isBoxingFollowingCast = contextType.isPrimitive() != coercionType.isPrimitive();
1✔
188
        // means boxing behavior is equivalent
189
        return !isBoxingFollowingCast || operandType.unbox().isSubtypeOf(contextType.unbox());
1✔
190
    }
191

192
    /**
193
     * Returns whether the context type actually depends on the cast.
194
     * This means our analysis as written above won't work, and usually
195
     * that the cast is necessary, because there's some primitive conversions
196
     * happening, or some other corner case.
197
     */
198
    private static boolean isCastDeterminingContext(ASTCastExpression castExpr, ExprContext context, @NonNull JTypeMirror coercionType, JTypeMirror operandType) {
199

200
        if (castExpr.getParent() instanceof ASTConditionalExpression && castExpr.getIndexInParent() != 0) {
1✔
201
            // a branch of a ternary
202
            return true;
1✔
203

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

206
            // inside string concatenation
207
            return !TypeTestUtil.isA(String.class, JavaAstUtils.getOtherOperandIfInInfixExpr(castExpr))
1✔
208
                && !TypeTestUtil.isA(String.class, operandType);
1!
209

210
        } else if (context.hasKind(ExprContextKind.NUMERIC) && castExpr.getParent() instanceof ASTInfixExpression) {
1✔
211
            // numeric expr
212
            ASTInfixExpression parent = (ASTInfixExpression) castExpr.getParent();
1✔
213

214
            if (isInfixExprWithOperator(parent, SHIFT_OPS)) {
1✔
215
                // if so, then the cast is determining the width of expr
216
                // the right operand is always int
217
                return castExpr == parent.getLeftOperand()
1✔
218
                        && !TypeOps.isStrictSubtype(operandType.unbox(), operandType.getTypeSystem().INT);
1✔
219
            } else if (isInfixExprWithOperator(parent, BINARY_PROMOTED_OPS)) {
1!
220
                ASTExpression otherOperand = JavaAstUtils.getOtherOperandIfInInfixExpr(castExpr);
1✔
221
                JTypeMirror otherType = otherOperand.getTypeMirror();
1✔
222

223
                // Ie, the type that is taken by the binary promotion
224
                // is the type of the cast, not the type of the operand.
225
                // Eg in
226
                //     int i; ((double) i) * i
227
                // the only reason the mult expr has type double is because of the cast
228
                JTypeMirror promotedTypeWithoutCast = TypeConversion.binaryNumericPromotion(operandType, otherType);
1✔
229
                JTypeMirror promotedTypeWithCast = TypeConversion.binaryNumericPromotion(coercionType, otherType);
1✔
230
                return !promotedTypeWithoutCast.equals(promotedTypeWithCast);
1✔
231
            }
232

233
        }
234
        return false;
1✔
235
    }
236

237
    private static @Nullable ASTLambdaExpression getLambdaParent(ASTCastExpression castExpr) {
238
        if (castExpr.getParent() instanceof ASTLambdaExpression) {
1✔
239
            return (ASTLambdaExpression) castExpr.getParent();
1✔
240
        }
241
        if (castExpr.getParent() instanceof ASTReturnStatement) {
1!
UNCOV
242
            JavaNode returnTarget = JavaAstUtils.getReturnTarget((ASTReturnStatement) castExpr.getParent());
×
243

UNCOV
244
            if (returnTarget instanceof ASTLambdaExpression) {
×
UNCOV
245
                return (ASTLambdaExpression) returnTarget;
×
246
            }
247
        }
248
        return null;
1✔
249
    }
250

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