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