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      private PMDConfiguration configured() {
97          this.config.setRuleSets(new ListOf<>("com/qulice/pmd/ruleset.xml"));
98          this.config.setThreads(0);
99          this.config.setMinimumPriority(RulePriority.LOW);
100         this.config.setIgnoreIncrementalAnalysis(true);
101         this.config.setShowSuppressedViolations(true);
102         this.config.setSourceEncoding(this.encoding);
103         return this.config;
104     }
105 
106     private boolean reportable(final Report.ProcessingError error) {
107         final boolean crash = SourceValidator.crashed(error.getError());
108         if (crash) {
109             Logger.warn(
110                 this,
111                 "PMD rule crashed on %s and was ignored: %s%n%s",
112                 error.getFileId().getAbsolutePath(),
113                 error.getMsg(),
114                 error.getDetail()
115             );
116         }
117         return !crash;
118     }
119 
120     private static boolean crashed(final Throwable error) {
121         boolean crash = false;
122         Throwable cause = error;
123         while (cause != null) {
124             if (cause instanceof IllegalStateException) {
125                 crash = true;
126                 break;
127             }
128             cause = cause.getCause();
129         }
130         return crash;
131     }
132 
133     private static boolean suppressesItself(final RuleViolation violation) {
134         final String name = "UnnecessaryWarningSuppression";
135         boolean result = false;
136         if (name.equals(violation.getRule().getName())) {
137             try {
138                 final List<String> lines = Files.readAllLines(
139                     Paths.get(violation.getFileId().getAbsolutePath())
140                 );
141                 final int start = Math.max(0, violation.getBeginLine() - 1);
142                 final int end = Math.min(lines.size(), violation.getEndLine());
143                 for (int idx = start; idx < end; ++idx) {
144                     if (lines.get(idx).contains(name)) {
145                         result = true;
146                         break;
147                     }
148                 }
149             } catch (final IOException ex) {
150                 Logger.debug(
151                     SourceValidator.class,
152                     "Failed to read %s: %s",
153                     violation.getFileId().getAbsolutePath(),
154                     ex.getMessage()
155                 );
156             }
157         }
158         return result;
159     }
160 }