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 org.checkstyle.base;
21  
22  import static com.google.common.truth.Truth.assertWithMessage;
23  
24  import java.io.BufferedReader;
25  import java.io.ByteArrayInputStream;
26  import java.io.ByteArrayOutputStream;
27  import java.io.File;
28  import java.io.IOException;
29  import java.io.InputStreamReader;
30  import java.io.LineNumberReader;
31  import java.nio.charset.StandardCharsets;
32  import java.nio.file.Files;
33  import java.nio.file.Path;
34  import java.text.MessageFormat;
35  import java.util.ArrayList;
36  import java.util.Arrays;
37  import java.util.Collections;
38  import java.util.HashMap;
39  import java.util.List;
40  import java.util.Locale;
41  import java.util.Map;
42  import java.util.Properties;
43  import java.util.regex.Pattern;
44  
45  import com.puppycrawl.tools.checkstyle.AbstractPathTestSupport;
46  import com.puppycrawl.tools.checkstyle.Checker;
47  import com.puppycrawl.tools.checkstyle.DefaultConfiguration;
48  import com.puppycrawl.tools.checkstyle.TreeWalker;
49  import com.puppycrawl.tools.checkstyle.api.AbstractViolationReporter;
50  import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
51  import com.puppycrawl.tools.checkstyle.api.Configuration;
52  import com.puppycrawl.tools.checkstyle.bdd.InlineConfigParser;
53  import com.puppycrawl.tools.checkstyle.bdd.TestInputViolation;
54  import com.puppycrawl.tools.checkstyle.internal.utils.BriefUtLogger;
55  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
56  
57  public abstract class AbstractItModuleTestSupport extends AbstractPathTestSupport {
58  
59      /**
60       * Enum to specify options for checker creation.
61       */
62      public enum ModuleCreationOption {
63  
64          /**
65           * Points that the module configurations
66           * has to be added under {@link TreeWalker}.
67           */
68          IN_TREEWALKER,
69          /**
70           * Points that checker will be created as
71           * a root of default configuration.
72           */
73          IN_CHECKER,
74  
75      }
76  
77      protected static final String ROOT_MODULE_NAME = "root";
78  
79      private static final Pattern WARN_PATTERN = CommonUtil
80              .createPattern(".* *// *warn *|/[*]\\*?\\s?warn\\s?[*]/");
81  
82      private final ByteArrayOutputStream stream = new ByteArrayOutputStream();
83  
84      /**
85       * Find the module creation option to use for the module name.
86       *
87       * @param moduleName the module name.
88       * @return the module creation option.
89       */
90      protected abstract ModuleCreationOption findModuleCreationOption(String moduleName);
91  
92      /**
93       * Returns test logger.
94       *
95       * @return logger for tests
96       */
97      protected final BriefUtLogger getBriefUtLogger() {
98          return new BriefUtLogger(stream);
99      }
100 
101     /**
102      * Creates a default module configuration {@link DefaultConfiguration} for a given object
103      * of type {@link Class}.
104      *
105      * @param clazz a {@code Class} type object.
106      * @return default module configuration for the given {@code Class} instance.
107      */
108     protected static DefaultConfiguration createModuleConfig(Class<?> clazz) {
109         return new DefaultConfiguration(clazz.getName());
110     }
111 
112     /**
113      * Returns {@link Configuration} instance for the given module name pulled
114      * from the {@code masterConfig}.
115      *
116      * @param masterConfig The master configuration to examine.
117      * @param moduleName module name.
118      * @param moduleId module id.
119      * @return {@code Configuration} instance for the given module name.
120      * @throws IllegalStateException if there is a problem retrieving the module
121      *         or config.
122      */
123     protected static Configuration getModuleConfig(Configuration masterConfig, String moduleName,
124             String moduleId) {
125         final Configuration result;
126         final List<Configuration> configs = getModuleConfigs(masterConfig, moduleName);
127         if (configs.size() == 1) {
128             result = configs.getFirst();
129         }
130         else if (configs.isEmpty()) {
131             throw new IllegalStateException("no instances of the Module was found: " + moduleName);
132         }
133         else if (moduleId == null) {
134             throw new IllegalStateException("multiple instances of the same Module are detected");
135         }
136         else {
137             result = configs.stream().filter(conf -> isSameModuleId(conf, moduleId))
138             .findFirst()
139             .orElseThrow(() -> new IllegalStateException("problem with module config"));
140         }
141 
142         return result;
143     }
144 
145     /**
146      * Verifies if the configuration's ID matches the expected {@code moduleId}.
147      *
148      * @param conf The config to examine.
149      * @param moduleId The module ID to match against.
150      * @return {@code true} if it matches.
151      * @throws IllegalStateException If there is an issue with finding the ID.
152      */
153     private static boolean isSameModuleId(Configuration conf, String moduleId) {
154         try {
155             return conf.getProperty("id").equals(moduleId);
156         }
157         catch (CheckstyleException exc) {
158             throw new IllegalStateException("problem to get ID attribute from " + conf, exc);
159         }
160     }
161 
162     /**
163      * Returns a list of all {@link Configuration} instances for the given module IDs in the
164      * {@code masterConfig}.
165      *
166      * @param masterConfig The master configuration to pull results from.
167      * @param moduleIds module IDs.
168      * @return List of {@code Configuration} instances.
169      * @throws CheckstyleException if there is an error with the config.
170      */
171     protected static List<Configuration> getModuleConfigsByIds(Configuration masterConfig,
172             String... moduleIds) throws CheckstyleException {
173         final List<Configuration> result = new ArrayList<>();
174         for (Configuration currentConfig : masterConfig.getChildren()) {
175             if ("TreeWalker".equals(currentConfig.getName())) {
176                 for (Configuration moduleConfig : currentConfig.getChildren()) {
177                     final String id = getProperty(moduleConfig, "id");
178                     if (id != null && isIn(id, moduleIds)) {
179                         result.add(moduleConfig);
180                     }
181                 }
182             }
183             else {
184                 final String id = getProperty(currentConfig, "id");
185                 if (id != null && isIn(id, moduleIds)) {
186                     result.add(currentConfig);
187                 }
188             }
189         }
190         return result;
191     }
192 
193     /**
194      * Finds the specific property {@code name} in the {@code config}.
195      *
196      * @param config The configuration to examine.
197      * @param name The property name to find.
198      * @return The property value or {@code null} if not found.
199      * @throws CheckstyleException if there is an error with the config.
200      */
201     private static String getProperty(Configuration config, String name)
202             throws CheckstyleException {
203         String result = null;
204 
205         if (isIn(name, config.getPropertyNames())) {
206             result = config.getProperty(name);
207         }
208 
209         return result;
210     }
211 
212     /**
213      * Finds the specific ID in a list of IDs.
214      *
215      * @param find The ID to find.
216      * @param list The list of module IDs.
217      * @return {@code true} if the ID is in the list.
218      */
219     private static boolean isIn(String find, String... list) {
220         boolean found = false;
221 
222         for (String item : list) {
223             if (find.equals(item)) {
224                 found = true;
225                 break;
226             }
227         }
228 
229         return found;
230     }
231 
232     /**
233      * Returns a list of all {@link Configuration} instances for the given
234      * module name pulled from the {@code masterConfig}.
235      *
236      * @param masterConfig The master configuration to examine.
237      * @param moduleName module name.
238      * @return {@code Configuration} instance for the given module name.
239      */
240     private static List<Configuration> getModuleConfigs(Configuration masterConfig,
241             String moduleName) {
242         final List<Configuration> result = new ArrayList<>();
243         for (Configuration currentConfig : masterConfig.getChildren()) {
244             if ("TreeWalker".equals(currentConfig.getName())) {
245                 for (Configuration moduleConfig : currentConfig.getChildren()) {
246                     if (moduleName.equals(moduleConfig.getName())) {
247                         result.add(moduleConfig);
248                     }
249                 }
250             }
251             else if (moduleName.equals(currentConfig.getName())) {
252                 result.add(currentConfig);
253             }
254         }
255         return result;
256     }
257 
258     /**
259      * Creates {@link Checker} instance based on the given {@link Configuration} instance.
260      *
261      * @param moduleConfig {@code Configuration} instance.
262      * @return {@code Checker} instance based on the given {@code Configuration} instance.
263      * @throws Exception if an exception occurs during checker configuration.
264      */
265     protected final Checker createChecker(Configuration moduleConfig)
266             throws Exception {
267         final String name = moduleConfig.getName();
268 
269         return createChecker(moduleConfig, findModuleCreationOption(name));
270     }
271 
272     /**
273      * Creates {@link Checker} instance based on the given {@link Configuration} instance.
274      *
275      * @param moduleConfig {@code Configuration} instance.
276      * @param moduleCreationOption {@code IN_TREEWALKER} if the {@code moduleConfig} should be added
277      *                                                  under {@code TreeWalker}.
278      * @return {@code Checker} instance based on the given {@code Configuration} instance.
279      * @throws Exception if an exception occurs during checker configuration.
280      */
281     protected final Checker createChecker(Configuration moduleConfig,
282                                  ModuleCreationOption moduleCreationOption)
283             throws Exception {
284         final Checker checker = new Checker();
285         checker.setModuleClassLoader(Thread.currentThread().getContextClassLoader());
286         // make sure the tests always run with English error messages
287         // so the tests don't fail in supported locales like German
288         final Locale locale = Locale.ENGLISH;
289         checker.setLocaleCountry(locale.getCountry());
290         checker.setLocaleLanguage(locale.getLanguage());
291 
292         if (moduleCreationOption == ModuleCreationOption.IN_TREEWALKER) {
293             final Configuration config = createTreeWalkerConfig(moduleConfig);
294             checker.configure(config);
295         }
296         else if (ROOT_MODULE_NAME.equals(moduleConfig.getName())
297                 || "Checker".equals(moduleConfig.getName())) {
298             checker.configure(moduleConfig);
299         }
300         else {
301             final Configuration config = createRootConfig(moduleConfig);
302             checker.configure(config);
303         }
304         checker.addListener(getBriefUtLogger());
305         return checker;
306     }
307 
308     /**
309      * Creates {@link DefaultConfiguration} for the {@link TreeWalker}
310      * based on the given {@link Configuration} instance.
311      *
312      * @param config {@code Configuration} instance.
313      * @return {@code DefaultConfiguration} for the {@code TreeWalker}
314      *     based on the given {@code Configuration} instance.
315      */
316     protected static DefaultConfiguration createTreeWalkerConfig(Configuration config) {
317         final DefaultConfiguration rootConfig =
318                 new DefaultConfiguration(ROOT_MODULE_NAME);
319         final DefaultConfiguration twConf = createModuleConfig(TreeWalker.class);
320         // make sure that the tests always run with this charset
321         rootConfig.addProperty("charset", StandardCharsets.UTF_8.name());
322         rootConfig.addChild(twConf);
323         twConf.addChild(config);
324         return rootConfig;
325     }
326 
327     /**
328      * Creates {@link DefaultConfiguration} or the Checker.
329      * based on the the list of {@link Configuration}.
330      *
331      * @param configs list of {@code Configuration} instances.
332      * @return {@code DefaultConfiguration} for the Checker.
333      */
334     protected static DefaultConfiguration createTreeWalkerConfig(
335             List<Configuration> configs) {
336         DefaultConfiguration result = null;
337 
338         for (Configuration config : configs) {
339             if (result == null) {
340                 result = (DefaultConfiguration) createTreeWalkerConfig(config).getChildren()[0];
341             }
342             else {
343                 result.addChild(config);
344             }
345         }
346 
347         return result;
348     }
349 
350     /**
351      * Creates {@link DefaultConfiguration} for the given {@link Configuration} instance.
352      *
353      * @param config {@code Configuration} instance.
354      * @return {@code DefaultConfiguration} for the given {@code Configuration} instance.
355      */
356     protected static DefaultConfiguration createRootConfig(Configuration config) {
357         final DefaultConfiguration rootConfig = new DefaultConfiguration(ROOT_MODULE_NAME);
358         rootConfig.addChild(config);
359         return rootConfig;
360     }
361 
362     /**
363      * Returns canonical path for the file with the given file name.
364      * The path is formed base on the non-compilable resources location.
365      *
366      * @param filename file name.
367      * @return canonical path for the file with the given file name.
368      * @throws IOException if I/O exception occurs while forming the path.
369      */
370     protected final String getNonCompilablePath(String filename) throws IOException {
371         return new File("src/" + getResourceLocation() + "/resources-noncompilable/"
372                 + getPackageLocation() + "/" + filename).getCanonicalPath();
373     }
374 
375     /**
376      * Performs verification of the file with the given file name. Uses specified configuration.
377      * Expected messages are represented by the array of strings, warning line numbers are
378      * represented by the array of integers.
379      * This implementation uses overloaded
380      * {@link AbstractItModuleTestSupport#verify(Checker, File[], String, String[], Integer...)}
381      * method inside.
382      *
383      * @param config configuration.
384      * @param fileName file name to verify.
385      * @param expected an array of expected messages.
386      * @param warnsExpected an array of expected warning numbers.
387      * @throws Exception if exception occurs during verification process.
388      */
389     protected final void verify(Configuration config, String fileName, String[] expected,
390             Integer... warnsExpected) throws Exception {
391         verify(createChecker(config),
392                 new File[] {new File(fileName)},
393                 fileName, expected, warnsExpected);
394     }
395 
396     /**
397      * Performs verification of files.
398      * Uses provided {@link Checker} instance.
399      *
400      * @param checker {@code Checker} instance.
401      * @param processedFiles files to process.
402      * @param messageFileName message file name.
403      * @param expected an array of expected messages.
404      * @param warnsExpected an array of expected warning line numbers.
405      * @throws Exception if exception occurs during verification process.
406      */
407     protected final void verify(Checker checker,
408             File[] processedFiles,
409             String messageFileName,
410             String[] expected,
411             Integer... warnsExpected)
412             throws Exception {
413         stream.flush();
414         stream.reset();
415         final List<File> theFiles = new ArrayList<>();
416         Collections.addAll(theFiles, processedFiles);
417         final List<Integer> expectedWarnings = Arrays.asList(warnsExpected);
418         final List<Integer> actualWarnings = new ArrayList<>();
419         final int errs = checker.process(theFiles);
420 
421         // process each of the lines
422         try (ByteArrayInputStream inputStream =
423                 new ByteArrayInputStream(stream.toByteArray());
424             LineNumberReader lnr = new LineNumberReader(
425                 new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
426             Integer previousLineNumber = 0;
427             for (int index = 0; index < expected.length; index++) {
428                 final String expectedResult = messageFileName + ":" + expected[index];
429                 final String actual = lnr.readLine();
430                 assertWithMessage("Error message at position %s of 'expected' does "
431                         + "not match actual message", index)
432                     .that(actual)
433                     .isEqualTo(expectedResult);
434 
435                 String parseInt = removeDeviceFromPathOnWindows(actual);
436                 parseInt = parseInt.substring(parseInt.indexOf(':') + 1);
437                 parseInt = parseInt.substring(0, parseInt.indexOf(':'));
438                 final Integer lineNumber = Integer.parseInt(parseInt);
439                 if (!previousLineNumber.equals(lineNumber)) {
440                     assertWithMessage(
441                             "input file is expected to have a warning comment on line number %s",
442                             lineNumber)
443                         .that(expectedWarnings.contains(lineNumber))
444                         .isTrue();
445 
446                     actualWarnings.add(lineNumber);
447                 }
448                 previousLineNumber = lineNumber;
449             }
450 
451             assertWithMessage("unexpected output: %s", lnr.readLine())
452                 .that(errs)
453                 .isEqualTo(expected.length);
454             assertWithMessage("warning line numbers should match expected")
455                 .that(actualWarnings)
456                 .containsExactlyElementsIn(expectedWarnings)
457                 .inOrder();
458         }
459 
460         checker.destroy();
461     }
462 
463     /**
464      * Performs the verification of the file with the given file path and config.
465      *
466      * @param config config to check against.
467      * @param filePath input file path.
468      * @throws Exception if exception occurs during verification process.
469      */
470     protected void verifyWithItConfig(Configuration config, String filePath) throws Exception {
471         final List<TestInputViolation> violations =
472             InlineConfigParser.getViolationsFromInputFile(filePath);
473         final List<String> actualViolations = getActualViolationsForFile(config, filePath);
474 
475         verifyViolations(filePath, violations, actualViolations);
476     }
477 
478     /**
479      * Tests the file with the check config.
480      *
481      * @param config check configuration.
482      * @param file input file path.
483      * @return list of actual violations.
484      * @throws Exception if exception occurs during verification process.
485      */
486     private List<String> getActualViolationsForFile(Configuration config,
487           String file) throws Exception {
488         stream.flush();
489         stream.reset();
490         final List<File> files = Collections.singletonList(new File(file));
491         final Checker checker = createChecker(config);
492         final Map<String, List<String>> actualViolations =
493                 getActualViolations(checker.process(files));
494         checker.destroy();
495         return actualViolations.getOrDefault(file, new ArrayList<>());
496     }
497 
498     /**
499      * Returns the actual violations for each file that has been checked against {@link Checker}.
500      * Each file is mapped to their corresponding violation messages. Reads input stream for these
501      * messages using instance of {@link InputStreamReader}.
502      *
503      * @param errorCount count of errors after checking set of files against {@code Checker}.
504      * @return a {@link Map} object containing file names and the corresponding violation messages.
505      * @throws IOException exception can occur when reading input stream.
506      */
507     private Map<String, List<String>> getActualViolations(int errorCount) throws IOException {
508         // process each of the lines
509         try (ByteArrayInputStream inputStream =
510                      new ByteArrayInputStream(stream.toByteArray());
511              LineNumberReader lnr = new LineNumberReader(
512                      new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
513             final Map<String, List<String>> actualViolations = new HashMap<>();
514             for (String line = lnr.readLine(); line != null && lnr.getLineNumber() <= errorCount;
515                  line = lnr.readLine()) {
516                 // have at least 2 characters before the splitting colon,
517                 // to not split after the drive letter on Windows
518                 final String[] actualViolation = line.split("(?<=.{2}):", 2);
519                 final String actualViolationFileName = actualViolation[0];
520                 final String actualViolationMessage = actualViolation[1];
521 
522                 actualViolations
523                         .computeIfAbsent(actualViolationFileName, key -> new ArrayList<>())
524                         .add(actualViolationMessage);
525             }
526 
527             return actualViolations;
528         }
529     }
530 
531     /**
532      * Performs verification of violation lines.
533      *
534      * @param file file path.
535      * @param testInputViolations List of TestInputViolation objects.
536      * @param actualViolations for a file
537      */
538     private static void verifyViolations(String file, List<TestInputViolation> testInputViolations,
539           List<String> actualViolations) {
540         final List<Integer> actualViolationLines = actualViolations.stream()
541                 .map(violation -> violation.substring(0, violation.indexOf(':')))
542                 .map(Integer::valueOf)
543                 .toList();
544         final List<Integer> expectedViolationLines = testInputViolations.stream()
545                 .map(TestInputViolation::getLineNo)
546                 .toList();
547         assertWithMessage("Violation lines for %s differ.", file)
548                 .that(actualViolationLines)
549                 .isEqualTo(expectedViolationLines);
550         for (int index = 0; index < actualViolations.size(); index++) {
551             assertWithMessage("Actual and expected violations differ.")
552                     .that(actualViolations.get(index))
553                     .matches(testInputViolations.get(index).toRegex());
554         }
555     }
556 
557     /**
558      * Gets the check message 'as is' from appropriate 'messages.properties'
559      * file.
560      *
561      * @param reporterClass the package the message is located in.
562      * @param messageKey the key of message in 'messages.properties' file.
563      * @param arguments  the arguments of message in 'messages.properties' file.
564      * @return The message of the check with the arguments applied.
565      * @throws IOException if there is a problem loading the property file.
566      */
567     protected static String getCheckMessage(
568             Class<? extends AbstractViolationReporter> reporterClass, String messageKey,
569             Object... arguments) throws IOException {
570         final Properties pr = new Properties();
571         pr.load(reporterClass.getResourceAsStream("messages.properties"));
572         final MessageFormat formatter = new MessageFormat(pr.getProperty(messageKey),
573                 Locale.ROOT);
574         return formatter.format(arguments);
575     }
576 
577     /**
578      * Gets the check message 'as is' from appropriate 'messages.properties' file.
579      *
580      * @param messages the map of messages to scan.
581      * @param messageKey the key of message in 'messages.properties' file.
582      * @param arguments the arguments of message in 'messages.properties' file.
583      * @return The message of the check with the arguments applied.
584      */
585     protected static String getCheckMessage(Map<String, String> messages, String messageKey,
586             Object... arguments) {
587         String checkMessage = null;
588         for (Map.Entry<String, String> entry : messages.entrySet()) {
589             if (messageKey.equals(entry.getKey())) {
590                 final MessageFormat formatter = new MessageFormat(entry.getValue(), Locale.ROOT);
591                 checkMessage = formatter.format(arguments);
592                 break;
593             }
594         }
595         return checkMessage;
596     }
597 
598     /**
599      * Remove device from path string for windows path.
600      *
601      * @param path path to correct.
602      * @return Path without device name.
603      */
604     private static String removeDeviceFromPathOnWindows(String path) {
605         String fixedPath = path;
606         final String os = System.getProperty("os.name", "Unix");
607         if (os.startsWith("Windows")) {
608             fixedPath = path.substring(path.indexOf(':') + 1);
609         }
610         return fixedPath;
611     }
612 
613     /**
614      * Returns an array of integers which represents the warning line numbers in the file
615      * with the given file name.
616      *
617      * @param fileName file name.
618      * @return an array of integers which represents the warning line numbers.
619      * @throws IOException if I/O exception occurs while reading the file.
620      */
621     protected Integer[] getLinesWithWarn(String fileName) throws IOException {
622         final List<Integer> result = new ArrayList<>();
623         try (BufferedReader br = Files.newBufferedReader(Path.of(fileName))) {
624             int lineNumber = 1;
625             while (true) {
626                 final String line = br.readLine();
627                 if (line == null) {
628                     break;
629                 }
630                 if (WARN_PATTERN.matcher(line).find()) {
631                     result.add(lineNumber);
632                 }
633                 lineNumber++;
634             }
635         }
636         return result.toArray(new Integer[0]);
637     }
638 
639     @Override
640     protected String getResourceLocation() {
641         return "it";
642     }
643 
644 }