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