View Javadoc
1   /*
2    * SPDX-FileCopyrightText: Copyright (c) 2011-2026 Yegor Bugayenko
3    * SPDX-License-Identifier: MIT
4    */
5   package com.qulice.errorprone;
6   
7   import com.google.errorprone.ErrorProneOptions;
8   import com.google.errorprone.scanner.BuiltInCheckerSuppliers;
9   import java.util.ArrayList;
10  import java.util.List;
11  import java.util.regex.Pattern;
12  
13  /**
14   * The {@code -Xplugin} argument that wires ErrorProne into a forked
15   * {@code javac}, together with the bug patterns it leaves out.
16   *
17   * <p>Qulice switches a few patterns off for every project it checks, and
18   * the project appends {@code -Xep} flags of its own after them, so that a
19   * bug pattern which contradicts the project's domain has somewhere to go
20   * other than a {@code @SuppressWarnings} on every affected class. See
21   * <a href="https://github.com/yegor256/qulice/issues/1734">#1734</a>.</p>
22   *
23   * @since 1.0
24   */
25  public final class Xplugin {
26  
27      /**
28       * The bug patterns Qulice switches off for every project it checks.
29       *
30       * <p>{@code InvalidBlockTag} is disabled because it rejects the
31       * {@code @checkstyle} block tags this codebase writes in Javadoc.</p>
32       *
33       * <p>{@code OperatorPrecedence} is disabled because it contradicts
34       * Checkstyle's {@code UnnecessaryParentheses}: a boolean expression
35       * that mixes {@code &&} and {@code ||} can satisfy only one of them at
36       * a time. {@code OperatorPrecedence} demands grouping parentheses
37       * around the {@code &&} operands (for example
38       * {@code (a && b) || (c && d)}), while {@code UnnecessaryParentheses}
39       * flags those very parentheses as redundant, since {@code &&} already
40       * binds tighter than {@code ||}. Keeping {@code UnnecessaryParentheses}
41       * as the single arbiter lets the terse, paren-free form pass both
42       * checks. See
43       * <a href="https://github.com/yegor256/qulice/issues/1705">#1705</a>.</p>
44       *
45       * <p>{@code UnicodeInCode} is disabled because it judges the project's
46       * domain instead of its quality: it rejects every non-ASCII character
47       * outside comments and literals, so a project whose own notation is
48       * not ASCII — such as
49       * <a href="https://github.com/objectionary/eo">objectionary/eo</a>,
50       * where {@code Phi.Φ} and {@code φTerm()} are public API — would have
51       * to rename its API in order to pass.</p>
52       */
53      private static final List<String> DISABLED = List.of(
54          "-Xep:InvalidBlockTag:OFF",
55          "-Xep:OperatorPrecedence:OFF",
56          "-Xep:UnicodeInCode:OFF"
57      );
58  
59      /**
60       * Splits the flags a project supplies, on commas or whitespace.
61       */
62      private static final Pattern SEPARATOR = Pattern.compile("[\\s,]+");
63  
64      /**
65       * The flags of the project, as it wrote them.
66       */
67      private final String extra;
68  
69      /**
70       * Constructor.
71       *
72       * @param flags ErrorProne flags of the project, if any, separated by
73       *  whitespace or commas
74       */
75      public Xplugin(final String flags) {
76          this.extra = flags;
77      }
78  
79      /**
80       * Build the {@code javac} argument.
81       *
82       * <p>The flags of the project come last, so the project has the final
83       * say on every pattern, the ones Qulice disables included: ErrorProne
84       * reads the flags left to right and the last one naming a pattern
85       * wins. A project can therefore both switch a pattern off, with
86       * {@code -Xep:UnusedVariable:OFF}, and switch one back on, with
87       * {@code -Xep:OperatorPrecedence:ERROR}.</p>
88       *
89       * @return The whole {@code -Xplugin:ErrorProne ...} argument
90       */
91      public String argument() {
92          return String.format(
93              "-Xplugin:ErrorProne %s", String.join(" ", this.flags())
94          );
95      }
96  
97      /**
98       * How many bug patterns fire with these flags.
99       *
100      * <p>ErrorProne answers this itself: the count starts at the patterns
101      * it enables by default and then the very flags the forked
102      * {@code javac} receives are applied to it, so a pattern Qulice
103      * switches off leaves the count and one the project switches back on
104      * rejoins it.</p>
105      *
106      * @return The number of bug patterns that judge every file
107      */
108     public int patterns() {
109         return BuiltInCheckerSuppliers.defaultChecks()
110             .applyOverrides(ErrorProneOptions.processArgs(this.flags()))
111             .getEnabledChecks()
112             .size();
113     }
114 
115     private List<String> flags() {
116         final List<String> flags = new ArrayList<>(
117             Xplugin.DISABLED.size() + 4
118         );
119         flags.addAll(Xplugin.DISABLED);
120         flags.addAll(this.extras());
121         return flags;
122     }
123 
124     private List<String> extras() {
125         return Xplugin.SEPARATOR.splitAsStream(this.extra.trim())
126             .filter(flag -> !flag.isEmpty())
127             .map(Xplugin::checked)
128             .toList();
129     }
130 
131     private static String checked(final String flag) {
132         if (!flag.startsWith("-Xep")) {
133             throw new IllegalArgumentException(
134                 String.format(
135                     "Only ErrorProne flags belong here, all of them starting with '-Xep', while '%s' does not",
136                     flag
137                 )
138             );
139         }
140         return flag;
141     }
142 }