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