1
2
3
4
5 package com.qulice.errorprone;
6
7 import com.google.common.base.Splitter;
8 import com.jcabi.log.Logger;
9 import com.qulice.spi.Environment;
10 import com.qulice.spi.Ignored;
11 import com.qulice.spi.Relative;
12 import com.qulice.spi.ResourceValidator;
13 import com.qulice.spi.Violation;
14 import com.yegor256.Jaxec;
15 import com.yegor256.Result;
16 import java.io.File;
17 import java.net.URL;
18 import java.net.URLClassLoader;
19 import java.security.CodeSource;
20 import java.util.ArrayList;
21 import java.util.Collection;
22 import java.util.LinkedHashSet;
23 import java.util.List;
24 import java.util.Map;
25 import java.util.Set;
26 import java.util.regex.Pattern;
27 import javax.inject.Inject;
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60 public final class ErrorProneValidator implements ResourceValidator {
61
62
63
64
65
66
67 private static final List<String> JVM_FLAGS = List.of(
68 "--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED",
69 "--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED",
70 "--add-exports=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED",
71 "--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED",
72 "--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED",
73 "--add-exports=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED",
74 "--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED",
75 "--add-exports=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED",
76 "--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED",
77 "--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED",
78 "--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED",
79 "--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED"
80 );
81
82
83
84
85
86 private static final String PARAM = "qulice.errorprone";
87
88
89
90
91
92 private static final Pattern NEWLINE = Pattern.compile("\\R");
93
94
95
96
97 private final Environment env;
98
99
100
101
102
103
104 public ErrorProneValidator(final Environment env) {
105 this.env = env;
106 }
107
108 @Override
109 public Collection<Violation> validate(final Collection<File> files) {
110 final List<File> sources = this.relevant(files);
111 final Collection<Violation> violations = new ArrayList<>(0);
112 if (sources.isEmpty()) {
113 Logger.debug(
114 this,
115 "No files to check with ErrorProne, all %d are excluded",
116 files.size()
117 );
118 } else {
119 Logger.debug(this, "ErrorProne processing %d files", sources.size());
120 final Diagnostics diagnostics = new Diagnostics(
121 this.name(), this.env.basedir().getAbsolutePath()
122 );
123 for (final Map.Entry<String, List<File>> batch
124 : new Batches(this.env, sources).split().entrySet()) {
125 violations.addAll(
126 diagnostics.violations(
127 this.run(batch.getKey(), batch.getValue())
128 )
129 );
130 }
131 Logger.debug(this, "ErrorProne processed %d files", sources.size());
132 }
133 return violations;
134 }
135
136 @Override
137 public String name() {
138 return "ErrorProne";
139 }
140
141 @Override
142 public int rules() {
143 return new Xplugin(
144 this.env.param(ErrorProneValidator.PARAM, "")
145 ).patterns();
146 }
147
148 private List<String> run(final String batch, final List<File> sources) {
149 final Result result = new Jaxec(this.command(batch, sources))
150 .withRedirect(true)
151 .withCheck(false)
152 .exec();
153 final String stdout = result.stdout();
154 final List<String> lines;
155 if (stdout.isEmpty()) {
156 lines = List.of();
157 } else {
158 lines = List.of(ErrorProneValidator.NEWLINE.split(stdout));
159 }
160 return lines;
161 }
162
163 private List<String> command(final String batch, final List<File> sources) {
164 final List<String> command = new ArrayList<>(
165 ErrorProneValidator.JVM_FLAGS.size() + 2
166 );
167 command.add(ErrorProneValidator.javac());
168 for (final String flag : ErrorProneValidator.JVM_FLAGS) {
169 command.add("-J".concat(flag));
170 }
171 final File outdir = new File(
172 this.env.tempdir(), String.format("errorprone-classes-%s", batch)
173 );
174 if (!outdir.exists() && !outdir.mkdirs()) {
175 throw new IllegalStateException(
176 String.format("Unable to create %s", outdir)
177 );
178 }
179 final List<String> args = new ArrayList<>(sources.size() + 12);
180 args.add("-XDcompilePolicy=simple");
181 args.add("-XDaddTypeAnnotationsToSymbol=true");
182 args.add("--should-stop=ifError=FLOW");
183 args.add("-proc:none");
184 args.add("-Xlint:-options");
185 args.add("-encoding");
186 args.add(this.env.encoding().name());
187 args.addAll(new Release(this.env, batch).flags());
188 args.add(
189 new Xplugin(
190 this.env.param(ErrorProneValidator.PARAM, "")
191 ).argument()
192 );
193 args.add("-processorpath");
194 args.add(ErrorProneValidator.pluginClasspath());
195 args.add("-d");
196 args.add(outdir.getAbsolutePath());
197 final Collection<String> classpath = this.env.classpath();
198 if (!classpath.isEmpty()) {
199 args.add("-classpath");
200 args.add(String.join(File.pathSeparator, classpath));
201 }
202 for (final File source : sources) {
203 args.add(source.getAbsolutePath());
204 }
205 command.add(
206 "@".concat(
207 new Argfile(
208 new File(
209 this.env.tempdir(),
210 String.format("errorprone-args-%s.txt", batch)
211 ),
212 args
213 ).save().getAbsolutePath()
214 )
215 );
216 return command;
217 }
218
219 private List<File> relevant(final Collection<File> files) {
220 final List<File> sources = new ArrayList<>(files.size());
221 for (final File file : files) {
222 final String name = new Relative(this.env.basedir(), file).path();
223 if (this.env.exclude("errorprone", name)) {
224 continue;
225 }
226 if (new Ignored(name).yes()) {
227 continue;
228 }
229 if (!name.endsWith(".java")) {
230 continue;
231 }
232 sources.add(file);
233 }
234 return sources;
235 }
236
237 private static String javac() {
238 return new File(
239 new File(System.getProperty("java.home"), "bin"),
240 "javac"
241 ).getAbsolutePath();
242 }
243
244 private static String pluginClasspath() {
245 final Set<String> entries = new LinkedHashSet<>();
246 ClassLoader loader = Thread.currentThread().getContextClassLoader();
247 while (loader != null) {
248 if (loader instanceof URLClassLoader) {
249 for (final URL url : ((URLClassLoader) loader).getURLs()) {
250 new Unencoded(url).path().ifPresent(entries::add);
251 }
252 }
253 loader = loader.getParent();
254 }
255 for (final String entry
256 : Splitter.on(File.pathSeparatorChar).split(
257 System.getProperty("java.class.path", "")
258 )
259 ) {
260 if (!entry.isEmpty()) {
261 entries.add(new File(entry).getAbsolutePath());
262 }
263 }
264 ErrorProneValidator.addCodeSource(entries, Inject.class);
265 return String.join(File.pathSeparator, entries);
266 }
267
268 private static void addCodeSource(
269 final Set<String> entries, final Class<?> klass
270 ) {
271 final CodeSource source =
272 klass.getProtectionDomain().getCodeSource();
273 if (source != null && source.getLocation() != null) {
274 new Unencoded(source.getLocation()).path().ifPresent(entries::add);
275 }
276 }
277 }