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

pmd / pmd / #3713

pending completion
#3713

push

github actions

adangel
Merge pull request #4263 from oowekyala:issue4234-reset-loggers

[test] Properly reset log level in slf4j-simple #4263

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

66788 of 127214 relevant lines covered (52.5%)

0.53 hits per line

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

80.56
/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.util.CollectionUtil.listOf;
8

9
import java.util.ArrayList;
10
import java.util.Collection;
11
import java.util.Collections;
12
import java.util.HashSet;
13
import java.util.List;
14
import java.util.Objects;
15
import java.util.Set;
16

17
import org.slf4j.Logger;
18
import org.slf4j.LoggerFactory;
19
import org.slf4j.event.Level;
20

21
import net.sourceforge.pmd.Report.GlobalReportBuilderListener;
22
import net.sourceforge.pmd.benchmark.TimeTracker;
23
import net.sourceforge.pmd.benchmark.TimedOperation;
24
import net.sourceforge.pmd.benchmark.TimedOperationCategory;
25
import net.sourceforge.pmd.cache.AnalysisCacheListener;
26
import net.sourceforge.pmd.cache.NoopAnalysisCache;
27
import net.sourceforge.pmd.cli.internal.ProgressBarListener;
28
import net.sourceforge.pmd.internal.LogMessages;
29
import net.sourceforge.pmd.internal.util.AssertionUtil;
30
import net.sourceforge.pmd.internal.util.FileCollectionUtil;
31
import net.sourceforge.pmd.lang.Language;
32
import net.sourceforge.pmd.lang.LanguageVersion;
33
import net.sourceforge.pmd.lang.LanguageVersionDiscoverer;
34
import net.sourceforge.pmd.lang.document.FileCollector;
35
import net.sourceforge.pmd.lang.document.TextFile;
36
import net.sourceforge.pmd.processor.AbstractPMDProcessor;
37
import net.sourceforge.pmd.renderers.Renderer;
38
import net.sourceforge.pmd.reporting.GlobalAnalysisListener;
39
import net.sourceforge.pmd.reporting.ReportStats;
40
import net.sourceforge.pmd.reporting.ReportStatsListener;
41
import net.sourceforge.pmd.util.ClasspathClassLoader;
42
import net.sourceforge.pmd.util.IOUtil;
43
import net.sourceforge.pmd.util.StringUtil;
44
import net.sourceforge.pmd.util.log.MessageReporter;
45

46
/**
47
 * Main programmatic API of PMD. Create and configure a {@link PMDConfiguration},
48
 * then use {@link #create(PMDConfiguration)} to obtain an instance.
49
 * You can perform additional configuration on the instance, eg adding
50
 * files to process, or additional rulesets and renderers. Then, call
51
 * {@link #performAnalysis()}. Example:
52
 * <pre>{@code
53
 *   PMDConfiguration config = new PMDConfiguration();
54
 *   config.setDefaultLanguageVersion(LanguageRegistry.findLanguageByTerseName("java").getVersion("11"));
55
 *   config.setInputPaths("src/main/java");
56
 *   config.prependClasspath("target/classes");
57
 *   config.setMinimumPriority(RulePriority.HIGH);
58
 *   config.addRuleSet("rulesets/java/quickstart.xml");
59
 *   config.setReportFormat("xml");
60
 *   config.setReportFile("target/pmd-report.xml");
61
 *
62
 *   try (PmdAnalysis pmd = PmdAnalysis.create(config)) {
63
 *     // note: don't use `config` once a PmdAnalysis has been created.
64
 *     // optional: add more rulesets
65
 *     pmd.addRuleSet(pmd.newRuleSetLoader().loadFromResource("custom-ruleset.xml"));
66
 *     // optional: add more files
67
 *     pmd.files().addFile(Paths.get("src", "main", "more-java", "ExtraSource.java"));
68
 *     // optional: add more renderers
69
 *     pmd.addRenderer(renderer);
70
 *
71
 *     pmd.performAnalysis();
72
 *   }
73
 * }</pre>
74
 *
75
 */
76
public final class PmdAnalysis implements AutoCloseable {
77

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

80
    private final FileCollector collector;
81
    private final List<Renderer> renderers = new ArrayList<>();
1✔
82
    private final List<GlobalAnalysisListener> listeners = new ArrayList<>();
1✔
83
    private final List<RuleSet> ruleSets = new ArrayList<>();
1✔
84
    private final PMDConfiguration configuration;
85
    private final MessageReporter reporter;
86

87
    private boolean closed;
88

89
    /**
90
     * Constructs a new instance. The files paths (input files, filelist,
91
     * exclude list, etc) given in the configuration are collected into
92
     * the file collector ({@link #files()}), but more can be added
93
     * programmatically using the file collector.
94
     */
95
    private PmdAnalysis(PMDConfiguration config) {
1✔
96
        this.configuration = config;
1✔
97
        this.reporter = config.getReporter();
1✔
98
        this.collector = FileCollector.newCollector(
1✔
99
            config.getLanguageVersionDiscoverer(),
1✔
100
            reporter
101
        );
102
    }
1✔
103

104
    /**
105
     * Constructs a new instance from a configuration.
106
     *
107
     * <ul>
108
     * <li> The files paths (input files, filelist,
109
     * exclude list, etc) are explored and the files to analyse are
110
     * collected into the file collector ({@link #files()}).
111
     * More can be added programmatically using the file collector.
112
     * <li>The rulesets given in the configuration are loaded ({@link PMDConfiguration#getRuleSets()})
113
     * <li>A renderer corresponding to the parameters of the configuration
114
     * is created and added (but not started).
115
     * </ul>
116
     */
117
    public static PmdAnalysis create(PMDConfiguration config) {
118
        PmdAnalysis pmd = new PmdAnalysis(config);
1✔
119

120
        // note: do not filter files by language
121
        // they could be ignored later. The problem is if you call
122
        // addRuleSet later, then you could be enabling new languages
123
        // So the files should not be pruned in advance
124
        FileCollectionUtil.collectFiles(config, pmd.files());
1✔
125

126
        if (config.getReportFormat() != null) {
1✔
127
            Renderer renderer = config.createRenderer(true);
1✔
128
            pmd.addRenderer(renderer);
1✔
129
        }
130

131
        if (!config.getRuleSetPaths().isEmpty()) {
1✔
132
            final RuleSetLoader ruleSetLoader = pmd.newRuleSetLoader();
1✔
133
            final List<RuleSet> ruleSets = ruleSetLoader.loadRuleSetsWithoutException(config.getRuleSetPaths());
1✔
134
            pmd.addRuleSets(ruleSets);
1✔
135
        }
136
        return pmd;
1✔
137
    }
138

139
    // test only
140
    List<RuleSet> rulesets() {
141
        return ruleSets;
1✔
142
    }
143

144
    // test only
145
    List<Renderer> renderers() {
146
        return renderers;
1✔
147
    }
148

149

150
    /**
151
     * Returns the file collector for the analysed sources.
152
     */
153
    public FileCollector files() {
154
        return collector; // todo user can close collector programmatically
1✔
155
    }
156

157
    /**
158
     * Returns a new ruleset loader, which can be used to create new
159
     * rulesets (add them then with {@link #addRuleSet(RuleSet)}).
160
     *
161
     * <pre>{@code
162
     * try (PmdAnalysis pmd = create(config)) {
163
     *     pmd.addRuleSet(pmd.newRuleSetLoader().loadFromResource("custom-ruleset.xml"));
164
     * }
165
     * }</pre>
166
     */
167
    public RuleSetLoader newRuleSetLoader() {
168
        return RuleSetLoader.fromPmdConfig(configuration);
1✔
169
    }
170

171
    /**
172
     * Add a new renderer. The given renderer must not already be started,
173
     * it will be started by {@link #performAnalysis()}.
174
     *
175
     * @throws NullPointerException If the parameter is null
176
     */
177
    public void addRenderer(Renderer renderer) {
178
        AssertionUtil.requireParamNotNull("renderer", renderer);
1✔
179
        this.renderers.add(renderer);
1✔
180
    }
1✔
181

182
    /**
183
     * Add several renderers at once.
184
     *
185
     * @throws NullPointerException If the parameter is null, or any of its items is null.
186
     */
187
    public void addRenderers(Collection<Renderer> renderers) {
188
        renderers.forEach(this::addRenderer);
×
189
    }
×
190

191
    /**
192
     * Add a new listener. As per the contract of {@link GlobalAnalysisListener},
193
     * this object must be ready for interaction. However, nothing will
194
     * be done with the listener until {@link #performAnalysis()} is called.
195
     * The listener will be closed by {@link #performAnalysis()}, or
196
     * {@link #close()}, whichever happens first.
197
     *
198
     * @throws NullPointerException If the parameter is null
199
     */
200
    public void addListener(GlobalAnalysisListener listener) {
201
        AssertionUtil.requireParamNotNull("listener", listener);
1✔
202
        this.listeners.add(listener);
1✔
203
    }
1✔
204

205
    /**
206
     * Add several listeners at once.
207
     *
208
     * @throws NullPointerException If the parameter is null, or any of its items is null.
209
     * @see #addListener(GlobalAnalysisListener)
210
     */
211
    public void addListeners(Collection<? extends GlobalAnalysisListener> listeners) {
212
        listeners.forEach(this::addListener);
×
213
    }
×
214

215
    /**
216
     * Add a new ruleset.
217
     *
218
     * @throws NullPointerException If the parameter is null
219
     */
220
    public void addRuleSet(RuleSet ruleSet) {
221
        AssertionUtil.requireParamNotNull("rule set", ruleSet);
1✔
222
        this.ruleSets.add(ruleSet);
1✔
223
    }
1✔
224

225
    /**
226
     * Add several rulesets at once.
227
     *
228
     * @throws NullPointerException If the parameter is null, or any of its items is null.
229
     */
230
    public void addRuleSets(Collection<RuleSet> ruleSets) {
231
        ruleSets.forEach(this::addRuleSet);
1✔
232
    }
1✔
233

234
    /**
235
     * Returns an unmodifiable view of the ruleset list. That will be
236
     * processed.
237
     */
238
    public List<RuleSet> getRulesets() {
239
        return Collections.unmodifiableList(ruleSets);
1✔
240
    }
241

242

243
    /**
244
     * Run PMD with the current state of this instance. This will start
245
     * and finish the registered renderers, and close all
246
     * {@linkplain #addListener(GlobalAnalysisListener) registered listeners}.
247
     * All files collected in the {@linkplain #files() file collector} are
248
     * processed. This does not return a report, as the analysis results
249
     * are consumed by {@link GlobalAnalysisListener} instances (of which
250
     * Renderers are a special case). Note that this does
251
     * not throw, errors are instead accumulated into a {@link MessageReporter}.
252
     */
253
    public void performAnalysis() {
254
        performAnalysisImpl(Collections.emptyList());
1✔
255
    }
1✔
256

257
    /**
258
     * Run PMD with the current state of this instance. This will start
259
     * and finish the registered renderers. All files collected in the
260
     * {@linkplain #files() file collector} are processed. Returns the
261
     * output report. Note that this does not throw, errors are instead
262
     * accumulated into a {@link MessageReporter}.
263
     */
264
    public Report performAnalysisAndCollectReport() {
265
        try (GlobalReportBuilderListener reportBuilder = new GlobalReportBuilderListener()) {
1✔
266
            performAnalysisImpl(listOf(reportBuilder)); // closes the report builder
1✔
267
            return reportBuilder.getResultImpl();
1✔
268
        }
269
    }
270

271
    void performAnalysisImpl(List<? extends GlobalReportBuilderListener> extraListeners) {
272
        try (FileCollector files = collector) {
1✔
273
            files.filterLanguages(getApplicableLanguages());
1✔
274
            performAnalysisImpl(extraListeners, files.getCollectedFiles());
1✔
275
        }
276
    }
1✔
277

278
    void performAnalysisImpl(List<? extends GlobalReportBuilderListener> extraListeners, List<TextFile> textFiles) {
279
        RuleSets rulesets = new RuleSets(this.ruleSets);
1✔
280

281
        GlobalAnalysisListener listener;
282
        try {
283
            @SuppressWarnings("PMD.CloseResource") AnalysisCacheListener cacheListener = new AnalysisCacheListener(configuration.getAnalysisCache(), rulesets, configuration.getClassLoader());
1✔
284
            if (configuration.isProgressBar()) {
1✔
285
                @SuppressWarnings("PMD.CloseResource") ProgressBarListener progressBarListener = new ProgressBarListener(textFiles.size(), System.out::print);
×
286
                addListener(progressBarListener);
×
287
            }
288
            listener = GlobalAnalysisListener.tee(listOf(createComposedRendererListener(renderers),
1✔
289
                                                         GlobalAnalysisListener.tee(listeners),
1✔
290
                                                         GlobalAnalysisListener.tee(extraListeners),
1✔
291
                                                         cacheListener));
292
        } catch (Exception e) {
×
293
            reporter.errorEx("Exception while initializing analysis listeners", e);
×
294
            throw new RuntimeException("Exception while initializing analysis listeners", e);
×
295
        }
1✔
296

297
        try (TimedOperation ignored = TimeTracker.startOperation(TimedOperationCategory.FILE_PROCESSING)) {
1✔
298

299
            for (final Rule rule : removeBrokenRules(rulesets)) {
1✔
300
                // todo Just like we throw for invalid properties, "broken rules"
301
                // shouldn't be a "config error". This is the only instance of
302
                // config errors...
303
                listener.onConfigError(new Report.ConfigurationError(rule, rule.dysfunctionReason()));
×
304
            }
×
305

306
            encourageToUseIncrementalAnalysis(configuration);
1✔
307
            try (AbstractPMDProcessor processor = AbstractPMDProcessor.newFileProcessor(configuration)) {
1✔
308
                processor.processFiles(rulesets, textFiles, listener);
1✔
309
            }
310
        } finally {
311
            try {
312
                listener.close();
1✔
313
            } catch (Exception e) {
×
314
                reporter.errorEx("Exception while initializing analysis listeners", e);
×
315
                // todo better exception
316
                throw new RuntimeException("Exception while initializing analysis listeners", e);
×
317
            }
1✔
318
        }
319
    }
1✔
320

321

322
    private static GlobalAnalysisListener createComposedRendererListener(List<Renderer> renderers) throws Exception {
323
        if (renderers.isEmpty()) {
1✔
324
            return GlobalAnalysisListener.noop();
1✔
325
        }
326

327
        List<GlobalAnalysisListener> rendererListeners = new ArrayList<>(renderers.size());
1✔
328
        for (Renderer renderer : renderers) {
1✔
329
            try {
330
                @SuppressWarnings("PMD.CloseResource")
331
                GlobalAnalysisListener listener =
1✔
332
                    Objects.requireNonNull(renderer.newListener(), "Renderer should provide non-null listener");
1✔
333
                rendererListeners.add(listener);
1✔
334
            } catch (Exception ioe) {
×
335
                // close listeners so far, throw their close exception or the ioe
336
                IOUtil.ensureClosed(rendererListeners, ioe);
×
337
                throw AssertionUtil.shouldNotReachHere("ensureClosed should have thrown");
×
338
            }
1✔
339
        }
1✔
340
        return GlobalAnalysisListener.tee(rendererListeners);
1✔
341
    }
342

343
    private Set<Language> getApplicableLanguages() {
344
        final Set<Language> languages = new HashSet<>();
1✔
345
        final LanguageVersionDiscoverer discoverer = configuration.getLanguageVersionDiscoverer();
1✔
346

347
        for (RuleSet ruleSet : ruleSets) {
1✔
348
            for (final Rule rule : ruleSet.getRules()) {
1✔
349
                final Language ruleLanguage = rule.getLanguage();
1✔
350
                Objects.requireNonNull(ruleLanguage, "Rule has no language " + rule);
1✔
351
                if (!languages.contains(ruleLanguage)) {
1✔
352
                    final LanguageVersion version = discoverer.getDefaultLanguageVersion(ruleLanguage);
1✔
353
                    if (RuleSet.applies(rule, version)) {
1✔
354
                        languages.add(ruleLanguage);
1✔
355
                        LOG.trace("Using {} version ''{}''", version.getLanguage().getName(), version.getTerseName());
1✔
356
                    }
357
                }
358
            }
1✔
359
        }
1✔
360
        return languages;
1✔
361
    }
362

363
    /**
364
     * Remove and return the misconfigured rules from the rulesets and log them
365
     * for good measure.
366
     */
367
    private Set<Rule> removeBrokenRules(final RuleSets ruleSets) {
368
        final Set<Rule> brokenRules = new HashSet<>();
1✔
369
        ruleSets.removeDysfunctionalRules(brokenRules);
1✔
370

371
        for (final Rule rule : brokenRules) {
1✔
372
            reporter.warn("Removed misconfigured rule: {0} cause: {1}",
×
373
                          rule.getName(), rule.dysfunctionReason());
×
374
        }
×
375

376
        return brokenRules;
1✔
377
    }
378

379

380
    public MessageReporter getReporter() {
381
        return reporter;
1✔
382
    }
383

384
    @Override
385
    public void close() {
386
        if (closed) {
1✔
387
            return;
×
388
        }
389
        closed = true;
1✔
390
        collector.close();
1✔
391

392
        // close listeners if analysis is not run.
393
        IOUtil.closeAll(listeners);
1✔
394

395
        /*
396
         * Make sure it's our own classloader before attempting to close it....
397
         * Maven + Jacoco provide us with a cloaseable classloader that if closed
398
         * will throw a ClassNotFoundException.
399
         */
400
        if (configuration.getClassLoader() instanceof ClasspathClassLoader) {
1✔
401
            IOUtil.tryCloseClassLoader(configuration.getClassLoader());
×
402
        }
403
    }
1✔
404

405
    public ReportStats runAndReturnStats() {
406
        if (getRulesets().isEmpty()) {
1✔
407
            return ReportStats.empty();
×
408
        }
409

410
        @SuppressWarnings("PMD.CloseResource")
411
        ReportStatsListener listener = new ReportStatsListener();
1✔
412

413
        addListener(listener);
1✔
414

415
        try {
416
            performAnalysis();
1✔
417
        } catch (Exception e) {
×
418
            getReporter().errorEx("Exception during processing", e);
×
419
            ReportStats stats = listener.getResult();
×
420
            printErrorDetected(1 + stats.getNumErrors());
×
421
            return stats; // should have been closed
×
422
        }
1✔
423
        ReportStats stats = listener.getResult();
1✔
424

425
        if (stats.getNumErrors() > 0) {
1✔
426
            printErrorDetected(stats.getNumErrors());
1✔
427
        }
428

429
        return stats;
1✔
430
    }
431

432
    static void printErrorDetected(MessageReporter reporter, int errors) {
433
        String msg = LogMessages.errorDetectedMessage(errors, "PMD");
1✔
434
        // note: using error level here increments the error count of the reporter,
435
        // which we don't want.
436
        reporter.info(StringUtil.quoteMessageFormat(msg));
1✔
437
    }
1✔
438

439
    void printErrorDetected(int errors) {
440
        printErrorDetected(getReporter(), errors);
1✔
441
    }
1✔
442

443
    private static void encourageToUseIncrementalAnalysis(final PMDConfiguration configuration) {
444
        final MessageReporter reporter = configuration.getReporter();
1✔
445

446
        if (!configuration.isIgnoreIncrementalAnalysis()
1✔
447
            && configuration.getAnalysisCache() instanceof NoopAnalysisCache
1✔
448
            && reporter.isLoggable(Level.WARN)) {
1✔
449
            final String version =
450
                PMDVersion.isUnknown() || PMDVersion.isSnapshot() ? "latest" : "pmd-" + PMDVersion.VERSION;
1✔
451
            reporter.warn("This analysis could be faster, please consider using Incremental Analysis: "
1✔
452
                            + "https://pmd.github.io/{0}/pmd_userdocs_incremental_analysis.html", version);
453
        }
454
    }
1✔
455
}
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