View Javadoc
1   /*
2    * SPDX-FileCopyrightText: Copyright (c) 2011-2026 Yegor Bugayenko
3    * SPDX-License-Identifier: MIT
4    */
5   package com.qulice.pmd;
6   
7   import com.jcabi.log.Logger;
8   import java.io.File;
9   import java.io.IOException;
10  import java.nio.charset.Charset;
11  import java.nio.file.Files;
12  import java.nio.file.Paths;
13  import java.util.ArrayList;
14  import java.util.Collection;
15  import java.util.List;
16  import net.sourceforge.pmd.PMDConfiguration;
17  import net.sourceforge.pmd.PmdAnalysis;
18  import net.sourceforge.pmd.lang.rule.RulePriority;
19  import net.sourceforge.pmd.reporting.Report;
20  import net.sourceforge.pmd.reporting.RuleViolation;
21  import org.cactoos.list.ListOf;
22  
23  /**
24   * Validates source files via <code>PmdValidator</code>.
25   * @since 0.3
26   */
27  final class SourceValidator {
28  
29      /**
30       * Rules.
31       */
32      private final PMDConfiguration config;
33  
34      /**
35       * Source files encoding.
36       */
37      private final Charset encoding;
38  
39      /**
40       * Creates new instance of <code>SourceValidator</code>.
41       * @param charset Source files encoding
42       */
43      SourceValidator(final Charset charset) {
44          this.config = new PMDConfiguration();
45          this.encoding = charset;
46      }
47  
48      /**
49       * Performs validation of the input source files.
50       * @param sources Input source files
51       * @param path Base path
52       * @return Collection of violations
53       */
54      Collection<PmdError> validate(
55          final Collection<File> sources, final String path) {
56          this.config.setRuleSets(new ListOf<>("com/qulice/pmd/ruleset.xml"));
57          this.config.setThreads(0);
58          this.config.setMinimumPriority(RulePriority.LOW);
59          this.config.setIgnoreIncrementalAnalysis(true);
60          this.config.setShowSuppressedViolations(true);
61          this.config.setSourceEncoding(this.encoding);
62          final List<PmdError> errors = new ArrayList<>(0);
63          try (PmdAnalysis analysis = PmdAnalysis.create(this.config)) {
64              for (final File source : sources) {
65                  Logger.debug(
66                      this,
67                      "Processing file: %s",
68                      source.toPath().toString()
69                  );
70                  analysis.files().addFile(source.toPath());
71              }
72              final Report report = analysis.performAnalysisAndCollectReport();
73              report.getConfigurationErrors().stream()
74                  .map(PmdError.OfConfigError::new).forEach(errors::add);
75              report.getProcessingErrors().stream()
76                  .filter(this::reportable)
77                  .map(PmdError.OfProcessingError::new).forEach(errors::add);
78              report.getViolations().stream()
79                  .filter(violation -> !SourceValidator.suppressesItself(violation))
80                  .map(PmdError.OfRuleViolation::new)
81                  .forEach(errors::add);
82          }
83          return errors;
84      }
85  
86      /**
87       * Tells whether a processing error should be reported as a violation.
88       * Some PMD rules crash internally with an {@link IllegalStateException}
89       * while analyzing perfectly valid Java. For example {@code
90       * UseDiamondOperator} throws {@code "overload resolution is not
91       * complete"} when it probes an overloaded method reference such as
92       * {@code BigDecimal::multiply} (see #1686). PMD itself only logs such a
93       * crash as a warning and keeps going, so it is a bug in the tool, not a
94       * problem in the code under analysis. We do the same here: log it as a
95       * warning and do not turn it into a build-breaking violation, since the
96       * user cannot fix it in their code. The full stack trace is logged too,
97       * so the crash can still be diagnosed and reported upstream.
98       * @param error The processing error to inspect
99       * @return True if it must be reported, false if it is an internal crash
100      */
101     private boolean reportable(final Report.ProcessingError error) {
102         final boolean crash = SourceValidator.crashed(error.getError());
103         if (crash) {
104             Logger.warn(
105                 this,
106                 "PMD rule crashed on %s and was ignored: %s%n%s",
107                 error.getFileId().getAbsolutePath(),
108                 error.getMsg(),
109                 error.getDetail()
110             );
111         }
112         return !crash;
113     }
114 
115     /**
116      * Tells whether a throwable is (or was caused by) an internal PMD rule
117      * crash, recognized by an {@link IllegalStateException} anywhere in its
118      * cause chain.
119      * @param error The throwable to inspect
120      * @return True if the cause chain contains an IllegalStateException
121      */
122     private static boolean crashed(final Throwable error) {
123         boolean crash = false;
124         Throwable cause = error;
125         while (cause != null) {
126             if (cause instanceof IllegalStateException) {
127                 crash = true;
128                 break;
129             }
130             cause = cause.getCause();
131         }
132         return crash;
133     }
134 
135     /**
136      * Tells whether a violation reports a {@code @SuppressWarnings} that
137      * tries to suppress {@code PMD.UnnecessaryWarningSuppression} itself.
138      * The PMD rule cannot suppress its own violations, so suppressing it is
139      * effectively a no-op and must not be reported as unused.
140      * @param violation Violation to inspect
141      * @return True if the violation is self-referential
142      */
143     private static boolean suppressesItself(final RuleViolation violation) {
144         final String name = "UnnecessaryWarningSuppression";
145         boolean result = false;
146         if (name.equals(violation.getRule().getName())) {
147             try {
148                 final List<String> lines = Files.readAllLines(
149                     Paths.get(violation.getFileId().getAbsolutePath())
150                 );
151                 final int start = Math.max(0, violation.getBeginLine() - 1);
152                 final int end = Math.min(lines.size(), violation.getEndLine());
153                 for (int idx = start; idx < end; ++idx) {
154                     if (lines.get(idx).contains(name)) {
155                         result = true;
156                         break;
157                     }
158                 }
159             } catch (final IOException ex) {
160                 Logger.debug(
161                     SourceValidator.class,
162                     "Failed to read %s: %s",
163                     violation.getFileId().getAbsolutePath(),
164                     ex.getMessage()
165                 );
166             }
167         }
168         return result;
169     }
170 }