View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2024 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.Paths;
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  import java.util.stream.Collectors;
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 {@link Class} type object.
106      * @return default module configuration for the given {@link 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 {@link 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.get(0);
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 ex) {
158             throw new IllegalStateException("problem to get ID attribute from " + conf, ex);
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 {@link 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 {@link 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 {@link Configuration} instance.
262      * @return {@link Checker} instance based on the given {@link 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 {@link Configuration} instance.
276      * @param moduleCreationOption {@code IN_TREEWALKER} if the {@code moduleConfig} should be added
277      *                                                  under {@link TreeWalker}.
278      * @return {@link Checker} instance based on the given {@link 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 {@link Configuration} instance.
313      * @return {@link DefaultConfiguration} for the {@link TreeWalker}
314      *     based on the given {@link 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 {@link Configuration} instances.
332      * @return {@link 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 {@link Configuration} instance.
354      * @return {@link DefaultConfiguration} for the given {@link 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 {@link 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> theWarnings = new ArrayList<>();
418         Collections.addAll(theWarnings, warnsExpected);
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             int 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 int lineNumber = Integer.parseInt(parseInt);
439                 assertWithMessage(
440                         "input file is expected to have a warning comment on line number %s",
441                         lineNumber)
442                     .that(previousLineNumber == lineNumber
443                             || theWarnings.remove((Integer) lineNumber))
444                     .isTrue();
445                 previousLineNumber = lineNumber;
446             }
447 
448             assertWithMessage("unexpected output: %s", lnr.readLine())
449                 .that(errs)
450                 .isEqualTo(expected.length);
451             assertWithMessage("unexpected warnings %s", theWarnings)
452                 .that(theWarnings)
453                 .isEmpty();
454         }
455 
456         checker.destroy();
457     }
458 
459     /**
460      * Performs the verification of the file with the given file path and config.
461      *
462      * @param config config to check against.
463      * @param filePath input file path.
464      * @throws Exception if exception occurs during verification process.
465      */
466     protected void verifyWithItConfig(Configuration config, String filePath) throws Exception {
467         final List<TestInputViolation> violations =
468             InlineConfigParser.getViolationsFromInputFile(filePath);
469         final List<String> actualViolations = getActualViolationsForFile(config, filePath);
470 
471         verifyViolations(filePath, violations, actualViolations);
472     }
473 
474     /**
475      * Tests the file with the check config.
476      *
477      * @param config check configuration.
478      * @param file input file path.
479      * @return list of actual violations.
480      * @throws Exception if exception occurs during verification process.
481      */
482     private List<String> getActualViolationsForFile(Configuration config,
483           String file) throws Exception {
484         stream.flush();
485         stream.reset();
486         final List<File> files = Collections.singletonList(new File(file));
487         final Checker checker = createChecker(config);
488         final Map<String, List<String>> actualViolations =
489                 getActualViolations(checker.process(files));
490         checker.destroy();
491         return actualViolations.getOrDefault(file, new ArrayList<>());
492     }
493 
494     /**
495      * Returns the actual violations for each file that has been checked against {@link Checker}.
496      * Each file is mapped to their corresponding violation messages. Reads input stream for these
497      * messages using instance of {@link InputStreamReader}.
498      *
499      * @param errorCount count of errors after checking set of files against {@link Checker}.
500      * @return a {@link Map} object containing file names and the corresponding violation messages.
501      * @throws IOException exception can occur when reading input stream.
502      */
503     private Map<String, List<String>> getActualViolations(int errorCount) throws IOException {
504         // process each of the lines
505         try (ByteArrayInputStream inputStream =
506                      new ByteArrayInputStream(stream.toByteArray());
507              LineNumberReader lnr = new LineNumberReader(
508                      new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
509             final Map<String, List<String>> actualViolations = new HashMap<>();
510             for (String line = lnr.readLine(); line != null && lnr.getLineNumber() <= errorCount;
511                  line = lnr.readLine()) {
512                 // have at least 2 characters before the splitting colon,
513                 // to not split after the drive letter on Windows
514                 final String[] actualViolation = line.split("(?<=.{2}):", 2);
515                 final String actualViolationFileName = actualViolation[0];
516                 final String actualViolationMessage = actualViolation[1];
517 
518                 actualViolations
519                         .computeIfAbsent(actualViolationFileName, key -> new ArrayList<>())
520                         .add(actualViolationMessage);
521             }
522 
523             return actualViolations;
524         }
525     }
526 
527     /**
528      * Performs verification of violation lines.
529      *
530      * @param file file path.
531      * @param testInputViolations List of TestInputViolation objects.
532      * @param actualViolations for a file
533      */
534     private static void verifyViolations(String file, List<TestInputViolation> testInputViolations,
535           List<String> actualViolations) {
536         final List<Integer> actualViolationLines = actualViolations.stream()
537                 .map(violation -> violation.substring(0, violation.indexOf(':')))
538                 .map(Integer::valueOf)
539                 .collect(Collectors.toUnmodifiableList());
540         final List<Integer> expectedViolationLines = testInputViolations.stream()
541                 .map(TestInputViolation::getLineNo)
542                 .collect(Collectors.toUnmodifiableList());
543         assertWithMessage("Violation lines for %s differ.", file)
544                 .that(actualViolationLines)
545                 .isEqualTo(expectedViolationLines);
546         for (int index = 0; index < actualViolations.size(); index++) {
547             assertWithMessage("Actual and expected violations differ.")
548                     .that(actualViolations.get(index))
549                     .matches(testInputViolations.get(index).toRegex());
550         }
551     }
552 
553     /**
554      * Gets the check message 'as is' from appropriate 'messages.properties'
555      * file.
556      *
557      * @param aClass the package the message is located in.
558      * @param messageKey the key of message in 'messages.properties' file.
559      * @param arguments  the arguments of message in 'messages.properties' file.
560      * @return The message of the check with the arguments applied.
561      * @throws IOException if there is a problem loading the property file.
562      */
563     protected static String getCheckMessage(Class<? extends AbstractViolationReporter> aClass,
564             String messageKey, Object... arguments) throws IOException {
565         final Properties pr = new Properties();
566         pr.load(aClass.getResourceAsStream("messages.properties"));
567         final MessageFormat formatter = new MessageFormat(pr.getProperty(messageKey),
568                 Locale.ROOT);
569         return formatter.format(arguments);
570     }
571 
572     /**
573      * Gets the check message 'as is' from appropriate 'messages.properties' file.
574      *
575      * @param messages the map of messages to scan.
576      * @param messageKey the key of message in 'messages.properties' file.
577      * @param arguments the arguments of message in 'messages.properties' file.
578      * @return The message of the check with the arguments applied.
579      */
580     protected static String getCheckMessage(Map<String, String> messages, String messageKey,
581             Object... arguments) {
582         String checkMessage = null;
583         for (Map.Entry<String, String> entry : messages.entrySet()) {
584             if (messageKey.equals(entry.getKey())) {
585                 final MessageFormat formatter = new MessageFormat(entry.getValue(), Locale.ROOT);
586                 checkMessage = formatter.format(arguments);
587                 break;
588             }
589         }
590         return checkMessage;
591     }
592 
593     /**
594      * Remove device from path string for windows path.
595      *
596      * @param path path to correct.
597      * @return Path without device name.
598      */
599     private static String removeDeviceFromPathOnWindows(String path) {
600         String fixedPath = path;
601         final String os = System.getProperty("os.name", "Unix");
602         if (os.startsWith("Windows")) {
603             fixedPath = path.substring(path.indexOf(':') + 1);
604         }
605         return fixedPath;
606     }
607 
608     /**
609      * Returns an array of integers which represents the warning line numbers in the file
610      * with the given file name.
611      *
612      * @param fileName file name.
613      * @return an array of integers which represents the warning line numbers.
614      * @throws IOException if I/O exception occurs while reading the file.
615      */
616     protected Integer[] getLinesWithWarn(String fileName) throws IOException {
617         final List<Integer> result = new ArrayList<>();
618         try (BufferedReader br = Files.newBufferedReader(
619                 Paths.get(fileName), StandardCharsets.UTF_8)) {
620             int lineNumber = 1;
621             while (true) {
622                 final String line = br.readLine();
623                 if (line == null) {
624                     break;
625                 }
626                 if (WARN_PATTERN.matcher(line).find()) {
627                     result.add(lineNumber);
628                 }
629                 lineNumber++;
630             }
631         }
632         return result.toArray(new Integer[0]);
633     }
634 
635     @Override
636     protected String getResourceLocation() {
637         return "it";
638     }
639 
640 }