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.FileNotFoundException;
24  import java.io.FileOutputStream;
25  import java.io.IOException;
26  import java.io.OutputStreamWriter;
27  import java.io.PrintStream;
28  import java.io.Writer;
29  import java.util.ArrayList;
30  import java.util.Arrays;
31  import java.util.Iterator;
32  import java.util.List;
33  import java.util.Locale;
34  import java.util.Properties;
35  import java.util.ResourceBundle;
36  
37  import org.apache.maven.doxia.sink.Sink;
38  import org.apache.maven.plugin.MojoExecutionException;
39  import org.apache.maven.plugins.annotations.Component;
40  import org.apache.maven.plugins.annotations.Mojo;
41  import org.apache.maven.plugins.annotations.Parameter;
42  import org.apache.maven.plugins.annotations.ResolutionScope;
43  import org.apache.maven.reporting.MavenReportException;
44  import org.codehaus.plexus.resource.ResourceManager;
45  import org.codehaus.plexus.resource.loader.FileResourceCreationException;
46  import org.codehaus.plexus.resource.loader.FileResourceLoader;
47  import org.codehaus.plexus.resource.loader.ResourceNotFoundException;
48  import org.codehaus.plexus.util.FileUtils;
49  import org.codehaus.plexus.util.ReaderFactory;
50  import org.codehaus.plexus.util.StringUtils;
51  
52  import net.sourceforge.pmd.PMD;
53  import net.sourceforge.pmd.PMDConfiguration;
54  import net.sourceforge.pmd.Report;
55  import net.sourceforge.pmd.RuleContext;
56  import net.sourceforge.pmd.RulePriority;
57  import net.sourceforge.pmd.RuleSetFactory;
58  import net.sourceforge.pmd.RuleSetReferenceId;
59  import net.sourceforge.pmd.RuleViolation;
60  import net.sourceforge.pmd.benchmark.Benchmarker;
61  import net.sourceforge.pmd.benchmark.TextReport;
62  import net.sourceforge.pmd.lang.LanguageRegistry;
63  import net.sourceforge.pmd.lang.LanguageVersion;
64  import net.sourceforge.pmd.renderers.CSVRenderer;
65  import net.sourceforge.pmd.renderers.HTMLRenderer;
66  import net.sourceforge.pmd.renderers.Renderer;
67  import net.sourceforge.pmd.renderers.TextRenderer;
68  import net.sourceforge.pmd.renderers.XMLRenderer;
69  import net.sourceforge.pmd.util.ResourceLoader;
70  import net.sourceforge.pmd.util.datasource.DataSource;
71  import net.sourceforge.pmd.util.datasource.FileDataSource;
72  
73  /**
74   * Creates a PMD report.
75   *
76   * @author Brett Porter
77   * @version $Id$
78   * @since 2.0
79   */
80  @Mojo( name = "pmd", threadSafe = true, requiresDependencyResolution = ResolutionScope.TEST )
81  public class PmdReport
82      extends AbstractPmdReport
83  {
84      /**
85       * The target JDK to analyze based on. Should match the source used in the compiler plugin. Valid values are
86       * currently <code>1.3</code>, <code>1.4</code>, <code>1.5</code>, <code>1.6</code>, <code>1.7</code>,
87       * <code>1.8</code> and <code>9</code>.
88       * <p>
89       *   <b>Note:</b> this parameter is only used if the language parameter is set to <code>java</code>.
90       * </p>
91       */
92      @Parameter( property = "targetJdk", defaultValue = "${maven.compiler.source}" )
93      private String targetJdk;
94  
95      /**
96       * The programming language to be analyzed by PMD. Valid values are currently <code>java</code>,
97       * <code>javascript</code> and <code>jsp</code>.
98       *
99       * @since 3.0
100      */
101     @Parameter( defaultValue = "java" )
102     private String language;
103 
104     /**
105      * The rule priority threshold; rules with lower priority than this will not be evaluated.
106      *
107      * @since 2.1
108      */
109     @Parameter( property = "minimumPriority", defaultValue = "5" )
110     private int minimumPriority = 5;
111 
112     /**
113      * Skip the PMD report generation. Most useful on the command line via "-Dpmd.skip=true".
114      *
115      * @since 2.1
116      */
117     @Parameter( property = "pmd.skip", defaultValue = "false" )
118     private boolean skip;
119 
120     /**
121      * The PMD rulesets to use. See the
122      * <a href="https://pmd.github.io/latest/pmd_rules_java.html">Stock Java Rulesets</a> for a
123      * list of available rules.
124      * Defaults to a custom ruleset provided by this maven plugin
125      * (<code>/rulesets/java/maven-pmd-plugin-default.xml</code>).
126      */
127     @Parameter
128     private String[] rulesets = new String[] { "/rulesets/java/maven-pmd-plugin-default.xml" };
129 
130     /**
131      * Controls whether the project's compile/test classpath should be passed to PMD to enable its type resolution
132      * feature.
133      *
134      * @since 3.0
135      */
136     @Parameter( property = "pmd.typeResolution", defaultValue = "true" )
137     private boolean typeResolution;
138 
139     /**
140      * Controls whether PMD will track benchmark information.
141      *
142      * @since 3.1
143      */
144     @Parameter( property = "pmd.benchmark", defaultValue = "false" )
145     private boolean benchmark;
146 
147     /**
148      * Benchmark output filename.
149      *
150      * @since 3.1
151      */
152     @Parameter( property = "pmd.benchmarkOutputFilename",
153                     defaultValue = "${project.build.directory}/pmd-benchmark.txt" )
154     private String benchmarkOutputFilename;
155 
156     /**
157      * Source level marker used to indicate whether a RuleViolation should be suppressed. If it is not set, PMD's
158      * default will be used, which is <code>NOPMD</code>. See also <a
159      * href="https://pmd.github.io/latest/usage/suppressing.html">PMD &#x2013; Suppressing warnings</a>.
160      *
161      * @since 3.4
162      */
163     @Parameter( property = "pmd.suppressMarker" )
164     private String suppressMarker;
165 
166     /**
167      */
168     @Component
169     private ResourceManager locator;
170 
171     /** The PMD renderer for collecting violations. */
172     private PmdCollectingRenderer renderer;
173 
174     /** Helper to exclude violations given as a properties file. */
175     private final ExcludeViolationsFromFile excludeFromFile = new ExcludeViolationsFromFile();
176 
177     /**
178      * per default pmd executions error are ignored to not break the whole
179      *
180      * @since 3.1
181      */
182     @Parameter( property = "pmd.skipPmdError", defaultValue = "true" )
183     private boolean skipPmdError;
184 
185     /**
186      * Enables the analysis cache, which speeds up PMD. This
187      * requires a cache file, that contains the results of the last
188      * PMD run. Thus the cache is only effective, if this file is
189      * not cleaned between runs.
190      *
191      * @since 3.8
192      */
193     @Parameter( property = "pmd.analysisCache", defaultValue = "false" )
194     private boolean analysisCache;
195 
196     /**
197      * The location of the analysis cache, if it is enabled.
198      * This file contains the results of the last PMD run and must not be cleaned
199      * between consecutive PMD runs. Otherwise the cache is not in use.
200      * If the file doesn't exist, PMD executes as if there is no cache enabled and
201      * all files are analyzed. Otherwise only changed files will be analyzed again.
202      *
203      * @since 3.8
204      */
205     @Parameter( property = "pmd.analysisCacheLocation", defaultValue = "${project.build.directory}/pmd/pmd.cache" )
206     private String analysisCacheLocation;
207 
208     /**
209      * Also render processing errors into the HTML report.
210      * Processing errors are problems, that PMD encountered while executing the rules.
211      * It can be parsing errors or exceptions during rule execution.
212      * Processing errors indicate a bug in PMD and the information provided help in
213      * reporting and fixing bugs in PMD.
214      *
215      * @since 3.9.0
216      */
217     @Parameter( property = "pmd.renderProcessingErrors", defaultValue = "true" )
218     private boolean renderProcessingErrors = true;
219 
220     /**
221      * {@inheritDoc}
222      */
223     public String getName( Locale locale )
224     {
225         return getBundle( locale ).getString( "report.pmd.name" );
226     }
227 
228     /**
229      * {@inheritDoc}
230      */
231     public String getDescription( Locale locale )
232     {
233         return getBundle( locale ).getString( "report.pmd.description" );
234     }
235 
236     /**
237      * Configures the PMD rulesets to be used directly.
238      * Note: Usually the rulesets are configured via the property.
239      *
240      * @param rulesets the PMD rulesets to be used.
241      * @see #rulesets
242      */
243     public void setRulesets( String[] rulesets )
244     {
245         this.rulesets = Arrays.copyOf( rulesets, rulesets.length );
246     }
247 
248     /**
249      * {@inheritDoc}
250      */
251     @Override
252     public void executeReport( Locale locale )
253         throws MavenReportException
254     {
255         try
256         {
257             execute( locale );
258         }
259         finally
260         {
261             if ( getSink() != null )
262             {
263                 getSink().close();
264             }
265         }
266     }
267 
268     private void execute( Locale locale )
269         throws MavenReportException
270     {
271         if ( !skip && canGenerateReport() )
272         {
273             ClassLoader origLoader = Thread.currentThread().getContextClassLoader();
274             try
275             {
276                 Thread.currentThread().setContextClassLoader( this.getClass().getClassLoader() );
277 
278                 Report report = generateReport( locale );
279 
280                 if ( !isHtml() && !isXml() )
281                 {
282                     writeNonHtml( report );
283                 }
284             }
285             finally
286             {
287                 Thread.currentThread().setContextClassLoader( origLoader );
288             }
289         }
290     }
291 
292     @Override
293     public boolean canGenerateReport()
294     {
295         if ( skip )
296         {
297             return false;
298         }
299 
300         boolean result = super.canGenerateReport();
301         if ( result )
302         {
303             try
304             {
305                 executePmdWithClassloader();
306                 if ( skipEmptyReport )
307                 {
308                     result = renderer.hasViolations();
309                     if ( result )
310                     {
311                         getLog().debug( "Skipping report since skipEmptyReport is true and"
312                                             + "there are no PMD violations." );
313                     }
314                 }
315             }
316             catch ( MavenReportException e )
317             {
318                 throw new RuntimeException( e );
319             }
320         }
321         return result;
322     }
323 
324     private void executePmdWithClassloader()
325         throws MavenReportException
326     {
327         ClassLoader origLoader = Thread.currentThread().getContextClassLoader();
328         try
329         {
330             Thread.currentThread().setContextClassLoader( this.getClass().getClassLoader() );
331             executePmd();
332         }
333         finally
334         {
335             Thread.currentThread().setContextClassLoader( origLoader );
336         }
337     }
338 
339     private void executePmd()
340         throws MavenReportException
341     {
342         setupPmdLogging();
343 
344         if ( renderer != null )
345         {
346             // PMD has already been run
347             getLog().debug( "PMD has already been run - skipping redundant execution." );
348             return;
349         }
350 
351         try
352         {
353             excludeFromFile.loadExcludeFromFailuresData( excludeFromFailureFile );
354         }
355         catch ( MojoExecutionException e )
356         {
357             throw new MavenReportException( "Unable to load exclusions", e );
358         }
359 
360         // configure ResourceManager
361         locator.addSearchPath( FileResourceLoader.ID, project.getFile().getParentFile().getAbsolutePath() );
362         locator.addSearchPath( "url", "" );
363         locator.setOutputDirectory( targetDirectory );
364 
365         renderer = new PmdCollectingRenderer();
366         PMDConfiguration pmdConfiguration = getPMDConfiguration();
367 
368         String[] sets = new String[rulesets.length];
369         try
370         {
371             for ( int idx = 0; idx < rulesets.length; idx++ )
372             {
373                 String set = rulesets[idx];
374                 getLog().debug( "Preparing ruleset: " + set );
375                 RuleSetReferenceId id = new RuleSetReferenceId( set );
376                 File ruleset = locator.getResourceAsFile( id.getRuleSetFileName(), getLocationTemp( set ) );
377                 if ( null == ruleset )
378                 {
379                     throw new MavenReportException( "Could not resolve " + set );
380                 }
381                 sets[idx] = ruleset.getAbsolutePath();
382             }
383         }
384         catch ( ResourceNotFoundException e )
385         {
386             throw new MavenReportException( e.getMessage(), e );
387         }
388         catch ( FileResourceCreationException e )
389         {
390             throw new MavenReportException( e.getMessage(), e );
391         }
392         pmdConfiguration.setRuleSets( StringUtils.join( sets, "," ) );
393 
394         try
395         {
396             if ( filesToProcess == null )
397             {
398                 filesToProcess = getFilesToProcess();
399             }
400 
401             if ( filesToProcess.isEmpty() && !"java".equals( language ) )
402             {
403                 getLog().warn( "No files found to process. Did you add your additional source folders like javascript?"
404                                    + " (see also build-helper-maven-plugin)" );
405             }
406         }
407         catch ( IOException e )
408         {
409             throw new MavenReportException( "Can't get file list", e );
410         }
411 
412         String encoding = getSourceEncoding();
413         if ( StringUtils.isEmpty( encoding ) )
414         {
415             encoding = ReaderFactory.FILE_ENCODING;
416             if ( !filesToProcess.isEmpty() )
417             {
418                 getLog().warn( "File encoding has not been set, using platform encoding " + ReaderFactory.FILE_ENCODING
419                                + ", i.e. build is platform dependent!" );
420             }
421         }
422         pmdConfiguration.setSourceEncoding( encoding );
423 
424         List<DataSource> dataSources = new ArrayList<>( filesToProcess.size() );
425         for ( File f : filesToProcess.keySet() )
426         {
427             dataSources.add( new FileDataSource( f ) );
428         }
429 
430         if ( sets.length > 0 )
431         {
432             processFilesWithPMD( pmdConfiguration, dataSources );
433         }
434         else
435         {
436             getLog().debug( "Skipping PMD execution as no rulesets are defined." );
437         }
438 
439         if ( renderer.hasErrors() )
440         {
441             if ( !skipPmdError )
442             {
443                 getLog().error( "PMD processing errors:" );
444                 getLog().error( renderer.getErrorsAsString( getLog().isDebugEnabled() ) );
445                 throw new MavenReportException( "Found " + renderer.getErrors().size() + " PMD processing errors" );
446             }
447             getLog().warn( "There are " + renderer.getErrors().size() + " PMD processing errors:" );
448             getLog().warn( renderer.getErrorsAsString( getLog().isDebugEnabled() ) );
449         }
450 
451         removeExcludedViolations( renderer.getViolations() );
452 
453         // if format is XML, we need to output it even if the file list is empty or we have no violations
454         // so the "check" goals can check for violations
455         if ( isXml() && renderer != null )
456         {
457             writeNonHtml( renderer.asReport() );
458         }
459 
460         if ( benchmark )
461         {
462             try ( PrintStream benchmarkFileStream = new PrintStream( benchmarkOutputFilename ) )
463             {
464                 ( new TextReport() ).generate( Benchmarker.values(), benchmarkFileStream );
465             }
466             catch ( FileNotFoundException fnfe )
467             {
468                 getLog().error( "Unable to generate benchmark file: " + benchmarkOutputFilename, fnfe );
469             }
470         }
471     }
472 
473     private void removeExcludedViolations( List<RuleViolation> violations )
474     {
475         getLog().debug( "Removing excluded violations. Using " + excludeFromFile.countExclusions()
476             + " configured exclusions." );
477         int violationsBefore = violations.size();
478 
479         Iterator<RuleViolation> iterator = violations.iterator();
480         while ( iterator.hasNext() )
481         {
482             RuleViolation rv = iterator.next();
483             if ( excludeFromFile.isExcludedFromFailure( rv ) )
484             {
485                 iterator.remove();
486             }
487         }
488 
489         int numberOfExcludedViolations = violationsBefore - violations.size();
490         getLog().debug( "Excluded " + numberOfExcludedViolations + " violations." );
491     }
492 
493     private void processFilesWithPMD( PMDConfiguration pmdConfiguration, List<DataSource> dataSources )
494             throws MavenReportException
495     {
496         RuleSetFactory ruleSetFactory = new RuleSetFactory( new ResourceLoader(),
497                 RulePriority.valueOf( this.minimumPriority ), false, true );
498         RuleContext ruleContext = new RuleContext();
499 
500         try
501         {
502             getLog().debug( "Executing PMD..." );
503             PMD.processFiles( pmdConfiguration, ruleSetFactory, dataSources, ruleContext,
504                               Arrays.<Renderer>asList( renderer ) );
505 
506             if ( getLog().isDebugEnabled() )
507             {
508                 getLog().debug( "PMD finished. Found " + renderer.getViolations().size() + " violations." );
509             }
510         }
511         catch ( Exception e )
512         {
513             String message = "Failure executing PMD: " + e.getLocalizedMessage();
514             if ( !skipPmdError )
515             {
516                 throw new MavenReportException( message, e );
517             }
518             getLog().warn( message, e );
519         }
520     }
521 
522     private Report generateReport( Locale locale )
523         throws MavenReportException
524     {
525         Sink sink = getSink();
526         PmdReportGenerator doxiaRenderer = new PmdReportGenerator( getLog(), sink, getBundle( locale ), aggregate );
527         doxiaRenderer.setFiles( filesToProcess );
528         doxiaRenderer.setViolations( renderer.getViolations() );
529         if ( renderProcessingErrors )
530         {
531             doxiaRenderer.setProcessingErrors( renderer.getErrors() );
532         }
533 
534         try
535         {
536             doxiaRenderer.beginDocument();
537             doxiaRenderer.render();
538             doxiaRenderer.endDocument();
539         }
540         catch ( IOException e )
541         {
542             getLog().warn( "Failure creating the report: " + e.getLocalizedMessage(), e );
543         }
544 
545         return renderer.asReport();
546     }
547 
548     /**
549      * Convenience method to get the location of the specified file name.
550      *
551      * @param name the name of the file whose location is to be resolved
552      * @return a String that contains the absolute file name of the file
553      */
554     protected String getLocationTemp( String name )
555     {
556         String loc = name;
557         if ( loc.indexOf( '/' ) != -1 )
558         {
559             loc = loc.substring( loc.lastIndexOf( '/' ) + 1 );
560         }
561         if ( loc.indexOf( '\\' ) != -1 )
562         {
563             loc = loc.substring( loc.lastIndexOf( '\\' ) + 1 );
564         }
565 
566         // MPMD-127 in the case that the rules are defined externally on a url
567         // we need to replace some special url characters that cannot be
568         // used in filenames on disk or produce ackward filenames.
569         // replace all occurrences of the following characters: ? : & = %
570         loc = loc.replaceAll( "[\\?\\:\\&\\=\\%]", "_" );
571 
572         if ( !loc.endsWith( ".xml" ) )
573         {
574             loc = loc + ".xml";
575         }
576 
577         getLog().debug( "Before: " + name + " After: " + loc );
578         return loc;
579     }
580 
581     /**
582      * Use the PMD renderers to render in any format aside from HTML.
583      *
584      * @param report
585      * @throws MavenReportException
586      */
587     private void writeNonHtml( Report report )
588         throws MavenReportException
589     {
590         Renderer r = createRenderer();
591 
592         if ( r == null )
593         {
594             return;
595         }
596 
597         File targetFile = new File( targetDirectory, "pmd." + format );
598         try ( Writer writer = new OutputStreamWriter( new FileOutputStream( targetFile ), getOutputEncoding() ) )
599         {
600             targetDirectory.mkdirs();
601 
602             r.setWriter( writer );
603             r.start();
604             r.renderFileReport( report );
605             r.end();
606             r.flush();
607 
608             if ( includeXmlInSite )
609             {
610                 File siteDir = getReportOutputDirectory();
611                 siteDir.mkdirs();
612                 FileUtils.copyFile( targetFile, new File( siteDir, "pmd." + format ) );
613             }
614         }
615         catch ( IOException ioe )
616         {
617             throw new MavenReportException( ioe.getMessage(), ioe );
618         }
619     }
620 
621     /**
622      * Constructs the PMD configuration class, passing it an argument that configures the target JDK.
623      *
624      * @return the resulting PMD
625      * @throws org.apache.maven.reporting.MavenReportException if targetJdk is not supported
626      */
627     public PMDConfiguration getPMDConfiguration()
628         throws MavenReportException
629     {
630         PMDConfiguration configuration = new PMDConfiguration();
631         LanguageVersion languageVersion = null;
632 
633         if ( ( "java".equals( language ) || null == language ) && null != targetJdk )
634         {
635             languageVersion = LanguageRegistry.findLanguageVersionByTerseName( "java " + targetJdk );
636             if ( languageVersion == null )
637             {
638                 throw new MavenReportException( "Unsupported targetJdk value '" + targetJdk + "'." );
639             }
640         }
641         else if ( "javascript".equals( language ) || "ecmascript".equals( language ) )
642         {
643             languageVersion = LanguageRegistry.findLanguageVersionByTerseName( "ecmascript" );
644         }
645         else if ( "jsp".equals( language ) )
646         {
647             languageVersion = LanguageRegistry.findLanguageVersionByTerseName( "jsp" );
648         }
649 
650         if ( languageVersion != null )
651         {
652             getLog().debug( "Using language " + languageVersion );
653             configuration.setDefaultLanguageVersion( languageVersion );
654         }
655 
656         if ( typeResolution )
657         {
658             try
659             {
660                 List<String> classpath =
661                     includeTests ? project.getTestClasspathElements() : project.getCompileClasspathElements();
662                 getLog().debug( "Using aux classpath: " + classpath );
663                 configuration.prependClasspath( StringUtils.join( classpath.iterator(), File.pathSeparator ) );
664             }
665             catch ( Exception e )
666             {
667                 throw new MavenReportException( e.getMessage(), e );
668             }
669         }
670 
671         if ( null != suppressMarker )
672         {
673             configuration.setSuppressMarker( suppressMarker );
674         }
675 
676         configuration.setBenchmark( benchmark );
677 
678         if ( analysisCache )
679         {
680             configuration.setAnalysisCacheLocation( analysisCacheLocation );
681             getLog().debug( "Using analysis cache location: " + analysisCacheLocation );
682         }
683 
684         return configuration;
685     }
686 
687     /**
688      * {@inheritDoc}
689      */
690     public String getOutputName()
691     {
692         return "pmd";
693     }
694 
695     private static ResourceBundle getBundle( Locale locale )
696     {
697         return ResourceBundle.getBundle( "pmd-report", locale, PmdReport.class.getClassLoader() );
698     }
699 
700     /**
701      * Create and return the correct renderer for the output type.
702      *
703      * @return the renderer based on the configured output
704      * @throws org.apache.maven.reporting.MavenReportException if no renderer found for the output type
705      */
706     public final Renderer createRenderer()
707         throws MavenReportException
708     {
709         Renderer result = null;
710         if ( "xml".equals( format ) )
711         {
712             result = new XMLRenderer( getOutputEncoding() );
713         }
714         else if ( "txt".equals( format ) )
715         {
716             result = new TextRenderer();
717         }
718         else if ( "csv".equals( format ) )
719         {
720             result = new CSVRenderer();
721         }
722         else if ( "html".equals( format ) )
723         {
724             result = new HTMLRenderer();
725         }
726         else if ( !"".equals( format ) && !"none".equals( format ) )
727         {
728             try
729             {
730                 result = (Renderer) Class.forName( format ).getConstructor( Properties.class ).
731                                 newInstance( new Properties() );
732             }
733             catch ( Exception e )
734             {
735                 throw new MavenReportException( "Can't find PMD custom format " + format + ": "
736                     + e.getClass().getName(), e );
737             }
738         }
739 
740         return result;
741     }
742 }