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