View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2026 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ///////////////////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle.internal;
21  
22  import static com.google.common.truth.Truth.assertWithMessage;
23  
24  import java.beans.PropertyDescriptor;
25  import java.nio.file.Files;
26  import java.nio.file.Path;
27  import java.util.ArrayList;
28  import java.util.Arrays;
29  import java.util.HashSet;
30  import java.util.List;
31  import java.util.Locale;
32  import java.util.Set;
33  import java.util.TreeSet;
34  import java.util.stream.Collectors;
35  import java.util.stream.Stream;
36  
37  import org.apache.commons.beanutils.PropertyUtils;
38  import org.junit.jupiter.api.Test;
39  
40  import com.puppycrawl.tools.checkstyle.bdd.InlineConfigParser;
41  import com.puppycrawl.tools.checkstyle.bdd.ModuleInputConfiguration;
42  import com.puppycrawl.tools.checkstyle.bdd.TestInputConfiguration;
43  import com.puppycrawl.tools.checkstyle.internal.utils.CheckUtil;
44  
45  /**
46   * Enforces that every TreeWalker check has compact source file (JEP 512) test coverage.
47   * A check is covered when a {@code compact/} input folder exists and its inline configs both
48   * run the check once with all properties at their default values and, together, exercise every
49   * settable property at a non-default value at least once. Not-yet-covered checks are listed in
50   * {@link #SUPPRESSED_CHECKS} and skipped; removing a check from that set arms this test for it.
51   */
52  public class AllChecksCompactSourceCoverageTest {
53  
54      /** Root of the non-compilable check test resources; compact inputs live under it. */
55      private static final Path NONCOMPILABLE_CHECKS_ROOT = Path.of(
56          "src/test/resources-noncompilable/com/puppycrawl/tools/checkstyle/checks");
57  
58      /** Package prefix common to all check classes, used to derive their resource folders. */
59      private static final String CHECKS_PACKAGE = "com.puppycrawl.tools.checkstyle.checks";
60  
61      /** Name of the folder holding a check's compact source inputs. */
62      private static final String COMPACT_FOLDER = "compact";
63  
64      /** Suffix stripped from a check's simple name to derive its resource folder name. */
65      private static final String CHECK_SUFFIX = "Check";
66  
67      /**
68       * Properties that are not specific to a single check and therefore do not need to be
69       * exercised by compact source inputs.
70       */
71      private static final Set<String> COMMON_PROPERTIES = Set.of(
72          "severity",
73          "id",
74          "fileExtensions",
75          "tabWidth",
76          "fileContents",
77          "tokens",
78          "javadocTokens",
79          "violateExecutionOnNonTightHtml"
80      );
81  
82      /**
83       * Checks not yet covered by compact source inputs; skipped until a contributor adds their
84       * {@code compact/} folder and removes the entry. This set should shrink to empty and then
85       * be removed.
86       */
87      // until https://github.com/checkstyle/checkstyle/issues/20590
88      private static final Set<String> SUPPRESSED_CHECKS = Set.of(
89          "AbbreviationAsWordInNameCheck",
90          "AbstractClassNameCheck",
91          "AnnotationLocationCheck",
92          "AnnotationOnSameLineCheck",
93          "AnnotationUseStyleCheck",
94          "AnonInnerLengthCheck",
95          "ArrayTrailingCommaCheck",
96          "ArrayTypeStyleCheck",
97          "AtclauseOrderCheck",
98          "AvoidDoubleBraceInitializationCheck",
99          "AvoidEscapedUnicodeCharactersCheck",
100         "AvoidInlineConditionalsCheck",
101         "AvoidNestedBlocksCheck",
102         "AvoidNoArgumentSuperConstructorCallCheck",
103         "AvoidStarImportCheck",
104         "BooleanExpressionComplexityCheck",
105         "CatchParameterNameCheck",
106         "ClassDataAbstractionCouplingCheck",
107         "ClassTypeParameterNameCheck",
108         "CommentsIndentationCheck",
109         "ConstantNameCheck",
110         "ConstructorsDeclarationGroupingCheck",
111         "CustomImportOrderCheck",
112         "CyclomaticComplexityCheck",
113         "DeclarationOrderCheck",
114         "DefaultComesLastCheck",
115         "DescendantTokenCheck",
116         "DesignForExtensionCheck",
117         "EmptyBlockCheck",
118         "EmptyCatchBlockCheck",
119         "EmptyForInitializerPadCheck",
120         "EmptyForIteratorPadCheck",
121         "EmptyLineSeparatorCheck",
122         "EqualsAvoidNullCheck",
123         "EqualsHashCodeCheck",
124         "ExecutableStatementCountCheck",
125         "ExplicitInitializationCheck",
126         "FallThroughCheck",
127         "FinalClassCheck",
128         "FinalLocalVariableCheck",
129         "FinalParametersCheck",
130         "GenericWhitespaceCheck",
131         "GoogleNonConstantFieldNameCheck",
132         "HexLiteralCaseCheck",
133         "HiddenFieldCheck",
134         "HideUtilityClassConstructorCheck",
135         "IllegalBlockTagCheck",
136         "IllegalCatchCheck",
137         "IllegalIdentifierNameCheck",
138         "IllegalImportCheck",
139         "IllegalInstantiationCheck",
140         "IllegalSymbolCheck",
141         "IllegalThrowsCheck",
142         "IllegalTokenCheck",
143         "IllegalTokenTextCheck",
144         "IllegalTypeCheck",
145         "ImportControlCheck",
146         "ImportOrderCheck",
147         "IndentationCheck",
148         "InnerAssignmentCheck",
149         "InterfaceIsTypeCheck",
150         "InterfaceMemberImpliedModifierCheck",
151         "InterfaceTypeParameterNameCheck",
152         "InvalidJavadocPositionCheck",
153         "JavaNCSSCheck",
154         "JavadocBlockTagLocationCheck",
155         "JavadocContentLocationCheck",
156         "JavadocLeadingAsteriskAlignCheck",
157         "JavadocLinkFirstOccurrenceCheck",
158         "JavadocLinkWellKnownApiCheck",
159         "JavadocMethodCheck",
160         "JavadocMissingLeadingAsteriskCheck",
161         "JavadocMissingWhitespaceAfterAsteriskCheck",
162         "JavadocParagraphCheck",
163         "JavadocStyleCheck",
164         "JavadocTagContinuationIndentationCheck",
165         "JavadocTypeCheck",
166         "JavadocVariableCheck",
167         "LambdaBodyLengthCheck",
168         "LambdaParameterNameCheck",
169         "LeftCurlyCheck",
170         "LocalFinalVariableNameCheck",
171         "LocalVariableNameCheck",
172         "MagicNumberCheck",
173         "MatchXpathCheck",
174         "MemberNameCheck",
175         "MethodCountCheck",
176         "MethodLengthCheck",
177         "MethodNameCheck",
178         "MethodParamPadCheck",
179         "MethodTypeParameterNameCheck",
180         "MissingCtorCheck",
181         "MissingDeprecatedCheck",
182         "MissingJavadocMethodCheck",
183         "MissingJavadocPackageCheck",
184         "MissingJavadocTypeCheck",
185         "MissingNullCaseInSwitchCheck",
186         "MissingOverrideCheck",
187         "MissingOverrideOnRecordAccessorCheck",
188         "MissingSwitchDefaultCheck",
189         "ModifiedControlVariableCheck",
190         "ModifierOrderCheck",
191         "MultipleStringLiteralsCheck",
192         "MultipleVariableDeclarationsCheck",
193         "MutableExceptionCheck",
194         "NPathComplexityCheck",
195         "NestedForDepthCheck",
196         "NestedIfDepthCheck",
197         "NestedTryDepthCheck",
198         "NoArrayTrailingCommaCheck",
199         "NoCloneCheck",
200         "NoCodeInFileCheck",
201         "NoEnumTrailingCommaCheck",
202         "NoLineWrapCheck",
203         "NoWhitespaceAfterCheck",
204         "NoWhitespaceBeforeCaseDefaultColonCheck",
205         "NoWhitespaceBeforeCheck",
206         "NonEmptyAtclauseDescriptionCheck",
207         "NumericalPrefixesInfixesSuffixesCharacterCaseCheck",
208         "OneStatementPerLineCheck",
209         "OneTopLevelClassCheck",
210         "OperatorWrapCheck",
211         "OuterTypeNumberCheck",
212         "OverloadMethodsDeclarationOrderCheck",
213         "PackageAnnotationCheck",
214         "PackageDeclarationCheck",
215         "PackageNameCheck",
216         "ParameterAssignmentCheck",
217         "ParameterNameCheck",
218         "ParameterNumberCheck",
219         "ParenPadCheck",
220         "PatternVariableAssignmentCheck",
221         "PatternVariableNameCheck",
222         "RecordComponentNameCheck",
223         "RecordComponentNumberCheck",
224         "RecordTypeParameterNameCheck",
225         "RedundantImportCheck",
226         "RedundantModifierCheck",
227         "RegexpCheck",
228         "RegexpSinglelineJavaCheck",
229         "RequireEmptyLineBeforeBlockTagGroupCheck",
230         "RequireThisCheck",
231         "ReturnCountCheck",
232         "RightCurlyCheck",
233         "SealedShouldHavePermitsListCheck",
234         "SeparatorWrapCheck",
235         "SimplifyBooleanExpressionCheck",
236         "SimplifyBooleanReturnCheck",
237         "SingleLineJavadocCheck",
238         "SingleSpaceSeparatorCheck",
239         "StaticVariableNameCheck",
240         "SummaryJavadocCheck",
241         "SuperCloneCheck",
242         "SuperFinalizeCheck",
243         "SuppressWarningsCheck",
244         "SuppressWarningsHolder",
245         "TextBlockGoogleStyleFormattingCheck",
246         "ThrowsCountCheck",
247         "TodoCommentCheck",
248         "TrailingCommentCheck",
249         "TypeNameCheck",
250         "TypecastParenPadCheck",
251         "UncommentedMainCheck",
252         "UnnecessaryNullCheckWithInstanceOfCheck",
253         "UnnecessaryParenthesesCheck",
254         "UnnecessarySemicolonAfterOuterTypeDeclarationCheck",
255         "UnnecessarySemicolonAfterTypeMemberDeclarationCheck",
256         "UnnecessarySemicolonInEnumerationCheck",
257         "UnnecessarySemicolonInTryWithResourcesCheck",
258         "UnusedCatchParameterShouldBeUnnamedCheck",
259         "UnusedImportsCheck",
260         "UnusedLambdaParameterShouldBeUnnamedCheck",
261         "UnusedLocalVariableCheck",
262         "UnusedTryResourceShouldBeUnnamedCheck",
263         "UnnecessaryFullyQualifiedTypeCheck",
264         "UpperEllCheck",
265         "UseEnhancedSwitchCheck",
266         "VariableDeclarationUsageDistanceCheck",
267         "WhenShouldBeUsedCheck",
268         "WhitespaceAfterCheck",
269         "WhitespaceAroundCheck"
270     );
271 
272     @Test
273     public void testAllChecksHaveCompactSourceCoverage() throws Exception {
274         final List<String> failures = new ArrayList<>();
275 
276         for (Class<?> check : CheckUtil.getCheckstyleChecks()) {
277             if (!SUPPRESSED_CHECKS.contains(check.getSimpleName())) {
278                 failures.addAll(findCoverageFailures(check));
279             }
280         }
281 
282         if (!failures.isEmpty()) {
283             assertWithMessage("The following checks lack compact source input coverage:\n"
284                     + String.join("\n", failures)).fail();
285         }
286     }
287 
288     /**
289      * Collects every coverage failure for a check: a missing {@code compact/} folder, a missing
290      * default-config input, or properties not exercised at a non-default value. Returns an empty
291      * list when the check is fully covered.
292      *
293      * @param check the check to evaluate.
294      * @return the failure messages, empty when covered.
295      * @throws Exception if an input file cannot be parsed.
296      */
297     private static List<String> findCoverageFailures(Class<?> check) throws Exception {
298         final Path folder = resolveCompactFolder(check);
299         final List<String> failures;
300         if (Files.isDirectory(folder)) {
301             failures = evaluateFolderCoverage(check, folder);
302         }
303         else {
304             failures = List.of(check.getSimpleName()
305                     + ": missing compact source input folder '" + folder + "'");
306         }
307         return failures;
308     }
309 
310     /**
311      * Evaluates the two content rules for a check whose {@code compact/} folder exists, adding a
312      * message for each unmet rule independently: it must contain at least one input running the
313      * check with all properties at their default values, and its inputs must together exercise
314      * every settable property at a non-default value.
315      *
316      * @param check the check to evaluate.
317      * @param folder the compact source input folder.
318      * @return the failure messages, empty when covered.
319      * @throws Exception if an input file cannot be parsed.
320      */
321     private static List<String> evaluateFolderCoverage(Class<?> check, Path folder)
322             throws Exception {
323         final List<Set<String>> nonDefaultsPerInput =
324                 collectNonDefaultPropertiesPerInput(folder, check.getName());
325         final Set<String> toExercise = propertiesToExercise(check);
326         final List<String> failures = new ArrayList<>();
327         if (nonDefaultsPerInput.stream().noneMatch(Set::isEmpty)) {
328             final String detail;
329             if (toExercise.isEmpty()) {
330                 detail = ": missing a compact source input";
331             }
332             else {
333                 detail = ": missing a default-config compact source input "
334                         + "(all properties at default)";
335             }
336             failures.add(check.getSimpleName() + detail);
337         }
338         final Set<String> exercised = nonDefaultsPerInput.stream()
339                 .flatMap(Set::stream)
340                 .collect(Collectors.toCollection(HashSet::new));
341         final Set<String> missing = toExercise.stream()
342                 .filter(property -> !exercised.contains(property))
343                 .collect(Collectors.toCollection(TreeSet::new));
344         if (!missing.isEmpty()) {
345             failures.add(check.getSimpleName()
346                     + ": compact source inputs do not exercise these properties at a "
347                     + "non-default value: " + missing);
348         }
349         return failures;
350     }
351 
352     /**
353      * Resolves the {@code compact/} input folder for a check from its package and simple name.
354      *
355      * @param check the check class.
356      * @return the expected compact source input folder.
357      */
358     private static Path resolveCompactFolder(Class<?> check) {
359         Path folder = NONCOMPILABLE_CHECKS_ROOT;
360         final String packageName = check.getPackageName();
361         if (!CHECKS_PACKAGE.equals(packageName)) {
362             final String subPackage = packageName.substring(CHECKS_PACKAGE.length() + 1);
363             folder = folder.resolve(subPackage.replace('.', '/'));
364         }
365         String simpleName = check.getSimpleName();
366         if (simpleName.endsWith(CHECK_SUFFIX)) {
367             simpleName = simpleName.substring(0, simpleName.length() - CHECK_SUFFIX.length());
368         }
369         return folder.resolve(simpleName.toLowerCase(Locale.ENGLISH)).resolve(COMPACT_FOLDER);
370     }
371 
372     /**
373      * Collects, for each input file that configures the given check, the set of properties the
374      * file sets to a non-default value. An empty set marks an input that runs the check with all
375      * properties at their default values.
376      *
377      * @param folder the compact source input folder.
378      * @param checkClassName the fully qualified name of the check module to match.
379      * @return the non-default property sets, one per input that configures the check.
380      * @throws Exception if an input file cannot be parsed.
381      */
382     private static List<Set<String>> collectNonDefaultPropertiesPerInput(
383             Path folder, String checkClassName) throws Exception {
384         final List<Set<String>> nonDefaultsPerInput = new ArrayList<>();
385         final List<Path> inputs;
386         try (Stream<Path> files = Files.list(folder)) {
387             inputs = files
388                 .filter(path -> path.getFileName().toString().endsWith(".java"))
389                 .toList();
390         }
391         for (Path input : inputs) {
392             final TestInputConfiguration config = InlineConfigParser.parse(input.toString());
393             for (ModuleInputConfiguration module : config.childrenModules()) {
394                 if (checkClassName.equals(module.getModuleName())) {
395                     nonDefaultsPerInput.add(module.getNonDefaultProperties().keySet());
396                 }
397             }
398         }
399         return nonDefaultsPerInput;
400     }
401 
402     /**
403      * Computes the settable, check-specific properties a check must exercise.
404      *
405      * @param check the check class.
406      * @return a sorted set of the property names to exercise.
407      */
408     private static Set<String> propertiesToExercise(Class<?> check) {
409         return Arrays.stream(PropertyUtils.getPropertyDescriptors(check))
410             .filter(descriptor -> descriptor.getWriteMethod() != null)
411             .map(PropertyDescriptor::getName)
412             .filter(property -> !COMMON_PROPERTIES.contains(property))
413             .collect(Collectors.toCollection(TreeSet::new));
414     }
415 
416 }