1   ////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code for adherence to a set of rules.
3   // Copyright (C) 2001-2019 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 com.google.checkstyle.test.base;
21  
22  import static org.junit.Assert.assertEquals;
23  import static org.junit.Assert.assertTrue;
24  
25  import java.io.BufferedReader;
26  import java.io.ByteArrayInputStream;
27  import java.io.ByteArrayOutputStream;
28  import java.io.File;
29  import java.io.IOException;
30  import java.io.InputStreamReader;
31  import java.io.LineNumberReader;
32  import java.nio.charset.StandardCharsets;
33  import java.nio.file.Files;
34  import java.nio.file.Paths;
35  import java.text.MessageFormat;
36  import java.util.ArrayList;
37  import java.util.Collections;
38  import java.util.List;
39  import java.util.Locale;
40  import java.util.Map;
41  import java.util.Properties;
42  import java.util.Set;
43  import java.util.regex.Pattern;
44  
45  import com.puppycrawl.tools.checkstyle.Checker;
46  import com.puppycrawl.tools.checkstyle.ConfigurationLoader;
47  import com.puppycrawl.tools.checkstyle.DefaultConfiguration;
48  import com.puppycrawl.tools.checkstyle.PropertiesExpander;
49  import com.puppycrawl.tools.checkstyle.TreeWalker;
50  import com.puppycrawl.tools.checkstyle.api.AbstractViolationReporter;
51  import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
52  import com.puppycrawl.tools.checkstyle.api.Configuration;
53  import com.puppycrawl.tools.checkstyle.internal.utils.BriefUtLogger;
54  import com.puppycrawl.tools.checkstyle.internal.utils.CheckUtil;
55  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
56  import com.puppycrawl.tools.checkstyle.utils.ModuleReflectionUtil;
57  
58  public abstract class AbstractModuleTestSupport extends AbstractPathTestSupport {
59  
60      /**
61       * Enum to specify options for checker creation.
62       */
63      public enum ModuleCreationOption {
64  
65          /**
66           * Points that the module configurations
67           * has to be added under {@link TreeWalker}.
68           */
69          IN_TREEWALKER,
70          /**
71           * Points that checker will be created as
72           * a root of default configuration.
73           */
74          IN_CHECKER,
75  
76      }
77  
78      private static final String ROOT_MODULE_NAME = "root";
79  
80      private static final Pattern WARN_PATTERN = CommonUtil
81              .createPattern(".*[ ]*//[ ]*warn[ ]*|/[*]\\s?warn\\s?[*]/");
82  
83      private static final String XML_NAME = "/google_checks.xml";
84  
85      private static final Configuration CONFIGURATION;
86  
87      private static final Set<Class<?>> CHECKSTYLE_MODULES;
88  
89      private final ByteArrayOutputStream stream = new ByteArrayOutputStream();
90  
91      static {
92          try {
93              CONFIGURATION = ConfigurationLoader.loadConfiguration(XML_NAME,
94                      new PropertiesExpander(System.getProperties()));
95          }
96          catch (CheckstyleException ex) {
97              throw new IllegalStateException(ex);
98          }
99          try {
100             CHECKSTYLE_MODULES = CheckUtil.getCheckstyleModules();
101         }
102         catch (IOException ex) {
103             throw new IllegalStateException(ex);
104         }
105     }
106 
107     /**
108      * Returns test logger.
109      * @return logger test logger
110      */
111     protected final BriefUtLogger getBriefUtLogger() {
112         return new BriefUtLogger(stream);
113     }
114 
115     /**
116      * Returns canonical path for the file with the given file name.
117      * The path is formed base on the non-compilable resources location.
118      * This implementation uses 'src/test/resources-noncompilable/'
119      * as a non-compilable resource location.
120      * @param filename file name.
121      * @return canonical path for the file with the given file name.
122      * @throws IOException if I/O exception occurs while forming the path.
123      */
124     protected final String getNonCompilablePath(String filename) throws IOException {
125         return new File("src/it/resources-noncompilable/" + getPackageLocation() + "/"
126                 + filename).getCanonicalPath();
127     }
128 
129     /**
130      * Creates {@link DefaultConfiguration} instance for the given module class.
131      * @param clazz module class.
132      * @return {@link DefaultConfiguration} instance.
133      */
134     protected static DefaultConfiguration createModuleConfig(Class<?> clazz) {
135         return new DefaultConfiguration(clazz.getSimpleName());
136     }
137 
138     /**
139      * Creates {@link Checker} instance based on the given {@link Configuration} instance.
140      * @param moduleConfig {@link Configuration} instance.
141      * @return {@link Checker} instance based on the given {@link Configuration} instance.
142      * @throws Exception if an exception occurs during checker configuration.
143      */
144     protected final Checker createChecker(Configuration moduleConfig)
145             throws Exception {
146         final String name = moduleConfig.getName();
147         ModuleCreationOption moduleCreationOption = ModuleCreationOption.IN_CHECKER;
148 
149         for (Class<?> moduleClass : CHECKSTYLE_MODULES) {
150             if (moduleClass.getSimpleName().equals(name)
151                     || moduleClass.getSimpleName().equals(name + "Check")) {
152                 if (ModuleReflectionUtil.isCheckstyleTreeWalkerCheck(moduleClass)
153                         || ModuleReflectionUtil.isTreeWalkerFilterModule(moduleClass)) {
154                     moduleCreationOption = ModuleCreationOption.IN_TREEWALKER;
155                 }
156                 break;
157             }
158         }
159 
160         return createChecker(moduleConfig, moduleCreationOption);
161     }
162 
163     /**
164      * Creates {@link Checker} instance based on specified {@link Configuration}.
165      * @param moduleConfig {@link Configuration} instance.
166      * @param moduleCreationOption {@code IN_TREEWALKER} if the {@code moduleConfig} should be added
167      *                                                  under {@link TreeWalker}.
168      * @return {@link Checker} instance.
169      * @throws Exception if an exception occurs during checker configuration.
170      */
171     protected final Checker createChecker(Configuration moduleConfig,
172                                     ModuleCreationOption moduleCreationOption)
173             throws Exception {
174         final Configuration dc;
175 
176         if (moduleCreationOption == ModuleCreationOption.IN_TREEWALKER) {
177             dc = createTreeWalkerConfig(moduleConfig);
178         }
179         else if (ROOT_MODULE_NAME.equals(moduleConfig.getName())) {
180             dc = moduleConfig;
181         }
182         else {
183             dc = createRootConfig(moduleConfig);
184         }
185 
186         final Checker checker = new Checker();
187         // make sure the tests always run with English error messages
188         // so the tests don't fail in supported locales like German
189         final Locale locale = Locale.ENGLISH;
190         checker.setLocaleCountry(locale.getCountry());
191         checker.setLocaleLanguage(locale.getLanguage());
192         checker.setModuleClassLoader(Thread.currentThread().getContextClassLoader());
193         checker.configure(dc);
194         checker.addListener(getBriefUtLogger());
195         return checker;
196     }
197 
198     /**
199      * Creates {@link DefaultConfiguration} or the {@link Checker}.
200      * based on the given {@link Configuration}.
201      * @param config {@link Configuration} instance.
202      * @return {@link DefaultConfiguration} for the {@link Checker}.
203      */
204     protected static DefaultConfiguration createTreeWalkerConfig(Configuration config) {
205         final DefaultConfiguration dc =
206                 new DefaultConfiguration("configuration");
207         final DefaultConfiguration twConf = createModuleConfig(TreeWalker.class);
208         // make sure that the tests always run with this charset
209         dc.addAttribute("charset", "iso-8859-1");
210         dc.addChild(twConf);
211         twConf.addChild(config);
212         return dc;
213     }
214 
215     /**
216      * Creates {@link DefaultConfiguration} for the given {@link Configuration} instance.
217      * @param config {@link Configuration} instance.
218      * @return {@link DefaultConfiguration} for the given {@link Configuration} instance.
219      */
220     protected static DefaultConfiguration createRootConfig(Configuration config) {
221         final DefaultConfiguration dc = new DefaultConfiguration(ROOT_MODULE_NAME);
222         dc.addChild(config);
223         return dc;
224     }
225 
226     /**
227      * Performs verification of the file with given file name. Uses specified configuration.
228      * Expected messages are represented by the array of strings, warning line numbers are
229      * represented by the array of integers.
230      * This implementation uses overloaded
231      * {@link AbstractModuleTestSupport#verify(Checker, File[], String, String[], Integer...)}
232      * method inside.
233      * @param config configuration.
234      * @param fileName file name to verify.
235      * @param expected an array of expected messages.
236      * @param warnsExpected an array of expected warning numbers.
237      * @throws Exception if exception occurs during verification process.
238      */
239     protected final void verify(Configuration config, String fileName, String[] expected,
240             Integer... warnsExpected) throws Exception {
241         verify(createChecker(config),
242                 new File[] {new File(fileName)},
243                 fileName, expected, warnsExpected);
244     }
245 
246     /**
247      * Performs verification of files. Uses provided {@link Checker} instance.
248      * @param checker {@link Checker} instance.
249      * @param processedFiles files to process.
250      * @param messageFileName message file name.
251      * @param expected an array of expected messages.
252      * @param warnsExpected an array of expected warning line numbers.
253      * @throws Exception if exception occurs during verification process.
254      */
255     protected final void verify(Checker checker,
256             File[] processedFiles,
257             String messageFileName,
258             String[] expected,
259             Integer... warnsExpected)
260             throws Exception {
261         stream.flush();
262         stream.reset();
263         final List<File> theFiles = new ArrayList<>();
264         Collections.addAll(theFiles, processedFiles);
265         final List<Integer> theWarnings = new ArrayList<>();
266         Collections.addAll(theWarnings, warnsExpected);
267         final int errs = checker.process(theFiles);
268 
269         // process each of the lines
270         try (ByteArrayInputStream inputStream =
271                 new ByteArrayInputStream(stream.toByteArray());
272             LineNumberReader lnr = new LineNumberReader(
273                 new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
274             int previousLineNumber = 0;
275             for (int i = 0; i < expected.length; i++) {
276                 final String expectedResult = messageFileName + ":" + expected[i];
277                 final String actual = lnr.readLine();
278                 assertEquals("error message " + i, expectedResult, actual);
279 
280                 String parseInt = removeDeviceFromPathOnWindows(actual);
281                 parseInt = parseInt.substring(parseInt.indexOf(':') + 1);
282                 parseInt = parseInt.substring(0, parseInt.indexOf(':'));
283                 final int lineNumber = Integer.parseInt(parseInt);
284                 assertTrue("input file is expected to have a warning comment on line number "
285                         + lineNumber, previousLineNumber == lineNumber
286                             || theWarnings.remove((Integer) lineNumber));
287                 previousLineNumber = lineNumber;
288             }
289 
290             assertEquals("unexpected output: " + lnr.readLine(),
291                     expected.length, errs);
292             assertEquals("unexpected warnings " + theWarnings, 0, theWarnings.size());
293         }
294 
295         checker.destroy();
296     }
297 
298     /**
299      * Gets the check message 'as is' from appropriate 'messages.properties'
300      * file.
301      *
302      * @param aClass The package the message is located in.
303      * @param messageKey the key of message in 'messages.properties' file.
304      * @param arguments  the arguments of message in 'messages.properties' file.
305      * @return The message of the check with the arguments applied.
306      * @throws IOException if there is a problem loading the property file.
307      */
308     protected static String getCheckMessage(Class<? extends AbstractViolationReporter> aClass,
309             String messageKey, Object... arguments) throws IOException {
310         final Properties pr = new Properties();
311         pr.load(aClass.getResourceAsStream("messages.properties"));
312         final MessageFormat formatter = new MessageFormat(pr.getProperty(messageKey),
313                 Locale.ROOT);
314         return formatter.format(arguments);
315     }
316 
317     /**
318      * Gets the check message 'as is' from appropriate 'messages.properties' file.
319      * @param messages The map of messages to scan.
320      * @param messageKey the key of message in 'messages.properties' file.
321      * @param arguments the arguments of message in 'messages.properties' file.
322      * @return The message of the check with the arguments applied.
323      */
324     protected static String getCheckMessage(Map<String, String> messages, String messageKey,
325             Object... arguments) {
326         String checkMessage = null;
327         for (Map.Entry<String, String> entry : messages.entrySet()) {
328             if (messageKey.equals(entry.getKey())) {
329                 final MessageFormat formatter = new MessageFormat(entry.getValue(), Locale.ROOT);
330                 checkMessage = formatter.format(arguments);
331                 break;
332             }
333         }
334         return checkMessage;
335     }
336 
337     /**
338      * Returns {@link Configuration} instance for the given module name.
339      * This implementation uses {@link #getModuleConfig(String, String)} method inside.
340      * @param moduleName module name.
341      * @return {@link Configuration} instance for the given module name.
342      */
343     protected static Configuration getModuleConfig(String moduleName) {
344         return getModuleConfig(moduleName, null);
345     }
346 
347     /**
348      * Returns {@link Configuration} instance for the given module name.
349      * This implementation uses {@link #getModuleConfig(String)} method inside.
350      * @param moduleName module name.
351      * @param moduleId module id.
352      * @return {@link Configuration} instance for the given module name.
353      */
354     protected static Configuration getModuleConfig(String moduleName, String moduleId) {
355         final Configuration result;
356         final List<Configuration> configs = getModuleConfigs(moduleName);
357         if (configs.size() == 1) {
358             result = configs.get(0);
359         }
360         else if (moduleId == null) {
361             throw new IllegalStateException("multiple instances of the same Module are detected");
362         }
363         else {
364             result = configs.stream().filter(conf -> {
365                 try {
366                     return conf.getAttribute("id").equals(moduleId);
367                 }
368                 catch (CheckstyleException ex) {
369                     throw new IllegalStateException("problem to get ID attribute from " + conf, ex);
370                 }
371             })
372             .findFirst().orElseGet(null);
373         }
374 
375         return result;
376     }
377 
378     /**
379      * Returns a list of all {@link Configuration} instances for the given module name.
380      * @param moduleName module name.
381      * @return {@link Configuration} instance for the given module name.
382      */
383     protected static List<Configuration> getModuleConfigs(String moduleName) {
384         final List<Configuration> result = new ArrayList<>();
385         for (Configuration currentConfig : CONFIGURATION.getChildren()) {
386             if ("TreeWalker".equals(currentConfig.getName())) {
387                 for (Configuration moduleConfig : currentConfig.getChildren()) {
388                     if (moduleName.equals(moduleConfig.getName())) {
389                         result.add(moduleConfig);
390                     }
391                 }
392             }
393             else if (moduleName.equals(currentConfig.getName())) {
394                 result.add(currentConfig);
395             }
396         }
397         return result;
398     }
399 
400     private static String removeDeviceFromPathOnWindows(String path) {
401         String fixedPath = path;
402         final String os = System.getProperty("os.name", "Unix");
403         if (os.startsWith("Windows")) {
404             fixedPath = path.substring(path.indexOf(':') + 1);
405         }
406         return fixedPath;
407     }
408 
409     /**
410      * Returns an array of integers which represents the warning line numbers in the file
411      * with the given file name.
412      * @param fileName file name.
413      * @return an array of integers which represents the warning line numbers.
414      * @throws IOException if I/O exception occurs while reading the file.
415      */
416     protected Integer[] getLinesWithWarn(String fileName) throws IOException {
417         final List<Integer> result = new ArrayList<>();
418         try (BufferedReader br = Files.newBufferedReader(
419                 Paths.get(fileName), StandardCharsets.UTF_8)) {
420             int lineNumber = 1;
421             while (true) {
422                 final String line = br.readLine();
423                 if (line == null) {
424                     break;
425                 }
426                 if (WARN_PATTERN.matcher(line).find()) {
427                     result.add(lineNumber);
428                 }
429                 lineNumber++;
430             }
431         }
432         return result.toArray(new Integer[0]);
433     }
434 
435 }