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