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