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 private List<String> command(final List<File> sources) {
148 final List<String> command = new ArrayList<>(
149 sources.size() + ErrorProneValidator.JVM_FLAGS.size() + 11
150 );
151 command.add(ErrorProneValidator.javac());
152 for (final String flag : ErrorProneValidator.JVM_FLAGS) {
153 command.add("-J".concat(flag));
154 }
155 command.add("-XDcompilePolicy=simple");
156 command.add("-XDaddTypeAnnotationsToSymbol=true");
157 command.add("--should-stop=ifError=FLOW");
158 command.add("-proc:none");
159 command.add("-Xplugin:ErrorProne");
160 command.add("-processorpath");
161 command.add(ErrorProneValidator.pluginClasspath());
162 final File outdir = new File(this.env.tempdir(), "errorprone-classes");
163 if (!outdir.exists() && !outdir.mkdirs()) {
164 throw new IllegalStateException(
165 String.format("Unable to create %s", outdir)
166 );
167 }
168 command.add("-d");
169 command.add(outdir.getAbsolutePath());
170 final Collection<String> classpath = this.env.classpath();
171 if (!classpath.isEmpty()) {
172 command.add("-classpath");
173 command.add(String.join(File.pathSeparator, classpath));
174 }
175 for (final File source : sources) {
176 command.add(source.getAbsolutePath());
177 }
178 return command;
179 }
180
181
182
183
184
185
186
187 private Collection<Violation> parse(final List<String> output) {
188 final Collection<Violation> violations = new LinkedList<>();
189 for (final String line : output) {
190 final Matcher matcher = ErrorProneValidator.DIAGNOSTIC.matcher(line);
191 if (matcher.matches()) {
192 final String check = matcher.group(3);
193 violations.add(
194 new Violation.Default(
195 this.name(),
196 check,
197 matcher.group(1),
198 matcher.group(2),
199 String.format("[%s] %s", check, matcher.group(4))
200 )
201 );
202 }
203 }
204 return violations;
205 }
206
207
208
209
210
211
212 private List<File> relevant(final Collection<File> files) {
213 final List<File> sources = new LinkedList<>();
214 for (final File file : files) {
215 final String name = new Relative(this.env.basedir(), file).path();
216 if (this.env.exclude("errorprone", name)) {
217 continue;
218 }
219 if (!name.endsWith(".java")) {
220 continue;
221 }
222 sources.add(file);
223 }
224 return sources;
225 }
226
227
228
229
230
231 private static String javac() {
232 return new File(
233 new File(System.getProperty("java.home"), "bin"),
234 "javac"
235 ).getAbsolutePath();
236 }
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254 private static String pluginClasspath() {
255 final Set<String> entries = new LinkedHashSet<>();
256 ClassLoader loader = Thread.currentThread().getContextClassLoader();
257 while (loader != null) {
258 if (loader instanceof URLClassLoader) {
259 for (final URL url : ((URLClassLoader) loader).getURLs()) {
260 entries.add(new File(url.getPath()).getAbsolutePath());
261 }
262 }
263 loader = loader.getParent();
264 }
265 for (final String entry
266 : System.getProperty("java.class.path", "").split(File.pathSeparator)) {
267 if (!entry.isEmpty()) {
268 entries.add(new File(entry).getAbsolutePath());
269 }
270 }
271 ErrorProneValidator.addCodeSource(entries, javax.inject.Inject.class);
272 return String.join(File.pathSeparator, entries);
273 }
274
275
276
277
278
279
280
281
282
283 private static void addCodeSource(
284 final Set<String> entries, final Class<?> klass
285 ) {
286 final java.security.CodeSource source =
287 klass.getProtectionDomain().getCodeSource();
288 if (source != null && source.getLocation() != null) {
289 try {
290 entries.add(
291 new File(source.getLocation().toURI()).getAbsolutePath()
292 );
293 } catch (final java.net.URISyntaxException | IllegalArgumentException ex) {
294 Logger.debug(
295 ErrorProneValidator.class,
296 "Cannot resolve code source for %s: %s", klass, ex.getMessage()
297 );
298 }
299 }
300 }
301 }