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