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

pmd / pmd / 852

06 Oct 2026 10:33AM UTC coverage: 79.758% (+0.03%) from 79.727%
852

push

github

web-flow
[java] Fix #6903: Rewrite UseExplicitTypes as Java rule to support allowLongTypeNames (#7088)

* [java] Rewrite UseExplicitTypes as Java rule

* Optimize with short-circuit &&

* Verify type names (WIP)

* Implement SimpleNameVisitor, add test cases with Map

* SimpleNameVisitor returns StringBuilder

* Support type annotations

* Use allowLongTypeNames=0 to disallow

* Improve rule documentation

* [doc] Update release notes (#6903)

* Refactor determineTypeName

* Apply batched suggestions from code review

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

---------

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

20268 of 26444 branches covered (76.64%)

Branch coverage included in aggregate %.

100 of 102 new or added lines in 1 file covered. (98.04%)

43568 of 53593 relevant lines covered (81.29%)

0.82 hits per line

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

96.58
/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/rule/codestyle/UseExplicitTypesRule.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 java.util.ArrayList;
8
import java.util.List;
9
import java.util.stream.Collectors;
10

11
import org.checkerframework.checker.nullness.qual.NonNull;
12
import org.pcollections.PSet;
13

14
import net.sourceforge.pmd.lang.java.ast.ASTCastExpression;
15
import net.sourceforge.pmd.lang.java.ast.ASTClassDeclaration;
16
import net.sourceforge.pmd.lang.java.ast.ASTConstructorCall;
17
import net.sourceforge.pmd.lang.java.ast.ASTForeachStatement;
18
import net.sourceforge.pmd.lang.java.ast.ASTLiteral;
19
import net.sourceforge.pmd.lang.java.ast.ASTLocalVariableDeclaration;
20
import net.sourceforge.pmd.lang.java.ast.ASTVariableDeclarator;
21
import net.sourceforge.pmd.lang.java.ast.ASTVariableId;
22
import net.sourceforge.pmd.lang.java.rule.AbstractJavaRulechainRule;
23
import net.sourceforge.pmd.lang.java.symbols.JClassSymbol;
24
import net.sourceforge.pmd.lang.java.symbols.SymbolicValue;
25
import net.sourceforge.pmd.lang.java.types.JClassType;
26
import net.sourceforge.pmd.lang.java.types.JPrimitiveType;
27
import net.sourceforge.pmd.lang.java.types.JTypeMirror;
28
import net.sourceforge.pmd.lang.java.types.JTypeVar;
29
import net.sourceforge.pmd.lang.java.types.JTypeVisitable;
30
import net.sourceforge.pmd.lang.java.types.JTypeVisitor;
31
import net.sourceforge.pmd.lang.java.types.JWildcardType;
32
import net.sourceforge.pmd.lang.java.types.TypePrettyPrint;
33
import net.sourceforge.pmd.properties.PropertyDescriptor;
34
import net.sourceforge.pmd.properties.PropertyFactory;
35
import net.sourceforge.pmd.reporting.RuleContext;
36

37
/**
38
 * @since 7.0.0 (as XPath)
39
 * @since 7.29.0 (as Java rule)
40
 */
41
public class UseExplicitTypesRule extends AbstractJavaRulechainRule {
42
    private static final PropertyDescriptor<Boolean> ALLOW_LITERALS = PropertyFactory
1 ✔
43
            .booleanProperty("allowLiterals")
1 ✔
44
            .desc("Allow when variables are directly initialized with literals")
1 ✔
45
            .defaultValue(false)
1 ✔
46
            .build();
1 ✔
47
    private static final PropertyDescriptor<Boolean> ALLOW_CTORS = PropertyFactory
1 ✔
48
            .booleanProperty("allowCtors")
1 ✔
49
            .desc("Allow when variables are directly initialized with a constructor call")
1 ✔
50
            .defaultValue(false)
1 ✔
51
            .build();
1 ✔
52
    private static final PropertyDescriptor<Boolean> ALLOW_CASTS = PropertyFactory
1 ✔
53
            .booleanProperty("allowCasts")
1 ✔
54
            .desc("Allow when variables are directly initialized with a the result of a cast")
1 ✔
55
            .defaultValue(false)
1 ✔
56
            .build();
1 ✔
57
    private static final PropertyDescriptor<Boolean> ALLOW_LOOP_VARIABLE = PropertyFactory
1 ✔
58
            .booleanProperty("allowLoopVariable")
1 ✔
59
            .desc("Allow when variables are used as loop variables in enhanced for loops")
1 ✔
60
            .defaultValue(false)
1 ✔
61
            .build();
1 ✔
62
    private static final PropertyDescriptor<Integer> ALLOW_LONG_TYPE_NAMES = PropertyFactory
1 ✔
63
            .intProperty("allowLongTypeNames")
1 ✔
64
            .desc("Allow when the explicit type name would be longer than the configured length. "
1 ✔
65
                    + "If the allowed length is 0, then no types are allowed regardless of the length.")
66
            .defaultValue(0)
1 ✔
67
            .build();
1 ✔
68

69
    public UseExplicitTypesRule() {
70
        super(ASTLocalVariableDeclaration.class);
1 ✔
71
        definePropertyDescriptor(ALLOW_LITERALS);
1 ✔
72
        definePropertyDescriptor(ALLOW_CTORS);
1 ✔
73
        definePropertyDescriptor(ALLOW_CASTS);
1 ✔
74
        definePropertyDescriptor(ALLOW_LOOP_VARIABLE);
1 ✔
75
        definePropertyDescriptor(ALLOW_LONG_TYPE_NAMES);
1 ✔
76
    }
1 ✔
77

78
    @Override
79
    public Object visit(ASTLocalVariableDeclaration node, Object data) {
80
        RuleContext ctx = (RuleContext) data;
1 ✔
81
        if (!node.isTypeInferred()) {
1 ✔
82
            return null;
1 ✔
83
        }
84

85
        if (getProperty(ALLOW_LITERALS) && node.children(ASTVariableDeclarator.class).descendants(ASTLiteral.class).nonEmpty()) {
1 ✔
86
            return null;
1 ✔
87
        }
88
        if (getProperty(ALLOW_CTORS) && node.children(ASTVariableDeclarator.class).children(ASTConstructorCall.class).nonEmpty()) {
1 ✔
89
            return null;
1 ✔
90
        }
91
        if (getProperty(ALLOW_CASTS) && node.children(ASTVariableDeclarator.class).children(ASTCastExpression.class).nonEmpty()) {
1 ✔
92
            return null;
1 ✔
93
        }
94
        if (getProperty(ALLOW_LOOP_VARIABLE) && node.getParent() instanceof ASTForeachStatement) {
1 ✔
95
            return null;
1 ✔
96
        }
97

98
        int requiredLongTypeNamesLength = getProperty(ALLOW_LONG_TYPE_NAMES);
1 ✔
99
        boolean allowLongTypeNames = requiredLongTypeNamesLength > 0;
1 ✔
100

101
        if (!allowLongTypeNames) {
1 ✔
102
            ctx.addViolation(node);
1 ✔
103
        } else {
104
            String typeName = determineTypeName(node);
1 ✔
105
            if (typeName.length() < requiredLongTypeNamesLength) {
1 ✔
106
                ctx.addViolationWithMessage(node, "The explicit type ''{0}'' is not long enough (< {1}) to justify the use of var",
1 ✔
107
                        typeName, requiredLongTypeNamesLength);
1 ✔
108
            }
109
        }
110
        return null;
1 ✔
111
    }
112

113
    private String determineTypeName(ASTLocalVariableDeclaration node) {
114
        // note: var declarations have exactly one varId
115
        ASTVariableId firstVarId = node.getVarIds().first();
1 ✔
116
        assert firstVarId != null : "Invalid java syntax? the local var declaration should have one varId";
1 !
117
        JTypeMirror typeMirror = firstVarId.getTypeMirror();
1 ✔
118

119
        List<@NonNull JClassSymbol> enclosingTypeSymbols = node.ancestors(ASTClassDeclaration.class)
1 ✔
120
                .toStream()
1 ✔
121
                .map(ASTClassDeclaration::getTypeMirror)
1 ✔
122
                .map(JClassType::getSymbol)
1 ✔
123
                .collect(Collectors.toList());
1 ✔
124
        SimpleNameVisitor visitor = new SimpleNameVisitor(enclosingTypeSymbols);
1 ✔
125

126
        StringBuilder sb = typeMirror.acceptVisitor(visitor, new StringBuilder());
1 ✔
127
        return sb.toString();
1 ✔
128
    }
129

130
    /**
131
     * This type visitor is very similar to {@link TypePrettyPrint#prettyPrintWithSimpleNames(JTypeVisitable)}
132
     * with the following differences:
133
     * <ul>
134
     *     <li>For nested types, display the enclosing type in canonical format (with dots) unless the nested type
135
     *         is defined within the same compilation unit and therefore doesn't need to be qualified.</li>
136
     *     <li>TypePrettyPrint would display only the simple name of the nested type (e.g. {@code Entry} instead
137
     *         of {@code Map.Entry})</li>
138
     * </ul>
139
     */
140
    static class SimpleNameVisitor implements JTypeVisitor<StringBuilder, StringBuilder> {
141
        private final List<JClassSymbol> enclosingTypeSymbols;
142

143
        /**
144
         * @param enclosingTypeSymbols List of type symbols, which are in scope. Nested types of these don't need
145
         *                             to be qualified.
146
         */
147
        SimpleNameVisitor(List<JClassSymbol> enclosingTypeSymbols) {
1 ✔
148
            this.enclosingTypeSymbols = enclosingTypeSymbols;
1 ✔
149
        }
1 ✔
150

151
        @Override
152
        public StringBuilder visit(JTypeMirror t, StringBuilder sb) {
NEW
153
            return sb;
×
154
        }
155

156
        @Override
157
        public StringBuilder visitClass(JClassType classType, StringBuilder sb) {
158
            PSet<SymbolicValue.SymAnnot> typeAnnotations = classType.getTypeAnnotations();
1 ✔
159
            if (typeAnnotations != null) {
1 !
160
                for (SymbolicValue.SymAnnot annot : typeAnnotations) {
1 ✔
161
                    sb.append("@").append(annot.getSimpleName()).append(" ");
1 ✔
162
                }
1 ✔
163
            }
164

165
            JClassSymbol symbol = classType.getSymbol();
1 ✔
166
            JClassSymbol enclosingClass = symbol.getEnclosingClass();
1 ✔
167
            List<String> enclosingNames = new ArrayList<>();
1 ✔
168
            while (enclosingClass != null) {
1 ✔
169
                if (!enclosingTypeSymbols.contains(enclosingClass)) {
1 ✔
170
                    enclosingNames.add(enclosingClass.getSimpleName());
1 ✔
171
                }
172
                enclosingClass = enclosingClass.getEnclosingClass();
1 ✔
173
            }
174
            if (!enclosingNames.isEmpty()) {
1 ✔
175
                sb.append(String.join(".", enclosingNames));
1 ✔
176
                sb.append(".");
1 ✔
177
            }
178
            sb.append(symbol.getSimpleName());
1 ✔
179

180
            List<JTypeMirror> typeArgs = classType.getTypeArgs();
1 ✔
181
            if (!typeArgs.isEmpty()) {
1 ✔
182
                sb.append("<");
1 ✔
183
                for (int i = 0; i < typeArgs.size(); i++) {
1 ✔
184
                    typeArgs.get(i).acceptVisitor(this, sb);
1 ✔
185
                    if (i != typeArgs.size() - 1) {
1 ✔
186
                        sb.append(", ");
1 ✔
187
                    }
188
                }
189
                sb.append(">");
1 ✔
190
            }
191
            return sb;
1 ✔
192
        }
193

194
        @Override
195
        public StringBuilder visitPrimitive(JPrimitiveType t, StringBuilder sb) {
196
            sb.append(t.getSimpleName());
1 ✔
197
            return sb;
1 ✔
198
        }
199

200
        @Override
201
        public StringBuilder visitTypeVar(JTypeVar t, StringBuilder sb) {
202
            sb.append(t.getName());
1 ✔
203
            return sb;
1 ✔
204
        }
205

206
        @Override
207
        public StringBuilder visitWildcard(JWildcardType wildcardType, StringBuilder sb) {
208
            sb.append("?");
1 ✔
209
            if (wildcardType.isUpperBound()) {
1 !
210
                sb.append(" extends ");
1 ✔
211
            } else {
NEW
212
                sb.append(" super ");
×
213
            }
214
            wildcardType.getBound().acceptVisitor(this, sb);
1 ✔
215
            return sb;
1 ✔
216
        }
217
    }
218
}
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