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)
173                     throws CheckstyleException {
174         final List<Configuration> result = new ArrayList<>();
175         for (Configuration currentConfig : masterConfig.getChildren()) {
176             if ("TreeWalker".equals(currentConfig.getName())) {
177                 for (Configuration moduleConfig : currentConfig.getChildren()) {
178                     final String id = getProperty(moduleConfig, "id");
179                     if (id != null && isIn(id, moduleIds)) {
180                         result.add(moduleConfig);
181                     }
182                 }
183             }
184             else {
185                 final String id = getProperty(currentConfig, "id");
186                 if (id != null && isIn(id, moduleIds)) {
187                     result.add(currentConfig);
188                 }
189             }
190         }
191         return result;
192     }
193 
194     /**
195      * Finds the specific property {@code name} in the {@code config}.
196      *
197      * @param config The configuration to examine.
198      * @param name The property name to find.
199      * @return The property value or {@code null} if not found.
200      * @throws CheckstyleException if there is an error with the config.
201      */
202     private static String getProperty(Configuration config, String name)
203             throws CheckstyleException {
204         String result = null;
205 
206         if (isIn(name, config.getPropertyNames())) {
207             result = config.getProperty(name);
208         }
209 
210         return result;
211     }
212 
213     /**
214      * Finds the specific ID in a list of IDs.
215      *
216      * @param find The ID to find.
217      * @param list The list of module IDs.
218      * @return {@code true} if the ID is in the list.
219      */
220     private static boolean isIn(String find, String... list) {
221         boolean found = false;
222 
223         for (String item : list) {
224             if (find.equals(item)) {
225                 found = true;
226                 break;
227             }
228         }
229 
230         return found;
231     }
232 
233     /**
234      * Returns a list of all {@link Configuration} instances for the given
235      * module name pulled from the {@code masterConfig}.
236      *
237      * @param masterConfig The master configuration to examine.
238      * @param moduleName module name.
239      * @return {@code Configuration} instance for the given module name.
240      */
241     private static List<Configuration> getModuleConfigs(Configuration masterConfig,
242             String moduleName) {
243         final List<Configuration> result = new ArrayList<>();
244         for (Configuration currentConfig : masterConfig.getChildren()) {
245             if ("TreeWalker".equals(currentConfig.getName())) {
246                 for (Configuration moduleConfig : currentConfig.getChildren()) {
247                     if (moduleName.equals(moduleConfig.getName())) {
248                         result.add(moduleConfig);
249                     }
250                 }
251             }
252             else if (moduleName.equals(currentConfig.getName())) {
253                 result.add(currentConfig);
254             }
255         }
256         return result;
257     }
258 
259     /**
260      * Creates {@link Checker} instance based on the given {@link Configuration} instance.
261      *
262      * @param moduleConfig {@code Configuration} instance.
263      * @return {@code Checker} instance based on the given {@code Configuration} instance.
264      * @throws Exception if an exception occurs during checker configuration.
265      */
266     protected final Checker createChecker(Configuration moduleConfig)
267             throws Exception {
268         final String name = moduleConfig.getName();
269 
270         return createChecker(moduleConfig, findModuleCreationOption(name));
271     }
272 
273     /**
274      * Creates {@link Checker} instance based on the given {@link Configuration} instance.
275      *
276      * @param moduleConfig {@code Configuration} instance.
277      * @param moduleCreationOption {@code IN_TREEWALKER} if the {@code moduleConfig} should be added
278      *                                                  under {@code TreeWalker}.
279      * @return {@code Checker} instance based on the given {@code Configuration} instance.
280      * @throws Exception if an exception occurs during checker configuration.
281      */
282     protected final Checker createChecker(Configuration moduleConfig,
283                                  ModuleCreationOption moduleCreationOption)
284             throws Exception {
285         final Checker checker = new Checker();
286         checker.setModuleClassLoader(Thread.currentThread().getContextClassLoader());
287         // make sure the tests always run with English error messages
288         // so the tests don't fail in supported locales like German
289         final Locale locale = Locale.ENGLISH;
290         checker.setLocaleCountry(locale.getCountry());
291         checker.setLocaleLanguage(locale.getLanguage());
292 
293         if (moduleCreationOption == ModuleCreationOption.IN_TREEWALKER) {
294             final Configuration config = createTreeWalkerConfig(moduleConfig);
295             checker.configure(config);
296         }
297         else if (ROOT_MODULE_NAME.equals(moduleConfig.getName())
298                 || "Checker".equals(moduleConfig.getName())) {
299             checker.configure(moduleConfig);
300         }
301         else {
302             final Configuration config = createRootConfig(moduleConfig);
303             checker.configure(config);
304         }
305         checker.addListener(getBriefUtLogger());
306         return checker;
307     }
308 
309     /**
310      * Creates {@link DefaultConfiguration} for the {@link TreeWalker}
311      * based on the given {@link Configuration} instance.
312      *
313      * @param config {@code Configuration} instance.
314      * @return {@code DefaultConfiguration} for the {@code TreeWalker}
315      *     based on the given {@code Configuration} instance.
316      */
317     protected static DefaultConfiguration createTreeWalkerConfig(Configuration config) {
318         final DefaultConfiguration rootConfig =
319                 new DefaultConfiguration(ROOT_MODULE_NAME);
320         final DefaultConfiguration twConf = createModuleConfig(TreeWalker.class);
321         // make sure that the tests always run with this charset
322         rootConfig.addProperty("charset", StandardCharsets.UTF_8.name());
323         rootConfig.addChild(twConf);
324         twConf.addChild(config);
325         return rootConfig;
326     }
327 
328     /**
329      * Creates {@link DefaultConfiguration} or the Checker.
330      * based on the the list of {@link Configuration}.
331      *
332      * @param configs list of {@code Configuration} instances.
333      * @return {@code DefaultConfiguration} for the Checker.
334      */
335     protected static DefaultConfiguration createTreeWalkerConfig(
336             List<Configuration> configs) {
337         DefaultConfiguration result = null;
338 
339         for (Configuration config : configs) {
340             if (result == null) {
341                 result = (DefaultConfiguration) createTreeWalkerConfig(config).getChildren()[0];
342             }
343             else {
344                 result.addChild(config);
345             }
346         }
347 
348         return result;
349     }
350 
351     /**
352      * Creates {@link DefaultConfiguration} for the given {@link Configuration} instance.
353      *
354      * @param config {@code Configuration} instance.
355      * @return {@code DefaultConfiguration} for the given {@code Configuration} instance.
356      */
357     protected static DefaultConfiguration createRootConfig(Configuration config) {
358         final DefaultConfiguration rootConfig = new DefaultConfiguration(ROOT_MODULE_NAME);
359         rootConfig.addChild(config);
360         return rootConfig;
361     }
362 
363     /**
364      * Returns canonical path for the file with the given file name.
365      * The path is formed base on the non-compilable resources location.
366      *
367      * @param filename file name.
368      * @return canonical path for the file with the given file name.
369      * @throws IOException if I/O exception occurs while forming the path.
370      */
371     protected final String getNonCompilablePath(String filename) throws IOException {
372         return new File("src/" + getResourceLocation() + "/resources-noncompilable/"
373                 + getPackageLocation() + "/" + filename).getCanonicalPath();
374     }
375 
376     /**
377      * Performs verification of the file with the given file name. Uses specified configuration.
378      * Expected messages are represented by the array of strings, warning line numbers are
379      * represented by the array of integers.
380      * This implementation uses overloaded
381      * {@link AbstractItModuleTestSupport#verify(Checker, File[], String, String[], Integer...)}
382      * method inside.
383      *
384      * @param config configuration.
385      * @param fileName file name to verify.
386      * @param expected an array of expected messages.
387      * @param warnsExpected an array of expected warning numbers.
388      * @throws Exception if exception occurs during verification process.
389      */
390     protected final void verify(Configuration config, String fileName, String[] expected,
391             Integer... warnsExpected)
392                     throws Exception {
393         verify(createChecker(config),
394                 new File[] {new File(fileName)},
395                 fileName, expected, warnsExpected);
396     }
397 
398     /**
399      * Performs verification of files.
400      * Uses provided {@link Checker} instance.
401      *
402      * @param checker {@code Checker} instance.
403      * @param processedFiles files to process.
404      * @param messageFileName message file name.
405      * @param expected an array of expected messages.
406      * @param warnsExpected an array of expected warning line numbers.
407      * @throws Exception if exception occurs during verification process.
408      */
409     protected final void verify(Checker checker,
410             File[] processedFiles,
411             String messageFileName,
412             String[] expected,
413             Integer... warnsExpected)
414                     throws Exception {
415         stream.flush();
416         stream.reset();
417         final List<File> theFiles = new ArrayList<>();
418         Collections.addAll(theFiles, processedFiles);
419         final List<Integer> expectedWarnings = Arrays.asList(warnsExpected);
420         final List<Integer> actualWarnings = new ArrayList<>();
421         final int errs = checker.process(theFiles);
422 
423         // process each of the lines
424         try (ByteArrayInputStream inputStream =
425                 new ByteArrayInputStream(stream.toByteArray());
426             LineNumberReader lnr = new LineNumberReader(
427                 new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
428             Integer previousLineNumber = 0;
429             for (int index = 0; index < expected.length; index++) {
430                 final String expectedResult = messageFileName + ":" + expected[index];
431                 final String actual = lnr.readLine();
432                 assertWithMessage("Error message at position %s of 'expected' does "
433                         + "not match actual message", index)
434                     .that(actual)
435                     .isEqualTo(expectedResult);
436 
437                 String parseInt = removeDeviceFromPathOnWindows(actual);
438                 parseInt = parseInt.substring(parseInt.indexOf(':') + 1);
439                 parseInt = parseInt.substring(0, parseInt.indexOf(':'));
440                 final Integer lineNumber = Integer.parseInt(parseInt);
441                 if (!previousLineNumber.equals(lineNumber)) {
442                     assertWithMessage(
443                             "input file is expected to have a warning comment on line number %s",
444                             lineNumber)
445                         .that(expectedWarnings.contains(lineNumber))
446                         .isTrue();
447 
448                     actualWarnings.add(lineNumber);
449                 }
450                 previousLineNumber = lineNumber;
451             }
452 
453             assertWithMessage("unexpected output: %s", lnr.readLine())
454                 .that(errs)
455                 .isEqualTo(expected.length);
456             assertWithMessage("warning line numbers should match expected")
457                 .that(actualWarnings)
458                 .containsExactlyElementsIn(expectedWarnings)
459                 .inOrder();
460         }
461 
462         checker.destroy();
463     }
464 
465     /**
466      * Performs the verification of the file with the given file path and config.
467      *
468      * @param config config to check against.
469      * @param filePath input file path.
470      * @throws Exception if exception occurs during verification process.
471      */
472     protected void verifyWithItConfig(Configuration config, String filePath) throws Exception {
473         final List<TestInputViolation> violations =
474             InlineConfigParser.getViolationsFromInputFile(filePath);
475         final List<String> actualViolations = getActualViolationsForFile(config, filePath);
476 
477         verifyViolations(filePath, violations, actualViolations);
478     }
479 
480     /**
481      * Tests the file with the check config.
482      *
483      * @param config check configuration.
484      * @param file input file path.
485      * @return list of actual violations.
486      * @throws Exception if exception occurs during verification process.
487      */
488     private List<String> getActualViolationsForFile(Configuration config,
489             String file)
490                     throws Exception {
491         stream.flush();
492         stream.reset();
493         final List<File> files = Collections.singletonList(new File(file));
494         final Checker checker = createChecker(config);
495         final Map<String, List<String>> actualViolations =
496                 getActualViolations(checker.process(files));
497         checker.destroy();
498         return actualViolations.getOrDefault(file, new ArrayList<>());
499     }
500 
501     /**
502      * Returns the actual violations for each file that has been checked against {@link Checker}.
503      * Each file is mapped to their corresponding violation messages. Reads input stream for these
504      * messages using instance of {@link InputStreamReader}.
505      *
506      * @param errorCount count of errors after checking set of files against {@code Checker}.
507      * @return a {@link Map} object containing file names and the corresponding violation messages.
508      * @throws IOException exception can occur when reading input stream.
509      */
510     private Map<String, List<String>> getActualViolations(int errorCount) throws IOException {
511         // process each of the lines
512         try (ByteArrayInputStream inputStream =
513                      new ByteArrayInputStream(stream.toByteArray());
514              LineNumberReader lnr = new LineNumberReader(
515                      new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
516             final Map<String, List<String>> actualViolations = new HashMap<>();
517             for (String line = lnr.readLine(); line != null && lnr.getLineNumber() <= errorCount;
518                  line = lnr.readLine()) {
519                 // have at least 2 characters before the splitting colon,
520                 // to not split after the drive letter on Windows
521                 final String[] actualViolation = line.split("(?<=.{2}):", 2);
522                 final String actualViolationFileName = actualViolation[0];
523                 final String actualViolationMessage = actualViolation[1];
524 
525                 actualViolations
526                         .computeIfAbsent(actualViolationFileName, key -> new ArrayList<>())
527                         .add(actualViolationMessage);
528             }
529 
530             return actualViolations;
531         }
532     }
533 
534     /**
535      * Performs verification of violation lines.
536      *
537      * @param file file path.
538      * @param testInputViolations List of TestInputViolation objects.
539      * @param actualViolations for a file
540      */
541     private static void verifyViolations(String file, List<TestInputViolation> testInputViolations,
542           List<String> actualViolations) {
543         final List<Integer> actualViolationLines = actualViolations.stream()
544                 .map(violation -> violation.substring(0, violation.indexOf(':')))
545                 .map(Integer::valueOf)
546                 .toList();
547         final List<Integer> expectedViolationLines = testInputViolations.stream()
548                 .map(TestInputViolation::getLineNo)
549                 .toList();
550         assertWithMessage("Violation lines for %s differ.", file)
551                 .that(actualViolationLines)
552                 .isEqualTo(expectedViolationLines);
553         for (int index = 0; index < actualViolations.size(); index++) {
554             assertWithMessage("Actual and expected violations differ.")
555                     .that(actualViolations.get(index))
556                     .matches(testInputViolations.get(index).toRegex());
557         }
558     }
559 
560     /**
561      * Gets the check message 'as is' from appropriate 'messages.properties'
562      * file.
563      *
564      * @param reporterClass the package the message is located in.
565      * @param messageKey the key of message in 'messages.properties' file.
566      * @param arguments  the arguments of message in 'messages.properties' file.
567      * @return The message of the check with the arguments applied.
568      * @throws IOException if there is a problem loading the property file.
569      */
570     protected static String getCheckMessage(
571             Class<? extends AbstractViolationReporter> reporterClass, String messageKey,
572             Object... arguments)
573                     throws IOException {
574         final Properties pr = new Properties();
575         pr.load(reporterClass.getResourceAsStream("messages.properties"));
576         final MessageFormat formatter = new MessageFormat(pr.getProperty(messageKey),
577                 Locale.ROOT);
578         return formatter.format(arguments);
579     }
580 
581     /**
582      * Gets the check message 'as is' from appropriate 'messages.properties' file.
583      *
584      * @param messages the map of messages to scan.
585      * @param messageKey the key of message in 'messages.properties' file.
586      * @param arguments the arguments of message in 'messages.properties' file.
587      * @return The message of the check with the arguments applied.
588      */
589     protected static String getCheckMessage(Map<String, String> messages, String messageKey,
590             Object... arguments) {
591         String checkMessage = null;
592         for (Map.Entry<String, String> entry : messages.entrySet()) {
593             if (messageKey.equals(entry.getKey())) {
594                 final MessageFormat formatter = new MessageFormat(entry.getValue(), Locale.ROOT);
595                 checkMessage = formatter.format(arguments);
596                 break;
597             }
598         }
599         return checkMessage;
600     }
601 
602     /**
603      * Remove device from path string for windows path.
604      *
605      * @param path path to correct.
606      * @return Path without device name.
607      */
608     private static String removeDeviceFromPathOnWindows(String path) {
609         String fixedPath = path;
610         final String os = System.getProperty("os.name", "Unix");
611         if (os.startsWith("Windows")) {
612             fixedPath = path.substring(path.indexOf(':') + 1);
613         }
614         return fixedPath;
615     }
616 
617     /**
618      * Returns an array of integers which represents the warning line numbers in the file
619      * with the given file name.
620      *
621      * @param fileName file name.
622      * @return an array of integers which represents the warning line numbers.
623      * @throws IOException if I/O exception occurs while reading the file.
624      */
625     protected Integer[] getLinesWithWarn(String fileName) throws IOException {
626         final List<Integer> result = new ArrayList<>();
627         try (BufferedReader br = Files.newBufferedReader(Path.of(fileName))) {
628             int lineNumber = 1;
629             while (true) {
630                 final String line = br.readLine();
631                 if (line == null) {
632                     break;
633                 }
634                 if (WARN_PATTERN.matcher(line).find()) {
635                     result.add(lineNumber);
636                 }
637                 lineNumber++;
638             }
639         }
640         return result.toArray(new Integer[0]);
641     }
642 
643     @Override
644     protected String getResourceLocation() {
645         return "it";
646     }
647 
648 }