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.util.ArrayList;
25  import java.util.Arrays;
26  import java.util.List;
27  import java.util.Locale;
28  import java.util.ResourceBundle;
29  
30  import org.apache.maven.doxia.sink.Sink;
31  import org.apache.maven.plugins.annotations.Component;
32  import org.apache.maven.plugins.annotations.Mojo;
33  import org.apache.maven.plugins.annotations.Parameter;
34  import org.apache.maven.plugins.annotations.ResolutionScope;
35  import org.apache.maven.plugins.pmd.exec.PmdExecutor;
36  import org.apache.maven.plugins.pmd.exec.PmdRequest;
37  import org.apache.maven.plugins.pmd.exec.PmdResult;
38  import org.apache.maven.project.DefaultProjectBuildingRequest;
39  import org.apache.maven.project.MavenProject;
40  import org.apache.maven.project.ProjectBuildingRequest;
41  import org.apache.maven.reporting.MavenReportException;
42  import org.apache.maven.shared.artifact.filter.resolve.AndFilter;
43  import org.apache.maven.shared.artifact.filter.resolve.ExclusionsFilter;
44  import org.apache.maven.shared.artifact.filter.resolve.ScopeFilter;
45  import org.apache.maven.shared.artifact.filter.resolve.TransformableFilter;
46  import org.apache.maven.shared.transfer.artifact.resolve.ArtifactResult;
47  import org.apache.maven.shared.transfer.dependencies.resolve.DependencyResolver;
48  import org.apache.maven.shared.utils.logging.MessageUtils;
49  import org.apache.maven.toolchain.Toolchain;
50  import org.codehaus.plexus.resource.ResourceManager;
51  import org.codehaus.plexus.resource.loader.FileResourceCreationException;
52  import org.codehaus.plexus.resource.loader.FileResourceLoader;
53  import org.codehaus.plexus.resource.loader.ResourceNotFoundException;
54  import org.codehaus.plexus.util.ReaderFactory;
55  import org.codehaus.plexus.util.StringUtils;
56  
57  import net.sourceforge.pmd.renderers.Renderer;
58  
59  /**
60   * Creates a PMD site report based on the rulesets and configuration set in the plugin.
61   * It can also generate a pmd output file aside from the site report in any of the following formats: xml, csv or txt.
62   *
63   * @author Brett Porter
64   * @version $Id$
65   * @since 2.0
66   */
67  @Mojo( name = "pmd", threadSafe = true, requiresDependencyResolution = ResolutionScope.TEST )
68  public class PmdReport
69      extends AbstractPmdReport
70  {
71      /**
72       * The target JDK to analyze based on. Should match the source used in the compiler plugin. Valid values
73       * with the default PMD version are
74       * currently <code>1.3</code>, <code>1.4</code>, <code>1.5</code>, <code>1.6</code>, <code>1.7</code>,
75       * <code>1.8</code>, <code>9</code>, <code>10</code>, <code>11</code>, <code>12</code>, <code>13</code>,
76       * <code>14</code>, <code>15</code>, <code>16</code>, <code>17</code>, and <code>18</code>.
77       *
78       * <p> You can override the default PMD version by specifying PMD as a dependency,
79       * see <a href="examples/upgrading-PMD-at-runtime.html">Upgrading PMD at Runtime</a>.</p>
80       *
81       * <p>
82       *   <b>Note:</b> this parameter is only used if the language parameter is set to <code>java</code>.
83       * </p>
84       */
85      @Parameter( property = "targetJdk", defaultValue = "${maven.compiler.source}" )
86      private String targetJdk;
87  
88      /**
89       * The programming language to be analyzed by PMD. Valid values are currently <code>java</code>,
90       * <code>javascript</code> and <code>jsp</code>.
91       *
92       * @since 3.0
93       */
94      @Parameter( defaultValue = "java" )
95      private String language;
96  
97      /**
98       * The rule priority threshold; rules with lower priority than this will not be evaluated.
99       *
100      * @since 2.1
101      */
102     @Parameter( property = "minimumPriority", defaultValue = "5" )
103     private int minimumPriority = 5;
104 
105     /**
106      * Skip the PMD report generation. Most useful on the command line via "-Dpmd.skip=true".
107      *
108      * @since 2.1
109      */
110     @Parameter( property = "pmd.skip", defaultValue = "false" )
111     private boolean skip;
112 
113     /**
114      * The PMD rulesets to use. See the
115      * <a href="https://pmd.github.io/latest/pmd_rules_java.html">Stock Java Rulesets</a> for a
116      * list of available rules.
117      * Defaults to a custom ruleset provided by this maven plugin
118      * (<code>/rulesets/java/maven-pmd-plugin-default.xml</code>).
119      */
120     @Parameter
121     String[] rulesets = new String[] { "/rulesets/java/maven-pmd-plugin-default.xml" };
122 
123     /**
124      * Controls whether the project's compile/test classpath should be passed to PMD to enable its type resolution
125      * feature.
126      *
127      * @since 3.0
128      */
129     @Parameter( property = "pmd.typeResolution", defaultValue = "true" )
130     private boolean typeResolution;
131 
132     /**
133      * Controls whether PMD will track benchmark information.
134      *
135      * @since 3.1
136      */
137     @Parameter( property = "pmd.benchmark", defaultValue = "false" )
138     private boolean benchmark;
139 
140     /**
141      * Benchmark output filename.
142      *
143      * @since 3.1
144      */
145     @Parameter( property = "pmd.benchmarkOutputFilename",
146                     defaultValue = "${project.build.directory}/pmd-benchmark.txt" )
147     private String benchmarkOutputFilename;
148 
149     /**
150      * Source level marker used to indicate whether a RuleViolation should be suppressed. If it is not set, PMD's
151      * default will be used, which is <code>NOPMD</code>. See also <a
152      * href="https://pmd.github.io/latest/pmd_userdocs_suppressing_warnings.html">PMD &#x2013; Suppressing warnings</a>.
153      *
154      * @since 3.4
155      */
156     @Parameter( property = "pmd.suppressMarker" )
157     private String suppressMarker;
158 
159     /**
160      * per default pmd executions error are ignored to not break the whole
161      *
162      * @since 3.1
163      */
164     @Parameter( property = "pmd.skipPmdError", defaultValue = "true" )
165     private boolean skipPmdError;
166 
167     /**
168      * Enables the analysis cache, which speeds up PMD. This
169      * requires a cache file, that contains the results of the last
170      * PMD run. Thus the cache is only effective, if this file is
171      * not cleaned between runs.
172      *
173      * @since 3.8
174      */
175     @Parameter( property = "pmd.analysisCache", defaultValue = "false" )
176     private boolean analysisCache;
177 
178     /**
179      * The location of the analysis cache, if it is enabled.
180      * This file contains the results of the last PMD run and must not be cleaned
181      * between consecutive PMD runs. Otherwise the cache is not in use.
182      * If the file doesn't exist, PMD executes as if there is no cache enabled and
183      * all files are analyzed. Otherwise only changed files will be analyzed again.
184      *
185      * @since 3.8
186      */
187     @Parameter( property = "pmd.analysisCacheLocation", defaultValue = "${project.build.directory}/pmd/pmd.cache" )
188     private String analysisCacheLocation;
189 
190     /**
191      * Also render processing errors into the HTML report.
192      * Processing errors are problems, that PMD encountered while executing the rules.
193      * It can be parsing errors or exceptions during rule execution.
194      * Processing errors indicate a bug in PMD and the information provided help in
195      * reporting and fixing bugs in PMD.
196      *
197      * @since 3.9.0
198      */
199     @Parameter( property = "pmd.renderProcessingErrors", defaultValue = "true" )
200     private boolean renderProcessingErrors = true;
201 
202     /**
203      * Also render the rule priority into the HTML report.
204      *
205      * @since 3.10.0
206      */
207     @Parameter( property = "pmd.renderRuleViolationPriority", defaultValue = "true" )
208     private boolean renderRuleViolationPriority = true;
209 
210     /**
211      * Add a section in the HTML report, that groups the found violations by rule priority
212      * in addition to grouping by file.
213      *
214      * @since 3.12.0
215      */
216     @Parameter( property = "pmd.renderViolationsByPriority", defaultValue = "true" )
217     private boolean renderViolationsByPriority = true;
218 
219     /**
220      * Add a section in the HTML report that lists the suppressed violations.
221      *
222      * @since 3.17.0
223      */
224     @Parameter( property = "pmd.renderSuppressedViolations", defaultValue = "true" )
225     private boolean renderSuppressedViolations = true;
226 
227     /**
228      * Before PMD is executed, the configured rulesets are resolved and copied into this directory.
229      * <p>Note: Before 3.13.0, this was by default ${project.build.directory}.
230      *
231      * @since 3.13.0
232      */
233     @Parameter( property = "pmd.rulesetsTargetDirectory", defaultValue = "${project.build.directory}/pmd/rulesets" )
234     private File rulesetsTargetDirectory;
235 
236     /**
237      * Used to locate configured rulesets. The rulesets could be on the plugin
238      * classpath or in the local project file system.
239      */
240     @Component
241     private ResourceManager locator;
242 
243     @Component
244     private DependencyResolver dependencyResolver;
245 
246     /**
247      * Contains the result of the last PMD execution.
248      * It might be <code>null</code> which means, that PMD
249      * has not been executed yet.
250      */
251     private PmdResult pmdResult;
252 
253     /**
254      * {@inheritDoc}
255      */
256     @Override
257     public String getName( Locale locale )
258     {
259         return getBundle( locale ).getString( "report.pmd.name" );
260     }
261 
262     /**
263      * {@inheritDoc}
264      */
265     @Override
266     public String getDescription( Locale locale )
267     {
268         return getBundle( locale ).getString( "report.pmd.description" );
269     }
270 
271     /**
272      * Configures the PMD rulesets to be used directly.
273      * Note: Usually the rulesets are configured via the property.
274      *
275      * @param rulesets the PMD rulesets to be used.
276      * @see #rulesets
277      */
278     public void setRulesets( String[] rulesets )
279     {
280         this.rulesets = Arrays.copyOf( rulesets, rulesets.length );
281     }
282 
283     /**
284      * {@inheritDoc}
285      */
286     @Override
287     public void executeReport( Locale locale )
288         throws MavenReportException
289     {
290         try
291         {
292             execute( locale );
293         }
294         finally
295         {
296             if ( getSink() != null )
297             {
298                 getSink().close();
299             }
300         }
301     }
302 
303     private void execute( Locale locale )
304         throws MavenReportException
305     {
306         if ( !skip && canGenerateReport() )
307         {
308             ClassLoader origLoader = Thread.currentThread().getContextClassLoader();
309             try
310             {
311                 Thread.currentThread().setContextClassLoader( this.getClass().getClassLoader() );
312 
313                 generateMavenSiteReport( locale );
314             }
315             finally
316             {
317                 Thread.currentThread().setContextClassLoader( origLoader );
318             }
319         }
320     }
321 
322     @Override
323     public boolean canGenerateReport()
324     {
325         if ( skip )
326         {
327             getLog().info( "Skipping PMD execution" );
328             return false;
329         }
330 
331         boolean result = super.canGenerateReport();
332         if ( result )
333         {
334             try
335             {
336                 executePmd();
337                 if ( skipEmptyReport )
338                 {
339                     result = pmdResult.hasViolations();
340                     if ( !result )
341                     {
342                         getLog().debug( "Skipping report since skipEmptyReport is true and "
343                                             + "there are no PMD violations." );
344                     }
345                 }
346             }
347             catch ( MavenReportException e )
348             {
349                 throw new RuntimeException( e );
350             }
351         }
352         return result;
353     }
354 
355     private void executePmd()
356         throws MavenReportException
357     {
358         if ( pmdResult != null )
359         {
360             // PMD has already been run
361             getLog().debug( "PMD has already been run - skipping redundant execution." );
362             return;
363         }
364 
365         try
366         {
367             filesToProcess = getFilesToProcess();
368 
369             if ( filesToProcess.isEmpty() && !"java".equals( language ) )
370             {
371                 getLog().warn( "No files found to process. Did you add your additional source folders like javascript?"
372                                    + " (see also build-helper-maven-plugin)" );
373             }
374         }
375         catch ( IOException e )
376         {
377             throw new MavenReportException( "Can't get file list", e );
378         }
379 
380 
381         PmdRequest request = new PmdRequest();
382         request.setLanguageAndVersion( language, targetJdk );
383         request.setRulesets( resolveRulesets() );
384         request.setAuxClasspath( typeResolution ? determineAuxClasspath() : null );
385         request.setSourceEncoding( getSourceEncoding() );
386         request.addFiles( filesToProcess.keySet() );
387         request.setMinimumPriority( minimumPriority );
388         request.setSuppressMarker( suppressMarker );
389         request.setBenchmarkOutputLocation( benchmark ? benchmarkOutputFilename : null );
390         request.setAnalysisCacheLocation( analysisCache ? analysisCacheLocation : null );
391         request.setExcludeFromFailureFile( excludeFromFailureFile );
392 
393         request.setTargetDirectory( targetDirectory.getAbsolutePath() );
394         request.setOutputEncoding( getOutputEncoding() );
395         request.setFormat( format );
396         request.setShowPmdLog( showPmdLog );
397         request.setColorizedLog( MessageUtils.isColorEnabled() );
398         request.setSkipPmdError( skipPmdError );
399         request.setIncludeXmlInSite( includeXmlInSite );
400         request.setReportOutputDirectory( getReportOutputDirectory().getAbsolutePath() );
401         request.setLogLevel( determineCurrentRootLogLevel() );
402 
403         Toolchain tc = getToolchain();
404         if ( tc != null )
405         {
406             getLog().info( "Toolchain in maven-pmd-plugin: " + tc );
407             String javaExecutable = tc.findTool( "java" ); //NOI18N
408             request.setJavaExecutable( javaExecutable );
409         }
410 
411         getLog().info( "PMD version: " + AbstractPmdReport.getPmdVersion() );
412         pmdResult = PmdExecutor.execute( request );
413     }
414 
415     protected String getSourceEncoding()
416     {
417         String encoding = super.getSourceEncoding();
418         if ( StringUtils.isEmpty( encoding ) )
419         {
420             encoding = ReaderFactory.FILE_ENCODING;
421             if ( !filesToProcess.isEmpty() )
422             {
423                 getLog().warn( "File encoding has not been set, using platform encoding " + ReaderFactory.FILE_ENCODING
424                                + ", i.e. build is platform dependent!" );
425             }
426         }
427         return encoding;
428     }
429 
430 
431     /**
432      * Resolves the configured rulesets and copies them as files into the {@link #rulesetsTargetDirectory}.
433      *
434      * @return comma separated list of absolute file paths of ruleset files
435      * @throws MavenReportException if a ruleset could not be found
436      */
437     private List<String> resolveRulesets() throws MavenReportException
438     {
439         // configure ResourceManager - will search for urls (URLResourceLoader) and files in various directories:
440         // in the directory of the current project's pom file - note: extensions might replace the pom file on the fly
441         locator.addSearchPath( FileResourceLoader.ID, project.getFile().getParentFile().getAbsolutePath() );
442         // in the current project's directory
443         locator.addSearchPath( FileResourceLoader.ID, project.getBasedir().getAbsolutePath() );
444         // in the base directory - that's the directory of the initial pom requested to build,
445         // e.g. the root of a multi module build
446         locator.addSearchPath( FileResourceLoader.ID, session.getRequest().getBaseDirectory() );
447         locator.setOutputDirectory( rulesetsTargetDirectory );
448 
449         String[] sets = new String[rulesets.length];
450         try
451         {
452             for ( int idx = 0; idx < rulesets.length; idx++ )
453             {
454                 String set = rulesets[idx];
455                 getLog().debug( "Preparing ruleset: " + set );
456                 String rulesetFilename = determineRulesetFilename( set );
457                 File ruleset = locator.getResourceAsFile( rulesetFilename, getLocationTemp( set ) );
458                 if ( null == ruleset )
459                 {
460                     throw new MavenReportException( "Could not resolve " + set );
461                 }
462                 sets[idx] = ruleset.getAbsolutePath();
463             }
464         }
465         catch ( ResourceNotFoundException | FileResourceCreationException e )
466         {
467             throw new MavenReportException( e.getMessage(), e );
468         }
469         return Arrays.asList( sets );
470     }
471 
472     private String determineRulesetFilename( String ruleset )
473     {
474         String result = ruleset.trim();
475         String lowercase = result.toLowerCase( Locale.ROOT );
476         if ( lowercase.startsWith( "http://" ) || lowercase.startsWith( "https://" ) || lowercase.endsWith( ".xml" ) )
477         {
478             return result;
479         }
480 
481         // assume last part is a single rule, e.g. myruleset.xml/SingleRule
482         if ( result.indexOf( '/' ) > -1 )
483         {
484             String rulesetFilename = result.substring( 0, result.lastIndexOf( '/' ) );
485             if ( rulesetFilename.toLowerCase( Locale.ROOT ).endsWith( ".xml" ) )
486             {
487                 return rulesetFilename;
488             }
489         }
490         // maybe a built-in ruleset name, e.g. java-design -> rulesets/java/design.xml
491         int dashIndex = lowercase.indexOf( '-' );
492         if ( dashIndex > -1 && lowercase.indexOf( '-', dashIndex + 1 ) == -1 )
493         {
494             String language = result.substring( 0, dashIndex );
495             String rulesetName = result.substring( dashIndex + 1 );
496             return "rulesets/" + language + "/" + rulesetName + ".xml";
497         }
498         // fallback - no change of the given ruleset specifier
499         return result;
500     }
501 
502     private void generateMavenSiteReport( Locale locale )
503         throws MavenReportException
504     {
505         Sink sink = getSink();
506         PmdReportGenerator doxiaRenderer = new PmdReportGenerator( getLog(), sink, getBundle( locale ),
507                 isAggregator() );
508         doxiaRenderer.setRenderRuleViolationPriority( renderRuleViolationPriority );
509         doxiaRenderer.setRenderViolationsByPriority( renderViolationsByPriority );
510         doxiaRenderer.setFiles( filesToProcess );
511         doxiaRenderer.setViolations( pmdResult.getViolations() );
512         if ( renderSuppressedViolations )
513         {
514             doxiaRenderer.setSuppressedViolations( pmdResult.getSuppressedViolations() );
515         }
516         if ( renderProcessingErrors )
517         {
518             doxiaRenderer.setProcessingErrors( pmdResult.getErrors() );
519         }
520 
521         try
522         {
523             doxiaRenderer.beginDocument();
524             doxiaRenderer.render();
525             doxiaRenderer.endDocument();
526         }
527         catch ( IOException e )
528         {
529             getLog().warn( "Failure creating the report: " + e.getLocalizedMessage(), e );
530         }
531     }
532 
533     /**
534      * Convenience method to get the location of the specified file name.
535      *
536      * @param name the name of the file whose location is to be resolved
537      * @return a String that contains the absolute file name of the file
538      */
539     protected String getLocationTemp( String name )
540     {
541         String loc = name;
542         if ( loc.indexOf( '/' ) != -1 )
543         {
544             loc = loc.substring( loc.lastIndexOf( '/' ) + 1 );
545         }
546         if ( loc.indexOf( '\\' ) != -1 )
547         {
548             loc = loc.substring( loc.lastIndexOf( '\\' ) + 1 );
549         }
550 
551         // MPMD-127 in the case that the rules are defined externally on a url
552         // we need to replace some special url characters that cannot be
553         // used in filenames on disk or produce ackward filenames.
554         // replace all occurrences of the following characters: ? : & = %
555         loc = loc.replaceAll( "[\\?\\:\\&\\=\\%]", "_" );
556 
557         if ( !loc.endsWith( ".xml" ) )
558         {
559             loc = loc + ".xml";
560         }
561 
562         getLog().debug( "Before: " + name + " After: " + loc );
563         return loc;
564     }
565 
566     private String determineAuxClasspath() throws MavenReportException
567     {
568         try
569         {
570             List<String> classpath = new ArrayList<>();
571             if ( isAggregator() )
572             {
573                 List<String> dependencies = new ArrayList<>();
574 
575                 // collect exclusions for projects within the reactor
576                 // if module a depends on module b and both are in the reactor
577                 // then we don't want to resolve the dependency as an artifact.
578                 List<String> exclusionPatterns = new ArrayList<>();
579                 for ( MavenProject localProject : getAggregatedProjects() )
580                 {
581                     exclusionPatterns.add( localProject.getGroupId() + ":" + localProject.getArtifactId() );
582                 }
583                 TransformableFilter filter = new AndFilter( Arrays.asList(
584                         new ExclusionsFilter( exclusionPatterns ),
585                         includeTests ? ScopeFilter.including( "compile", "provided", "test" )
586                                      : ScopeFilter.including( "compile", "provided" )
587                 ) );
588 
589                 for ( MavenProject localProject : getAggregatedProjects() )
590                 {
591                     ProjectBuildingRequest buildingRequest = new DefaultProjectBuildingRequest(
592                             session.getProjectBuildingRequest() );
593 
594                     Iterable<ArtifactResult> resolvedDependencies = dependencyResolver.resolveDependencies(
595                             buildingRequest, localProject.getDependencies(), null, filter );
596 
597                     for ( ArtifactResult resolvedArtifact : resolvedDependencies )
598                     {
599                         dependencies.add( resolvedArtifact.getArtifact().getFile().toString() );
600                     }
601 
602                     List<String> projectClasspath = includeTests ? localProject.getTestClasspathElements()
603                             : localProject.getCompileClasspathElements();
604 
605                     // Add the project's target folder first
606                     classpath.addAll( projectClasspath );
607                     if ( !localProject.isExecutionRoot() )
608                     {
609                         for ( String path : projectClasspath )
610                         {
611                             File pathFile = new File( path );
612                             String[] children = pathFile.list();
613 
614                             if ( !pathFile.exists() || ( children != null && children.length == 0 ) )
615                             {
616                                 getLog().warn( "The project " + localProject.getArtifactId()
617                                     + " does not seem to be compiled. PMD results might be inaccurate." );
618                             }
619                         }
620                     }
621 
622                 }
623 
624                 // Add the dependencies as last entries
625                 classpath.addAll( dependencies );
626 
627                 getLog().debug( "Using aggregated aux classpath: " + classpath );
628             }
629             else
630             {
631                 classpath.addAll( includeTests ? project.getTestClasspathElements()
632                                                : project.getCompileClasspathElements() );
633 
634                 getLog().debug( "Using aux classpath: " + classpath );
635             }
636             String path = StringUtils.join( classpath.iterator(), File.pathSeparator );
637             return path;
638         }
639         catch ( Exception e )
640         {
641             throw new MavenReportException( e.getMessage(), e );
642         }
643     }
644 
645     /**
646      * {@inheritDoc}
647      */
648     @Override
649     public String getOutputName()
650     {
651         return "pmd";
652     }
653 
654     private static ResourceBundle getBundle( Locale locale )
655     {
656         return ResourceBundle.getBundle( "pmd-report", locale, PmdReport.class.getClassLoader() );
657     }
658 
659     /**
660      * Create and return the correct renderer for the output type.
661      *
662      * @return the renderer based on the configured output
663      * @throws org.apache.maven.reporting.MavenReportException if no renderer found for the output type
664      * @deprecated Use {@link PmdExecutor#createRenderer(String, String)} instead.
665      */
666     @Deprecated
667     public final Renderer createRenderer() throws MavenReportException
668     {
669         return PmdExecutor.createRenderer( format, getOutputEncoding() );
670     }
671 }