| package org.apache.maven.plugins.pmd; |
| |
| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.ResourceBundle; |
| |
| import org.apache.maven.doxia.sink.Sink; |
| import org.apache.maven.plugins.annotations.Component; |
| import org.apache.maven.plugins.annotations.Mojo; |
| import org.apache.maven.plugins.annotations.Parameter; |
| import org.apache.maven.plugins.annotations.ResolutionScope; |
| import org.apache.maven.plugins.pmd.exec.PmdExecutor; |
| import org.apache.maven.plugins.pmd.exec.PmdRequest; |
| import org.apache.maven.plugins.pmd.exec.PmdResult; |
| import org.apache.maven.project.DefaultProjectBuildingRequest; |
| import org.apache.maven.project.MavenProject; |
| import org.apache.maven.project.ProjectBuildingRequest; |
| import org.apache.maven.reporting.MavenReportException; |
| import org.apache.maven.shared.artifact.filter.resolve.AndFilter; |
| import org.apache.maven.shared.artifact.filter.resolve.ExclusionsFilter; |
| import org.apache.maven.shared.artifact.filter.resolve.ScopeFilter; |
| import org.apache.maven.shared.artifact.filter.resolve.TransformableFilter; |
| import org.apache.maven.shared.transfer.artifact.resolve.ArtifactResult; |
| import org.apache.maven.shared.transfer.dependencies.resolve.DependencyResolver; |
| import org.apache.maven.shared.utils.logging.MessageUtils; |
| import org.apache.maven.toolchain.Toolchain; |
| import org.codehaus.plexus.resource.ResourceManager; |
| import org.codehaus.plexus.resource.loader.FileResourceCreationException; |
| import org.codehaus.plexus.resource.loader.FileResourceLoader; |
| import org.codehaus.plexus.resource.loader.ResourceNotFoundException; |
| import org.codehaus.plexus.util.ReaderFactory; |
| import org.codehaus.plexus.util.StringUtils; |
| |
| import net.sourceforge.pmd.renderers.Renderer; |
| |
| /** |
| * Creates a PMD report. |
| * |
| * @author Brett Porter |
| * @version $Id$ |
| * @since 2.0 |
| */ |
| @Mojo( name = "pmd", threadSafe = true, requiresDependencyResolution = ResolutionScope.TEST ) |
| public class PmdReport |
| extends AbstractPmdReport |
| { |
| /** |
| * The target JDK to analyze based on. Should match the source used in the compiler plugin. Valid values |
| * with the default PMD version are |
| * currently <code>1.3</code>, <code>1.4</code>, <code>1.5</code>, <code>1.6</code>, <code>1.7</code>, |
| * <code>1.8</code>, <code>9</code>, <code>10</code>, <code>11</code>, <code>12</code>, <code>13</code>, |
| * <code>14</code>, <code>15</code>, and <code>16</code>. |
| * |
| * <p> You can override the default PMD version by specifying PMD as a dependency, |
| * see <a href="examples/upgrading-PMD-at-runtime.html">Upgrading PMD at Runtime</a>.</p> |
| * |
| * <p> |
| * <b>Note:</b> this parameter is only used if the language parameter is set to <code>java</code>. |
| * </p> |
| */ |
| @Parameter( property = "targetJdk", defaultValue = "${maven.compiler.source}" ) |
| private String targetJdk; |
| |
| /** |
| * The programming language to be analyzed by PMD. Valid values are currently <code>java</code>, |
| * <code>javascript</code> and <code>jsp</code>. |
| * |
| * @since 3.0 |
| */ |
| @Parameter( defaultValue = "java" ) |
| private String language; |
| |
| /** |
| * The rule priority threshold; rules with lower priority than this will not be evaluated. |
| * |
| * @since 2.1 |
| */ |
| @Parameter( property = "minimumPriority", defaultValue = "5" ) |
| private int minimumPriority = 5; |
| |
| /** |
| * Skip the PMD report generation. Most useful on the command line via "-Dpmd.skip=true". |
| * |
| * @since 2.1 |
| */ |
| @Parameter( property = "pmd.skip", defaultValue = "false" ) |
| private boolean skip; |
| |
| /** |
| * The PMD rulesets to use. See the |
| * <a href="https://pmd.github.io/latest/pmd_rules_java.html">Stock Java Rulesets</a> for a |
| * list of available rules. |
| * Defaults to a custom ruleset provided by this maven plugin |
| * (<code>/rulesets/java/maven-pmd-plugin-default.xml</code>). |
| */ |
| @Parameter |
| String[] rulesets = new String[] { "/rulesets/java/maven-pmd-plugin-default.xml" }; |
| |
| /** |
| * Controls whether the project's compile/test classpath should be passed to PMD to enable its type resolution |
| * feature. |
| * |
| * @since 3.0 |
| */ |
| @Parameter( property = "pmd.typeResolution", defaultValue = "true" ) |
| private boolean typeResolution; |
| |
| /** |
| * Controls whether PMD will track benchmark information. |
| * |
| * @since 3.1 |
| */ |
| @Parameter( property = "pmd.benchmark", defaultValue = "false" ) |
| private boolean benchmark; |
| |
| /** |
| * Benchmark output filename. |
| * |
| * @since 3.1 |
| */ |
| @Parameter( property = "pmd.benchmarkOutputFilename", |
| defaultValue = "${project.build.directory}/pmd-benchmark.txt" ) |
| private String benchmarkOutputFilename; |
| |
| /** |
| * Source level marker used to indicate whether a RuleViolation should be suppressed. If it is not set, PMD's |
| * default will be used, which is <code>NOPMD</code>. See also <a |
| * href="https://pmd.github.io/latest/pmd_userdocs_suppressing_warnings.html">PMD – Suppressing warnings</a>. |
| * |
| * @since 3.4 |
| */ |
| @Parameter( property = "pmd.suppressMarker" ) |
| private String suppressMarker; |
| |
| /** |
| * per default pmd executions error are ignored to not break the whole |
| * |
| * @since 3.1 |
| */ |
| @Parameter( property = "pmd.skipPmdError", defaultValue = "true" ) |
| private boolean skipPmdError; |
| |
| /** |
| * Enables the analysis cache, which speeds up PMD. This |
| * requires a cache file, that contains the results of the last |
| * PMD run. Thus the cache is only effective, if this file is |
| * not cleaned between runs. |
| * |
| * @since 3.8 |
| */ |
| @Parameter( property = "pmd.analysisCache", defaultValue = "false" ) |
| private boolean analysisCache; |
| |
| /** |
| * The location of the analysis cache, if it is enabled. |
| * This file contains the results of the last PMD run and must not be cleaned |
| * between consecutive PMD runs. Otherwise the cache is not in use. |
| * If the file doesn't exist, PMD executes as if there is no cache enabled and |
| * all files are analyzed. Otherwise only changed files will be analyzed again. |
| * |
| * @since 3.8 |
| */ |
| @Parameter( property = "pmd.analysisCacheLocation", defaultValue = "${project.build.directory}/pmd/pmd.cache" ) |
| private String analysisCacheLocation; |
| |
| /** |
| * Also render processing errors into the HTML report. |
| * Processing errors are problems, that PMD encountered while executing the rules. |
| * It can be parsing errors or exceptions during rule execution. |
| * Processing errors indicate a bug in PMD and the information provided help in |
| * reporting and fixing bugs in PMD. |
| * |
| * @since 3.9.0 |
| */ |
| @Parameter( property = "pmd.renderProcessingErrors", defaultValue = "true" ) |
| private boolean renderProcessingErrors = true; |
| |
| /** |
| * Also render the rule priority into the HTML report. |
| * |
| * @since 3.10.0 |
| */ |
| @Parameter( property = "pmd.renderRuleViolationPriority", defaultValue = "true" ) |
| private boolean renderRuleViolationPriority = true; |
| |
| /** |
| * Add a section in the HTML report, that groups the found violations by rule priority |
| * in addition to grouping by file. |
| * |
| * @since 3.12.0 |
| */ |
| @Parameter( property = "pmd.renderViolationsByPriority", defaultValue = "true" ) |
| private boolean renderViolationsByPriority = true; |
| |
| /** |
| * Before PMD is executed, the configured rulesets are resolved and copied into this directory. |
| * <p>Note: Before 3.13.0, this was by default ${project.build.directory}. |
| * |
| * @since 3.13.0 |
| */ |
| @Parameter( property = "pmd.rulesetsTargetDirectory", defaultValue = "${project.build.directory}/pmd/rulesets" ) |
| private File rulesetsTargetDirectory; |
| |
| /** |
| * Used to locate configured rulesets. The rulesets could be on the plugin |
| * classpath or in the local project file system. |
| */ |
| @Component |
| private ResourceManager locator; |
| |
| @Component |
| private DependencyResolver dependencyResolver; |
| |
| /** |
| * Contains the result of the last PMD execution. |
| * It might be <code>null</code> which means, that PMD |
| * has not been executed yet. |
| */ |
| private PmdResult pmdResult; |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public String getName( Locale locale ) |
| { |
| return getBundle( locale ).getString( "report.pmd.name" ); |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public String getDescription( Locale locale ) |
| { |
| return getBundle( locale ).getString( "report.pmd.description" ); |
| } |
| |
| /** |
| * Configures the PMD rulesets to be used directly. |
| * Note: Usually the rulesets are configured via the property. |
| * |
| * @param rulesets the PMD rulesets to be used. |
| * @see #rulesets |
| */ |
| public void setRulesets( String[] rulesets ) |
| { |
| this.rulesets = Arrays.copyOf( rulesets, rulesets.length ); |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public void executeReport( Locale locale ) |
| throws MavenReportException |
| { |
| try |
| { |
| execute( locale ); |
| } |
| finally |
| { |
| if ( getSink() != null ) |
| { |
| getSink().close(); |
| } |
| } |
| } |
| |
| private void execute( Locale locale ) |
| throws MavenReportException |
| { |
| if ( !skip && canGenerateReport() ) |
| { |
| ClassLoader origLoader = Thread.currentThread().getContextClassLoader(); |
| try |
| { |
| Thread.currentThread().setContextClassLoader( this.getClass().getClassLoader() ); |
| |
| generateMavenSiteReport( locale ); |
| } |
| finally |
| { |
| Thread.currentThread().setContextClassLoader( origLoader ); |
| } |
| } |
| } |
| |
| @Override |
| public boolean canGenerateReport() |
| { |
| if ( skip ) |
| { |
| return false; |
| } |
| |
| boolean result = super.canGenerateReport(); |
| if ( result ) |
| { |
| try |
| { |
| executePmd(); |
| if ( skipEmptyReport ) |
| { |
| result = pmdResult.hasViolations(); |
| if ( result ) |
| { |
| getLog().debug( "Skipping report since skipEmptyReport is true and " |
| + "there are no PMD violations." ); |
| } |
| } |
| } |
| catch ( MavenReportException e ) |
| { |
| throw new RuntimeException( e ); |
| } |
| } |
| return result; |
| } |
| |
| private void executePmd() |
| throws MavenReportException |
| { |
| if ( pmdResult != null ) |
| { |
| // PMD has already been run |
| getLog().debug( "PMD has already been run - skipping redundant execution." ); |
| return; |
| } |
| |
| try |
| { |
| filesToProcess = getFilesToProcess(); |
| |
| if ( filesToProcess.isEmpty() && !"java".equals( language ) ) |
| { |
| getLog().warn( "No files found to process. Did you add your additional source folders like javascript?" |
| + " (see also build-helper-maven-plugin)" ); |
| } |
| } |
| catch ( IOException e ) |
| { |
| throw new MavenReportException( "Can't get file list", e ); |
| } |
| |
| |
| PmdRequest request = new PmdRequest(); |
| request.setLanguageAndVersion( language, targetJdk ); |
| request.setRulesets( resolveRulesets() ); |
| request.setAuxClasspath( typeResolution ? determineAuxClasspath() : null ); |
| request.setSourceEncoding( getSourceEncoding() ); |
| request.addFiles( filesToProcess.keySet() ); |
| request.setMinimumPriority( minimumPriority ); |
| request.setSuppressMarker( suppressMarker ); |
| request.setBenchmarkOutputLocation( benchmark ? benchmarkOutputFilename : null ); |
| request.setAnalysisCacheLocation( analysisCache ? analysisCacheLocation : null ); |
| request.setExcludeFromFailureFile( excludeFromFailureFile ); |
| |
| request.setTargetDirectory( targetDirectory.getAbsolutePath() ); |
| request.setOutputEncoding( getOutputEncoding() ); |
| request.setFormat( format ); |
| request.setShowPmdLog( showPmdLog ); |
| request.setColorizedLog( MessageUtils.isColorEnabled() ); |
| request.setSkipPmdError( skipPmdError ); |
| request.setIncludeXmlInSite( includeXmlInSite ); |
| request.setReportOutputDirectory( getReportOutputDirectory().getAbsolutePath() ); |
| request.setLogLevel( determineCurrentRootLogLevel() ); |
| |
| Toolchain tc = getToolchain(); |
| if ( tc != null ) |
| { |
| getLog().info( "Toolchain in maven-pmd-plugin: " + tc ); |
| String javaExecutable = tc.findTool( "java" ); //NOI18N |
| request.setJavaExecutable( javaExecutable ); |
| } |
| |
| pmdResult = PmdExecutor.execute( request ); |
| } |
| |
| protected String getSourceEncoding() |
| { |
| String encoding = super.getSourceEncoding(); |
| if ( StringUtils.isEmpty( encoding ) ) |
| { |
| encoding = ReaderFactory.FILE_ENCODING; |
| if ( !filesToProcess.isEmpty() ) |
| { |
| getLog().warn( "File encoding has not been set, using platform encoding " + ReaderFactory.FILE_ENCODING |
| + ", i.e. build is platform dependent!" ); |
| } |
| } |
| return encoding; |
| } |
| |
| |
| /** |
| * Resolves the configured rulesets and copies them as files into the {@link #rulesetsTargetDirectory}. |
| * |
| * @return comma separated list of absolute file paths of ruleset files |
| * @throws MavenReportException if a ruleset could not be found |
| */ |
| private String resolveRulesets() throws MavenReportException |
| { |
| // configure ResourceManager |
| locator.addSearchPath( FileResourceLoader.ID, project.getFile().getParentFile().getAbsolutePath() ); |
| locator.addSearchPath( "url", "" ); |
| locator.setOutputDirectory( rulesetsTargetDirectory ); |
| |
| String[] sets = new String[rulesets.length]; |
| try |
| { |
| for ( int idx = 0; idx < rulesets.length; idx++ ) |
| { |
| String set = rulesets[idx]; |
| getLog().debug( "Preparing ruleset: " + set ); |
| String rulesetFilename = determineRulesetFilename( set ); |
| File ruleset = locator.getResourceAsFile( rulesetFilename, getLocationTemp( set ) ); |
| if ( null == ruleset ) |
| { |
| throw new MavenReportException( "Could not resolve " + set ); |
| } |
| sets[idx] = ruleset.getAbsolutePath(); |
| } |
| } |
| catch ( ResourceNotFoundException | FileResourceCreationException e ) |
| { |
| throw new MavenReportException( e.getMessage(), e ); |
| } |
| return StringUtils.join( sets, "," ); |
| } |
| |
| private String determineRulesetFilename( String ruleset ) |
| { |
| String result = ruleset.trim(); |
| String lowercase = result.toLowerCase( Locale.ROOT ); |
| if ( lowercase.endsWith( ".xml" ) ) |
| { |
| return result; |
| } |
| |
| // assume last part is a single rule, e.g. myruleset.xml/SingleRule |
| if ( result.indexOf( '/' ) > -1 ) |
| { |
| String rulesetFilename = result.substring( 0, result.lastIndexOf( '/' ) ); |
| if ( rulesetFilename.toLowerCase( Locale.ROOT ).endsWith( ".xml" ) ) |
| { |
| return rulesetFilename; |
| } |
| } |
| // maybe a built-in ruleset name, e.g. java-design -> rulesets/java/design.xml |
| int dashIndex = lowercase.indexOf( '-' ); |
| if ( dashIndex > -1 && lowercase.indexOf( '-', dashIndex + 1 ) == -1 ) |
| { |
| String language = result.substring( 0, dashIndex ); |
| String rulesetName = result.substring( dashIndex + 1 ); |
| return "rulesets/" + language + "/" + rulesetName + ".xml"; |
| } |
| // fallback - no change of the given ruleset specifier |
| return result; |
| } |
| |
| private void generateMavenSiteReport( Locale locale ) |
| throws MavenReportException |
| { |
| Sink sink = getSink(); |
| PmdReportGenerator doxiaRenderer = new PmdReportGenerator( getLog(), sink, getBundle( locale ), aggregate ); |
| doxiaRenderer.setRenderRuleViolationPriority( renderRuleViolationPriority ); |
| doxiaRenderer.setRenderViolationsByPriority( renderViolationsByPriority ); |
| doxiaRenderer.setFiles( filesToProcess ); |
| doxiaRenderer.setViolations( pmdResult.getViolations() ); |
| if ( renderProcessingErrors ) |
| { |
| doxiaRenderer.setProcessingErrors( pmdResult.getErrors() ); |
| } |
| |
| try |
| { |
| doxiaRenderer.beginDocument(); |
| doxiaRenderer.render(); |
| doxiaRenderer.endDocument(); |
| } |
| catch ( IOException e ) |
| { |
| getLog().warn( "Failure creating the report: " + e.getLocalizedMessage(), e ); |
| } |
| } |
| |
| /** |
| * Convenience method to get the location of the specified file name. |
| * |
| * @param name the name of the file whose location is to be resolved |
| * @return a String that contains the absolute file name of the file |
| */ |
| protected String getLocationTemp( String name ) |
| { |
| String loc = name; |
| if ( loc.indexOf( '/' ) != -1 ) |
| { |
| loc = loc.substring( loc.lastIndexOf( '/' ) + 1 ); |
| } |
| if ( loc.indexOf( '\\' ) != -1 ) |
| { |
| loc = loc.substring( loc.lastIndexOf( '\\' ) + 1 ); |
| } |
| |
| // MPMD-127 in the case that the rules are defined externally on a url |
| // we need to replace some special url characters that cannot be |
| // used in filenames on disk or produce ackward filenames. |
| // replace all occurrences of the following characters: ? : & = % |
| loc = loc.replaceAll( "[\\?\\:\\&\\=\\%]", "_" ); |
| |
| if ( !loc.endsWith( ".xml" ) ) |
| { |
| loc = loc + ".xml"; |
| } |
| |
| getLog().debug( "Before: " + name + " After: " + loc ); |
| return loc; |
| } |
| |
| private String determineAuxClasspath() throws MavenReportException |
| { |
| try |
| { |
| List<String> classpath = new ArrayList<>(); |
| if ( aggregate ) |
| { |
| List<String> dependencies = new ArrayList<>(); |
| |
| // collect exclusions for projects within the reactor |
| // if module a depends on module b and both are in the reactor |
| // then we don't want to resolve the dependency as an artifact. |
| List<String> exclusionPatterns = new ArrayList<>(); |
| for ( MavenProject localProject : reactorProjects ) |
| { |
| exclusionPatterns.add( localProject.getGroupId() + ":" + localProject.getArtifactId() ); |
| } |
| TransformableFilter filter = new AndFilter( Arrays.asList( |
| new ExclusionsFilter( exclusionPatterns ), |
| includeTests ? ScopeFilter.including( "compile", "provided", "test" ) |
| : ScopeFilter.including( "compile", "provided" ) |
| ) ); |
| |
| for ( MavenProject localProject : reactorProjects ) |
| { |
| ProjectBuildingRequest buildingRequest = new DefaultProjectBuildingRequest( |
| session.getProjectBuildingRequest() ); |
| |
| Iterable<ArtifactResult> resolvedDependencies = dependencyResolver.resolveDependencies( |
| buildingRequest, localProject.getDependencies(), null, filter ); |
| |
| for ( ArtifactResult resolvedArtifact : resolvedDependencies ) |
| { |
| dependencies.add( resolvedArtifact.getArtifact().getFile().toString() ); |
| } |
| |
| List<String> projectClasspath = includeTests ? localProject.getTestClasspathElements() |
| : localProject.getCompileClasspathElements(); |
| |
| // Add the project's target folder first |
| classpath.addAll( projectClasspath ); |
| if ( !localProject.isExecutionRoot() ) |
| { |
| for ( String path : projectClasspath ) |
| { |
| File pathFile = new File( path ); |
| String[] children = pathFile.list(); |
| |
| if ( !pathFile.exists() || ( children != null && children.length == 0 ) ) |
| { |
| getLog().warn( "The project " + localProject.getArtifactId() |
| + " does not seem to be compiled. PMD results might be inaccurate." ); |
| } |
| } |
| } |
| |
| } |
| |
| // Add the dependencies as last entries |
| classpath.addAll( dependencies ); |
| |
| getLog().debug( "Using aggregated aux classpath: " + classpath ); |
| } |
| else |
| { |
| classpath.addAll( includeTests ? project.getTestClasspathElements() |
| : project.getCompileClasspathElements() ); |
| |
| getLog().debug( "Using aux classpath: " + classpath ); |
| } |
| String path = StringUtils.join( classpath.iterator(), File.pathSeparator ); |
| return path; |
| } |
| catch ( Exception e ) |
| { |
| throw new MavenReportException( e.getMessage(), e ); |
| } |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public String getOutputName() |
| { |
| return "pmd"; |
| } |
| |
| private static ResourceBundle getBundle( Locale locale ) |
| { |
| return ResourceBundle.getBundle( "pmd-report", locale, PmdReport.class.getClassLoader() ); |
| } |
| |
| /** |
| * Create and return the correct renderer for the output type. |
| * |
| * @return the renderer based on the configured output |
| * @throws org.apache.maven.reporting.MavenReportException if no renderer found for the output type |
| * @deprecated Use {@link PmdExecutor#createRenderer(String, String)} instead. |
| */ |
| @Deprecated |
| public final Renderer createRenderer() throws MavenReportException |
| { |
| return PmdExecutor.createRenderer( format, getOutputEncoding() ); |
| } |
| } |