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