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