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.Collection;
14  import java.util.LinkedList;
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 LinkedList<>();
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                  .map(PmdError.OfProcessingError::new).forEach(errors::add);
77              report.getViolations().stream()
78                  .filter(violation -> !SourceValidator.suppressesItself(violation))
79                  .map(PmdError.OfRuleViolation::new)
80                  .forEach(errors::add);
81          }
82          return errors;
83      }
84  
85      /**
86       * Tells whether a violation reports a {@code @SuppressWarnings} that
87       * tries to suppress {@code PMD.UnnecessaryWarningSuppression} itself.
88       * The PMD rule cannot suppress its own violations, so suppressing it is
89       * effectively a no-op and must not be reported as unused.
90       * @param violation Violation to inspect
91       * @return True if the violation is self-referential
92       */
93      private static boolean suppressesItself(final RuleViolation violation) {
94          final String name = "UnnecessaryWarningSuppression";
95          boolean result = false;
96          if (name.equals(violation.getRule().getName())) {
97              try {
98                  final List<String> lines = Files.readAllLines(
99                      Paths.get(violation.getFileId().getAbsolutePath())
100                 );
101                 final int start = Math.max(0, violation.getBeginLine() - 1);
102                 final int end = Math.min(lines.size(), violation.getEndLine());
103                 for (int idx = start; idx < end; ++idx) {
104                     if (lines.get(idx).contains(name)) {
105                         result = true;
106                         break;
107                     }
108                 }
109             } catch (final IOException ex) {
110                 Logger.debug(
111                     SourceValidator.class,
112                     "Failed to read %s: %s",
113                     violation.getFileId().getAbsolutePath(),
114                     ex.getMessage()
115                 );
116             }
117         }
118         return result;
119     }
120 }