1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one
3 * or more contributor license agreements. See the NOTICE file
4 * distributed with this work for additional information
5 * regarding copyright ownership. The ASF licenses this file
6 * to you under the Apache License, Version 2.0 (the
7 * "License"); you may not use this file except in compliance
8 * with the License. You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing,
13 * software distributed under the License is distributed on an
14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 * KIND, either express or implied. See the License for the
16 * specific language governing permissions and limitations
17 * under the License.
18 */
19 package org.apache.maven.plugins.pmd;
20
21 import java.io.File;
22 import java.io.IOException;
23 import java.nio.file.Path;
24 import java.util.ArrayList;
25 import java.util.Collection;
26 import java.util.Collections;
27 import java.util.HashMap;
28 import java.util.HashSet;
29 import java.util.LinkedHashSet;
30 import java.util.LinkedList;
31 import java.util.List;
32 import java.util.Map;
33 import java.util.Set;
34 import java.util.TreeMap;
35
36 import net.sourceforge.pmd.PMDVersion;
37 import org.apache.maven.execution.MavenSession;
38 import org.apache.maven.plugin.MojoExecution;
39 import org.apache.maven.plugins.annotations.Parameter;
40 import org.apache.maven.project.MavenProject;
41 import org.apache.maven.reporting.AbstractMavenReport;
42 import org.apache.maven.reporting.MavenReportException;
43 import org.codehaus.plexus.util.FileUtils;
44
45 /**
46 * Base class for the PMD reports.
47 *
48 * @author <a href="mailto:brett@apache.org">Brett Porter</a>
49 * @version $Id$
50 */
51 public abstract class AbstractPmdReport extends AbstractMavenReport {
52 // ----------------------------------------------------------------------
53 // Configurables
54 // ----------------------------------------------------------------------
55
56 /**
57 * The output directory for the intermediate XML report.
58 */
59 @Parameter(property = "project.build.directory", required = true)
60 protected File targetDirectory;
61
62 /**
63 * Set the output format type, in addition to the HTML report. Must be one of: "none", "csv", "xml", "txt" or the
64 * full class name of the PMD renderer to use. See the net.sourceforge.pmd.renderers package javadoc for available
65 * renderers. XML is produced in any case, since this format is needed
66 * for the check goals (pmd:check, pmd:aggregator-check, pmd:cpd-check, pmd:aggregator-cpd-check).
67 */
68 @Parameter(property = "format", defaultValue = "xml")
69 protected String format = "xml";
70
71 /**
72 * Link the violation line numbers to the (Test) Source XRef. Links will be created automatically if the JXR plugin is
73 * being used.
74 */
75 @Parameter(property = "linkXRef", defaultValue = "true")
76 private boolean linkXRef;
77
78 /**
79 * Location where Source XRef is generated for this project.
80 * <br>
81 * <strong>Default</strong>: {@link #getReportOutputDirectory()} + {@code /xref}
82 */
83 @Parameter
84 private File xrefLocation;
85
86 /**
87 * Location where Test Source XRef is generated for this project.
88 * <br>
89 * <strong>Default</strong>: {@link #getReportOutputDirectory()} + {@code /xref-test}
90 */
91 @Parameter
92 private File xrefTestLocation;
93
94 /**
95 * A list of files to exclude from checks. Can contain Ant-style wildcards and double wildcards. Note that these
96 * exclusion patterns only operate on the path of a source file relative to its source root directory. In other
97 * words, files are excluded based on their package and/or class name. If you want to exclude entire source root
98 * directories, use the parameter <code>excludeRoots</code> instead.
99 * If a file matches both includes and excludes, it is excluded.
100 *
101 * @since 2.2
102 */
103 @Parameter
104 private List<String> excludes;
105
106 /**
107 * A list of files to check. Can contain Ant-style wildcards and double wildcards. Defaults to
108 * **\/*.java. If a file matches both includes and excludes, it is excluded.
109 *
110 * @since 2.2
111 */
112 @Parameter
113 private List<String> includes;
114
115 /**
116 * Specifies the location of the source directories to be used for PMD.
117 * Defaults to <code>project.compileSourceRoots</code>.
118 * @since 3.7
119 */
120 @Parameter(defaultValue = "${project.compileSourceRoots}")
121 private List<String> compileSourceRoots;
122
123 /**
124 * The directories containing the test-sources to be used for PMD.
125 * Defaults to <code>project.testCompileSourceRoots</code>
126 * @since 3.7
127 */
128 @Parameter(defaultValue = "${project.testCompileSourceRoots}")
129 private List<String> testSourceRoots;
130
131 /**
132 * The project source directories that should be excluded.
133 *
134 * @since 2.2
135 */
136 @Parameter
137 private File[] excludeRoots;
138
139 /**
140 * Run PMD on the tests as well.
141 *
142 * @since 2.2
143 */
144 @Parameter(defaultValue = "false")
145 protected boolean includeTests;
146
147 /**
148 * Whether to build an aggregated report at the root, or build individual reports.
149 *
150 * @since 2.2
151 * @deprecated since 3.15.0 Use the goals <code>pmd:aggregate-pmd</code> and <code>pmd:aggregate-cpd</code>
152 * instead. See <a href="https://maven.apache.org/plugins/maven-pmd-plugin/faq.html#typeresolution_aggregate">FAQ:
153 * Why do I get sometimes false positive and/or false negative violations?</a> for an explanation.
154 */
155 @Parameter(property = "aggregate", defaultValue = "false")
156 @Deprecated
157 protected boolean aggregate;
158
159 /**
160 * Whether to include the XML files generated by PMD/CPD in the {@link #getReportOutputDirectory()}.
161 *
162 * @since 3.0
163 */
164 @Parameter(defaultValue = "false")
165 protected boolean includeXmlInReports;
166
167 /**
168 * Skip the PMD/CPD report generation if there are no violations or duplications found. Defaults to
169 * <code>false</code>.
170 *
171 * <p>Note: the default value was changed from <code>true</code> to <code>false</code> with version 3.13.0.
172 *
173 * @since 3.1
174 */
175 @Parameter(defaultValue = "false")
176 protected boolean skipEmptyReport;
177
178 /**
179 * File that lists classes and rules to be excluded from failures.
180 * For PMD, this is a properties file. For CPD, this
181 * is a text file that contains comma-separated lists of classes
182 * that are allowed to duplicate.
183 *
184 * @since 3.7
185 */
186 @Parameter(property = "pmd.excludeFromFailureFile", defaultValue = "")
187 protected String excludeFromFailureFile;
188
189 /**
190 * Redirect PMD log into maven log out.
191 * When enabled, the PMD log output is redirected to maven, so that
192 * it is visible in the console together with all the other log output.
193 * Also, if maven is started with the debug flag (<code>-X</code> or <code>--debug</code>),
194 * the PMD logger is also configured for debug.
195 *
196 * @since 3.9.0
197 * @deprecated With 3.22.0 and the upgrade to PMD 7, this parameter has no effect anymore. The PMD log
198 * is now always redirected into the maven log and this can't be disabled by this parameter anymore.
199 * In order to disable the logging, see <a href="https://maven.apache.org/maven-logging.html">Maven Logging</a>.
200 * You'd need to start maven with <code>MAVEN_OPTS=-Dorg.slf4j.simpleLogger.log.net.sourceforge.pmd=off mvn <goals></code>.
201 */
202 @Parameter(defaultValue = "true", property = "pmd.showPmdLog")
203 @Deprecated // (since = "3.22.0", forRemoval = true)
204 protected boolean showPmdLog = true;
205
206 /**
207 * Used to avoid showing the deprecation warning for "showPmdLog" multiple times.
208 */
209 private boolean warnedAboutShowPmdLog = false;
210
211 /**
212 * <p>
213 * Allow for configuration of the jvm used to run PMD via maven toolchains.
214 * This permits a configuration where the project is built with one jvm and PMD is executed with another.
215 * This overrules the toolchain selected by the maven-toolchain-plugin.
216 * </p>
217 *
218 * <p>Examples:</p>
219 * (see <a href="https://maven.apache.org/guides/mini/guide-using-toolchains.html">
220 * Guide to Toolchains</a> for more info)
221 *
222 * <pre>
223 * {@code
224 * <configuration>
225 * ...
226 * <jdkToolchain>
227 * <version>1.11</version>
228 * </jdkToolchain>
229 * </configuration>
230 *
231 * <configuration>
232 * ...
233 * <jdkToolchain>
234 * <version>1.8</version>
235 * <vendor>zulu</vendor>
236 * </jdkToolchain>
237 * </configuration>
238 * }
239 * </pre>
240 *
241 * <strong>note:</strong> requires at least Maven 3.3.1
242 *
243 * @since 3.14.0
244 */
245 @Parameter
246 private Map<String, String> jdkToolchain;
247
248 // ----------------------------------------------------------------------
249 // Read-only parameters
250 // ----------------------------------------------------------------------
251
252 /**
253 * The current build session instance. This is used for
254 * toolchain manager API calls and for dependency resolver API calls.
255 */
256 @Parameter(defaultValue = "${session}", required = true, readonly = true)
257 protected MavenSession session;
258
259 /** The files that are being analyzed. */
260 protected Map<File, PmdFileInfo> filesToProcess;
261
262 @Override
263 protected MavenProject getProject() {
264 return project;
265 }
266
267 protected List<MavenProject> getReactorProjects() {
268 return reactorProjects;
269 }
270
271 protected MojoExecution getMojoExecution() {
272 return mojoExecution;
273 }
274
275 /**
276 * Convenience method to get files the PMD tool will analyze.
277 *
278 * @return the files the PMD tool will analyze
279 * @throws IOException if an I/O error occurs during construction of the
280 * canonical paths of the files
281 */
282 protected Map<File, PmdFileInfo> getFilesToProcess() throws IOException {
283 if (aggregate && !project.isExecutionRoot()) {
284 return Collections.emptyMap();
285 }
286
287 if (excludeRoots == null) {
288 excludeRoots = new File[0];
289 }
290
291 Collection<File> excludeRootFiles = new HashSet<>(excludeRoots.length);
292
293 for (File file : excludeRoots) {
294 if (file.isDirectory()) {
295 excludeRootFiles.add(file);
296 }
297 }
298
299 List<PmdFileInfo> directories = new ArrayList<>();
300
301 if (null == compileSourceRoots) {
302 compileSourceRoots = project.getCompileSourceRoots();
303 }
304 if (compileSourceRoots != null) {
305 for (String root : compileSourceRoots) {
306 File sroot = new File(root);
307 if (sroot.exists()) {
308 String sourceXref = linkXRef ? constructXrefLocation(xrefLocation, false) : null;
309 directories.add(new PmdFileInfo(project, sroot, sourceXref));
310 }
311 }
312 }
313
314 if (null == testSourceRoots) {
315 testSourceRoots = project.getTestCompileSourceRoots();
316 }
317 if (includeTests && testSourceRoots != null) {
318 for (String root : testSourceRoots) {
319 File sroot = new File(root);
320 if (sroot.exists()) {
321 String testSourceXref = linkXRef ? constructXrefLocation(xrefTestLocation, true) : null;
322 directories.add(new PmdFileInfo(project, sroot, testSourceXref));
323 }
324 }
325 }
326 if (isAggregator()) {
327 for (MavenProject localProject : getAggregatedProjects()) {
328 List<String> localCompileSourceRoots = localProject.getCompileSourceRoots();
329 for (String root : localCompileSourceRoots) {
330 File sroot = new File(root);
331 if (sroot.exists()) {
332 String sourceXref = linkXRef ? constructXrefLocation(xrefLocation, false) : null;
333 directories.add(new PmdFileInfo(localProject, sroot, sourceXref));
334 }
335 }
336 if (includeTests) {
337 List<String> localTestCompileSourceRoots = localProject.getTestCompileSourceRoots();
338 for (String root : localTestCompileSourceRoots) {
339 File sroot = new File(root);
340 if (sroot.exists()) {
341 String testSourceXref = linkXRef ? constructXrefLocation(xrefTestLocation, true) : null;
342 directories.add(new PmdFileInfo(localProject, sroot, testSourceXref));
343 }
344 }
345 }
346 }
347 }
348
349 String excluding = getExcludes();
350 getLog().debug("Exclusions: " + excluding);
351 String including = getIncludes();
352 getLog().debug("Inclusions: " + including);
353
354 Map<File, PmdFileInfo> files = new TreeMap<>();
355
356 for (PmdFileInfo finfo : directories) {
357 getLog().debug("Searching for files in directory "
358 + finfo.getSourceDirectory().toString());
359 File sourceDirectory = finfo.getSourceDirectory();
360 if (sourceDirectory.isDirectory() && !isDirectoryExcluded(excludeRootFiles, sourceDirectory)) {
361 List<File> newfiles = FileUtils.getFiles(sourceDirectory, including, excluding);
362 for (File newfile : newfiles) {
363 files.put(newfile.getCanonicalFile(), finfo);
364 }
365 }
366 }
367
368 return files;
369 }
370
371 private boolean isDirectoryExcluded(Collection<File> excludedRootFiles, File sourceDirectoryToCheck) {
372 for (File excludedDirectory : excludedRootFiles) {
373 try {
374 if (sourceDirectoryToCheck
375 .getCanonicalFile()
376 .toPath()
377 .startsWith(excludedDirectory.getCanonicalFile().toPath())) {
378 getLog().debug("Directory " + sourceDirectoryToCheck.getAbsolutePath()
379 + " has been excluded as it matches excludeRoot "
380 + excludedDirectory.getAbsolutePath());
381 return true;
382 }
383 } catch (IOException e) {
384 getLog().warn("Error while checking whether " + sourceDirectoryToCheck + " should be excluded.", e);
385 }
386 }
387 return false;
388 }
389
390 /**
391 * Gets the comma separated list of effective include patterns.
392 *
393 * @return the comma separated list of effective include patterns, never <code>null</code>.
394 */
395 private String getIncludes() {
396 Collection<String> patterns = new LinkedHashSet<>();
397 if (includes != null) {
398 patterns.addAll(includes);
399 }
400 if (patterns.isEmpty()) {
401 patterns.add("**/*.java");
402 }
403 return String.join(",", patterns);
404 }
405
406 /**
407 * Gets the comma separated list of effective exclude patterns.
408 *
409 * @return the comma separated list of effective exclude patterns, never <code>null</code>.
410 */
411 private String getExcludes() {
412 Collection<String> patterns = new LinkedHashSet<>(FileUtils.getDefaultExcludesAsList());
413 if (excludes != null) {
414 patterns.addAll(excludes);
415 }
416 return String.join(",", patterns);
417 }
418
419 protected boolean isXml() {
420 return "xml".equals(format);
421 }
422
423 protected boolean canGenerateReportInternal() throws MavenReportException {
424 if (!showPmdLog && !warnedAboutShowPmdLog) {
425 getLog().warn("The parameter \"showPmdLog\" has been deprecated and will be removed."
426 + "Setting it to \"false\" has no effect.");
427 warnedAboutShowPmdLog = true;
428 }
429
430 if (aggregate && !project.isExecutionRoot()) {
431 return false;
432 }
433
434 if (!isAggregator() && "pom".equalsIgnoreCase(project.getPackaging())) {
435 return false;
436 }
437
438 // if format is XML, we need to output it even if the file list is empty
439 // so the "check" goals can check for failures
440 if (isXml()) {
441 return true;
442 }
443 try {
444 filesToProcess = getFilesToProcess();
445 if (filesToProcess.isEmpty()) {
446 return false;
447 }
448 } catch (IOException e) {
449 throw new MavenReportException("Failed to determine files to process for PMD", e);
450 }
451 return true;
452 }
453
454 static String getPmdVersion() {
455 return PMDVersion.VERSION;
456 }
457
458 public Map<String, String> getJdkToolchain() {
459 return jdkToolchain;
460 }
461
462 protected boolean isAggregator() {
463 // returning here aggregate for backwards compatibility
464 return aggregate;
465 }
466
467 // Note: same logic as in m-javadoc-p (MJAVADOC-134)
468 protected Collection<MavenProject> getAggregatedProjects() {
469 Map<Path, MavenProject> reactorProjectsMap = new HashMap<>();
470 for (MavenProject reactorProject : this.reactorProjects) {
471 reactorProjectsMap.put(reactorProject.getBasedir().toPath(), reactorProject);
472 }
473
474 return modulesForAggregatedProject(project, reactorProjectsMap);
475 }
476
477 /**
478 * Recursively add the modules of the aggregatedProject to the set of aggregatedModules.
479 *
480 * @param aggregatedProject the project being aggregated
481 * @param reactorProjectsMap map of (still) available reactor projects
482 */
483 private Set<MavenProject> modulesForAggregatedProject(
484 MavenProject aggregatedProject, Map<Path, MavenProject> reactorProjectsMap) {
485 // Maven does not supply an easy way to get the projects representing
486 // the modules of a project. So we will get the paths to the base
487 // directories of the modules from the project and compare with the
488 // base directories of the projects in the reactor.
489
490 if (aggregatedProject.getModules().isEmpty()) {
491 return Collections.singleton(aggregatedProject);
492 }
493
494 List<Path> modulePaths = new LinkedList<Path>();
495 for (String module : aggregatedProject.getModules()) {
496 modulePaths.add(new File(aggregatedProject.getBasedir(), module).toPath());
497 }
498
499 Set<MavenProject> aggregatedModules = new LinkedHashSet<>();
500
501 for (Path modulePath : modulePaths) {
502 MavenProject module = reactorProjectsMap.remove(modulePath);
503 if (module != null) {
504 aggregatedModules.addAll(modulesForAggregatedProject(module, reactorProjectsMap));
505 }
506 }
507
508 return aggregatedModules;
509 }
510 }