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