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

pmd / pmd / 44

22 Jun 2025 12:54PM UTC coverage: 78.379% (+0.004%) from 78.375%
44

push

github

adangel
Fix #2304: [java] UnnecessaryImport FP for on-demand imports in JavaDoc (#5818)

Merge pull request #5818 from lukasgraef:issue2304

17724 of 23450 branches covered (75.58%)

Branch coverage included in aggregate %.

14 of 15 new or added lines in 1 file covered. (93.33%)

38922 of 48822 relevant lines covered (79.72%)

0.81 hits per line

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

92.29
/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/rule/codestyle/UnnecessaryImportRule.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.HashSet;
8
import java.util.List;
9
import java.util.Set;
10
import java.util.function.Predicate;
11
import java.util.regex.Matcher;
12
import java.util.regex.Pattern;
13
import java.util.stream.Collectors;
14

15
import org.apache.commons.lang3.StringUtils;
16
import org.slf4j.Logger;
17
import org.slf4j.LoggerFactory;
18

19
import net.sourceforge.pmd.lang.java.ast.ASTAmbiguousName;
20
import net.sourceforge.pmd.lang.java.ast.ASTClassType;
21
import net.sourceforge.pmd.lang.java.ast.ASTCompilationUnit;
22
import net.sourceforge.pmd.lang.java.ast.ASTImportDeclaration;
23
import net.sourceforge.pmd.lang.java.ast.ASTMethodCall;
24
import net.sourceforge.pmd.lang.java.ast.ASTSwitchLabel;
25
import net.sourceforge.pmd.lang.java.ast.ASTSwitchLike;
26
import net.sourceforge.pmd.lang.java.ast.ASTVariableAccess;
27
import net.sourceforge.pmd.lang.java.ast.JavaComment;
28
import net.sourceforge.pmd.lang.java.ast.JavaNode;
29
import net.sourceforge.pmd.lang.java.ast.JavadocComment;
30
import net.sourceforge.pmd.lang.java.ast.internal.PrettyPrintingUtil;
31
import net.sourceforge.pmd.lang.java.rule.AbstractJavaRule;
32
import net.sourceforge.pmd.lang.java.symbols.JAccessibleElementSymbol;
33
import net.sourceforge.pmd.lang.java.symbols.JClassSymbol;
34
import net.sourceforge.pmd.lang.java.symbols.JExecutableSymbol;
35
import net.sourceforge.pmd.lang.java.symbols.JFieldSymbol;
36
import net.sourceforge.pmd.lang.java.symbols.JModuleSymbol;
37
import net.sourceforge.pmd.lang.java.symbols.JTypeDeclSymbol;
38
import net.sourceforge.pmd.lang.java.symbols.JVariableSymbol;
39
import net.sourceforge.pmd.lang.java.symbols.table.ScopeInfo;
40
import net.sourceforge.pmd.lang.java.symbols.table.coreimpl.ShadowChainIterator;
41
import net.sourceforge.pmd.lang.java.types.JClassType;
42
import net.sourceforge.pmd.lang.java.types.JMethodSig;
43
import net.sourceforge.pmd.lang.java.types.JTypeMirror;
44
import net.sourceforge.pmd.lang.java.types.JVariableSig;
45
import net.sourceforge.pmd.lang.java.types.OverloadSelectionResult;
46
import net.sourceforge.pmd.lang.java.types.TypeSystem;
47
import net.sourceforge.pmd.lang.java.types.TypeTestUtil;
48
import net.sourceforge.pmd.lang.java.types.TypesFromReflection;
49
import net.sourceforge.pmd.util.CollectionUtil;
50
import net.sourceforge.pmd.util.IteratorUtil;
51

52
/**
53
 * Detects unnecessary imports.
54
 *
55
 * <p>For PMD 7 I had hoped this rule could be rewritten to use the
56
 * symbol table implementation directly instead of reimplementing a
57
 * symbol table (with less care). This would be good for performance
58
 * and correctness. Modifying the symbol table chain to track which
59
 * import is used is hard though, mostly because the API to expose
60
 * is unclear (we wouldn't want symbol tables to expose a mutable API).
61
 */
62
public class UnnecessaryImportRule extends AbstractJavaRule {
1✔
63

64
    private static final String UNUSED_IMPORT_MESSAGE = "Unused import ''{0}''";
65
    private static final String UNUSED_STATIC_IMPORT_MESSAGE = "Unused static import ''{0}''";
66
    private static final String DUPLICATE_IMPORT_MESSAGE = "Duplicate import ''{0}''";
67
    private static final String IMPORT_FROM_SAME_PACKAGE_MESSAGE = "Unnecessary import from the current package ''{0}''";
68
    private static final String IMPORT_FROM_JAVA_LANG_MESSAGE = "Unnecessary import from the java.lang package ''{0}''";
69

70

71
    private static final Logger LOG = LoggerFactory.getLogger(UnnecessaryImportRule.class);
1✔
72

73
    private final Set<ImportWrapper> allSingleNameImports = new HashSet<>();
1✔
74
    private final Set<ImportWrapper> staticImportsOnDemand = new HashSet<>();
1✔
75
    private final Set<ImportWrapper> typeImportsOnDemand = new HashSet<>();
1✔
76
    private final Set<ImportWrapper> moduleImports = new HashSet<>();
1✔
77
    private final Set<ImportWrapper> unnecessaryJavaLangImports = new HashSet<>();
1✔
78
    private final Set<ImportWrapper> unnecessaryImportsFromSamePackage = new HashSet<>();
1✔
79

80
    /*
81
     * Patterns to match the following constructs:
82
     *
83
     * @see package.class#member(param, param) label
84
     * {@linkplain package.class#member(param, param) label}
85
     * {@link package.class#member(param, param) label}
86
     * {@link package.class#field}
87
     * {@value package.class#field}
88
     *
89
     * @throws package.class label
90
     * @exception package.class label
91
     */
92

93
    /* package.class#member(param, param) */
94
    private static final String TYPE_PART_GROUP = "((?:\\p{Alpha}\\w*\\.)*(?:\\p{Alpha}\\w*))?(?:#\\w*(?:\\(([.\\w\\s,\\[\\]]*)\\))?)?";
95

96
    private static final Pattern SEE_PATTERN = Pattern.compile("@see\\s+" + TYPE_PART_GROUP);
1✔
97

98

99
    private static final Pattern LINK_PATTERNS = Pattern.compile("\\{@link(?:plain)?\\s+" + TYPE_PART_GROUP + "[\\s\\}]");
1✔
100

101
    private static final Pattern VALUE_PATTERN = Pattern.compile("\\{@value\\s+(\\p{Alpha}\\w*)[\\s#\\}]");
1✔
102

103
    private static final Pattern THROWS_PATTERN = Pattern.compile("@throws\\s+(\\p{Alpha}\\w*)");
1✔
104

105
    private static final Pattern EXCEPTION_PATTERN = Pattern.compile("@exception\\s+(\\p{Alpha}\\w*)");
1✔
106

107
    /* // @link substring="a" target="package.class#member(param, param)" */
108
    private static final Pattern LINK_IN_SNIPPET = Pattern
1✔
109
        .compile("//\\s*@link\\s+(?:.*?)?target=[\"']?" + TYPE_PART_GROUP + "[\"']?");
1✔
110

111
    /*
112
     * Java 23, JEP 467: Markdown Documentation Comments
113
     *
114
     * [Type#method()]
115
     * [Type]
116
     * [alternative Text][Type#method()]
117
     * [alternative Text][Type]
118
     */
119
    private static final Pattern MARKDOWN_PATTERN = Pattern.compile("\\[" + TYPE_PART_GROUP + "]");
1✔
120

121
    private static final Pattern[] PATTERNS = { SEE_PATTERN, LINK_PATTERNS, VALUE_PATTERN, THROWS_PATTERN,
1✔
122
                                                EXCEPTION_PATTERN, LINK_IN_SNIPPET, MARKDOWN_PATTERN };
123

124
    @Override
125
    public Object visit(ASTCompilationUnit node, Object data) {
126
        this.moduleImports.clear();
1✔
127
        this.allSingleNameImports.clear();
1✔
128
        this.staticImportsOnDemand.clear();
1✔
129
        this.typeImportsOnDemand.clear();
1✔
130
        this.unnecessaryJavaLangImports.clear();
1✔
131
        this.unnecessaryImportsFromSamePackage.clear();
1✔
132
        String packageName = node.getPackageName();
1✔
133

134
        for (ASTImportDeclaration importDecl : node.children(ASTImportDeclaration.class)) {
1✔
135
            visitImport(importDecl, data, packageName);
1✔
136
        }
1✔
137

138
        for (ImportWrapper wrapper : allSingleNameImports) {
1✔
139
            if ("java.lang".equals(wrapper.node.getPackageName())) {
1✔
140
                if (!isJavaLangImportNecessary(node, wrapper)) {
1✔
141
                    // the import is not shadowing something
142
                    unnecessaryJavaLangImports.add(wrapper);
1✔
143
                }
144
            }
145
        }
1✔
146

147
        super.visit(node, data);
1✔
148
        visitComments(node);
1✔
149

150
        doReporting(data);
1✔
151

152
        return data;
1✔
153
    }
154

155
    private void doReporting(Object data) {
156
        for (ImportWrapper wrapper : allSingleNameImports) {
1✔
157
            String message = wrapper.isStatic() ? UNUSED_STATIC_IMPORT_MESSAGE : UNUSED_IMPORT_MESSAGE;
1✔
158
            reportWithMessage(wrapper.node, data, message);
1✔
159
        }
1✔
160
        for (ImportWrapper wrapper : staticImportsOnDemand) {
1✔
161
            reportWithMessage(wrapper.node, data, UNUSED_STATIC_IMPORT_MESSAGE);
1✔
162
        }
1✔
163
        for (ImportWrapper wrapper : typeImportsOnDemand) {
1✔
164
            reportWithMessage(wrapper.node, data, UNUSED_IMPORT_MESSAGE);
1✔
165
        }
1✔
166
        for (ImportWrapper wrapper : moduleImports) {
1✔
167
            reportWithMessage(wrapper.node, data, "Unused module import ''{0}''");
1✔
168
        }
1✔
169

170
        // remove unused ones, they have already been reported
171
        unnecessaryJavaLangImports.removeAll(allSingleNameImports);
1✔
172
        unnecessaryJavaLangImports.removeAll(staticImportsOnDemand);
1✔
173
        unnecessaryJavaLangImports.removeAll(typeImportsOnDemand);
1✔
174
        unnecessaryImportsFromSamePackage.removeAll(allSingleNameImports);
1✔
175
        unnecessaryImportsFromSamePackage.removeAll(staticImportsOnDemand);
1✔
176
        unnecessaryImportsFromSamePackage.removeAll(typeImportsOnDemand);
1✔
177
        for (ImportWrapper wrapper : unnecessaryJavaLangImports) {
1✔
178
            reportWithMessage(wrapper.node, data, IMPORT_FROM_JAVA_LANG_MESSAGE);
1✔
179
        }
1✔
180
        for (ImportWrapper wrapper : unnecessaryImportsFromSamePackage) {
1✔
181
            reportWithMessage(wrapper.node, data, IMPORT_FROM_SAME_PACKAGE_MESSAGE);
1✔
182
        }
1✔
183
    }
1✔
184

185
    private boolean isJavaLangImportNecessary(ASTCompilationUnit node, ImportWrapper wrapper) {
186
        ShadowChainIterator<JTypeMirror, ScopeInfo> iter =
1✔
187
            node.getSymbolTable().types().iterateResults(wrapper.node.getImportedSimpleName());
1✔
188
        if (iter.hasNext()) {
1!
189
            iter.next();
1✔
190
            if (iter.getScopeTag() == ScopeInfo.SINGLE_IMPORT) {
1!
191
                if (iter.hasNext()) {
1!
192
                    iter.next();
1✔
193
                    // the import is shadowing something else
194
                    return iter.getScopeTag() != ScopeInfo.JAVA_LANG;
1✔
195
                }
196
            }
197
        }
198
        return false;
×
199
    }
200

201
    private void visitComments(ASTCompilationUnit node) {
202
        // todo improve that when we have a javadoc parser
203
        for (JavaComment comment : node.getComments()) {
1✔
204
            if (!(comment instanceof JavadocComment)) {
1✔
205
                continue;
1✔
206
            }
207

208
            String filteredCommentText = IteratorUtil.toStream(comment.getFilteredLines(true))
1✔
209
                    .collect(Collectors.joining("\n"));
1✔
210

211
            for (Pattern p : PATTERNS) {
1✔
212
                Matcher m = p.matcher(filteredCommentText);
1✔
213
                while (m.find()) {
1✔
214
                    String fullname = m.group(1);
1✔
215

216
                    if (fullname != null) { // may be null for "@see #" and "@link #"
1✔
217
                        removeReferenceSingleImport(fullname);
1✔
218
                        removeReferenceOnDemandImport(fullname);
1✔
219
                    }
220

221
                    if (m.groupCount() > 1) {
1✔
222
                        fullname = m.group(2);
1✔
223
                        if (fullname != null) {
1✔
224
                            for (String param : fullname.split("\\s*,\\s*")) {
1✔
225
                                removeReferenceSingleImport(param);
1✔
226
                                removeReferenceOnDemandImport(param);
1✔
227
                            }
228
                        }
229
                    }
230

231
                    if (allSingleNameImports.isEmpty()) {
1✔
232
                        return;
1✔
233
                    }
234
                }
1✔
235
            }
236
        }
1✔
237
    }
1✔
238

239
    private void visitImport(ASTImportDeclaration node, Object data, String thisPackageName) {
240
        if (thisPackageName.equals(node.getPackageName())) {
1✔
241
            unnecessaryImportsFromSamePackage.add(new ImportWrapper(node));
1✔
242
        }
243

244
        Set<ImportWrapper> container = getImportContainer(node);
1✔
245

246

247
        if (!container.add(new ImportWrapper(node))) {
1✔
248
            // duplicate
249
            reportWithMessage(node, data, DUPLICATE_IMPORT_MESSAGE);
1✔
250
        }
251
    }
1✔
252

253
    private Set<ImportWrapper> getImportContainer(ASTImportDeclaration node) {
254
        if (node.isModuleImport()) {
1✔
255
            return moduleImports;
1✔
256
        } else if (node.isImportOnDemand()) {
1✔
257
            if (node.isStatic()) {
1✔
258
                return staticImportsOnDemand;
1✔
259
            }
260
            return typeImportsOnDemand;
1✔
261
        }
262
        return allSingleNameImports;
1✔
263
    }
264

265
    private void reportWithMessage(ASTImportDeclaration node, Object data, String message) {
266
        asCtx(data).addViolationWithMessage(node, message, PrettyPrintingUtil.prettyImport(node));
1✔
267
    }
1✔
268

269
    @Override
270
    public Object visit(ASTClassType node, Object data) {
271
        if (node.getQualifier() == null
1✔
272
            && !node.isFullyQualified()
1✔
273
            && node.getTypeMirror().isClassOrInterface()) {
1✔
274

275
            JClassSymbol symbol = ((JClassType) node.getTypeMirror()).getSymbol();
1✔
276
            ShadowChainIterator<JTypeMirror, ScopeInfo> scopeIter =
1✔
277
                node.getSymbolTable().types().iterateResults(node.getSimpleName());
1✔
278
            checkScopeChain(false, symbol, scopeIter, ts -> true, false);
1✔
279
        }
280
        return super.visit(node, data);
1✔
281
    }
282

283
    @Override
284
    public Object visit(ASTAmbiguousName node, Object data) {
285
        // ambiguous name means the symbol table could not resolve the first name
286

287
        // only consider static imports
288
        boolean onlyStatic = !(node.getParent() instanceof ASTClassType);
1!
289
        recordFailedTypeResWithName(node, node.getFirstToken().getImage(), onlyStatic);
1✔
290
        return null;
1✔
291
    }
292

293
    private void recordFailedTypeResWithName(JavaNode location, String name, boolean onlyStatics) {
294
        String target = onlyStatics ? "static " : "";
1!
295
        LOG.debug("UnnecessaryImport: Failed type res for {} will cause all {}imports named {} to be marked as used", location, target, name);
1✔
296
        boolean foundNamedImport = allSingleNameImports.removeIf(
1✔
297
            decl -> (!onlyStatics || decl.isStatic())
1!
298
                && name.equals(decl.node.getImportedSimpleName()));
1✔
299
        if (!foundNamedImport) {
1✔
300
            LOG.debug("+ Since no such named import can be found, all {}on-demand-imports will be marked as used", target);
1✔
301

302
            if (onlyStatics) {
1!
303
                staticImportsOnDemand.clear();
1✔
304
            } else {
305
                typeImportsOnDemand.clear();
×
306
            }
307
        }
308
    }
1✔
309

310
    @Override
311
    public Object visit(ASTMethodCall node, Object data) {
312
        if (node.getQualifier() == null) {
1✔
313
            OverloadSelectionResult overload = node.getOverloadSelectionInfo();
1✔
314
            if (overload.isFailed()) {
1✔
315
                // don't try further, but still visit all ASTClassType nodes in the AST.
316
                recordFailedTypeResWithName(node, node.getMethodName(), true);
1✔
317
                return super.visit(node, data); // todo we're erring towards FPs
1✔
318
            }
319

320
            ShadowChainIterator<JMethodSig, ScopeInfo> scopeIter =
1✔
321
                node.getSymbolTable().methods().iterateResults(node.getMethodName());
1✔
322

323

324
            JExecutableSymbol symbol = overload.getMethodType().getSymbol();
1✔
325
            checkScopeChain(true,
1✔
326
                            symbol,
327
                            scopeIter,
328
                            methods -> CollectionUtil.any(methods, m -> m.getSymbol().equals(symbol)),
1✔
329
                            true);
330
        }
331
        return super.visit(node, data);
1✔
332
    }
333

334
    @Override
335
    public Object visit(ASTVariableAccess node, Object data) {
336
        JVariableSymbol sym = node.getReferencedSym();
1✔
337
        if (sym != null
1✔
338
            && sym.isField()
1✔
339
            && ((JFieldSymbol) sym).isStatic()) {
1✔
340

341
            if (node.getParent() instanceof ASTSwitchLabel
1✔
342
                && node.ancestors(ASTSwitchLike.class).take(1).any(ASTSwitchLike::isEnumSwitch)) {
1✔
343
                // special scoping rules, see JSymbolTable#variables doc
344
                return null;
1✔
345
            }
346

347
            ShadowChainIterator<JVariableSig, ScopeInfo> scopeIter = node.getSymbolTable().variables().iterateResults(node.getName());
1✔
348
            checkScopeChain(false, (JFieldSymbol) sym, scopeIter, ts -> true, true);
1✔
349
        }
350
        if (sym == null) {
1✔
351
            recordFailedTypeResWithName(node, node.getName(), true);
1✔
352
        }
353
        return null;
1✔
354
    }
355

356
    private <T> void checkScopeChain(boolean recursive,
357
                                     JAccessibleElementSymbol symbol,
358
                                     ShadowChainIterator<T, ScopeInfo> scopeIter,
359
                                     Predicate<List<T>> containsTarget,
360
                                     boolean onlyStatic) {
361
        while (scopeIter.hasNext()) {
1✔
362
            scopeIter.next();
1✔
363
            // must be the first result
364
            // todo make sure new Outer().new Inner() does not mark Inner as used
365
            if (containsTarget.test(scopeIter.getResults())) {
1!
366
                // We found the declaration bringing the symbol in scope
367
                // If it's an import, then it's used. However, maybe it's from java.lang.
368

369
                if (scopeIter.getScopeTag() == ScopeInfo.SINGLE_IMPORT) {
1✔
370

371
                    allSingleNameImports.removeIf(
1✔
372
                        it -> (it.isStatic() || !onlyStatic)
1✔
373
                            && symbol.getSimpleName().equals(it.node.getImportedSimpleName())
1✔
374
                    );
375

376
                } else if (scopeIter.getScopeTag() == ScopeInfo.IMPORT_ON_DEMAND) {
1✔
377

378
                    boolean found = typeImportsOnDemand.removeIf(it -> importOnDemandImportsSymbol(symbol, onlyStatic, it));
1✔
379
                    if (!found) {
1✔
380
                        staticImportsOnDemand.removeIf(it -> importOnDemandImportsSymbol(symbol, onlyStatic, it));
1✔
381
                    }
382
                } else if (scopeIter.getScopeTag() == ScopeInfo.MODULE_IMPORT) {
1✔
383
                    moduleImports.removeIf(it -> {
1✔
384
                        if (!(symbol instanceof JTypeDeclSymbol)) {
1!
385
                            return false;
×
386
                        }
387

388
                        JTypeDeclSymbol typeSymbol = (JTypeDeclSymbol) symbol;
1✔
389
                        String moduleName = it.node.getImportedName();
1✔
390
                        String simpleName = typeSymbol.getSimpleName();
1✔
391
                        TypeSystem typeSystem = typeSymbol.getTypeSystem();
1✔
392
                        JModuleSymbol moduleSymbol = typeSystem.getModuleSymbol(moduleName);
1✔
393
                        boolean found = false;
1✔
394
                        for (String packageName : moduleSymbol.getExportedPackages()) {
1!
395
                            JClassSymbol classSymbol = typeSystem.getClassSymbol(packageName + "." + simpleName);
1✔
396
                            if (classSymbol != null) {
1✔
397
                                found = TypeTestUtil.isA(typeSystem.rawType(typeSymbol), typeSystem.rawType(classSymbol));
1✔
398
                            }
399
                            if (found) {
1✔
400
                                break;
1✔
401
                            }
402
                        }
1✔
403
                        return found;
1✔
404
                    });
405
                }
406
                return;
1✔
407
            }
408
            if (!recursive) {
×
409
                break;
×
410
            }
411
        }
412
        // unknown reference
413
    }
1✔
414

415
    private static boolean importOnDemandImportsSymbol(JAccessibleElementSymbol symbol, boolean onlyStatic, ImportWrapper it) {
416
        if (!it.isStatic() && onlyStatic) {
1✔
417
            return false;
1✔
418
        }
419
        // This is the class that contains the symbol
420
        // we're looking for.
421
        // We have to test whether this symbol is contained
422
        // by the imported type or package.
423
        JClassSymbol symbolOwner = symbol.getEnclosingClass();
1✔
424
        if (symbolOwner == null) {
1✔
425
            // package import on demand
426
            return it.node.getImportedName().equals(symbol.getPackageName());
1✔
427
        } else {
428
            if (it.node.getImportedName().equals(symbolOwner.getCanonicalName())) {
1✔
429
                // If the import is not static, then it imports static and non-static types.
430
                // Otherwise, it imports static members (types + other things)
431
                return !it.isStatic() || symbol.isStatic();
1!
432
            }
433
            // maybe we're importing a subclass of the container.
434
            TypeSystem ts = symbolOwner.getTypeSystem();
1✔
435
            JClassSymbol importedContainer = ts.getClassSymbol(it.node.getImportedName());
1✔
436
            return importedContainer == null // insufficient classpath, err towards FNs
1!
437
                || TypeTestUtil.isA(ts.rawType(symbolOwner), ts.rawType(importedContainer));
1✔
438
        }
439
    }
440

441

442
    /** We found a reference to the type given by the name. */
443
    private void removeReferenceSingleImport(String referenceName) {
444
        String expectedImport = StringUtils.substringBefore(referenceName, ".");
1✔
445
        allSingleNameImports.removeIf(it -> expectedImport.equals(it.node.getImportedSimpleName()));
1✔
446
    }
1✔
447

448
    private void removeReferenceOnDemandImport(String referenceName) {
449
        if (referenceName.isEmpty()) {
1✔
450
            return;
1✔
451
        }
452

453
        typeImportsOnDemand.removeIf(it -> {
1✔
454
            final ASTImportDeclaration importNode = it.node;
1✔
455
            return importNode.isImportOnDemand()
1!
456
                    && TypesFromReflection.loadSymbol(importNode.getTypeSystem(), importNode.getPackageName() + "." + referenceName) != null;
1✔
457
        });
458
        staticImportsOnDemand.removeIf(it -> {
1✔
459
            final ASTImportDeclaration importNode = it.node;
1✔
460
            if (importNode.isImportOnDemand()) {
1!
461
                final JClassSymbol symbol = TypesFromReflection.loadSymbol(importNode.getTypeSystem(), importNode.getImportedName());
1✔
462
                return symbol != null && symbol.getDeclaredClass(referenceName) != null;
1!
463
            }
464

NEW
465
            return false;
×
466
        });
467
    }
1✔
468

469
    /** Override the equal behaviour of ASTImportDeclaration to put it into a set. */
470
    private static final class ImportWrapper {
471

472
        private final ASTImportDeclaration node;
473

474
        private ImportWrapper(ASTImportDeclaration node) {
1✔
475
            this.node = node;
1✔
476
        }
1✔
477

478
        @Override
479
        public boolean equals(Object o) {
480
            if (this == o) {
1!
481
                return true;
×
482
            }
483
            if (getClass() != o.getClass()) {
1!
484
                return false;
×
485
            }
486
            ImportWrapper that = (ImportWrapper) o;
1✔
487
            return node.getImportedName().equals(that.node.getImportedName())
1!
488
                && node.isImportOnDemand() == that.node.isImportOnDemand()
1!
489
                && this.isStatic() == that.isStatic();
1!
490
        }
491

492
        @Override
493
        public int hashCode() {
494
            return node.getImportedName().hashCode() * 31
1✔
495
                + Boolean.hashCode(node.isStatic())
1✔
496
                + 37 * Boolean.hashCode(node.isImportOnDemand());
1✔
497
        }
498

499
        private boolean isStatic() {
500
            return this.node.isStatic();
1✔
501
        }
502
    }
503
}
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