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