1
2
3
4
5 package com.qulice.errorprone;
6
7 import com.qulice.spi.Environment;
8 import com.qulice.spi.Violation;
9 import java.io.File;
10 import java.util.Arrays;
11 import java.util.Collection;
12 import java.util.Collections;
13 import org.hamcrest.MatcherAssert;
14 import org.hamcrest.Matchers;
15 import org.junit.jupiter.api.Test;
16
17
18
19
20
21
22 final class ErrorProneValidatorTest {
23
24 @Test
25 void countsRulesItApplies() throws Exception {
26 MatcherAssert.assertThat(
27 "ErrorProne must tell how many bug patterns it applies",
28 new ErrorProneValidator(new Environment.Mock()).rules(),
29 Matchers.greaterThan(100)
30 );
31 }
32
33 @Test
34 void findsViolationInBadJavaFile() throws Exception {
35 final String file = "src/main/java/Bad.java";
36 final Environment env = new Environment.Mock().withFile(
37 file,
38 "class Bad { private int value; void set(int v) { this.value = this.value; } }"
39 );
40 MatcherAssert.assertThat(
41 "ErrorProne must flag the self-assignment in Bad.java",
42 new ErrorProneValidator(env).validate(
43 Collections.singletonList(new File(env.basedir(), file))
44 ),
45 Matchers.not(Matchers.<Violation>empty())
46 );
47 }
48
49 @Test
50 void doesNotFlagCleanJavaFile() throws Exception {
51 final String file = "src/main/java/com/qulice/Clean.java";
52 final Environment env = new Environment.Mock().withFile(
53 file,
54 "package com.qulice; final class Clean { int square(final int num) { return num * num; } }"
55 );
56 final Collection<Violation> violations =
57 new ErrorProneValidator(env).validate(
58 Collections.singletonList(new File(env.basedir(), file))
59 );
60 MatcherAssert.assertThat(
61 String.format("Clean code must not produce ErrorProne violations: %s", violations),
62 violations,
63 Matchers.<Violation>empty()
64 );
65 }
66
67 @Test
68 void doesNotFlagCheckstyleJavadocTag() throws Exception {
69 final String file = "src/main/java/com/qulice/Tagged.java";
70 final Environment env = new Environment.Mock().withFile(
71 file,
72 String.join(
73 System.lineSeparator(),
74 "package com.qulice;",
75 "/**",
76 " * Sample.",
77 " * @since 1.0",
78 " * @checkstyle MethodNameCheck (1 line)",
79 " */",
80 "final class Tagged {",
81 " int square(final int num) { return num * num; }",
82 "}"
83 )
84 );
85 final Collection<Violation> violations =
86 new ErrorProneValidator(env).validate(
87 Collections.singletonList(new File(env.basedir(), file))
88 );
89 MatcherAssert.assertThat(
90 String.format(
91 "@checkstyle Javadoc tag must not trigger ErrorProne violations: %s",
92 violations
93 ),
94 violations,
95 Matchers.<Violation>empty()
96 );
97 }
98
99 @Test
100 void doesNotFlagMixedBooleanOperators() throws Exception {
101 final String file = "src/main/java/com/qulice/Mixed.java";
102 final Environment env = new Environment.Mock().withFile(
103 file,
104 String.join(
105 System.lineSeparator(),
106 "package com.qulice;",
107 "/**",
108 " * Sample.",
109 " * @since 1.0",
110 " */",
111 "final class Mixed {",
112 " boolean hex(final char glyph) {",
113 " return glyph >= '0' && glyph <= '9'",
114 " || glyph >= 'A' && glyph <= 'F';",
115 " }",
116 "}"
117 )
118 );
119 final Collection<Violation> violations =
120 new ErrorProneValidator(env).validate(
121 Collections.singletonList(new File(env.basedir(), file))
122 );
123 MatcherAssert.assertThat(
124 String.format(
125 "Mixed && and || without parens must not trigger OperatorPrecedence: %s",
126 violations
127 ),
128 violations,
129 Matchers.<Violation>empty()
130 );
131 }
132
133 @Test
134 void doesNotBlameProjectForTwinPackageInfo() throws Exception {
135 final String main = "src/main/java/com/qulice/package-info.java";
136 final String test = "src/test/java/com/qulice/package-info.java";
137 final String body = String.join(
138 System.lineSeparator(),
139 "/**",
140 " * Sample.",
141 " * @since 1.0",
142 " */",
143 "package com.qulice;"
144 );
145 final Environment env = new Environment.Mock()
146 .withFile(main, body).withFile(test, body);
147 final Collection<Violation> violations =
148 new ErrorProneValidator(env).validate(
149 Arrays.asList(
150 new File(env.basedir(), main), new File(env.basedir(), test)
151 )
152 );
153 MatcherAssert.assertThat(
154 String.format(
155 "twin package-info files are legal in Maven and must pass: %s",
156 violations
157 ),
158 violations,
159 Matchers.<Violation>empty()
160 );
161 }
162
163 @Test
164 void doesNotBlameProjectForPackageInfoInExtraTestRoot() throws Exception {
165 final String main = "src/main/java/com/qulice/package-info.java";
166 final String mock = "src/mock/java/com/qulice/package-info.java";
167 final String body = String.join(
168 System.lineSeparator(),
169 "/**",
170 " * Sample.",
171 " * @since 1.0",
172 " */",
173 "package com.qulice;"
174 );
175 final Environment env = new Environment.Mock()
176 .withTestdir("src/mock/java")
177 .withFile(main, body).withFile(mock, body);
178 final Collection<Violation> violations =
179 new ErrorProneValidator(env).validate(
180 Arrays.asList(
181 new File(env.basedir(), main), new File(env.basedir(), mock)
182 )
183 );
184 MatcherAssert.assertThat(
185 String.format(
186 "a test source root outside src/test must compile apart: %s",
187 violations
188 ),
189 violations,
190 Matchers.<Violation>empty()
191 );
192 }
193
194 @Test
195 void doesNotBlameProjectForTwinPackageInfoInTestRoots() throws Exception {
196 final String test = "src/test/java/com/qulice/package-info.java";
197 final String mock = "src/mock/java/com/qulice/package-info.java";
198 final String body = String.join(
199 System.lineSeparator(),
200 "/**",
201 " * Sample.",
202 " * @since 1.0",
203 " */",
204 "package com.qulice;"
205 );
206 final Environment env = new Environment.Mock()
207 .withTestdir("src/mock/java")
208 .withFile(test, body).withFile(mock, body);
209 final Collection<Violation> violations =
210 new ErrorProneValidator(env).validate(
211 Arrays.asList(
212 new File(env.basedir(), test), new File(env.basedir(), mock)
213 )
214 );
215 MatcherAssert.assertThat(
216 String.format(
217 "two test source roots must compile apart from each other: %s",
218 violations
219 ),
220 violations,
221 Matchers.<Violation>empty()
222 );
223 }
224
225 @Test
226 void reportsPlainCompilerError() throws Exception {
227 final String file = "src/main/java/com/qulice/Broken.java";
228 final Environment env = new Environment.Mock().withFile(
229 file,
230 String.join(
231 System.lineSeparator(),
232 "package com.qulice;",
233 "/**",
234 " * Sample.",
235 " * @since 1.0",
236 " */",
237 "final class Broken {",
238 " int count() { return Missing.value(); }",
239 "}"
240 )
241 );
242 MatcherAssert.assertThat(
243 "cannot find symbol must not slip through as a green build",
244 new ErrorProneValidator(env).validate(
245 Collections.singletonList(new File(env.basedir(), file))
246 ).stream().map(Violation::message).toList(),
247 Matchers.hasItem(Matchers.containsString("cannot find symbol"))
248 );
249 }
250
251 @Test
252 void reportsPlainCompilerWarning() throws Exception {
253 final String file = "src/main/java/com/qulice/Removal.java";
254 final Environment env = new Environment.Mock().withFile(
255 file,
256 String.join(
257 System.lineSeparator(),
258 "package com.qulice;",
259 "/**",
260 " * Sample.",
261 " * @since 1.0",
262 " */",
263 "final class Removal {",
264 " @Deprecated(forRemoval = true)",
265 " static int old() { return 1; }",
266 "}",
267 "final class Caller {",
268 " int call() { return Removal.old(); }",
269 "}"
270 )
271 );
272 MatcherAssert.assertThat(
273 "deprecated-for-removal warning must not slip through as a green build",
274 new ErrorProneValidator(env).validate(
275 Collections.singletonList(new File(env.basedir(), file))
276 ).stream().map(Violation::message).toList(),
277 Matchers.hasItem(Matchers.containsString("marked for removal"))
278 );
279 }
280
281 @Test
282 void doesNotFlagNonAsciiIdentifiers() throws Exception {
283 final String file = "src/main/java/com/qulice/Greek.java";
284 final Environment env = new Environment.Mock().withFile(
285 file,
286 String.join(
287 System.lineSeparator(),
288 "package com.qulice;",
289 "/**",
290 " * Sample.",
291 " * @since 1.0",
292 " */",
293 "final class Greek {",
294 " int φ() { return 1; }",
295 "}"
296 )
297 );
298 final Collection<Violation> violations =
299 new ErrorProneValidator(env).validate(
300 Collections.singletonList(new File(env.basedir(), file))
301 );
302 MatcherAssert.assertThat(
303 String.format(
304 "a domain whose own notation is not ASCII must pass: %s",
305 violations
306 ),
307 violations,
308 Matchers.<Violation>empty()
309 );
310 }
311
312 @Test
313 void letsProjectSwitchBugPatternOff() throws Exception {
314 final String file = "src/main/java/com/qulice/Assigned.java";
315 final Environment env = new Environment.Mock()
316 .withParam("qulice.errorprone", "-Xep:SelfAssignment:OFF").withFile(
317 file,
318 String.join(
319 System.lineSeparator(),
320 "package com.qulice;",
321 "/**",
322 " * Sample.",
323 " * @since 1.0",
324 " */",
325 "final class Assigned {",
326 " private int value;",
327 " void set(final int num) { this.value = this.value; }",
328 "}"
329 )
330 );
331 final Collection<Violation> violations =
332 new ErrorProneValidator(env).validate(
333 Collections.singletonList(new File(env.basedir(), file))
334 );
335 MatcherAssert.assertThat(
336 String.format(
337 "a project must be able to switch a bug pattern off: %s",
338 violations
339 ),
340 violations,
341 Matchers.<Violation>empty()
342 );
343 }
344
345 @Test
346 void letsProjectSwitchBugPatternBackOn() throws Exception {
347 final String file = "src/main/java/com/qulice/Ordered.java";
348 final Environment env = new Environment.Mock()
349 .withParam("qulice.errorprone", "-Xep:OperatorPrecedence:ERROR").withFile(
350 file,
351 String.join(
352 System.lineSeparator(),
353 "package com.qulice;",
354 "/**",
355 " * Sample.",
356 " * @since 1.0",
357 " */",
358 "final class Ordered {",
359 " boolean hex(final char glyph) {",
360 " return glyph >= '0' && glyph <= '9'",
361 " || glyph >= 'A' && glyph <= 'F';",
362 " }",
363 "}"
364 )
365 );
366 MatcherAssert.assertThat(
367 "a project must have the final say on the patterns Qulice disables",
368 new ErrorProneValidator(env).validate(
369 Collections.singletonList(new File(env.basedir(), file))
370 ).stream().map(Violation::message).toList(),
371 Matchers.hasItem(Matchers.containsString("OperatorPrecedence"))
372 );
373 }
374
375 @Test
376 void doesNotBlameProjectForOwnCompilerOptions() throws Exception {
377 final String file = "src/main/java/com/qulice/Pinned.java";
378 final Environment env = new Environment.Mock()
379 .withParam("maven.compiler.source", "21").withFile(
380 file,
381 String.join(
382 System.lineSeparator(),
383 "package com.qulice;",
384 "/**",
385 " * Sample.",
386 " * @since 1.0",
387 " */",
388 "final class Pinned {",
389 " int square(final int num) { return num * num; }",
390 "}"
391 )
392 );
393 final Collection<Violation> violations =
394 new ErrorProneValidator(env).validate(
395 Collections.singletonList(new File(env.basedir(), file))
396 );
397 MatcherAssert.assertThat(
398 String.format(
399 "Qulice must not blame the project for its own -source flag: %s",
400 violations
401 ),
402 violations,
403 Matchers.<Violation>empty()
404 );
405 }
406
407 @Test
408 void compilesTestsAtTheirOwnSourceLevel() throws Exception {
409 final String main = "src/main/java/com/qulice/Old.java";
410 final String test = "src/test/java/com/qulice/OldTest.java";
411 final Environment env = new Environment.Mock()
412 .withParam("maven.compiler.release", "8")
413 .withParam("maven.compiler.testRelease", "17")
414 .withFile(main, ErrorProneValidatorTest.ancient("Old"))
415 .withFile(test, ErrorProneValidatorTest.modern("OldTest"));
416 final Collection<Violation> violations =
417 new ErrorProneValidator(env).validate(
418 Arrays.asList(
419 new File(env.basedir(), main), new File(env.basedir(), test)
420 )
421 );
422 MatcherAssert.assertThat(
423 String.format(
424 "tests pinned to a higher level must compile at it: %s",
425 violations
426 ),
427 violations,
428 Matchers.<Violation>empty()
429 );
430 }
431
432 @Test
433 void keepsMainSourcesAtTheirOwnLevel() throws Exception {
434 final String file = "src/main/java/com/qulice/Modern.java";
435 final Environment env = new Environment.Mock()
436 .withParam("maven.compiler.release", "8")
437 .withParam("maven.compiler.testRelease", "17")
438 .withFile(file, ErrorProneValidatorTest.modern("Modern"));
439 MatcherAssert.assertThat(
440 "the level of the tests must not leak into the main batch",
441 new ErrorProneValidator(env).validate(
442 Collections.singletonList(new File(env.basedir(), file))
443 ).stream().map(Violation::message).toList(),
444 Matchers.hasItem(Matchers.containsString("text block"))
445 );
446 }
447
448 private static String ancient(final String name) {
449 return String.join(
450 System.lineSeparator(),
451 "package com.qulice;",
452 "/**",
453 " * Sample.",
454 " * @since 1.0",
455 " */",
456 String.format("final class %s {", name),
457 " int square(final int num) { return num * num; }",
458 "}"
459 );
460 }
461
462 private static String modern(final String name) {
463 return String.join(
464 System.lineSeparator(),
465 "package com.qulice;",
466 "/**",
467 " * Sample.",
468 " * @since 1.0",
469 " */",
470 String.format("final class %s {", name),
471 " String text() {",
472 " return \"\"\"",
473 " hello",
474 " \"\"\";",
475 " }",
476 "}"
477 );
478 }
479 }