View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.plugins.pmd;
20  
21  import javax.inject.Inject;
22  
23  import java.io.File;
24  import java.io.IOException;
25  import java.util.ArrayList;
26  import java.util.Arrays;
27  import java.util.Collection;
28  import java.util.List;
29  import java.util.Locale;
30  
31  import net.sourceforge.pmd.renderers.Renderer;
32  import org.apache.maven.plugins.annotations.Mojo;
33  import org.apache.maven.plugins.annotations.Parameter;
34  import org.apache.maven.plugins.annotations.ResolutionScope;
35  import org.apache.maven.plugins.pmd.exec.PmdExecutor;
36  import org.apache.maven.plugins.pmd.exec.PmdRequest;
37  import org.apache.maven.plugins.pmd.exec.PmdResult;
38  import org.apache.maven.plugins.pmd.exec.PmdServiceExecutor;
39  import org.apache.maven.project.MavenProject;
40  import org.apache.maven.reporting.MavenReportException;
41  import org.codehaus.plexus.i18n.I18N;
42  import org.codehaus.plexus.resource.ResourceManager;
43  import org.codehaus.plexus.resource.loader.FileResourceCreationException;
44  import org.codehaus.plexus.resource.loader.FileResourceLoader;
45  import org.codehaus.plexus.resource.loader.ResourceNotFoundException;
46  
47  /**
48   * Creates a PMD site report based on the rulesets and configuration set in the plugin.
49   * It can also generate a pmd output file aside from the site report in any of the following formats: xml, csv or txt.
50   *
51   * @author Brett Porter
52   * @version $Id$
53   * @since 2.0
54   */
55  @Mojo(name = "pmd", threadSafe = true, requiresDependencyResolution = ResolutionScope.TEST)
56  public class PmdReport extends AbstractPmdReport {
57      /**
58       * The target JDK to analyze based on. Should match the source used in the compiler plugin.
59       * Valid values depend on the used PMD version. Most common values are
60       * <code>8</code>, <code>11</code>, <code>17</code>, and <code>21</code>.
61       *
62       * <p>The full list of supported Java versions for each PMD version is available at
63       * <a href="https://docs.pmd-code.org/latest/pmd_languages_java.html">Java support (PMD)</a>.</p>
64       *
65       * <p>You can override the default PMD version by specifying PMD as a dependency,
66       * see <a href="examples/upgrading-PMD-at-runtime.html">Upgrading PMD at Runtime</a>.</p>
67       *
68       * <p>
69       *   <b>Note:</b> this parameter is only used if the language parameter is set to <code>java</code>.
70       * </p>
71       */
72      @Parameter(property = "targetJdk", defaultValue = "${maven.compiler.source}")
73      private String targetJdk;
74  
75      /**
76       * The programming language to be analyzed by PMD. Valid values are currently <code>java</code>,
77       * <code>javascript</code> and <code>jsp</code>.
78       *
79       * @since 3.0
80       */
81      @Parameter(defaultValue = "java")
82      private String language;
83  
84      /**
85       * The rule priority threshold; rules with lower priority than this will not be evaluated.
86       *
87       * @since 2.1
88       */
89      @Parameter(property = "minimumPriority", defaultValue = "5")
90      private int minimumPriority = 5;
91  
92      /**
93       * Skip the PMD report generation. Most useful on the command line via "-Dpmd.skip=true".
94       *
95       * @since 2.1
96       */
97      @Parameter(property = "pmd.skip", defaultValue = "false")
98      private boolean skip;
99  
100     /**
101      * The PMD rulesets to use. See the
102      * <a href="https://pmd.github.io/latest/pmd_rules_java.html">Stock Java Rulesets</a> for a
103      * list of available rules.
104      * Defaults to a custom ruleset provided by this maven plugin
105      * (<code>/rulesets/java/maven-pmd-plugin-default.xml</code>).
106      */
107     @Parameter
108     String[] rulesets = new String[] {"/rulesets/java/maven-pmd-plugin-default.xml"};
109 
110     /**
111      * Controls whether the project's compile/test classpath should be passed to PMD to enable its type resolution
112      * feature.
113      *
114      * @since 3.0
115      */
116     @Parameter(property = "pmd.typeResolution", defaultValue = "true")
117     private boolean typeResolution;
118 
119     /**
120      * Controls whether PMD will track benchmark information.
121      *
122      * @since 3.1
123      */
124     @Parameter(property = "pmd.benchmark", defaultValue = "false")
125     private boolean benchmark;
126 
127     /**
128      * Benchmark output filename.
129      *
130      * @since 3.1
131      */
132     @Parameter(property = "pmd.benchmarkOutputFilename", defaultValue = "${project.build.directory}/pmd-benchmark.txt")
133     private String benchmarkOutputFilename;
134 
135     /**
136      * Source level marker used to indicate whether a RuleViolation should be suppressed. If it is not set, PMD's
137      * default will be used, which is <code>NOPMD</code>. See also <a
138      * href="https://pmd.github.io/latest/pmd_userdocs_suppressing_warnings.html">PMD &#x2013; Suppressing warnings</a>.
139      *
140      * @since 3.4
141      */
142     @Parameter(property = "pmd.suppressMarker")
143     private String suppressMarker;
144 
145     /**
146      * per default pmd executions error are ignored to not break the whole
147      *
148      * @since 3.1
149      */
150     @Parameter(property = "pmd.skipPmdError", defaultValue = "true")
151     private boolean skipPmdError;
152 
153     /**
154      * Enables the analysis cache, which speeds up PMD. This
155      * requires a cache file, that contains the results of the last
156      * PMD run. Thus the cache is only effective, if this file is
157      * not cleaned between runs.
158      *
159      * @since 3.8
160      */
161     @Parameter(property = "pmd.analysisCache", defaultValue = "false")
162     private boolean analysisCache;
163 
164     /**
165      * The location of the analysis cache, if it is enabled.
166      * This file contains the results of the last PMD run and must not be cleaned
167      * between consecutive PMD runs. Otherwise the cache is not in use.
168      * If the file doesn't exist, PMD executes as if there is no cache enabled and
169      * all files are analyzed. Otherwise only changed files will be analyzed again.
170      *
171      * @since 3.8
172      */
173     @Parameter(property = "pmd.analysisCacheLocation", defaultValue = "${project.build.directory}/pmd/pmd.cache")
174     private String analysisCacheLocation;
175 
176     /**
177      * Also render processing errors into the HTML report.
178      * Processing errors are problems, that PMD encountered while executing the rules.
179      * It can be parsing errors or exceptions during rule execution.
180      * Processing errors indicate a bug in PMD and the information provided help in
181      * reporting and fixing bugs in PMD.
182      *
183      * @since 3.9.0
184      */
185     @Parameter(property = "pmd.renderProcessingErrors", defaultValue = "true")
186     private boolean renderProcessingErrors = true;
187 
188     /**
189      * Also render the rule priority into the HTML report.
190      *
191      * @since 3.10.0
192      */
193     @Parameter(property = "pmd.renderRuleViolationPriority", defaultValue = "true")
194     private boolean renderRuleViolationPriority = true;
195 
196     /**
197      * Add a section in the HTML report, that groups the found violations by rule priority
198      * in addition to grouping by file.
199      *
200      * @since 3.12.0
201      */
202     @Parameter(property = "pmd.renderViolationsByPriority", defaultValue = "true")
203     private boolean renderViolationsByPriority = true;
204 
205     /**
206      * Add a section in the HTML report that lists the suppressed violations.
207      *
208      * @since 3.17.0
209      */
210     @Parameter(property = "pmd.renderSuppressedViolations", defaultValue = "true")
211     private boolean renderSuppressedViolations = true;
212 
213     /**
214      * Before PMD is executed, the configured rulesets are resolved and copied into this directory.
215      * <p>Note: Before 3.13.0, this was by default ${project.build.directory}.
216      *
217      * @since 3.13.0
218      */
219     @Parameter(property = "pmd.rulesetsTargetDirectory", defaultValue = "${project.build.directory}/pmd/rulesets")
220     private File rulesetsTargetDirectory;
221 
222     /**
223      * Used to locate configured rulesets. The rulesets could be on the plugin
224      * classpath or in the local project file system.
225      */
226     private final ResourceManager locator;
227 
228     /**
229      * Internationalization component
230      */
231     private final I18N i18n;
232 
233     private final PmdServiceExecutor serviceExecutor;
234 
235     private final ConfigurationService configurationService;
236 
237     /**
238      * Contains the result of the last PMD execution.
239      * It might be <code>null</code> which means, that PMD
240      * has not been executed yet.
241      */
242     private PmdResult pmdResult;
243 
244     @Inject
245     public PmdReport(
246             ResourceManager locator,
247             ConfigurationService configurationService,
248             I18N i18n,
249             PmdServiceExecutor serviceExecutor) {
250         this.locator = locator;
251         this.configurationService = configurationService;
252         this.i18n = i18n;
253         this.serviceExecutor = serviceExecutor;
254     }
255 
256     /** {@inheritDoc} */
257     public String getName(Locale locale) {
258         return getI18nString(locale, "name");
259     }
260 
261     /** {@inheritDoc} */
262     public String getDescription(Locale locale) {
263         return getI18nString(locale, "description");
264     }
265 
266     /**
267      * @param locale The locale
268      * @param key The key to search for
269      * @return The text appropriate for the locale.
270      */
271     protected String getI18nString(Locale locale, String key) {
272         return i18n.getString("pmd-report", locale, "report.pmd." + key);
273     }
274 
275     /**
276      * Configures the PMD rulesets to be used directly.
277      * Note: Usually the rulesets are configured via the property.
278      *
279      * @param rulesets the PMD rulesets to be used.
280      * @see #rulesets
281      */
282     public void setRulesets(String[] rulesets) {
283         this.rulesets = Arrays.copyOf(rulesets, rulesets.length);
284     }
285 
286     /**
287      * {@inheritDoc}
288      */
289     @Override
290     public void executeReport(Locale locale) throws MavenReportException {
291         ClassLoader origLoader = Thread.currentThread().getContextClassLoader();
292         try {
293             Thread.currentThread().setContextClassLoader(this.getClass().getClassLoader());
294 
295             PmdReportRenderer renderer = new PmdReportRenderer(
296                     getLog(),
297                     getSink(),
298                     i18n,
299                     locale,
300                     filesToProcess,
301                     pmdResult.getViolations(),
302                     renderRuleViolationPriority,
303                     renderViolationsByPriority,
304                     isAggregator());
305             if (renderSuppressedViolations) {
306                 renderer.setSuppressedViolations(pmdResult.getSuppressedViolations());
307             }
308             if (renderProcessingErrors) {
309                 renderer.setProcessingErrors(pmdResult.getErrors());
310             }
311 
312             renderer.render();
313         } finally {
314             Thread.currentThread().setContextClassLoader(origLoader);
315         }
316     }
317 
318     @Override
319     public boolean canGenerateReport() throws MavenReportException {
320         if (skip) {
321             return false;
322         }
323 
324         boolean result = canGenerateReportInternal();
325         if (result) {
326             executePmd();
327             if (skipEmptyReport) {
328                 result = pmdResult.hasViolations();
329             }
330         }
331         return result;
332     }
333 
334     private void executePmd() throws MavenReportException {
335         if (pmdResult != null) {
336             // PMD has already been run
337             getLog().debug("PMD has already been run - skipping redundant execution.");
338             return;
339         }
340 
341         try {
342             filesToProcess = getFilesToProcess();
343 
344             if (filesToProcess.isEmpty() && !"java".equals(language)) {
345                 getLog().warn("No files found to process. Did you forget to add additional source directories?"
346                         + " (see also build-helper-maven-plugin)");
347             }
348         } catch (IOException e) {
349             throw new MavenReportException("Can't get file list", e);
350         }
351 
352         PmdRequest request = new PmdRequest();
353         request.setLanguageAndVersion(language, targetJdk);
354         request.setRulesets(resolveRulesets());
355         request.setAuxClasspath(typeResolution ? determineAuxClasspath() : null);
356         request.setSourceEncoding(getInputEncoding());
357         request.addFiles(filesToProcess.keySet());
358         request.setMinimumPriority(minimumPriority);
359         request.setSuppressMarker(suppressMarker);
360         request.setBenchmarkOutputLocation(benchmark ? benchmarkOutputFilename : null);
361         request.setAnalysisCacheLocation(analysisCache ? analysisCacheLocation : null);
362         request.setExcludeFromFailureFile(excludeFromFailureFile);
363         request.setTargetDirectory(targetDirectory.getAbsolutePath());
364         request.setOutputEncoding(getOutputEncoding());
365         request.setFormat(format);
366         request.setSkipPmdError(skipPmdError);
367         request.setIncludeXmlInReports(includeXmlInReports);
368         request.setReportOutputDirectory(getReportOutputDirectory().getAbsolutePath());
369         request.setJdkToolchain(getJdkToolchain());
370 
371         getLog().info("PMD version: " + AbstractPmdReport.getPmdVersion());
372         pmdResult = serviceExecutor.execute(request);
373     }
374 
375     /**
376      * Resolves the configured rulesets and copies them as files into the {@link #rulesetsTargetDirectory}.
377      *
378      * @return comma separated list of absolute file paths of ruleset files
379      * @throws MavenReportException if a ruleset could not be found
380      */
381     private List<String> resolveRulesets() throws MavenReportException {
382         // configure ResourceManager - will search for urls (URLResourceLoader) and files in various directories:
383         // in the directory of the current project's pom file - note: extensions might replace the pom file on the fly
384         locator.addSearchPath(
385                 FileResourceLoader.ID, project.getFile().getParentFile().getAbsolutePath());
386         // in the current project's directory
387         locator.addSearchPath(FileResourceLoader.ID, project.getBasedir().getAbsolutePath());
388         // in the base directory - that's the directory of the initial pom requested to build,
389         // e.g. the root of a multi module build
390         locator.addSearchPath(FileResourceLoader.ID, session.getRequest().getBaseDirectory());
391         locator.setOutputDirectory(rulesetsTargetDirectory);
392 
393         String[] sets = new String[rulesets.length];
394         try {
395             for (int idx = 0; idx < rulesets.length; idx++) {
396                 String set = rulesets[idx];
397                 getLog().debug("Preparing ruleset: " + set);
398                 String rulesetFilename = determineRulesetFilename(set);
399                 File ruleset = locator.getResourceAsFile(rulesetFilename, getLocationTemp(set, idx + 1));
400                 if (null == ruleset) {
401                     throw new MavenReportException("Could not resolve " + set);
402                 }
403                 sets[idx] = ruleset.getAbsolutePath();
404             }
405         } catch (ResourceNotFoundException | FileResourceCreationException e) {
406             throw new MavenReportException(e.getMessage(), e);
407         }
408         return Arrays.asList(sets);
409     }
410 
411     private String determineRulesetFilename(String ruleset) {
412         String result = ruleset.trim();
413         String lowercase = result.toLowerCase(Locale.ROOT);
414         if (lowercase.startsWith("http://") || lowercase.startsWith("https://") || lowercase.endsWith(".xml")) {
415             return result;
416         }
417 
418         // assume last part is a single rule, e.g. myruleset.xml/SingleRule
419         if (result.indexOf('/') > -1) {
420             String rulesetFilename = result.substring(0, result.lastIndexOf('/'));
421             if (rulesetFilename.toLowerCase(Locale.ROOT).endsWith(".xml")) {
422                 return rulesetFilename;
423             }
424         }
425         // maybe a built-in ruleset name, e.g. java-design -> rulesets/java/design.xml
426         int dashIndex = lowercase.indexOf('-');
427         if (dashIndex > -1 && lowercase.indexOf('-', dashIndex + 1) == -1) {
428             String language = result.substring(0, dashIndex);
429             String rulesetName = result.substring(dashIndex + 1);
430             return "rulesets/" + language + "/" + rulesetName + ".xml";
431         }
432         // fallback - no change of the given ruleset specifier
433         return result;
434     }
435 
436     /**
437      * Convenience method to get the location of the specified file name.
438      *
439      * @param name the name of the file whose location is to be resolved
440      * @param position position in the list of rulesets (1-based)
441      * @return a String that contains the absolute file name of the file
442      */
443     protected String getLocationTemp(String name, int position) {
444         String loc = name;
445         if (loc.indexOf('/') != -1) {
446             loc = loc.substring(loc.lastIndexOf('/') + 1);
447         }
448         if (loc.indexOf('\\') != -1) {
449             loc = loc.substring(loc.lastIndexOf('\\') + 1);
450         }
451 
452         // MPMD-127 in the case that the rules are defined externally on a URL,
453         // we need to replace some special URL characters that cannot be
454         // used in filenames on disk or produce awkward filenames.
455         // Replace all occurrences of the following characters: ? : & = %
456         loc = loc.replaceAll("[\\?\\:\\&\\=\\%]", "_");
457 
458         if (loc.endsWith(".xml")) {
459             loc = loc.substring(0, loc.length() - 4);
460         }
461         loc = String.format("%03d-%s.xml", position, loc);
462 
463         getLog().debug("Before: " + name + " After: " + loc);
464         return loc;
465     }
466 
467     private String determineAuxClasspath() throws MavenReportException {
468         try {
469             List<String> classpath = new ArrayList<>();
470             if (isAggregator()) {
471                 List<String> dependencies = new ArrayList<>();
472                 Collection<MavenProject> aggregatedProjects = getAggregatedProjects();
473                 for (MavenProject localProject : aggregatedProjects) {
474                     configurationService
475                             .resolveDependenciesAsFile(localProject, aggregatedProjects, includeTests)
476                             .forEach(file -> dependencies.add(file.getAbsolutePath()));
477                     // Add the project's classes first
478                     classpath.addAll(
479                             includeTests
480                                     ? localProject.getTestClasspathElements()
481                                     : localProject.getCompileClasspathElements());
482                 }
483 
484                 // Add the dependencies as last entries
485                 classpath.addAll(dependencies);
486 
487                 getLog().debug("Using aggregated aux classpath: " + classpath);
488             } else {
489                 classpath.addAll(
490                         includeTests ? project.getTestClasspathElements() : project.getCompileClasspathElements());
491 
492                 getLog().debug("Using aux classpath: " + classpath);
493             }
494             return String.join(File.pathSeparator, classpath);
495         } catch (Exception e) {
496             throw new MavenReportException(e.getMessage(), e);
497         }
498     }
499 
500     /**
501      * {@inheritDoc}
502      */
503     @Override
504     @Deprecated
505     public String getOutputName() {
506         return "pmd";
507     }
508 
509     @Override
510     public String getOutputPath() {
511         return "pmd";
512     }
513 
514     /**
515      * Create and return the correct renderer for the output type.
516      *
517      * @return the renderer based on the configured output
518      * @throws org.apache.maven.reporting.MavenReportException if no renderer found for the output type
519      * @deprecated Use {@link PmdExecutor#createRenderer(String, String)} instead.
520      */
521     @Deprecated
522     public final Renderer createRenderer() throws MavenReportException {
523         return PmdExecutor.createRenderer(format, getOutputEncoding());
524     }
525 }