| /* |
| * 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. |
| */ |
| package org.apache.maven.plugins.pmd; |
| |
| 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 net.sourceforge.pmd.renderers.Renderer; |
| 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.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.StringUtils; |
| |
| /** |
| * Creates a PMD site report based on the rulesets and configuration set in the plugin. |
| * It can also generate a pmd output file aside from the site report in any of the following formats: xml, csv or txt. |
| * |
| * @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>, <code>16</code>, <code>17</code>, <code>18</code>, <code>19</code>, |
| * and <code>20</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; |
| |
| /** |
| * Add a section in the HTML report that lists the suppressed violations. |
| * |
| * @since 3.17.0 |
| */ |
| @Parameter(property = "pmd.renderSuppressedViolations", defaultValue = "true") |
| private boolean renderSuppressedViolations = 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 { |
| 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) { |
| getLog().info("Skipping PMD execution"); |
| 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(getInputEncoding()); |
| 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.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); |
| } |
| |
| getLog().info("PMD version: " + AbstractPmdReport.getPmdVersion()); |
| pmdResult = PmdExecutor.execute(request); |
| } |
| |
| /** |
| * 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 List<String> resolveRulesets() throws MavenReportException { |
| // configure ResourceManager - will search for urls (URLResourceLoader) and files in various directories: |
| // in the directory of the current project's pom file - note: extensions might replace the pom file on the fly |
| locator.addSearchPath( |
| FileResourceLoader.ID, project.getFile().getParentFile().getAbsolutePath()); |
| // in the current project's directory |
| locator.addSearchPath(FileResourceLoader.ID, project.getBasedir().getAbsolutePath()); |
| // in the base directory - that's the directory of the initial pom requested to build, |
| // e.g. the root of a multi module build |
| locator.addSearchPath(FileResourceLoader.ID, session.getRequest().getBaseDirectory()); |
| 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 Arrays.asList(sets); |
| } |
| |
| private String determineRulesetFilename(String ruleset) { |
| String result = ruleset.trim(); |
| String lowercase = result.toLowerCase(Locale.ROOT); |
| if (lowercase.startsWith("http://") || lowercase.startsWith("https://") || 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), isAggregator()); |
| doxiaRenderer.setRenderRuleViolationPriority(renderRuleViolationPriority); |
| doxiaRenderer.setRenderViolationsByPriority(renderViolationsByPriority); |
| doxiaRenderer.setFiles(filesToProcess); |
| doxiaRenderer.setViolations(pmdResult.getViolations()); |
| if (renderSuppressedViolations) { |
| doxiaRenderer.setSuppressedViolations(pmdResult.getSuppressedViolations()); |
| } |
| 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 (isAggregator()) { |
| 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 : getAggregatedProjects()) { |
| 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 : getAggregatedProjects()) { |
| ProjectBuildingRequest buildingRequest = |
| new DefaultProjectBuildingRequest(session.getProjectBuildingRequest()); |
| // use any additional configured repo as well |
| buildingRequest.getRemoteRepositories().addAll(localProject.getRemoteArtifactRepositories()); |
| |
| 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()); |
| } |
| } |