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

pmd / pmd / 670

16 Jul 2026 02:41PM UTC coverage: 79.205% (+0.002%) from 79.203%
670

push

github

web-flow
[core] Fix #4953: Deprecate PMDConfiguration#getClassLoader (#6845)

* [core] Fix #4953: Deprecate PMDConfiguration#getClassLoader

PMDConfiguration now stores the given raw auxClasspath as string.
For backwards compatibility, a classloader can still be set externally
when using the programmatic API.
But usually (e.g. via CLI), the auxClasspath is set now via the
JvmLanguagePropertyBundle only,
which creates the actual classloader only when requested.
This means, the classloader is not created anymore eagerly by
PMDConfiguration.

The given auxClasspath is checked early for validity.
All files/directories must exist.

* [core] fix #4952: Document PMDConfiguration classLoader closing

* Add TODO for #6841

* Fix test auxClasspathWithRelativeFile() under Windows

* fix: when verifying auxclasspath allow not existing directory entries

At least maven-pmd-plugin might provide a directory, which doesn't
exist, e.g. "target/test-classes". As we don't check the auxclasspath
for completeness (which we can't anyway at this point), there is
no difference between a non-existing directory or an empty
directory or an incomplete directory. The result is the same:
the provided auxclasspath is incomplete.
The reason for the empty/non-existing directory might be, that the
project being analyzed was not compiled yet - but we don't know.

Later on, when trying to resolve classes from the auxclasspath
and we don't find classes, we can report this problem, as we
know for sure, that the auxclasspath is incomplete (see #5064).

For now, we only log a warning for each suspicious auxClasspath entry.

* Revert formatting changes in release notes

* Fix tests

* Fix unused import

* refactor: Use AuxClasspathUtil in PMDConfiguration

19235 of 25221 branches covered (76.27%)

Branch coverage included in aggregate %.

42 of 56 new or added lines in 2 files covered. (75.0%)

41684 of 51692 relevant lines covered (80.64%)

0.81 hits per line

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

79.79
/pmd-core/src/main/java/net/sourceforge/pmd/PmdAnalysis.java
1
/*
2
 * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
3
 */
4

5
package net.sourceforge.pmd;
6

7
import static net.sourceforge.pmd.lang.document.InternalApiBridge.newCollector;
8
import static net.sourceforge.pmd.lang.rule.InternalApiBridge.loadRuleSetsWithoutException;
9
import static net.sourceforge.pmd.lang.rule.InternalApiBridge.ruleSetApplies;
10
import static net.sourceforge.pmd.util.CollectionUtil.listOf;
11

12
import java.nio.file.Path;
13
import java.util.ArrayList;
14
import java.util.Collection;
15
import java.util.Collections;
16
import java.util.HashMap;
17
import java.util.HashSet;
18
import java.util.List;
19
import java.util.Map;
20
import java.util.Objects;
21
import java.util.Set;
22

23
import org.slf4j.Logger;
24
import org.slf4j.LoggerFactory;
25
import org.slf4j.event.Level;
26

27
import net.sourceforge.pmd.benchmark.TimeTracker;
28
import net.sourceforge.pmd.benchmark.TimedOperation;
29
import net.sourceforge.pmd.benchmark.TimedOperationCategory;
30
import net.sourceforge.pmd.cache.internal.AnalysisCacheListener;
31
import net.sourceforge.pmd.cache.internal.NoopAnalysisCache;
32
import net.sourceforge.pmd.internal.LogMessages;
33
import net.sourceforge.pmd.internal.util.ClasspathClassLoader;
34
import net.sourceforge.pmd.internal.util.FileCollectionUtil;
35
import net.sourceforge.pmd.internal.util.IOUtil;
36
import net.sourceforge.pmd.lang.InternalApiBridge;
37
import net.sourceforge.pmd.lang.JvmLanguagePropertyBundle;
38
import net.sourceforge.pmd.lang.Language;
39
import net.sourceforge.pmd.lang.LanguageProcessor.AnalysisTask;
40
import net.sourceforge.pmd.lang.LanguageProcessorRegistry;
41
import net.sourceforge.pmd.lang.LanguageProcessorRegistry.LanguageTerminationException;
42
import net.sourceforge.pmd.lang.LanguagePropertyBundle;
43
import net.sourceforge.pmd.lang.LanguageRegistry;
44
import net.sourceforge.pmd.lang.LanguageVersion;
45
import net.sourceforge.pmd.lang.LanguageVersionDiscoverer;
46
import net.sourceforge.pmd.lang.document.FileCollector;
47
import net.sourceforge.pmd.lang.document.TextFile;
48
import net.sourceforge.pmd.lang.rule.Rule;
49
import net.sourceforge.pmd.lang.rule.RuleSet;
50
import net.sourceforge.pmd.lang.rule.RuleSetLoader;
51
import net.sourceforge.pmd.lang.rule.internal.RuleSets;
52
import net.sourceforge.pmd.renderers.AbstractAccumulatingRenderer;
53
import net.sourceforge.pmd.renderers.Renderer;
54
import net.sourceforge.pmd.reporting.ConfigurableFileNameRenderer;
55
import net.sourceforge.pmd.reporting.DeterministicOutputListenerWrapper;
56
import net.sourceforge.pmd.reporting.FileAnalysisListener;
57
import net.sourceforge.pmd.reporting.GlobalAnalysisListener;
58
import net.sourceforge.pmd.reporting.ListenerInitializer;
59
import net.sourceforge.pmd.reporting.Report;
60
import net.sourceforge.pmd.reporting.Report.GlobalReportBuilderListener;
61
import net.sourceforge.pmd.reporting.ReportStats;
62
import net.sourceforge.pmd.reporting.ReportStatsListener;
63
import net.sourceforge.pmd.util.AssertionUtil;
64
import net.sourceforge.pmd.util.CollectionUtil;
65
import net.sourceforge.pmd.util.StringUtil;
66
import net.sourceforge.pmd.util.internal.AuxClasspathUtil;
67
import net.sourceforge.pmd.util.log.PmdReporter;
68

69
/**
70
 * Main programmatic API of PMD. This is not a CLI entry point, see module
71
 * {@code pmd-cli} for that.
72
 *
73
 * <h2>Usage overview</h2>
74
 *
75
 * <p>Create and configure a {@link PMDConfiguration},
76
 * then use {@link #create(PMDConfiguration)} to obtain an instance.
77
 * You can perform additional configuration on the instance, e.g. adding
78
 * files to process, or additional rulesets and renderers. Then, call
79
 * {@link #performAnalysis()} or one of the related terminal methods.
80
 *
81
 * <h2>Simple example</h2>
82
 *
83
 * <pre>{@code
84
 *   PMDConfiguration config = new PMDConfiguration();
85
 *   config.setDefaultLanguageVersion(LanguageRegistry.findLanguageByTerseName("java").getVersion("11"));
86
 *   config.addInputPath(Path.of("src/main/java"));
87
 *   config.prependClasspath("target/classes");
88
 *   config.setMinimumPriority(RulePriority.HIGH);
89
 *   config.addRuleSet("rulesets/java/quickstart.xml");
90
 *   config.setReportFormat("xml");
91
 *   config.setReportFile("target/pmd-report.xml");
92
 *
93
 *   try (PmdAnalysis pmd = PmdAnalysis.create(config)) {
94
 *     // note: don't use `config` once a PmdAnalysis has been created.
95
 *     // optional: add more rulesets
96
 *     pmd.addRuleSet(pmd.newRuleSetLoader().loadFromResource("custom-ruleset.xml"));
97
 *     // optional: add more files
98
 *     pmd.files().addFile(Paths.get("src", "main", "more-java", "ExtraSource.java"));
99
 *     // optional: add more renderers
100
 *     pmd.addRenderer(renderer);
101
 *
102
 *     pmd.performAnalysis();
103
 *   }
104
 * }</pre>
105
 *
106
 * <h2>Rendering reports</h2>
107
 *
108
 * <p>If you just want to render a report to a file like with the CLI, you
109
 * should use a {@link Renderer}. You can add a custom one with {@link PmdAnalysis#addRenderer(Renderer)}.
110
 * You can add one of the builtin renderers from its ID using {@link PMDConfiguration#setReportFormat(String)}.
111
 *
112
 * <h2>Reports and events</h2>
113
 *
114
 * <p>If you want strongly typed access to violations and other analysis events,
115
 * you can implement and register a {@link GlobalAnalysisListener} with {@link #addListener(GlobalAnalysisListener)}.
116
 * The listener needs to provide a new {@link FileAnalysisListener} for each file,
117
 * which will receive events from the analysis. The listener's lifecycle
118
 * happens only once the analysis is started ({@link #performAnalysis()}).
119
 *
120
 * <p>If you want access to all events once the analysis ends instead of processing
121
 * events as they go, you can obtain a {@link Report} instance from {@link #performAnalysisAndCollectReport()},
122
 * or use {@link Report.GlobalReportBuilderListener} manually. Keep in
123
 * mind collecting a report is less memory-efficient than using a listener.
124
 *
125
 * <p>If you want to process events in batches, one per file, you can
126
 * use {@link Report.ReportBuilderListener}. to implement {@link GlobalAnalysisListener#startFileAnalysis(TextFile)}.
127
 *
128
 * <p>Listeners can be used alongside renderers.
129
 *
130
 * <h2>Specifying the Java classpath</h2>
131
 *
132
 * <p>Java rules work better if you specify the path to the compiled classes
133
 * of the analysed sources. See {@link PMDConfiguration#prependAuxClasspath(String)}.
134
 *
135
 * <h2>Customizing message output</h2>
136
 *
137
 * <p>The analysis reports messages like meta warnings and errors through a
138
 * {@link PmdReporter} instance. To override how those messages are output,
139
 * you can set it in {@link PMDConfiguration#setReporter(PmdReporter)}.
140
 * By default, it forwards messages to SLF4J.
141
 *
142
 */
143
public final class PmdAnalysis implements AutoCloseable {
144

145
    private static final Logger LOG = LoggerFactory.getLogger(PmdAnalysis.class);
1✔
146

147
    private final FileCollector collector;
148
    private final List<Renderer> renderers = new ArrayList<>();
1✔
149
    private final List<GlobalAnalysisListener> listeners = new ArrayList<>();
1✔
150
    private final List<RuleSet> ruleSets = new ArrayList<>();
1✔
151
    private final PMDConfiguration configuration;
152
    private final PmdReporter reporter;
153

154
    private final Map<Language, LanguagePropertyBundle> langProperties = new HashMap<>();
1✔
155
    private boolean closed;
156
    private final ConfigurableFileNameRenderer fileNameRenderer = new ConfigurableFileNameRenderer();
1✔
157

158
    /**
159
     * Constructs a new instance. The files paths (input files, filelist,
160
     * exclude list, etc) given in the configuration are collected into
161
     * the file collector ({@link #files()}), but more can be added
162
     * programmatically using the file collector.
163
     */
164
    private PmdAnalysis(PMDConfiguration config) {
1✔
165
        this.configuration = config;
1✔
166
        this.reporter = config.getReporter();
1✔
167
        this.collector = newCollector(
1✔
168
            config.getLanguageVersionDiscoverer(),
1✔
169
            reporter
170
        );
171

172
    }
1✔
173

174
    /**
175
     * Constructs a new instance from a configuration.
176
     *
177
     * <ul>
178
     * <li> The files paths (input files, filelist,
179
     * exclude list, etc) are explored and the files to analyse are
180
     * collected into the file collector ({@link #files()}).
181
     * More can be added programmatically using the file collector.
182
     * <li>The rulesets given in the configuration are loaded ({@link PMDConfiguration#getRuleSetPaths()})
183
     * <li>A renderer corresponding to the parameters of the configuration
184
     * is created and added (but not started).
185
     * </ul>
186
     */
187
    public static PmdAnalysis create(PMDConfiguration config) {
188
        PmdAnalysis pmd = new PmdAnalysis(config);
1✔
189

190
        // note: do not filter files by language
191
        // they could be ignored later. The problem is if you call
192
        // addRuleSet later, then you could be enabling new languages
193
        // So the files should not be pruned in advance
194
        FileCollectionUtil.collectFiles(config, pmd.files());
1✔
195

196
        if (config.getReportFormat() != null) {
1✔
197
            Renderer renderer = config.createRenderer(true);
1✔
198
            pmd.addRenderer(renderer);
1✔
199
        }
200

201
        if (!config.getRuleSetPaths().isEmpty()) {
1✔
202
            final RuleSetLoader ruleSetLoader = pmd.newRuleSetLoader();
1✔
203
            final List<RuleSet> ruleSets = loadRuleSetsWithoutException(ruleSetLoader, config.getRuleSetPaths());
1✔
204
            pmd.addRuleSets(ruleSets);
1✔
205
        }
206

207
        for (Language language : config.getLanguageRegistry()) {
1✔
208
            LanguagePropertyBundle props = config.getLanguageProperties(language);
1✔
209
            assert props.getLanguage().equals(language);
1!
210
            pmd.langProperties.put(language, props);
1✔
211

212
            LanguageVersion forcedVersion = config.getForceLanguageVersion();
1✔
213
            if (forcedVersion != null && forcedVersion.getLanguage().equals(language)) {
1✔
214
                props.setLanguageVersion(forcedVersion.getVersion());
1✔
215
            }
216

217
            // TODO replace those with actual language properties when the
218
            //  CLI syntax is implemented. #2947
219
            props.setProperty(LanguagePropertyBundle.SUPPRESS_MARKER, config.getSuppressMarker());
1✔
220
            if (props instanceof JvmLanguagePropertyBundle) {
1!
NEW
221
                ClassLoader externallyConfiguredClassLoader = config.getClassLoader();
×
NEW
222
                if (externallyConfiguredClassLoader != null) {
×
NEW
223
                    ((JvmLanguagePropertyBundle) props).setClassLoader(externallyConfiguredClassLoader);
×
224
                } else {
NEW
225
                    props.setProperty(JvmLanguagePropertyBundle.AUX_CLASSPATH, config.getAuxClasspath());
×
226
                }
227
            }
228
        }
1✔
229

230
        for (Path path : config.getRelativizeRoots()) {
1✔
231
            pmd.fileNameRenderer.relativizeWith(path);
1✔
232
        }
1✔
233

234
        return pmd;
1✔
235
    }
236

237
    // test only
238
    List<RuleSet> rulesets() {
239
        return ruleSets;
1✔
240
    }
241

242
    // test only
243
    List<Renderer> renderers() {
244
        return renderers;
1✔
245
    }
246

247

248
    /**
249
     * Returns the file collector for the analysed sources.
250
     */
251
    public FileCollector files() {
252
        return collector; // todo user can close collector programmatically
1✔
253
    }
254

255
    /**
256
     * Returns a new ruleset loader, which can be used to create new
257
     * rulesets (add them then with {@link #addRuleSet(RuleSet)}).
258
     *
259
     * <pre>{@code
260
     * try (PmdAnalysis pmd = create(config)) {
261
     *     pmd.addRuleSet(pmd.newRuleSetLoader().loadFromResource("custom-ruleset.xml"));
262
     * }
263
     * }</pre>
264
     */
265
    public RuleSetLoader newRuleSetLoader() {
266
        return RuleSetLoader.fromPmdConfig(configuration);
1✔
267
    }
268

269
    /**
270
     * Add a new renderer. The given renderer must not already be started,
271
     * it will be started by {@link #performAnalysis()}.
272
     *
273
     * @throws NullPointerException If the parameter is null
274
     */
275
    public void addRenderer(Renderer renderer) {
276
        AssertionUtil.requireParamNotNull("renderer", renderer);
1✔
277
        this.renderers.add(renderer);
1✔
278
    }
1✔
279

280
    /**
281
     * Add several renderers at once.
282
     *
283
     * @throws NullPointerException If the parameter is null, or any of its items is null.
284
     */
285
    public void addRenderers(Collection<Renderer> renderers) {
286
        renderers.forEach(this::addRenderer);
×
287
    }
×
288

289
    /**
290
     * Add a new listener. As per the contract of {@link GlobalAnalysisListener},
291
     * this object must be ready for interaction. However, nothing will
292
     * be done with the listener until {@link #performAnalysis()} is called.
293
     * The listener will be closed by {@link #performAnalysis()}, or
294
     * {@link #close()}, whichever happens first.
295
     *
296
     * @throws NullPointerException If the parameter is null
297
     */
298
    public void addListener(GlobalAnalysisListener listener) {
299
        AssertionUtil.requireParamNotNull("listener", listener);
1✔
300
        this.listeners.add(listener);
1✔
301
    }
1✔
302

303
    /**
304
     * Add several listeners at once.
305
     *
306
     * @throws NullPointerException If the parameter is null, or any of its items is null.
307
     * @see #addListener(GlobalAnalysisListener)
308
     */
309
    public void addListeners(Collection<? extends GlobalAnalysisListener> listeners) {
310
        listeners.forEach(this::addListener);
×
311
    }
×
312

313
    /**
314
     * Add a new ruleset.
315
     *
316
     * @throws NullPointerException If the parameter is null
317
     */
318
    public void addRuleSet(RuleSet ruleSet) {
319
        AssertionUtil.requireParamNotNull("rule set", ruleSet);
1✔
320
        this.ruleSets.add(ruleSet);
1✔
321
    }
1✔
322

323
    /**
324
     * Add several rulesets at once.
325
     *
326
     * @throws NullPointerException If the parameter is null, or any of its items is null.
327
     */
328
    public void addRuleSets(Collection<RuleSet> ruleSets) {
329
        ruleSets.forEach(this::addRuleSet);
1✔
330
    }
1✔
331

332
    /**
333
     * Returns an unmodifiable view of the ruleset list. That will be
334
     * processed.
335
     */
336
    public List<RuleSet> getRulesets() {
337
        return Collections.unmodifiableList(ruleSets);
1✔
338
    }
339

340

341
    /**
342
     * Returns a mutable bundle of language properties that are associated
343
     * to the given language (always the same for a given language).
344
     *
345
     * @param language A language, which must be registered
346
     */
347
    public LanguagePropertyBundle getLanguageProperties(Language language) {
348
        configuration.checkLanguageIsRegistered(language);
1✔
349
        return langProperties.computeIfAbsent(language, Language::newPropertyBundle);
1✔
350
    }
351

352

353
    public ConfigurableFileNameRenderer fileNameRenderer() {
354
        return fileNameRenderer;
1✔
355
    }
356

357
    /**
358
     * Run PMD with the current state of this instance. This will start
359
     * and finish the registered renderers, and close all
360
     * {@linkplain #addListener(GlobalAnalysisListener) registered listeners}.
361
     * All files collected in the {@linkplain #files() file collector} are
362
     * processed. This does not return a report, as the analysis results
363
     * are consumed by {@link GlobalAnalysisListener} instances (of which
364
     * Renderers are a special case). Note that this does
365
     * not throw, errors are instead accumulated into a {@link PmdReporter}.
366
     */
367
    public void performAnalysis() {
368
        performAnalysisImpl(Collections.emptyList());
1✔
369
    }
1✔
370

371
    /**
372
     * Run PMD with the current state of this instance. This will start
373
     * and finish the registered renderers. All files collected in the
374
     * {@linkplain #files() file collector} are processed. Returns the
375
     * output report. Note that this does not throw, errors are instead
376
     * accumulated into a {@link PmdReporter}.
377
     */
378
    public Report performAnalysisAndCollectReport() {
379
        try (GlobalReportBuilderListener reportBuilder = new GlobalReportBuilderListener()) {
1✔
380
            performAnalysisImpl(listOf(reportBuilder)); // closes the report builder
1✔
381
            return reportBuilder.getResultImpl();
1✔
382
        }
383
    }
384

385
    void performAnalysisImpl(List<? extends GlobalReportBuilderListener> extraListeners) {
386
        try (FileCollector files = collector) {
1✔
387
            files.filterLanguages(getApplicableLanguages(false));
1✔
388
            performAnalysisImpl(extraListeners, files.getCollectedFiles());
1✔
389
        }
390
    }
1✔
391

392
    void performAnalysisImpl(List<? extends GlobalReportBuilderListener> extraListeners, List<TextFile> textFiles) {
393
        if (textFiles.isEmpty()) {
1✔
394
            reporter.warn("No files to analyze. Check input paths and exclude parameters, use --debug to see file collection traces.");
1✔
395
        }
396

397
        RuleSets rulesets = new RuleSets(this.ruleSets);
1✔
398

399
        GlobalAnalysisListener listener;
400
        try {
401
            @SuppressWarnings("PMD.CloseResource")
402
            AnalysisCacheListener cacheListener = new AnalysisCacheListener(configuration.getAnalysisCache(), rulesets,
1✔
403
                    AuxClasspathUtil.getAuxClasspath(configuration), textFiles);
1✔
404
            listener = GlobalAnalysisListener.tee(listOf(createComposedRendererListener(renderers),
1✔
405
                                                         GlobalAnalysisListener.tee(listeners),
1✔
406
                                                         GlobalAnalysisListener.tee(extraListeners),
1✔
407
                                                         cacheListener));
408
            
409
            // Initialize listeners
410
            try (ListenerInitializer initializer = listener.initializer()) {
1✔
411
                initializer.setNumberOfFilesToAnalyze(textFiles.size());
1✔
412
                initializer.setFilesToAnalyze(CollectionUtil.map(textFiles, TextFile::getFileId));
1✔
413
                initializer.setFileNameRenderer(fileNameRenderer());
1✔
414
            }
415
        } catch (Exception e) {
×
416
            reporter.errorEx("Exception while initializing analysis listeners", e);
×
417
            throw new RuntimeException("Exception while initializing analysis listeners", e);
×
418
        }
1✔
419

420
        try (TimedOperation ignored = TimeTracker.startOperation(TimedOperationCategory.FILE_PROCESSING)) {
1✔
421
            for (final Rule rule : removeBrokenRules(rulesets)) {
1!
422
                // todo Just like we throw for invalid properties, "broken rules"
423
                // shouldn't be a "config error". This is the only instance of
424
                // config errors...
425
                // see https://github.com/pmd/pmd/issues/3901
426
                listener.onConfigError(new Report.ConfigurationError(rule, rule.dysfunctionReason()));
×
427
            }
×
428

429
            encourageToUseIncrementalAnalysis(configuration);
1✔
430

431
            try (LanguageProcessorRegistry lpRegistry = LanguageProcessorRegistry.create(
1✔
432
                // only start the applicable languages (and dependencies)
433
                new LanguageRegistry(getApplicableLanguages(true)),
1✔
434
                langProperties,
435
                reporter
436
            )) {
437
                // Note the analysis task is shared: all processors see
438
                // the same file list, which may contain files for other
439
                // languages.
440
                AnalysisTask analysisTask = InternalApiBridge.createAnalysisTask(
1✔
441
                    rulesets,
442
                    textFiles,
443
                    listener,
444
                    configuration.getThreads(),
1✔
445
                    configuration.getAnalysisCache(),
1✔
446
                    reporter,
447
                    lpRegistry
448
                );
449

450
                List<AutoCloseable> analyses = new ArrayList<>();
1✔
451
                try {
452
                    for (Language lang : lpRegistry.getLanguages()) {
1✔
453
                        analyses.add(lpRegistry.getProcessor(lang).launchAnalysis(analysisTask));
1✔
454
                    }
1✔
455
                } finally {
456
                    Exception e = IOUtil.closeAll(analyses);
1✔
457
                    if (e != null) {
1!
458
                        reporter.errorEx("Error while joining analysis", e);
×
459
                    }
460
                }
461

462
            } catch (LanguageTerminationException e) {
×
463
                reporter.errorEx("Error while closing language processors", e);
×
464
            }
1✔
465
        } finally {
466
            try {
467
                listener.close();
1✔
468
            } catch (Exception e) {
×
469
                reporter.errorEx("Exception while closing analysis listeners", e);
×
470
                // todo better exception
471
                throw new RuntimeException("Exception while closing analysis listeners", e);
×
472
            }
1✔
473
        }
474
    }
1✔
475

476

477
    private GlobalAnalysisListener createComposedRendererListener(List<Renderer> renderers) throws Exception {
478
        if (renderers.isEmpty()) {
1✔
479
            return GlobalAnalysisListener.noop();
1✔
480
        }
481

482
        boolean isAnyIncremental = false;
1✔
483

484
        List<GlobalAnalysisListener> rendererListeners = new ArrayList<>(renderers.size());
1✔
485
        for (Renderer renderer : renderers) {
1✔
486
            isAnyIncremental |= !(renderer instanceof AbstractAccumulatingRenderer);
1!
487

488
            try {
489
                @SuppressWarnings("PMD.CloseResource")
490
                GlobalAnalysisListener listener =
1✔
491
                    Objects.requireNonNull(renderer.newListener(), "Renderer should provide non-null listener");
1✔
492
                rendererListeners.add(listener);
1✔
493
            } catch (Exception ioe) {
×
494
                // close listeners so far, throw their close exception or the ioe
495
                IOUtil.ensureClosed(rendererListeners, ioe);
×
496
                throw AssertionUtil.shouldNotReachHere("ensureClosed should have thrown", ioe);
×
497
            }
1✔
498
        }
1✔
499
        GlobalAnalysisListener rendererListener = GlobalAnalysisListener.tee(rendererListeners);
1✔
500
        // If all are non-incremental then they do their own buffering and should sort the events.
501
        // If any is incremental then we need to reorder the events for deterministic output.
502
        if (isAnyIncremental) {
1!
503
            rendererListener = new DeterministicOutputListenerWrapper(rendererListener);
1✔
504
        }
505
        return rendererListener;
1✔
506
    }
507

508
    private Set<Language> getApplicableLanguages(boolean quiet) {
509
        Set<Language> languages = new HashSet<>();
1✔
510
        LanguageVersionDiscoverer discoverer = configuration.getLanguageVersionDiscoverer();
1✔
511

512
        for (RuleSet ruleSet : ruleSets) {
1✔
513
            for (Rule rule : ruleSet.getRules()) {
1✔
514
                Language ruleLanguage = rule.getLanguage();
1✔
515
                Objects.requireNonNull(ruleLanguage, "Rule has no language " + rule);
1✔
516
                if (!languages.contains(ruleLanguage)) {
1!
517
                    LanguageVersion version = discoverer.getDefaultLanguageVersion(ruleLanguage);
1✔
518
                    if (ruleSetApplies(rule, version)) {
1!
519
                        configuration.checkLanguageIsRegistered(ruleLanguage);
1✔
520
                        languages.add(ruleLanguage);
1✔
521
                        if (!quiet) {
1✔
522
                            LOG.trace("Using {} version ''{}''", version.getLanguage().getName(), version.getTerseName());
1✔
523
                        }
524
                    }
525
                }
526
            }
1✔
527
        }
1✔
528

529
        // collect all dependencies, they shouldn't be filtered out
530
        LanguageRegistry reg = configuration.getLanguageRegistry();
1✔
531
        boolean changed;
532
        do {
533
            changed = false;
1✔
534
            for (Language lang : new HashSet<>(languages)) {
1✔
535
                for (String depId : lang.getDependencies()) {
1!
536
                    Language depLang = reg.getLanguageById(depId);
×
537
                    if (depLang == null) {
×
538
                        // todo maybe report all then throw
539
                        throw new IllegalStateException(
×
540
                            "Language " + lang.getId() + " has unsatisfied dependencies: "
×
541
                                + depId + " is not found in " + reg
542
                        );
543
                    }
544
                    changed |= languages.add(depLang);
×
545
                }
×
546
            }
1✔
547
        } while (changed);
1!
548

549
        // include all available dialects of applicable languages - ie: if we have XML rules, all XML dialects are applicable
550
        do {
551
            changed = false;
1✔
552
            for (Language lang : reg) {
1✔
553
                if (lang.getBaseLanguageId() != null) {
1✔
554
                    Language baseLang = reg.getLanguageById(lang.getBaseLanguageId());
1✔
555
                    if (baseLang != null && languages.contains(baseLang)) {
1!
556
                        changed |= languages.add(lang);
1✔
557
                    }
558
                }
559
            }
1✔
560
        } while (changed);
1✔
561

562
        return languages;
1✔
563
    }
564

565
    /**
566
     * Remove and return the misconfigured rules from the rulesets and log them
567
     * for good measure.
568
     */
569
    private Set<Rule> removeBrokenRules(final RuleSets ruleSets) {
570
        final Set<Rule> brokenRules = new HashSet<>();
1✔
571
        ruleSets.removeDysfunctionalRules(brokenRules);
1✔
572

573
        for (final Rule rule : brokenRules) {
1!
574
            reporter.warn("Removed misconfigured rule: {0} cause: {1}",
×
575
                          rule.getName(), rule.dysfunctionReason());
×
576
        }
×
577

578
        return brokenRules;
1✔
579
    }
580

581

582
    public PmdReporter getReporter() {
583
        return reporter;
1✔
584
    }
585

586
    @Override
587
    public void close() {
588
        if (closed) {
1!
589
            return;
×
590
        }
591
        closed = true;
1✔
592
        collector.close();
1✔
593

594
        // close listeners if analysis is not run.
595
        IOUtil.closeAll(listeners);
1✔
596

597
        /*
598
         * Make sure it's our own classloader before attempting to close it....
599
         * Maven + Jacoco provide us with a cloaseable classloader that if closed
600
         * will throw a ClassNotFoundException.
601
         */
602
        if (configuration.getClassLoader() instanceof ClasspathClassLoader) {
1!
603
            IOUtil.tryCloseClassLoader(configuration.getClassLoader());
×
604
        }
605
    }
1✔
606

607
    public ReportStats runAndReturnStats() {
608
        if (getRulesets().isEmpty()) {
1✔
609
            return ReportStats.empty();
1✔
610
        }
611

612
        @SuppressWarnings("PMD.CloseResource")
613
        ReportStatsListener listener = new ReportStatsListener();
1✔
614

615
        addListener(listener);
1✔
616

617
        try {
618
            performAnalysis();
1✔
619
        } catch (Exception e) {
×
620
            getReporter().errorEx("Exception during processing", e);
×
621
            ReportStats stats = listener.getResult();
×
622
            printErrorDetected(1 + stats.getNumErrors());
×
623
            return stats; // should have been closed
×
624
        }
1✔
625
        ReportStats stats = listener.getResult();
1✔
626

627
        if (stats.getNumErrors() > 0) {
1✔
628
            printErrorDetected(stats.getNumErrors());
1✔
629
        }
630

631
        return stats;
1✔
632
    }
633

634
    static void printErrorDetected(PmdReporter reporter, int errors) {
635
        String msg = LogMessages.errorDetectedMessage(errors, "PMD");
1✔
636
        // note: using error level here increments the error count of the reporter,
637
        // which we don't want.
638
        reporter.info(StringUtil.quoteMessageFormat(msg));
1✔
639
    }
1✔
640

641
    void printErrorDetected(int errors) {
642
        printErrorDetected(getReporter(), errors);
1✔
643
    }
1✔
644

645
    private static void encourageToUseIncrementalAnalysis(final PMDConfiguration configuration) {
646
        final PmdReporter reporter = configuration.getReporter();
1✔
647

648
        if (!configuration.isIgnoreIncrementalAnalysis()
1✔
649
            && configuration.getAnalysisCache() instanceof NoopAnalysisCache
1!
650
            && reporter.isLoggable(Level.WARN)) {
1✔
651
            final String version =
652
                PMDVersion.isUnknown() || PMDVersion.isSnapshot() ? "latest" : "pmd-doc-" + PMDVersion.VERSION;
1!
653
            reporter.warn("This analysis could be faster, please consider using Incremental Analysis: "
1✔
654
                            + "https://docs.pmd-code.org/{0}/pmd_userdocs_incremental_analysis.html", version);
655
        }
656
    }
1✔
657

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