View Javadoc
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.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   * Test case for {@link ErrorProneValidator}.
19   *
20   * @since 1.0
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 }