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 has to be added under {@link TreeWalker}.
66           */
67          IN_TREEWALKER,
68          /**
69           * Points that checker will be created as a root of default configuration.
70           */
71          IN_CHECKER,
72  
73      }
74  
75      protected static final String ROOT_MODULE_NAME = "root";
76  
77      private static final Pattern WARN_PATTERN = CommonUtil
78              .createPattern(".* *// *warn *|/[*]\\*?\\s?warn\\s?[*]/");
79  
80      private final ByteArrayOutputStream stream = new ByteArrayOutputStream();
81  
82      /**
83       * Find the module creation option to use for the module name.
84       *
85       * @param moduleName the module name.
86       * @return the module creation option.
87       */
88      protected abstract ModuleCreationOption findModuleCreationOption(String moduleName);
89  
90      /**
91       * Returns test logger.
92       *
93       * @return logger for tests
94       */
95      protected final BriefUtLogger getBriefUtLogger() {
96          return new BriefUtLogger(stream);
97      }
98  
99      /**
100      * Creates a default module configuration {@link DefaultConfiguration} for a given object
101      * of type {@link Class}.
102      *
103      * @param clazz a {@code Class} type object.
104      * @return default module configuration for the given {@code Class} instance.
105      */
106     protected static DefaultConfiguration createModuleConfig(Class<?> clazz) {
107         return new DefaultConfiguration(clazz.getName());
108     }
109 
110     /**
111      * Returns {@link Configuration} instance for the given module name pulled
112      * from the {@code masterConfig}.
113      *
114      * @param masterConfig The master configuration to examine.
115      * @param moduleName module name.
116      * @param moduleId module id.
117      * @return {@code Configuration} instance for the given module name.
118      * @throws IllegalStateException if there is a problem retrieving the module or config.
119      */
120     protected static Configuration getModuleConfig(Configuration masterConfig, String moduleName,
121             String moduleId) {
122         final Configuration result;
123         final List<Configuration> configs = getModuleConfigs(masterConfig, moduleName);
124         if (configs.size() == 1) {
125             result = configs.getFirst();
126         }
127         else if (configs.isEmpty()) {
128             throw new IllegalStateException("no instances of the Module was found: " + moduleName);
129         }
130         else if (moduleId == null) {
131             throw new IllegalStateException("multiple instances of the same Module are detected");
132         }
133         else {
134             result = configs.stream().filter(conf -> isSameModuleId(conf, moduleId))
135             .findFirst()
136             .orElseThrow(() -> new IllegalStateException("problem with module config"));
137         }
138 
139         return result;
140     }
141 
142     /**
143      * Verifies if the configuration's ID matches the expected {@code moduleId}.
144      *
145      * @param conf The config to examine.
146      * @param moduleId The module ID to match against.
147      * @return {@code true} if it matches.
148      * @throws IllegalStateException If there is an issue with finding the ID.
149      */
150     private static boolean isSameModuleId(Configuration conf, String moduleId) {
151         try {
152             return conf.getProperty("id").equals(moduleId);
153         }
154         catch (CheckstyleException exc) {
155             throw new IllegalStateException("problem to get ID attribute from " + conf, exc);
156         }
157     }
158 
159     /**
160      * Returns a list of all {@link Configuration} instances for the given module IDs in the
161      * {@code masterConfig}.
162      *
163      * @param masterConfig The master configuration to pull results from.
164      * @param moduleIds module IDs.
165      * @return List of {@code Configuration} instances.
166      * @throws CheckstyleException if there is an error with the config.
167      */
168     protected static List<Configuration> getModuleConfigsByIds(Configuration masterConfig,
169             String... moduleIds)
170                     throws CheckstyleException {
171         final List<Configuration> result = new ArrayList<>();
172         for (Configuration currentConfig : masterConfig.getChildren()) {
173             if ("TreeWalker".equals(currentConfig.getName())) {
174                 for (Configuration moduleConfig : currentConfig.getChildren()) {
175                     final String id = getProperty(moduleConfig, "id");
176                     if (id != null && isIn(id, moduleIds)) {
177                         result.add(moduleConfig);
178                     }
179                 }
180             }
181             else {
182                 final String id = getProperty(currentConfig, "id");
183                 if (id != null && isIn(id, moduleIds)) {
184                     result.add(currentConfig);
185                 }
186             }
187         }
188         return result;
189     }
190 
191     /**
192      * Finds the specific property {@code name} in the {@code config}.
193      *
194      * @param config The configuration to examine.
195      * @param name The property name to find.
196      * @return The property value or {@code null} if not found.
197      * @throws CheckstyleException if there is an error with the config.
198      */
199     private static String getProperty(Configuration config, String name)
200             throws CheckstyleException {
201         String result = null;
202 
203         if (isIn(name, config.getPropertyNames())) {
204             result = config.getProperty(name);
205         }
206 
207         return result;
208     }
209 
210     /**
211      * Finds the specific ID in a list of IDs.
212      *
213      * @param find The ID to find.
214      * @param list The list of module IDs.
215      * @return {@code true} if the ID is in the list.
216      */
217     private static boolean isIn(String find, String... list) {
218         boolean found = false;
219 
220         for (String item : list) {
221             if (find.equals(item)) {
222                 found = true;
223                 break;
224             }
225         }
226 
227         return found;
228     }
229 
230     /**
231      * Returns a list of all {@link Configuration} instances for the given
232      * module name pulled from the {@code masterConfig}.
233      *
234      * @param masterConfig The master configuration to examine.
235      * @param moduleName module name.
236      * @return {@code Configuration} instance for the given module name.
237      */
238     private static List<Configuration> getModuleConfigs(Configuration masterConfig,
239             String moduleName) {
240         final List<Configuration> result = new ArrayList<>();
241         for (Configuration currentConfig : masterConfig.getChildren()) {
242             if ("TreeWalker".equals(currentConfig.getName())) {
243                 for (Configuration moduleConfig : currentConfig.getChildren()) {
244                     if (moduleName.equals(moduleConfig.getName())) {
245                         result.add(moduleConfig);
246                     }
247                 }
248             }
249             else if (moduleName.equals(currentConfig.getName())) {
250                 result.add(currentConfig);
251             }
252         }
253         return result;
254     }
255 
256     /**
257      * Creates {@link Checker} instance based on the given {@link Configuration} instance.
258      *
259      * @param moduleConfig {@code Configuration} instance.
260      * @return {@code Checker} instance based on the given {@code Configuration} instance.
261      * @throws Exception if an exception occurs during checker configuration.
262      */
263     protected final Checker createChecker(Configuration moduleConfig)
264             throws Exception {
265         final String name = moduleConfig.getName();
266 
267         return createChecker(moduleConfig, findModuleCreationOption(name));
268     }
269 
270     /**
271      * Creates {@link Checker} instance based on the given {@link Configuration} instance.
272      *
273      * @param moduleConfig {@code Configuration} instance.
274      * @param moduleCreationOption {@code IN_TREEWALKER} if the {@code moduleConfig} should be added
275      *                                                  under {@code TreeWalker}.
276      * @return {@code Checker} instance based on the given {@code Configuration} instance.
277      * @throws Exception if an exception occurs during checker configuration.
278      */
279     protected final Checker createChecker(Configuration moduleConfig,
280                                  ModuleCreationOption moduleCreationOption)
281             throws Exception {
282         final Checker checker = new Checker();
283         checker.setModuleClassLoader(Thread.currentThread().getContextClassLoader());
284         // make sure the tests always run with English error messages
285         // so the tests don't fail in supported locales like German
286         final Locale locale = Locale.ENGLISH;
287         checker.setLocaleCountry(locale.getCountry());
288         checker.setLocaleLanguage(locale.getLanguage());
289 
290         if (moduleCreationOption == ModuleCreationOption.IN_TREEWALKER) {
291             final Configuration config = createTreeWalkerConfig(moduleConfig);
292             checker.configure(config);
293         }
294         else if (ROOT_MODULE_NAME.equals(moduleConfig.getName())
295                 || "Checker".equals(moduleConfig.getName())) {
296             checker.configure(moduleConfig);
297         }
298         else {
299             final Configuration config = createRootConfig(moduleConfig);
300             checker.configure(config);
301         }
302         checker.addListener(getBriefUtLogger());
303         return checker;
304     }
305 
306     /**
307      * Creates {@link DefaultConfiguration} for the {@link TreeWalker}
308      * based on the given {@link Configuration} instance.
309      *
310      * @param config {@code Configuration} instance.
311      * @return {@code DefaultConfiguration} for the {@code TreeWalker}
312      *     based on the given {@code Configuration} instance.
313      */
314     protected static DefaultConfiguration createTreeWalkerConfig(Configuration config) {
315         final DefaultConfiguration rootConfig =
316                 new DefaultConfiguration(ROOT_MODULE_NAME);
317         final DefaultConfiguration twConf = createModuleConfig(TreeWalker.class);
318         // make sure that the tests always run with this charset
319         rootConfig.addProperty("charset", StandardCharsets.UTF_8.name());
320         rootConfig.addChild(twConf);
321         twConf.addChild(config);
322         return rootConfig;
323     }
324 
325     /**
326      * Creates {@link DefaultConfiguration} or the Checker.
327      * based on the the list of {@link Configuration}.
328      *
329      * @param configs list of {@code Configuration} instances.
330      * @return {@code DefaultConfiguration} for the Checker.
331      */
332     protected static DefaultConfiguration createTreeWalkerConfig(
333             List<Configuration> configs) {
334         DefaultConfiguration result = null;
335 
336         for (Configuration config : configs) {
337             if (result == null) {
338                 result = (DefaultConfiguration) createTreeWalkerConfig(config).getChildren()[0];
339             }
340             else {
341                 result.addChild(config);
342             }
343         }
344 
345         return result;
346     }
347 
348     /**
349      * Creates {@link DefaultConfiguration} for the given {@link Configuration} instance.
350      *
351      * @param config {@code Configuration} instance.
352      * @return {@code DefaultConfiguration} for the given {@code Configuration} instance.
353      */
354     protected static DefaultConfiguration createRootConfig(Configuration config) {
355         final DefaultConfiguration rootConfig = new DefaultConfiguration(ROOT_MODULE_NAME);
356         rootConfig.addChild(config);
357         return rootConfig;
358     }
359 
360     /**
361      * Returns canonical path for the file with the given file name.
362      * The path is formed base on the non-compilable resources location.
363      *
364      * @param filename file name.
365      * @return canonical path for the file with the given file name.
366      * @throws IOException if I/O exception occurs while forming the path.
367      */
368     protected final String getNonCompilablePath(String filename) throws IOException {
369         return new File("src/" + getResourceLocation() + "/resources-noncompilable/"
370                 + getPackageLocation() + "/" + filename).getCanonicalPath();
371     }
372 
373     /**
374      * Performs verification of the file with the given file name. Uses specified configuration.
375      * Expected messages are represented by the array of strings, warning line numbers are
376      * represented by the array of integers.
377      * This implementation uses overloaded
378      * {@link AbstractItModuleTestSupport#verify(Checker, File[], String, String[], Integer...)}
379      * method inside.
380      *
381      * @param config configuration.
382      * @param fileName file name to verify.
383      * @param expected an array of expected messages.
384      * @param warnsExpected an array of expected warning numbers.
385      * @throws Exception if exception occurs during verification process.
386      */
387     protected final void verify(Configuration config, String fileName, String[] expected,
388             Integer... warnsExpected)
389                     throws Exception {
390         verify(createChecker(config),
391                 new File[] {new File(fileName)},
392                 fileName, expected, warnsExpected);
393     }
394 
395     /**
396      * Performs verification of files.
397      * Uses provided {@link Checker} instance.
398      *
399      * @param checker {@code Checker} instance.
400      * @param processedFiles files to process.
401      * @param messageFileName message file name.
402      * @param expected an array of expected messages.
403      * @param warnsExpected an array of expected warning line numbers.
404      * @throws Exception if exception occurs during verification process.
405      */
406     protected final void verify(Checker checker,
407             File[] processedFiles,
408             String messageFileName,
409             String[] expected,
410             Integer... warnsExpected)
411                     throws Exception {
412         stream.flush();
413         stream.reset();
414         final List<File> theFiles = new ArrayList<>();
415         Collections.addAll(theFiles, processedFiles);
416         final List<Integer> expectedWarnings = Arrays.asList(warnsExpected);
417         final List<Integer> actualWarnings = new ArrayList<>();
418         final int errs = checker.process(theFiles);
419 
420         // process each of the lines
421         try (ByteArrayInputStream inputStream =
422                 new ByteArrayInputStream(stream.toByteArray());
423             LineNumberReader lnr = new LineNumberReader(
424                 new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
425             Integer previousLineNumber = 0;
426             for (int index = 0; index < expected.length; index++) {
427                 final String expectedResult = messageFileName + ":" + expected[index];
428                 final String actual = lnr.readLine();
429                 assertWithMessage("Error message at position %s of 'expected' does "
430                         + "not match actual message", index)
431                     .that(actual)
432                     .isEqualTo(expectedResult);
433 
434                 String parseInt = removeDeviceFromPathOnWindows(actual);
435                 parseInt = parseInt.substring(parseInt.indexOf(':') + 1);
436                 parseInt = parseInt.substring(0, parseInt.indexOf(':'));
437                 final Integer lineNumber = Integer.parseInt(parseInt);
438                 if (!previousLineNumber.equals(lineNumber)) {
439                     assertWithMessage(
440                             "input file is expected to have a warning comment on line number %s",
441                             lineNumber)
442                         .that(expectedWarnings.contains(lineNumber))
443                         .isTrue();
444 
445                     actualWarnings.add(lineNumber);
446                 }
447                 previousLineNumber = lineNumber;
448             }
449 
450             assertWithMessage("unexpected output: %s", lnr.readLine())
451                 .that(errs)
452                 .isEqualTo(expected.length);
453             assertWithMessage("warning line numbers should match expected")
454                 .that(actualWarnings)
455                 .containsExactlyElementsIn(expectedWarnings)
456                 .inOrder();
457         }
458 
459         checker.destroy();
460     }
461 
462     /**
463      * Performs the verification of the file with the given file path and config.
464      *
465      * @param config config to check against.
466      * @param filePath input file path.
467      * @throws Exception if exception occurs during verification process.
468      */
469     protected void verifyWithItConfig(Configuration config, String filePath) throws Exception {
470         final List<TestInputViolation> violations =
471             InlineConfigParser.getViolationsFromInputFile(filePath);
472         final List<String> actualViolations = getActualViolationsForFile(config, filePath);
473 
474         verifyViolations(filePath, violations, actualViolations);
475     }
476 
477     /**
478      * Tests the file with the check config.
479      *
480      * @param config check configuration.
481      * @param file input file path.
482      * @return list of actual violations.
483      * @throws Exception if exception occurs during verification process.
484      */
485     private List<String> getActualViolationsForFile(Configuration config,
486             String file)
487                     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' file.
559      *
560      * @param reporterClass the package the message is located in.
561      * @param messageKey the key of message in 'messages.properties' file.
562      * @param arguments  the arguments of message in 'messages.properties' file.
563      * @return The message of the check with the arguments applied.
564      * @throws IOException if there is a problem loading the property file.
565      */
566     protected static String getCheckMessage(
567             Class<? extends AbstractViolationReporter> reporterClass, String messageKey,
568             Object... arguments)
569                     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 }