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.lang.reflect.InvocationTargetException;
24  import java.lang.reflect.Method;
25  import java.nio.file.Path;
26  import java.util.ArrayList;
27  import java.util.Collection;
28  import java.util.Collections;
29  import java.util.HashMap;
30  import java.util.HashSet;
31  import java.util.LinkedHashSet;
32  import java.util.LinkedList;
33  import java.util.List;
34  import java.util.Map;
35  import java.util.Set;
36  import java.util.TreeMap;
37  
38  import net.sourceforge.pmd.PMDVersion;
39  import org.apache.maven.execution.MavenSession;
40  import org.apache.maven.model.ReportPlugin;
41  import org.apache.maven.model.Reporting;
42  import org.apache.maven.plugins.annotations.Component;
43  import org.apache.maven.plugins.annotations.Parameter;
44  import org.apache.maven.project.MavenProject;
45  import org.apache.maven.reporting.AbstractMavenReport;
46  import org.apache.maven.reporting.MavenReportException;
47  import org.apache.maven.toolchain.Toolchain;
48  import org.apache.maven.toolchain.ToolchainManager;
49  import org.codehaus.plexus.util.FileUtils;
50  import org.codehaus.plexus.util.PathTool;
51  import org.codehaus.plexus.util.StringUtils;
52  
53  /**
54   * Base class for the PMD reports.
55   *
56   * @author <a href="mailto:brett@apache.org">Brett Porter</a>
57   * @version $Id$
58   */
59  public abstract class AbstractPmdReport extends AbstractMavenReport {
60      // ----------------------------------------------------------------------
61      // Configurables
62      // ----------------------------------------------------------------------
63  
64      /**
65       * The output directory for the intermediate XML report.
66       */
67      @Parameter(property = "project.build.directory", required = true)
68      protected File targetDirectory;
69  
70      /**
71       * Set the output format type, in addition to the HTML report. Must be one of: "none", "csv", "xml", "txt" or the
72       * full class name of the PMD renderer to use. See the net.sourceforge.pmd.renderers package javadoc for available
73       * renderers. XML is produced in any case, since this format is needed
74       * for the check goals (pmd:check, pmd:aggregator-check, pmd:cpd-check, pmd:aggregator-cpd-check).
75       */
76      @Parameter(property = "format", defaultValue = "xml")
77      protected String format = "xml";
78  
79      /**
80       * Link the violation line numbers to the source xref. Links will be created automatically if the jxr plugin is
81       * being used.
82       */
83      @Parameter(property = "linkXRef", defaultValue = "true")
84      private boolean linkXRef;
85  
86      /**
87       * Location of the Xrefs to link to.
88       */
89      @Parameter(defaultValue = "${project.reporting.outputDirectory}/xref")
90      private File xrefLocation;
91  
92      /**
93       * Location of the Test Xrefs to link to.
94       */
95      @Parameter(defaultValue = "${project.reporting.outputDirectory}/xref-test")
96      private File xrefTestLocation;
97  
98      /**
99       * A list of files to exclude from checking. Can contain Ant-style wildcards and double wildcards. Note that these
100      * exclusion patterns only operate on the path of a source file relative to its source root directory. In other
101      * words, files are excluded based on their package and/or class name. If you want to exclude entire source root
102      * directories, use the parameter <code>excludeRoots</code> instead.
103      *
104      * @since 2.2
105      */
106     @Parameter
107     private List<String> excludes;
108 
109     /**
110      * A list of files to include from checking. Can contain Ant-style wildcards and double wildcards. Defaults to
111      * **\/*.java.
112      *
113      * @since 2.2
114      */
115     @Parameter
116     private List<String> includes;
117 
118     /**
119      * Specifies the location of the source directories to be used for PMD.
120      * Defaults to <code>project.compileSourceRoots</code>.
121      * @since 3.7
122      */
123     @Parameter(defaultValue = "${project.compileSourceRoots}")
124     private List<String> compileSourceRoots;
125 
126     /**
127      * The directories containing the test-sources to be used for PMD.
128      * Defaults to <code>project.testCompileSourceRoots</code>
129      * @since 3.7
130      */
131     @Parameter(defaultValue = "${project.testCompileSourceRoots}")
132     private List<String> testSourceRoots;
133 
134     /**
135      * The project source directories that should be excluded.
136      *
137      * @since 2.2
138      */
139     @Parameter
140     private File[] excludeRoots;
141 
142     /**
143      * Run PMD on the tests as well.
144      *
145      * @since 2.2
146      */
147     @Parameter(defaultValue = "false")
148     protected boolean includeTests;
149 
150     /**
151      * Whether to build an aggregated report at the root, or build individual reports.
152      *
153      * @since 2.2
154      * @deprecated since 3.15.0 Use the goals <code>pmd:aggregate-pmd</code> and <code>pmd:aggregate-cpd</code>
155      * instead. See <a href="https://maven.apache.org/plugins/maven-pmd-plugin/faq.html#typeresolution_aggregate">FAQ:
156      * Why do I get sometimes false positive and/or false negative violations?</a> for an explanation.
157      */
158     @Parameter(property = "aggregate", defaultValue = "false")
159     @Deprecated
160     protected boolean aggregate;
161 
162     /**
163      * Whether to include the xml files generated by PMD/CPD in the site.
164      *
165      * @since 3.0
166      */
167     @Parameter(defaultValue = "false")
168     protected boolean includeXmlInSite;
169 
170     /**
171      * Skip the PMD/CPD report generation if there are no violations or duplications found. Defaults to
172      * <code>false</code>.
173      *
174      * <p>Note: the default value was changed from <code>true</code> to <code>false</code> with version 3.13.0.
175      *
176      * @since 3.1
177      */
178     @Parameter(defaultValue = "false")
179     protected boolean skipEmptyReport;
180 
181     /**
182      * File that lists classes and rules to be excluded from failures.
183      * For PMD, this is a properties file. For CPD, this
184      * is a text file that contains comma-separated lists of classes
185      * that are allowed to duplicate.
186      *
187      * @since 3.7
188      */
189     @Parameter(property = "pmd.excludeFromFailureFile", defaultValue = "")
190     protected String excludeFromFailureFile;
191 
192     /**
193      * Redirect PMD log into maven log out.
194      * When enabled, the PMD log output is redirected to maven, so that
195      * it is visible in the console together with all the other log output.
196      * Also, if maven is started with the debug flag (<code>-X</code> or <code>--debug</code>),
197      * the PMD logger is also configured for debug.
198      *
199      * @since 3.9.0
200      * @deprecated With 3.22.0 and the upgrade to PMD 7, this parameter has no effect anymore. The PMD log
201      * is now always redirected into the maven log and this can't be disabled by this parameter anymore.
202      * In order to disable the logging, see <a href="https://maven.apache.org/maven-logging.html">Maven Logging</a>.
203      * You'd need to start maven with <code>MAVEN_OPTS=-Dorg.slf4j.simpleLogger.log.net.sourceforge.pmd=off mvn &lt;goals&gt;</code>.
204      */
205     @Parameter(defaultValue = "true", property = "pmd.showPmdLog")
206     @Deprecated // (since = "3.22.0", forRemoval = true)
207     protected boolean showPmdLog = true;
208 
209     /**
210      * Used to avoid showing the deprecation warning for "showPmdLog" multiple times.
211      */
212     private boolean warnedAboutShowPmdLog = false;
213 
214     /**
215      * <p>
216      * Allow for configuration of the jvm used to run PMD via maven toolchains.
217      * This permits a configuration where the project is built with one jvm and PMD is executed with another.
218      * This overrules the toolchain selected by the maven-toolchain-plugin.
219      * </p>
220      *
221      * <p>Examples:</p>
222      * (see <a href="https://maven.apache.org/guides/mini/guide-using-toolchains.html">
223      *     Guide to Toolchains</a> for more info)
224      *
225      * <pre>
226      * {@code
227      *    <configuration>
228      *        ...
229      *        <jdkToolchain>
230      *            <version>1.11</version>
231      *        </jdkToolchain>
232      *    </configuration>
233      *
234      *    <configuration>
235      *        ...
236      *        <jdkToolchain>
237      *            <version>1.8</version>
238      *            <vendor>zulu</vendor>
239      *        </jdkToolchain>
240      *    </configuration>
241      *    }
242      * </pre>
243      *
244      * <strong>note:</strong> requires at least Maven 3.3.1
245      *
246      * @since 3.14.0
247      */
248     @Parameter
249     private Map<String, String> jdkToolchain;
250 
251     // ----------------------------------------------------------------------
252     // Read-only parameters
253     // ----------------------------------------------------------------------
254 
255     /**
256      * The projects in the reactor for aggregation report.
257      */
258     @Parameter(property = "reactorProjects", readonly = true)
259     protected List<MavenProject> reactorProjects;
260 
261     /**
262      * The current build session instance. This is used for
263      * toolchain manager API calls and for dependency resolver API calls.
264      */
265     @Parameter(defaultValue = "${session}", required = true, readonly = true)
266     protected MavenSession session;
267 
268     @Component
269     private ToolchainManager toolchainManager;
270 
271     /** The files that are being analyzed. */
272     protected Map<File, PmdFileInfo> filesToProcess;
273 
274     @Override
275     protected MavenProject getProject() {
276         return project;
277     }
278 
279     protected String constructXRefLocation(boolean test) {
280         String location = null;
281         if (linkXRef) {
282             File xrefLoc = test ? xrefTestLocation : xrefLocation;
283 
284             String relativePath =
285                     PathTool.getRelativePath(getReportOutputDirectory().getAbsolutePath(), xrefLoc.getAbsolutePath());
286             if (relativePath == null || relativePath.isEmpty()) {
287                 relativePath = ".";
288             }
289             relativePath = relativePath + "/" + xrefLoc.getName();
290             if (xrefLoc.exists()) {
291                 // XRef was already generated by manual execution of a lifecycle binding
292                 location = relativePath;
293             } else {
294                 // Not yet generated - check if the report is on its way
295                 Reporting reporting = project.getModel().getReporting();
296                 List<ReportPlugin> reportPlugins =
297                         reporting != null ? reporting.getPlugins() : Collections.<ReportPlugin>emptyList();
298                 for (ReportPlugin plugin : reportPlugins) {
299                     String artifactId = plugin.getArtifactId();
300                     if ("maven-jxr-plugin".equals(artifactId) || "jxr-maven-plugin".equals(artifactId)) {
301                         location = relativePath;
302                     }
303                 }
304             }
305 
306             if (location == null) {
307                 getLog().warn("Unable to locate Source XRef to link to - DISABLED");
308             }
309         }
310         return location;
311     }
312 
313     /**
314      * Convenience method to get the list of files where the PMD tool will be executed
315      *
316      * @return a List of the files where the PMD tool will be executed
317      * @throws IOException If an I/O error occurs during construction of the
318      *                     canonical pathnames of the files
319      */
320     protected Map<File, PmdFileInfo> getFilesToProcess() throws IOException {
321         if (aggregate && !project.isExecutionRoot()) {
322             return Collections.emptyMap();
323         }
324 
325         if (excludeRoots == null) {
326             excludeRoots = new File[0];
327         }
328 
329         Collection<File> excludeRootFiles = new HashSet<>(excludeRoots.length);
330 
331         for (File file : excludeRoots) {
332             if (file.isDirectory()) {
333                 excludeRootFiles.add(file);
334             }
335         }
336 
337         List<PmdFileInfo> directories = new ArrayList<>();
338 
339         if (null == compileSourceRoots) {
340             compileSourceRoots = project.getCompileSourceRoots();
341         }
342         if (compileSourceRoots != null) {
343             for (String root : compileSourceRoots) {
344                 File sroot = new File(root);
345                 if (sroot.exists()) {
346                     String sourceXref = constructXRefLocation(false);
347                     directories.add(new PmdFileInfo(project, sroot, sourceXref));
348                 }
349             }
350         }
351 
352         if (null == testSourceRoots) {
353             testSourceRoots = project.getTestCompileSourceRoots();
354         }
355         if (includeTests && testSourceRoots != null) {
356             for (String root : testSourceRoots) {
357                 File sroot = new File(root);
358                 if (sroot.exists()) {
359                     String testXref = constructXRefLocation(true);
360                     directories.add(new PmdFileInfo(project, sroot, testXref));
361                 }
362             }
363         }
364         if (isAggregator()) {
365             for (MavenProject localProject : getAggregatedProjects()) {
366                 List<String> localCompileSourceRoots = localProject.getCompileSourceRoots();
367                 for (String root : localCompileSourceRoots) {
368                     File sroot = new File(root);
369                     if (sroot.exists()) {
370                         String sourceXref = constructXRefLocation(false);
371                         directories.add(new PmdFileInfo(localProject, sroot, sourceXref));
372                     }
373                 }
374                 if (includeTests) {
375                     List<String> localTestCompileSourceRoots = localProject.getTestCompileSourceRoots();
376                     for (String root : localTestCompileSourceRoots) {
377                         File sroot = new File(root);
378                         if (sroot.exists()) {
379                             String testXref = constructXRefLocation(true);
380                             directories.add(new PmdFileInfo(localProject, sroot, testXref));
381                         }
382                     }
383                 }
384             }
385         }
386 
387         String excluding = getExcludes();
388         getLog().debug("Exclusions: " + excluding);
389         String including = getIncludes();
390         getLog().debug("Inclusions: " + including);
391 
392         Map<File, PmdFileInfo> files = new TreeMap<>();
393 
394         for (PmdFileInfo finfo : directories) {
395             getLog().debug("Searching for files in directory "
396                     + finfo.getSourceDirectory().toString());
397             File sourceDirectory = finfo.getSourceDirectory();
398             if (sourceDirectory.isDirectory() && !isDirectoryExcluded(excludeRootFiles, sourceDirectory)) {
399                 List<File> newfiles = FileUtils.getFiles(sourceDirectory, including, excluding);
400                 for (File newfile : newfiles) {
401                     files.put(newfile.getCanonicalFile(), finfo);
402                 }
403             }
404         }
405 
406         return files;
407     }
408 
409     private boolean isDirectoryExcluded(Collection<File> excludeRootFiles, File sourceDirectoryToCheck) {
410         boolean returnVal = false;
411         for (File excludeDir : excludeRootFiles) {
412             try {
413                 if (sourceDirectoryToCheck
414                         .getCanonicalFile()
415                         .toPath()
416                         .startsWith(excludeDir.getCanonicalFile().toPath())) {
417                     getLog().debug("Directory " + sourceDirectoryToCheck.getAbsolutePath()
418                             + " has been excluded as it matches excludeRoot "
419                             + excludeDir.getAbsolutePath());
420                     returnVal = true;
421                     break;
422                 }
423             } catch (IOException e) {
424                 getLog().warn("Error while checking " + sourceDirectoryToCheck + " whether it should be excluded.", e);
425             }
426         }
427         return returnVal;
428     }
429 
430     /**
431      * Gets the comma separated list of effective include patterns.
432      *
433      * @return The comma separated list of effective include patterns, never <code>null</code>.
434      */
435     private String getIncludes() {
436         Collection<String> patterns = new LinkedHashSet<>();
437         if (includes != null) {
438             patterns.addAll(includes);
439         }
440         if (patterns.isEmpty()) {
441             patterns.add("**/*.java");
442         }
443         return StringUtils.join(patterns.iterator(), ",");
444     }
445 
446     /**
447      * Gets the comma separated list of effective exclude patterns.
448      *
449      * @return The comma separated list of effective exclude patterns, never <code>null</code>.
450      */
451     private String getExcludes() {
452         Collection<String> patterns = new LinkedHashSet<>(FileUtils.getDefaultExcludesAsList());
453         if (excludes != null) {
454             patterns.addAll(excludes);
455         }
456         return StringUtils.join(patterns.iterator(), ",");
457     }
458 
459     protected boolean isXml() {
460         return "xml".equals(format);
461     }
462 
463     /**
464      * {@inheritDoc}
465      */
466     @Override
467     public boolean canGenerateReport() {
468         if (!showPmdLog && !warnedAboutShowPmdLog) {
469             getLog().warn("The parameter \"showPmdLog\" has been deprecated and will be removed."
470                     + "Setting it to \"false\" has no effect.");
471             warnedAboutShowPmdLog = true;
472         }
473 
474         if (aggregate && !project.isExecutionRoot()) {
475             return false;
476         }
477 
478         if (!isAggregator() && "pom".equalsIgnoreCase(project.getPackaging())) {
479             return false;
480         }
481 
482         // if format is XML, we need to output it even if the file list is empty
483         // so the "check" goals can check for failures
484         if (isXml()) {
485             return true;
486         }
487         try {
488             filesToProcess = getFilesToProcess();
489             if (filesToProcess.isEmpty()) {
490                 return false;
491             }
492         } catch (IOException e) {
493             getLog().error(e);
494         }
495         return true;
496     }
497 
498     protected String determineCurrentRootLogLevel() {
499         String logLevel = System.getProperty("org.slf4j.simpleLogger.defaultLogLevel");
500         if (logLevel == null) {
501             logLevel = System.getProperty("maven.logging.root.level");
502         }
503         if (logLevel == null) {
504             // TODO: logback level
505             logLevel = "info";
506         }
507         return logLevel;
508     }
509 
510     static String getPmdVersion() {
511         return PMDVersion.VERSION;
512     }
513 
514     // TODO remove the part with ToolchainManager lookup once we depend on
515     // 3.0.9 (have it as prerequisite). Define as regular component field then.
516     protected final Toolchain getToolchain() {
517         Toolchain tc = null;
518 
519         if (jdkToolchain != null) {
520             // Maven 3.3.1 has plugin execution scoped Toolchain Support
521             try {
522                 Method getToolchainsMethod = toolchainManager
523                         .getClass()
524                         .getMethod("getToolchains", MavenSession.class, String.class, Map.class);
525 
526                 @SuppressWarnings("unchecked")
527                 List<Toolchain> tcs =
528                         (List<Toolchain>) getToolchainsMethod.invoke(toolchainManager, session, "jdk", jdkToolchain);
529 
530                 if (tcs != null && !tcs.isEmpty()) {
531                     tc = tcs.get(0);
532                 }
533             } catch (NoSuchMethodException
534                     | SecurityException
535                     | IllegalAccessException
536                     | IllegalArgumentException
537                     | InvocationTargetException e) {
538                 // ignore
539             }
540         }
541 
542         if (tc == null) {
543             tc = toolchainManager.getToolchainFromBuildContext("jdk", session);
544         }
545 
546         return tc;
547     }
548 
549     protected boolean isAggregator() {
550         // returning here aggregate for backwards compatibility
551         return aggregate;
552     }
553 
554     // Note: same logic as in m-javadoc-p (MJAVADOC-134)
555     protected Collection<MavenProject> getAggregatedProjects() {
556         Map<Path, MavenProject> reactorProjectsMap = new HashMap<>();
557         for (MavenProject reactorProject : this.reactorProjects) {
558             reactorProjectsMap.put(reactorProject.getBasedir().toPath(), reactorProject);
559         }
560 
561         return modulesForAggregatedProject(project, reactorProjectsMap);
562     }
563 
564     /**
565      * Recursively add the modules of the aggregatedProject to the set of aggregatedModules.
566      *
567      * @param aggregatedProject the project being aggregated
568      * @param reactorProjectsMap map of (still) available reactor projects
569      * @throws MavenReportException if any
570      */
571     private Set<MavenProject> modulesForAggregatedProject(
572             MavenProject aggregatedProject, Map<Path, MavenProject> reactorProjectsMap) {
573         // Maven does not supply an easy way to get the projects representing
574         // the modules of a project. So we will get the paths to the base
575         // directories of the modules from the project and compare with the
576         // base directories of the projects in the reactor.
577 
578         if (aggregatedProject.getModules().isEmpty()) {
579             return Collections.singleton(aggregatedProject);
580         }
581 
582         List<Path> modulePaths = new LinkedList<Path>();
583         for (String module : aggregatedProject.getModules()) {
584             modulePaths.add(new File(aggregatedProject.getBasedir(), module).toPath());
585         }
586 
587         Set<MavenProject> aggregatedModules = new LinkedHashSet<>();
588 
589         for (Path modulePath : modulePaths) {
590             MavenProject module = reactorProjectsMap.remove(modulePath);
591             if (module != null) {
592                 aggregatedModules.addAll(modulesForAggregatedProject(module, reactorProjectsMap));
593             }
594         }
595 
596         return aggregatedModules;
597     }
598 }