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