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

pmd / pmd / 119

15 Aug 2025 10:06AM UTC coverage: 78.488% (+0.01%) from 78.476%
119

push

github

adangel
[java] ShortVariable - improve detection of unnamed variables

Refs #5914

Co-authored-by: Juan Martín Sotuyo Dodero <juan.sotuyo@pedidosya.com>

17911 of 23661 branches covered (75.7%)

Branch coverage included in aggregate %.

39213 of 49120 relevant lines covered (79.83%)

0.81 hits per line

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

91.67
/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/types/InvocationMatcher.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.types;
6

7
import static net.sourceforge.pmd.util.CollectionUtil.listOf;
8

9
import java.util.ArrayList;
10
import java.util.Collections;
11
import java.util.List;
12
import java.util.Objects;
13

14
import org.apache.commons.lang3.StringUtils;
15
import org.checkerframework.checker.nullness.qual.Nullable;
16

17
import net.sourceforge.pmd.lang.java.ast.ASTConstructorCall;
18
import net.sourceforge.pmd.lang.java.ast.ASTExpression;
19
import net.sourceforge.pmd.lang.java.ast.ASTList;
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.util.AssertionUtil;
24
import net.sourceforge.pmd.util.CollectionUtil;
25
import net.sourceforge.pmd.util.OptionalBool;
26
import net.sourceforge.pmd.util.StringUtil;
27

28
/**
29
 * Matches a method or constructor call against a particular overload.
30
 * Use {@link #parse(String)} to create one. For example,
31
 *
32
 * <pre>
33
 *     java.lang.String#toString()   // match calls to toString on String instances
34
 *     _#toString()                  // match calls to toString on any receiver
35
 *     _#_()                         // match all calls to a method with no parameters
36
 *     _#toString(_*)                // match calls to a "toString" method with any number of parameters
37
 *     _#eq(_, _)                    // match calls to an "eq" method that has 2 parameters of unspecified type
38
 *     _#eq(java.lang.String, _)     // like the previous, but the first parameter must be String
39
 *     java.util.ArrayList#new(int)  // match constructor calls of this overload of the ArrayList constructor
40
 * </pre>
41
 *
42
 * <p>The receiver matcher (first half) is matched against the
43
 * static type of the <i>receiver</i> of the call, and not the
44
 * declaration site of the method, unless the called method is
45
 * static, or a constructor.
46
 *
47
 * <p>The parameters are matched against the declared parameters
48
 * types of the called overload, and not the actual argument types.
49
 * In particular, for vararg methods, the signature should mention
50
 * a single parameter, with an array type.
51
 *
52
 * <p>For example {@code Integer.valueOf('0')} will be matched by
53
 * {@code _#valueOf(int)} but not {@code _#valueOf(char)}, which is
54
 * an overload that does not exist (the char is widened to an int,
55
 * so the int overload is selected).
56
 *
57
 * <h2 id='ebnf'>Full EBNF grammar</h2>
58
 *
59
 * <p>(no whitespace is tolerated anywhere):
60
 * <pre>{@code
61
 * sig         ::= type '#' method_name param_list
62
 * type        ::= qname ( '[]' )* | '_'
63
 * method_name ::= '_' | ident | 'new'
64
 * param_list  ::= '(_*)' | '(' type (',' type )* ')'
65
 * qname       ::= java binary name
66
 * }</pre>
67
 */
68
public final class InvocationMatcher {
69

70
    final @Nullable String expectedName;
71
    final @Nullable List<TypeMatcher> argMatchers;
72
    final TypeMatcher qualifierMatcher;
73

74
    InvocationMatcher(TypeMatcher qualifierMatcher, String expectedName, @Nullable List<TypeMatcher> argMatchers) {
1✔
75
        this.expectedName = "_".equals(expectedName) ? null : expectedName;
1✔
76
        this.argMatchers = argMatchers;
1✔
77
        this.qualifierMatcher = qualifierMatcher;
1✔
78
    }
1✔
79

80
    /**
81
     * See {@link #matchesCall(InvocationNode)}.
82
     */
83
    public boolean matchesCall(@Nullable JavaNode node) {
84
        return node instanceof InvocationNode && matchesCall((InvocationNode) node);
1✔
85
    }
86

87
    /**
88
     * Returns true if the call matches this matcher. This means,
89
     * the called overload is the one identified by the argument
90
     * matchers, and the actual qualifier type is a subtype of the
91
     * one mentioned by the qualifier matcher.
92
     */
93
    public boolean matchesCall(@Nullable InvocationNode node) {
94
        if (node == null) {
1!
95
            return false;
×
96
        }
97
        if (expectedName != null && !node.getMethodName().equals(expectedName)
1✔
98
            || argMatchers != null && ASTList.sizeOrZero(node.getArguments()) != argMatchers.size()) {
1✔
99
            return false;
1✔
100
        }
101
        OverloadSelectionResult info = node.getOverloadSelectionInfo();
1✔
102
        return !info.isFailed() && matchQualifier(node)
1✔
103
                && argsMatchOverload(info.getMethodType());
1✔
104
    }
105

106
    private boolean matchQualifier(InvocationNode node) {
107
        if (qualifierMatcher == TypeMatcher.ANY) {
1✔
108
            return true;
1✔
109
        }
110
        if (node instanceof ASTConstructorCall) {
1✔
111
            JTypeMirror newType = ((ASTConstructorCall) node).getTypeNode().getTypeMirror();
1✔
112
            return qualifierMatcher.matches(newType, true);
1✔
113
        }
114
        JMethodSig m = node.getMethodType();
1✔
115
        JTypeMirror qualType;
116
        if (node instanceof QualifiableExpression) {
1!
117
            ASTExpression qualifier = ((QualifiableExpression) node).getQualifier();
1✔
118
            if (qualifier != null) {
1✔
119
                qualType = qualifier.getTypeMirror();
1✔
120
            } else {
121
                // todo: if qualifier == null, then we should take the type of the
122
                // implicit receiver, ie `this` or `SomeOuter.this`
123
                qualType = m.getDeclaringType();
1✔
124
            }
125
        } else {
1✔
126
            qualType = m.getDeclaringType();
×
127
        }
128

129
        return qualifierMatcher.matches(qualType, m.isStatic());
1✔
130
    }
131

132
    private boolean argsMatchOverload(JMethodSig invoc) {
133
        if (argMatchers == null) {
1✔
134
            return true;
1✔
135
        }
136
        List<JTypeMirror> formals = invoc.getFormalParameters();
1✔
137
        if (invoc.getArity() != argMatchers.size()) {
1!
138
            return false;
×
139
        }
140
        for (int i = 0; i < formals.size(); i++) {
1✔
141
            if (!argMatchers.get(i).matches(formals.get(i), true)) {
1✔
142
                return false;
1✔
143
            }
144
        }
145
        return true;
1✔
146
    }
147

148

149
    /**
150
     * Parses a {@link CompoundInvocationMatcher} which matches any of
151
     * the provided matchers.
152
     *
153
     * @param first First signature, in the format described on this class
154
     * @param rest  Other signatures, in the format described on this class
155
     *
156
     * @return A sig matcher
157
     *
158
     * @throws IllegalArgumentException If any signature is malformed (see <a href='#ebnf'>EBNF</a>)
159
     * @throws NullPointerException     If any signature is null
160
     * @see #parse(String)
161
     */
162
    public static CompoundInvocationMatcher parseAll(String first, String... rest) {
163
        List<InvocationMatcher> matchers = CollectionUtil.map(listOf(first, rest), InvocationMatcher::parse);
1✔
164
        return new CompoundInvocationMatcher(matchers);
1✔
165
    }
166

167
    /**
168
     * Parses an {@link InvocationMatcher}.
169
     *
170
     * @param sig A signature in the format described on this class
171
     *
172
     * @return A sig matcher
173
     *
174
     * @throws IllegalArgumentException If the signature is malformed (see <a href='#ebnf'>EBNF</a>)
175
     * @throws NullPointerException     If the signature is null
176
     * @see #parseAll(String, String...)
177
     */
178
    public static InvocationMatcher parse(String sig) {
179
        int i = parseType(sig, 0);
1✔
180
        final TypeMatcher qualifierMatcher = newMatcher(sig.substring(0, i));
1✔
181
        i = consumeChar(sig, i, '#');
1✔
182
        final int nameStart = i;
1✔
183
        i = parseSimpleName(sig, i);
1✔
184
        final String methodName = sig.substring(nameStart, i);
1✔
185
        i = consumeChar(sig, i, '(');
1✔
186
        if (isChar(sig, i, ')')) {
1✔
187
            return new InvocationMatcher(qualifierMatcher, methodName, Collections.emptyList());
1✔
188
        }
189
        // (_*) matches any argument list
190
        List<TypeMatcher> argMatchers;
191
        if (isChar(sig, i, '_')
1✔
192
            && isChar(sig, i + 1, '*')
1✔
193
            && isChar(sig, i + 2, ')')) {
1!
194
            argMatchers = null;
1✔
195
            i = i + 3;
1✔
196
        } else {
197
            argMatchers = new ArrayList<>();
1✔
198
            i = parseArgList(sig, i, argMatchers);
1✔
199
        }
200
        if (i != sig.length()) {
1!
201
            throw new IllegalArgumentException("Not a valid signature " + sig);
×
202
        }
203
        return new InvocationMatcher(qualifierMatcher, methodName, argMatchers);
1✔
204
    }
205

206
    private static int parseSimpleName(String sig, final int start) {
207
        int i = start;
1✔
208
        while (i < sig.length() && Character.isJavaIdentifierPart(sig.charAt(i))) {
1✔
209
            i++;
1✔
210
        }
211
        if (i == start) {
1✔
212
            throw new IllegalArgumentException("Not a valid signature " + sig);
1✔
213
        }
214
        return i;
1✔
215
    }
216

217
    private static int parseArgList(String sig, int i, List<TypeMatcher> argMatchers) {
218
        while (i < sig.length()) {
1!
219
            i = parseType(sig, i, argMatchers);
1✔
220
            if (isChar(sig, i, ')')) {
1✔
221
                return i + 1;
1✔
222
            }
223
            i = consumeChar(sig, i, ',');
1✔
224
        }
225
        throw new IllegalArgumentException("Not a valid signature " + sig);
×
226
    }
227

228
    private static int consumeChar(String source, int i, char c) {
229
        if (isChar(source, i, c)) {
1!
230
            return i + 1;
1✔
231
        }
232
        throw newParseException(source, i, "character '" + c + "'");
×
233
    }
234

235
    private static RuntimeException newParseException(String source, int i, String expectedWhat) {
236
        final String indent = "    ";
1✔
237
        String message = "Expected " + expectedWhat + " at index " + i + ":\n";
1✔
238
        message += indent + "\"" + StringUtil.escapeJava(source) + "\"\n";
1✔
239
        message += indent + StringUtils.repeat(' ', i + 1) + '^' + "\n";
1✔
240
        return new IllegalArgumentException(message);
1✔
241
    }
242

243
    private static boolean isChar(String source, int i, char c) {
244
        return i < source.length() && source.charAt(i) == c;
1!
245
    }
246

247
    private static int parseType(String source, int i, List<TypeMatcher> result) {
248
        final int start = i;
1✔
249
        i = parseType(source, i);
1✔
250
        result.add(newMatcher(source.substring(start, i)));
1✔
251
        return i;
1✔
252
    }
253

254
    private static int parseType(String source, int i) {
255
        final int start = i;
1✔
256
        while (i < source.length() && (Character.isJavaIdentifierPart(source.charAt(i))
1!
257
            || source.charAt(i) == '.')) {
1✔
258
            i++;
1✔
259
        }
260
        if (i == start) {
1✔
261
            throw newParseException(source, i, "type");
1✔
262
        }
263

264
        AssertionUtil.assertValidJavaBinaryName(source.substring(start, i));
1✔
265
        // array dimensions
266
        while (isChar(source, i, '[')) {
1✔
267
            i = consumeChar(source, i + 1, ']');
1✔
268
        }
269
        return i;
1✔
270
    }
271

272
    private static TypeMatcher newMatcher(String name) {
273
        return "_".equals(name) ? TypeMatcher.ANY : new TypeMatcher(name);
1✔
274
    }
275

276
    @Override
277
    public boolean equals(Object o) {
278
        if (o == null || getClass() != o.getClass()) {
1✔
279
            return false;
1✔
280
        }
281
        InvocationMatcher that = (InvocationMatcher) o;
1✔
282
        return Objects.equals(expectedName, that.expectedName)
1✔
283
                && Objects.equals(argMatchers, that.argMatchers)
1✔
284
                && Objects.equals(qualifierMatcher, that.qualifierMatcher);
1✔
285
    }
286

287
    @Override
288
    public int hashCode() {
289
        return Objects.hash(expectedName, argMatchers, qualifierMatcher);
1✔
290
    }
291

292
    private static final class TypeMatcher {
293

294
        /** Matches any type. */
295
        public static final TypeMatcher ANY = new TypeMatcher(null);
1✔
296

297
        final @Nullable String name;
298

299
        private TypeMatcher(@Nullable String name) {
1✔
300
            this.name = name;
1✔
301
        }
1✔
302

303
        boolean matches(JTypeMirror type, boolean exact) {
304
            return name == null
1✔
305
                   || (exact ? TypeTestUtil.isExactlyAOrAnon(name, type) == OptionalBool.YES
1✔
306
                         : TypeTestUtil.isA(name, type));
1✔
307
        }
308

309
        @Override
310
        public boolean equals(Object o) {
311
            if (o == null || getClass() != o.getClass()) {
1!
312
                return false;
×
313
            }
314
            TypeMatcher that = (TypeMatcher) o;
1✔
315
            return Objects.equals(name, that.name);
1✔
316
        }
317

318
        @Override
319
        public int hashCode() {
320
            return Objects.hashCode(name);
1✔
321
        }
322
    }
323

324
    /**
325
     * A compound of several matchers (logical OR). Get one from
326
     * {@link InvocationMatcher#parseAll(String, String...)};
327
     */
328
    public static final class CompoundInvocationMatcher {
329

330
        private final List<InvocationMatcher> matchers;
331

332
        private CompoundInvocationMatcher(List<InvocationMatcher> matchers) {
1✔
333
            this.matchers = matchers;
1✔
334
        }
1✔
335

336
        // todo make this smarter. Like collecting all possible names
337
        //  into a set to do a quick pre-screening before we test
338
        //  everything linearly
339

340
        /**
341
         * Returns true if any of the matchers match the node.
342
         *
343
         * @see #matchesCall(JavaNode)
344
         */
345
        public boolean anyMatch(InvocationNode node) {
346
            return CollectionUtil.any(matchers, it -> it.matchesCall(node));
1✔
347
        }
348

349
        /**
350
         * Returns true if any of the matchers match the node.
351
         *
352
         * @see #matchesCall(JavaNode)
353
         */
354
        public boolean anyMatch(JavaNode node) {
355
            return CollectionUtil.any(matchers, it -> it.matchesCall(node));
1✔
356
        }
357
    }
358
}
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