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

pmd / pmd / 197

19 Oct 2025 09:43PM UTC coverage: 78.676% (+0.03%) from 78.642%
197

push

github

oowekyala
Fix reviow comments on #6116

18231 of 24023 branches covered (75.89%)

Branch coverage included in aggregate %.

2 of 2 new or added lines in 1 file covered. (100.0%)

38 existing lines in 5 files now uncovered.

39748 of 49670 relevant lines covered (80.02%)

0.81 hits per line

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

85.44
/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/rule/codestyle/UnnecessaryBoxingRule.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.util.CollectionUtil.setOf;
8

9
import java.util.Set;
10

11
import org.apache.commons.lang3.StringUtils;
12
import org.checkerframework.checker.nullness.qual.Nullable;
13

14
import net.sourceforge.pmd.lang.java.ast.ASTConstructorCall;
15
import net.sourceforge.pmd.lang.java.ast.ASTExpression;
16
import net.sourceforge.pmd.lang.java.ast.ASTLambdaExpression;
17
import net.sourceforge.pmd.lang.java.ast.ASTList;
18
import net.sourceforge.pmd.lang.java.ast.ASTMethodCall;
19
import net.sourceforge.pmd.lang.java.ast.ASTReturnStatement;
20
import net.sourceforge.pmd.lang.java.ast.InvocationNode;
21
import net.sourceforge.pmd.lang.java.ast.JavaNode;
22
import net.sourceforge.pmd.lang.java.ast.QualifiableExpression;
23
import net.sourceforge.pmd.lang.java.ast.internal.JavaAstUtils;
24
import net.sourceforge.pmd.lang.java.rule.AbstractJavaRulechainRule;
25
import net.sourceforge.pmd.lang.java.types.JClassType;
26
import net.sourceforge.pmd.lang.java.types.JMethodSig;
27
import net.sourceforge.pmd.lang.java.types.JTypeMirror;
28
import net.sourceforge.pmd.lang.java.types.OverloadSelectionResult;
29
import net.sourceforge.pmd.lang.java.types.TypePrettyPrint;
30
import net.sourceforge.pmd.lang.java.types.TypeTestUtil;
31
import net.sourceforge.pmd.lang.java.types.ast.ExprContext;
32
import net.sourceforge.pmd.lang.java.types.ast.ExprContext.ExprContextKind;
33
import net.sourceforge.pmd.lang.java.types.internal.infer.OverloadSet;
34
import net.sourceforge.pmd.reporting.RuleContext;
35

36
/**
37
 *
38
 */
39
public class UnnecessaryBoxingRule extends AbstractJavaRulechainRule {
40

41
    private static final Set<String> INTERESTING_NAMES = setOf(
1✔
42
        "valueOf",
43
        "booleanValue",
44
        "charValue",
45
        "byteValue",
46
        "shortValue",
47
        "intValue",
48
        "longValue",
49
        "floatValue",
50
        "doubleValue"
51
    );
52

53
    public UnnecessaryBoxingRule() {
54
        super(ASTMethodCall.class, ASTConstructorCall.class);
1✔
55
    }
1✔
56

57
    @Override
58
    public Object visit(ASTConstructorCall node, Object data) {
59
        if (node.getTypeMirror().isBoxedPrimitive()) {
1✔
60
            ASTExpression arg = ASTList.singleOrNull(node.getArguments());
1✔
61
            if (arg == null) {
1!
62
                return null;
×
63
            }
64
            JTypeMirror argT = arg.getTypeMirror();
1✔
65
            if (argT.isPrimitive()) {
1✔
66
                checkBox((RuleContext) data, node, arg);
1✔
67
            }
68
        }
69
        return null;
1✔
70
    }
71

72

73
    @Override
74
    public Object visit(ASTMethodCall node, Object data) {
75
        if (INTERESTING_NAMES.contains(node.getMethodName())) {
1✔
76
            OverloadSelectionResult overload = node.getOverloadSelectionInfo();
1✔
77
            if (overload.isFailed()) {
1!
UNCOV
78
                return null;
×
79
            }
80
            JMethodSig m = overload.getMethodType();
1✔
81
            boolean isValueOf = "valueOf".equals(node.getMethodName());
1✔
82
            ASTExpression qualifier = node.getQualifier();
1✔
83

84
            if (isValueOf && isWrapperValueOf(m)) {
1✔
85
                checkBox((RuleContext) data, node, node.getArguments().get(0));
1✔
86
            } else if (isValueOf && isBoxValueOfString(m)) {
1✔
87
                checkUnboxing((RuleContext) data, node, m.getDeclaringType());
1✔
88
            } else if (!isValueOf && qualifier != null && isUnboxingCall(m)) {
1✔
89
                checkBox((RuleContext) data, node, qualifier);
1✔
90
            }
91
        }
92
        return null;
1✔
93
    }
94

95
    private boolean isUnboxingCall(JMethodSig m) {
96
        return !m.isStatic() && m.getDeclaringType().isBoxedPrimitive() && m.getArity() == 0
1!
97
            && m.getReturnType().isPrimitive();
1!
98
    }
99

100
    private boolean isWrapperValueOf(JMethodSig m) {
101
        return m.isStatic()
1!
102
            && m.getArity() == 1
1✔
103
            && m.getDeclaringType().isBoxedPrimitive()
1✔
104
            && m.getFormalParameters().get(0).isPrimitive();
1✔
105
    }
106

107
    private boolean isBoxValueOfString(JMethodSig m) {
108
        // eg Integer.valueOf("2")
109
        return m.isStatic()
1!
110
                && (m.getArity() == 1 || m.getArity() == 2)
1!
111
                && m.getDeclaringType().isBoxedPrimitive()
1✔
112
                && TypeTestUtil.isA(String.class, m.getFormalParameters().get(0));
1!
113
    }
114

115
    private void checkBox(
116
        RuleContext rctx,
117
        ASTExpression conversionExpr,
118
        ASTExpression convertedExpr
119
    ) {
120
        // the conversion looks like
121
        //  CTX _ = conversion(sourceExpr)
122

123
        // we have the following data flow:
124
        //      sourceExpr -> convInput -> convOutput -> ctx
125
        //                 1            2             3
126
        // where 1 and 3 are implicit conversions which we assume are
127
        // valid because the code should compile.
128

129
        // we want to report a violation if this is equivalent to
130
        //      sourceExpr -> ctx
131

132
        // which means testing that
133
        // 1. the result of the implicit conversion of sourceExpr
134
        // with context type ctx is the same type as the result of conversion 3
135
        // 2. conversion 2 does not truncate the value
136

137
        // We cannot just test compatibility of the source to the ctx,
138
        // because of situations like
139
        //   int i = integer.byteValue()
140
        // where the conversion actually truncates the input value
141
        // (here we do sourceExpr=Integer (-> convInput=Integer) -> convOutput=byte -> ctx=int).
142

143
        JTypeMirror sourceType = convertedExpr.getTypeMirror();
1✔
144
        JTypeMirror conversionOutput = conversionExpr.getTypeMirror();
1✔
145
        ExprContext ctx = conversionExpr.getConversionContext();
1✔
146
        JTypeMirror ctxType = ctx.getTargetType();
1✔
147

148
        if (sourceType.isPrimitive()
1✔
149
            && !conversionOutput.isPrimitive()
1!
150
            && ctxType == null
151
            && isObjectConversionNecessary(conversionExpr)) {
1✔
152
            // eg Integer.valueOf(2).equals(otherInteger)
153
            return;
1✔
154
        }
155

156
        String reason = null;
1✔
157
        if (sourceType.equals(conversionOutput)) {
1✔
158
            reason = "boxing of boxed value";
1✔
159
        } else if (isImplicitlyTypedLambdaReturnExpr(conversionExpr)
1✔
160
            || ctxType != null && conversionIsImplicitlyRealisable(sourceType, ctxType, ctx, conversionOutput)) {
1✔
161
            
162
            // Check if this unboxing is required for correct overload selection
163
            if (sourceType.isBoxedPrimitive() && conversionOutput.isPrimitive() 
1!
164
                && isUnboxingRequiredForOverloadSelection(conversionExpr, convertedExpr)) {
1✔
165
                return;
1✔
166
            }
167
            if (sourceType.unbox().equals(conversionOutput)) {
1✔
168
                reason = "explicit unboxing";
1✔
169
            } else if (sourceType.box().equals(conversionOutput)) {
1✔
170
                reason = "explicit boxing";
1✔
171
            } else if (ctxType != null) {
1✔
172
                reason = "explicit conversion from " + TypePrettyPrint.prettyPrintWithSimpleNames(sourceType)
1✔
173
                    + " to " + TypePrettyPrint.prettyPrintWithSimpleNames(ctxType);
1✔
174
                if (!conversionOutput.equals(ctxType)) {
1✔
175
                    reason += " through " + TypePrettyPrint.prettyPrintWithSimpleNames(conversionOutput);
1✔
176
                }
177
            }
178

179
        }
180
        if (reason != null) {
1✔
181
            rctx.addViolation(conversionExpr, reason);
1✔
182
        }
183
    }
1✔
184

185

186
    private static boolean conversionIsImplicitlyRealisable(JTypeMirror sourceType, JTypeMirror ctxType, ExprContext ctx, JTypeMirror conversionOutput) {
187
        JTypeMirror conv = implicitConversionResult(sourceType, ctxType, ctx.getKind());
1✔
188
        return conv != null
1✔
189
            && conv.equals(implicitConversionResult(conversionOutput, ctxType, ctx.getKind()))
1✔
190
            && conversionDoesNotChangesValue(sourceType, conversionOutput);
1✔
191
    }
192

193
    /**
194
     * Check if the unboxing conversion is required for correct method overload selection.
195
     * Returns true if removing the unboxing would cause a different method overload to be selected.
196
     */
197
    private boolean isUnboxingRequiredForOverloadSelection(ASTExpression conversionExpr, ASTExpression convertedExpr) {
198
        // Find the invocation and argument index
199
        JavaNode parent = conversionExpr.getParent();
1✔
200
        InvocationNode invocation;
201
        int argIndex;
202
        
203
        if (parent instanceof ASTList && parent.getParent() instanceof InvocationNode) {
1!
204
            invocation = (InvocationNode) parent.getParent();
1✔
205
            argIndex = conversionExpr.getIndexInParent();
1✔
206
        } else if (parent instanceof InvocationNode) {
1!
207
            invocation = (InvocationNode) parent;
×
208
            argIndex = 0;
×
209
        } else {
210
            return false;
1✔
211
        }
212
        
213
        // Get the method and validate we have a boxed->primitive conversion
214
        JMethodSig currentMethod;
215
        try {
216
            currentMethod = invocation.getMethodType();
1✔
217
        } catch (Exception e) {
×
218
            return false;
×
219
        }
1✔
220
        
221
        if (!convertedExpr.getTypeMirror().isBoxedPrimitive() || !conversionExpr.getTypeMirror().isPrimitive()) {
1!
222
            return false;
×
223
        }
224
        
225
        // Check if there are overloads that would accept the boxed type differently
226
        return hasObjectOverloadAtPosition(currentMethod, argIndex, invocation instanceof ASTConstructorCall);
1✔
227
    }
228
    
229
    /**
230
     * Check if there are other overloads that would accept the boxed type differently,
231
     * making the unboxing necessary for correct overload selection.
232
     */
233
    private boolean hasObjectOverloadAtPosition(JMethodSig currentMethod, int argIndex, boolean isConstructor) {
234
        JTypeMirror declaringType = currentMethod.getDeclaringType();
1✔
235
        if (!(declaringType instanceof JClassType) || argIndex >= currentMethod.getFormalParameters().size()) {
1!
236
            return false;
×
237
        }
238
        
239
        JClassType classType = (JClassType) declaringType;
1✔
240
        JTypeMirror currentParamType = currentMethod.getFormalParameters().get(argIndex);
1✔
241
        if (!currentParamType.isPrimitive()) {
1!
242
            return false;
×
243
        }
244
        
245
        JTypeMirror boxedType = currentParamType.box();
1✔
246
        
247
        // Get all overloads and check if any would accept the boxed type differently
248
        java.util.List<JMethodSig> overloads = isConstructor 
1✔
249
            ? classType.getConstructors()
1✔
250
            : classType.streamMethods(method -> method.nameEquals(currentMethod.getName()))
1✔
251
                      .collect(OverloadSet.collectMostSpecific(classType));
1✔
252
        
253
        return overloads.stream()
1✔
254
            .filter(overload -> !overload.equals(currentMethod))
1✔
255
            .filter(overload -> argIndex < overload.getFormalParameters().size())
1!
256
            .map(overload -> overload.getFormalParameters().get(argIndex))
1✔
257
            .anyMatch(overloadParamType -> 
1✔
258
                // Boxed type is assignable to overload parameter (Object, generic, etc.)
259
                boxedType.isSubtypeOf(overloadParamType) 
1✔
260
                // Or overload takes reference type while current takes primitive (different conversion paths)
261
                || overloadParamType.isTypeVariable() || !overloadParamType.isPrimitive() && currentParamType.isPrimitive()
1!
262
            );
263
    }
264

265

266
    private boolean isImplicitlyTypedLambdaReturnExpr(ASTExpression e) {
267
        JavaNode parent = e.getParent();
1✔
268
        if (isImplicitlyTypedLambda(parent)) {
1✔
269
            return true;
1✔
270
        } else if (parent instanceof ASTReturnStatement) {
1✔
271
            JavaNode target = JavaAstUtils.getReturnTarget((ASTReturnStatement) parent);
1✔
272
            return isImplicitlyTypedLambda(target);
1✔
273
        }
274
        return false;
1✔
275
    }
276

277

278
    private static boolean isImplicitlyTypedLambda(JavaNode e) {
279
        return e instanceof ASTLambdaExpression && !((ASTLambdaExpression) e).isExplicitlyTyped();
1✔
280
    }
281

282
    private boolean isObjectConversionNecessary(ASTExpression e) {
283
        JavaNode parent = e.getParent();
1✔
284
        return e.getIndexInParent() == 0 && parent instanceof QualifiableExpression;
1!
285
    }
286

287

288
    private void checkUnboxing(
289
            RuleContext rctx,
290
            ASTMethodCall methodCall,
291
            JTypeMirror conversionOutput
292
    ) {
293
        // methodCall is e.g. Integer.valueOf("42")
294
        // this checks, whether the resulting type "Integer" is e.g. assigned to an "int"
295
        // which triggers implicit unboxing.
296
        ExprContext ctx = methodCall.getConversionContext();
1✔
297
        JTypeMirror ctxType = ctx.getTargetType();
1✔
298

299
        if (ctxType != null) {
1!
300
            if (isImplicitlyConvertible(conversionOutput, ctxType)) {
1!
301
                if (conversionOutput.unbox().equals(ctxType)) {
1✔
302
                    rctx.addViolation(methodCall, "implicit unboxing. Use "
1✔
303
                            + conversionOutput.getSymbol().getSimpleName() + ".parse"
1✔
304
                            + StringUtils.capitalize(ctxType.getSymbol().getSimpleName()) + "(...) instead");
1✔
305
                }
306
            }
307
        }
308
    }
1✔
309

310
    private boolean isImplicitlyConvertible(JTypeMirror i, JTypeMirror o) {
311
        if (i.isBoxedPrimitive() && o.isBoxedPrimitive()) {
1!
312
            // There is no implicit conversions between box types,
313
            // only between primitives
314
            return i.equals(o);
1✔
315
        } else if (i.isPrimitive() && o.isPrimitive()) {
1!
316
            return i.isSubtypeOf(o);
×
317
        } else {
318
            return i.unbox().equals(o.unbox());
1✔
319
        }
320
    }
321

322

323
    /**
324
     * Type of the converted i in context ctx. If no implicit
325
     * conversion is possible then return null.
326
     */
327
    private static @Nullable JTypeMirror implicitConversionResult(JTypeMirror i, JTypeMirror ctx, ExprContextKind kind) {
328
        if (kind == ExprContextKind.CAST) {
1✔
329
            // In cast contexts conversions are less restrictive.
330
            if (i.isPrimitive() != ctx.isPrimitive()) {
1✔
331
                // Whether an unboxing or boxing conversion may occur depends on whether
332
                // the expression has a primitive type or not (not on the cast type).
333
                // https://docs.oracle.com/javase/specs/jls/se22/html/jls-5.html#jls-5.5
334
                return i.isPrimitive() ? i.box() : i.unbox();
1✔
335
            } else if (i.isNumeric() && ctx.isNumeric()) {
1!
336
                // then narrowing or widening conversions occur to transform i to ctx
337
                return ctx;
1✔
338
            }
339
            // otherwise no conversion occurs
340
            return i;
1✔
341
        }
342
        if (!ctx.isPrimitive()) {
1✔
343
            // boxing
344
            return i.box().isSubtypeOf(ctx) ? i.box() : null;
1✔
345
        } else if (i.isBoxedPrimitive()) {
1✔
346
            // unboxing then optional widening
347
            return i.unbox().isSubtypeOf(ctx) ? ctx : null;
1✔
348
        } else if (i.isPrimitive()) {
1!
349
            // widening
350
            return i.isSubtypeOf(ctx) ? ctx : null;
1!
351
        }
352
        return null;
×
353
    }
354

355

356
    /**
357
     * Whether the explicit conversion from i to o changes the value.
358
     * This is e.g. truncating an integer.
359
     */
360
    private static boolean conversionDoesNotChangesValue(JTypeMirror i, JTypeMirror o) {
361
        return i.box().isSubtypeOf(o.box()) || i.unbox().isSubtypeOf(o.unbox());
1✔
362
    }
363

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