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