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   *
27   * @since 0.3
28   */
29  final class SourceValidator {
30  
31      /**
32       * Rules.
33       */
34      private final PMDConfiguration config;
35  
36      /**
37       * Source files encoding.
38       */
39      private final Charset encoding;
40  
41      /**
42       * Creates new instance of <code>SourceValidator</code>.
43       *
44       * @param charset Source files encoding
45       */
46      SourceValidator(final Charset charset) {
47          this.config = new PMDConfiguration();
48          this.encoding = charset;
49      }
50  
51      /**
52       * Performs validation of the input source files.
53       *
54       * @param sources Input source files
55       * @param path Base path
56       * @return Collection of violations
57       */
58      Collection<PmdError> validate(
59          final Collection<File> sources, final String path) {
60          final List<PmdError> errors = new ArrayList<>(0);
61          try (PmdAnalysis analysis = PmdAnalysis.create(this.configured())) {
62              for (final File source : sources) {
63                  Logger.debug(
64                      this,
65                      "Processing file: %s",
66                      source.toPath().toString()
67                  );
68                  analysis.files().addFile(source.toPath());
69              }
70              final Report report = analysis.performAnalysisAndCollectReport();
71              report.getConfigurationErrors().stream()
72                  .map(PmdError.OfConfigError::new).forEach(errors::add);
73              report.getProcessingErrors().stream()
74                  .filter(this::reportable)
75                  .map(PmdError.OfProcessingError::new).forEach(errors::add);
76              report.getViolations().stream()
77                  .filter(violation -> !SourceValidator.suppressesItself(violation))
78                  .map(PmdError.OfRuleViolation::new)
79                  .forEach(errors::add);
80          }
81          return errors;
82      }
83  
84      /**
85       * How many rules the ruleset holds, once PMD has resolved the
86       * categories it refers to and taken the exclusions out.
87       *
88       * @return The number of rules
89       */
90      int rules() {
91          int total = 0;
92          try (PmdAnalysis analysis = PmdAnalysis.create(this.configured())) {
93              for (final RuleSet set : analysis.getRulesets()) {
94                  total += set.size();
95              }
96          }
97          return total;
98      }
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     private boolean reportable(final Report.ProcessingError error) {
111         final boolean crash = SourceValidator.crashed(error.getError());
112         if (crash) {
113             Logger.warn(
114                 this,
115                 "PMD rule crashed on %s and was ignored: %s%n%s",
116                 error.getFileId().getAbsolutePath(),
117                 error.getMsg(),
118                 error.getDetail()
119             );
120         }
121         return !crash;
122     }
123 
124     private static boolean crashed(final Throwable error) {
125         boolean crash = false;
126         Throwable cause = error;
127         while (cause != null) {
128             if (cause instanceof IllegalStateException) {
129                 crash = true;
130                 break;
131             }
132             cause = cause.getCause();
133         }
134         return crash;
135     }
136 
137     private static boolean suppressesItself(final RuleViolation violation) {
138         final String name = "UnnecessaryWarningSuppression";
139         boolean result = false;
140         if (name.equals(violation.getRule().getName())) {
141             try {
142                 final List<String> lines = Files.readAllLines(
143                     Paths.get(violation.getFileId().getAbsolutePath())
144                 );
145                 final int start = Math.max(0, violation.getBeginLine() - 1);
146                 final int end = Math.min(lines.size(), violation.getEndLine());
147                 for (int idx = start; idx < end; ++idx) {
148                     if (lines.get(idx).contains(name)) {
149                         result = true;
150                         break;
151                     }
152                 }
153             } catch (final IOException ex) {
154                 Logger.debug(
155                     SourceValidator.class,
156                     "Failed to read %s: %s",
157                     violation.getFileId().getAbsolutePath(),
158                     ex.getMessage()
159                 );
160             }
161         }
162         return result;
163     }
164 }