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 }