001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2019 the original author or authors. 004// 005// This library is free software; you can redistribute it and/or 006// modify it under the terms of the GNU Lesser General Public 007// License as published by the Free Software Foundation; either 008// version 2.1 of the License, or (at your option) any later version. 009// 010// This library is distributed in the hope that it will be useful, 011// but WITHOUT ANY WARRANTY; without even the implied warranty of 012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 013// Lesser General Public License for more details. 014// 015// You should have received a copy of the GNU Lesser General Public 016// License along with this library; if not, write to the Free Software 017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 018//////////////////////////////////////////////////////////////////////////////// 019 020package com.puppycrawl.tools.checkstyle; 021 022import java.io.File; 023import java.io.IOException; 024import java.io.PrintWriter; 025import java.io.StringWriter; 026import java.io.UnsupportedEncodingException; 027import java.nio.charset.Charset; 028import java.nio.charset.StandardCharsets; 029import java.util.ArrayList; 030import java.util.HashSet; 031import java.util.List; 032import java.util.Locale; 033import java.util.Set; 034import java.util.SortedSet; 035import java.util.TreeSet; 036import java.util.stream.Collectors; 037 038import org.apache.commons.logging.Log; 039import org.apache.commons.logging.LogFactory; 040 041import com.puppycrawl.tools.checkstyle.api.AuditEvent; 042import com.puppycrawl.tools.checkstyle.api.AuditListener; 043import com.puppycrawl.tools.checkstyle.api.AutomaticBean; 044import com.puppycrawl.tools.checkstyle.api.BeforeExecutionFileFilter; 045import com.puppycrawl.tools.checkstyle.api.BeforeExecutionFileFilterSet; 046import com.puppycrawl.tools.checkstyle.api.CheckstyleException; 047import com.puppycrawl.tools.checkstyle.api.Configuration; 048import com.puppycrawl.tools.checkstyle.api.Context; 049import com.puppycrawl.tools.checkstyle.api.ExternalResourceHolder; 050import com.puppycrawl.tools.checkstyle.api.FileSetCheck; 051import com.puppycrawl.tools.checkstyle.api.FileText; 052import com.puppycrawl.tools.checkstyle.api.Filter; 053import com.puppycrawl.tools.checkstyle.api.FilterSet; 054import com.puppycrawl.tools.checkstyle.api.LocalizedMessage; 055import com.puppycrawl.tools.checkstyle.api.MessageDispatcher; 056import com.puppycrawl.tools.checkstyle.api.RootModule; 057import com.puppycrawl.tools.checkstyle.api.SeverityLevel; 058import com.puppycrawl.tools.checkstyle.api.SeverityLevelCounter; 059import com.puppycrawl.tools.checkstyle.utils.CommonUtil; 060 061/** 062 * This class provides the functionality to check a set of files. 063 */ 064public class Checker extends AutomaticBean implements MessageDispatcher, RootModule { 065 066 /** Message to use when an exception occurs and should be printed as a violation. */ 067 public static final String EXCEPTION_MSG = "general.exception"; 068 069 /** Logger for Checker. */ 070 private final Log log; 071 072 /** Maintains error count. */ 073 private final SeverityLevelCounter counter = new SeverityLevelCounter( 074 SeverityLevel.ERROR); 075 076 /** Vector of listeners. */ 077 private final List<AuditListener> listeners = new ArrayList<>(); 078 079 /** Vector of fileset checks. */ 080 private final List<FileSetCheck> fileSetChecks = new ArrayList<>(); 081 082 /** The audit event before execution file filters. */ 083 private final BeforeExecutionFileFilterSet beforeExecutionFileFilters = 084 new BeforeExecutionFileFilterSet(); 085 086 /** The audit event filters. */ 087 private final FilterSet filters = new FilterSet(); 088 089 /** Class loader to resolve classes with. **/ 090 private ClassLoader classLoader = Thread.currentThread() 091 .getContextClassLoader(); 092 093 /** The basedir to strip off in file names. */ 094 private String basedir; 095 096 /** Locale country to report messages . **/ 097 private String localeCountry = Locale.getDefault().getCountry(); 098 /** Locale language to report messages . **/ 099 private String localeLanguage = Locale.getDefault().getLanguage(); 100 101 /** The factory for instantiating submodules. */ 102 private ModuleFactory moduleFactory; 103 104 /** The classloader used for loading Checkstyle module classes. */ 105 private ClassLoader moduleClassLoader; 106 107 /** The context of all child components. */ 108 private Context childContext; 109 110 /** The file extensions that are accepted. */ 111 private String[] fileExtensions = CommonUtil.EMPTY_STRING_ARRAY; 112 113 /** 114 * The severity level of any violations found by submodules. 115 * The value of this property is passed to submodules via 116 * contextualize(). 117 * 118 * <p>Note: Since the Checker is merely a container for modules 119 * it does not make sense to implement logging functionality 120 * here. Consequently Checker does not extend AbstractViolationReporter, 121 * leading to a bit of duplicated code for severity level setting. 122 */ 123 private SeverityLevel severity = SeverityLevel.ERROR; 124 125 /** Name of a charset. */ 126 private String charset = System.getProperty("file.encoding", StandardCharsets.UTF_8.name()); 127 128 /** Cache file. **/ 129 private PropertyCacheFile cacheFile; 130 131 /** Controls whether exceptions should halt execution or not. */ 132 private boolean haltOnException = true; 133 134 /** 135 * Creates a new {@code Checker} instance. 136 * The instance needs to be contextualized and configured. 137 */ 138 public Checker() { 139 addListener(counter); 140 log = LogFactory.getLog(Checker.class); 141 } 142 143 /** 144 * Sets cache file. 145 * @param fileName the cache file. 146 * @throws IOException if there are some problems with file loading. 147 */ 148 public void setCacheFile(String fileName) throws IOException { 149 final Configuration configuration = getConfiguration(); 150 cacheFile = new PropertyCacheFile(configuration, fileName); 151 cacheFile.load(); 152 } 153 154 /** 155 * Removes before execution file filter. 156 * @param filter before execution file filter to remove. 157 */ 158 public void removeBeforeExecutionFileFilter(BeforeExecutionFileFilter filter) { 159 beforeExecutionFileFilters.removeBeforeExecutionFileFilter(filter); 160 } 161 162 /** 163 * Removes filter. 164 * @param filter filter to remove. 165 */ 166 public void removeFilter(Filter filter) { 167 filters.removeFilter(filter); 168 } 169 170 @Override 171 public void destroy() { 172 listeners.clear(); 173 fileSetChecks.clear(); 174 beforeExecutionFileFilters.clear(); 175 filters.clear(); 176 if (cacheFile != null) { 177 try { 178 cacheFile.persist(); 179 } 180 catch (IOException ex) { 181 throw new IllegalStateException("Unable to persist cache file.", ex); 182 } 183 } 184 } 185 186 /** 187 * Removes a given listener. 188 * @param listener a listener to remove 189 */ 190 public void removeListener(AuditListener listener) { 191 listeners.remove(listener); 192 } 193 194 /** 195 * Sets base directory. 196 * @param basedir the base directory to strip off in file names 197 */ 198 public void setBasedir(String basedir) { 199 this.basedir = basedir; 200 } 201 202 @Override 203 public int process(List<File> files) throws CheckstyleException { 204 if (cacheFile != null) { 205 cacheFile.putExternalResources(getExternalResourceLocations()); 206 } 207 208 // Prepare to start 209 fireAuditStarted(); 210 for (final FileSetCheck fsc : fileSetChecks) { 211 fsc.beginProcessing(charset); 212 } 213 214 final List<File> targetFiles = files.stream() 215 .filter(file -> CommonUtil.matchesFileExtension(file, fileExtensions)) 216 .collect(Collectors.toList()); 217 processFiles(targetFiles); 218 219 // Finish up 220 // It may also log!!! 221 fileSetChecks.forEach(FileSetCheck::finishProcessing); 222 223 // It may also log!!! 224 fileSetChecks.forEach(FileSetCheck::destroy); 225 226 final int errorCount = counter.getCount(); 227 fireAuditFinished(); 228 return errorCount; 229 } 230 231 /** 232 * Returns a set of external configuration resource locations which are used by all file set 233 * checks and filters. 234 * @return a set of external configuration resource locations which are used by all file set 235 * checks and filters. 236 */ 237 private Set<String> getExternalResourceLocations() { 238 final Set<String> externalResources = new HashSet<>(); 239 fileSetChecks.stream().filter(check -> check instanceof ExternalResourceHolder) 240 .forEach(check -> { 241 final Set<String> locations = 242 ((ExternalResourceHolder) check).getExternalResourceLocations(); 243 externalResources.addAll(locations); 244 }); 245 filters.getFilters().stream().filter(filter -> filter instanceof ExternalResourceHolder) 246 .forEach(filter -> { 247 final Set<String> locations = 248 ((ExternalResourceHolder) filter).getExternalResourceLocations(); 249 externalResources.addAll(locations); 250 }); 251 return externalResources; 252 } 253 254 /** Notify all listeners about the audit start. */ 255 private void fireAuditStarted() { 256 final AuditEvent event = new AuditEvent(this); 257 for (final AuditListener listener : listeners) { 258 listener.auditStarted(event); 259 } 260 } 261 262 /** Notify all listeners about the audit end. */ 263 private void fireAuditFinished() { 264 final AuditEvent event = new AuditEvent(this); 265 for (final AuditListener listener : listeners) { 266 listener.auditFinished(event); 267 } 268 } 269 270 /** 271 * Processes a list of files with all FileSetChecks. 272 * @param files a list of files to process. 273 * @throws CheckstyleException if error condition within Checkstyle occurs. 274 * @noinspection ProhibitedExceptionThrown 275 */ 276 //-@cs[CyclomaticComplexity] no easy way to split this logic of processing the file 277 private void processFiles(List<File> files) throws CheckstyleException { 278 for (final File file : files) { 279 String fileName = null; 280 try { 281 fileName = file.getAbsolutePath(); 282 final long timestamp = file.lastModified(); 283 if (cacheFile != null && cacheFile.isInCache(fileName, timestamp) 284 || !acceptFileStarted(fileName)) { 285 continue; 286 } 287 if (cacheFile != null) { 288 cacheFile.put(fileName, timestamp); 289 } 290 fireFileStarted(fileName); 291 final SortedSet<LocalizedMessage> fileMessages = processFile(file); 292 fireErrors(fileName, fileMessages); 293 fireFileFinished(fileName); 294 } 295 // -@cs[IllegalCatch] There is no other way to deliver filename that was under 296 // processing. See https://github.com/checkstyle/checkstyle/issues/2285 297 catch (Exception ex) { 298 if (fileName != null && cacheFile != null) { 299 cacheFile.remove(fileName); 300 } 301 302 // We need to catch all exceptions to put a reason failure (file name) in exception 303 throw new CheckstyleException("Exception was thrown while processing " 304 + file.getPath(), ex); 305 } 306 catch (Error error) { 307 if (fileName != null && cacheFile != null) { 308 cacheFile.remove(fileName); 309 } 310 311 // We need to catch all errors to put a reason failure (file name) in error 312 throw new Error("Error was thrown while processing " + file.getPath(), error); 313 } 314 } 315 } 316 317 /** 318 * Processes a file with all FileSetChecks. 319 * @param file a file to process. 320 * @return a sorted set of messages to be logged. 321 * @throws CheckstyleException if error condition within Checkstyle occurs. 322 * @noinspection ProhibitedExceptionThrown 323 */ 324 private SortedSet<LocalizedMessage> processFile(File file) throws CheckstyleException { 325 final SortedSet<LocalizedMessage> fileMessages = new TreeSet<>(); 326 try { 327 final FileText theText = new FileText(file.getAbsoluteFile(), charset); 328 for (final FileSetCheck fsc : fileSetChecks) { 329 fileMessages.addAll(fsc.process(file, theText)); 330 } 331 } 332 catch (final IOException ioe) { 333 log.debug("IOException occurred.", ioe); 334 fileMessages.add(new LocalizedMessage(1, 335 Definitions.CHECKSTYLE_BUNDLE, EXCEPTION_MSG, 336 new String[] {ioe.getMessage()}, null, getClass(), null)); 337 } 338 // -@cs[IllegalCatch] There is no other way to obey haltOnException field 339 catch (Exception ex) { 340 if (haltOnException) { 341 throw ex; 342 } 343 344 log.debug("Exception occurred.", ex); 345 346 final StringWriter sw = new StringWriter(); 347 final PrintWriter pw = new PrintWriter(sw, true); 348 349 ex.printStackTrace(pw); 350 351 fileMessages.add(new LocalizedMessage(1, 352 Definitions.CHECKSTYLE_BUNDLE, EXCEPTION_MSG, 353 new String[] {sw.getBuffer().toString()}, 354 null, getClass(), null)); 355 } 356 return fileMessages; 357 } 358 359 /** 360 * Check if all before execution file filters accept starting the file. 361 * 362 * @param fileName 363 * the file to be audited 364 * @return {@code true} if the file is accepted. 365 */ 366 private boolean acceptFileStarted(String fileName) { 367 final String stripped = CommonUtil.relativizeAndNormalizePath(basedir, fileName); 368 return beforeExecutionFileFilters.accept(stripped); 369 } 370 371 /** 372 * Notify all listeners about the beginning of a file audit. 373 * 374 * @param fileName 375 * the file to be audited 376 */ 377 @Override 378 public void fireFileStarted(String fileName) { 379 final String stripped = CommonUtil.relativizeAndNormalizePath(basedir, fileName); 380 final AuditEvent event = new AuditEvent(this, stripped); 381 for (final AuditListener listener : listeners) { 382 listener.fileStarted(event); 383 } 384 } 385 386 /** 387 * Notify all listeners about the errors in a file. 388 * 389 * @param fileName the audited file 390 * @param errors the audit errors from the file 391 */ 392 @Override 393 public void fireErrors(String fileName, SortedSet<LocalizedMessage> errors) { 394 final String stripped = CommonUtil.relativizeAndNormalizePath(basedir, fileName); 395 boolean hasNonFilteredViolations = false; 396 for (final LocalizedMessage element : errors) { 397 final AuditEvent event = new AuditEvent(this, stripped, element); 398 if (filters.accept(event)) { 399 hasNonFilteredViolations = true; 400 for (final AuditListener listener : listeners) { 401 listener.addError(event); 402 } 403 } 404 } 405 if (hasNonFilteredViolations && cacheFile != null) { 406 cacheFile.remove(fileName); 407 } 408 } 409 410 /** 411 * Notify all listeners about the end of a file audit. 412 * 413 * @param fileName 414 * the audited file 415 */ 416 @Override 417 public void fireFileFinished(String fileName) { 418 final String stripped = CommonUtil.relativizeAndNormalizePath(basedir, fileName); 419 final AuditEvent event = new AuditEvent(this, stripped); 420 for (final AuditListener listener : listeners) { 421 listener.fileFinished(event); 422 } 423 } 424 425 @Override 426 protected void finishLocalSetup() throws CheckstyleException { 427 final Locale locale = new Locale(localeLanguage, localeCountry); 428 LocalizedMessage.setLocale(locale); 429 430 if (moduleFactory == null) { 431 if (moduleClassLoader == null) { 432 throw new CheckstyleException( 433 "if no custom moduleFactory is set, " 434 + "moduleClassLoader must be specified"); 435 } 436 437 final Set<String> packageNames = PackageNamesLoader 438 .getPackageNames(moduleClassLoader); 439 moduleFactory = new PackageObjectFactory(packageNames, 440 moduleClassLoader); 441 } 442 443 final DefaultContext context = new DefaultContext(); 444 context.add("charset", charset); 445 context.add("classLoader", classLoader); 446 context.add("moduleFactory", moduleFactory); 447 context.add("severity", severity.getName()); 448 context.add("basedir", basedir); 449 childContext = context; 450 } 451 452 /** 453 * {@inheritDoc} Creates child module. 454 * @noinspection ChainOfInstanceofChecks 455 */ 456 @Override 457 protected void setupChild(Configuration childConf) 458 throws CheckstyleException { 459 final String name = childConf.getName(); 460 final Object child; 461 462 try { 463 child = moduleFactory.createModule(name); 464 465 if (child instanceof AutomaticBean) { 466 final AutomaticBean bean = (AutomaticBean) child; 467 bean.contextualize(childContext); 468 bean.configure(childConf); 469 } 470 } 471 catch (final CheckstyleException ex) { 472 throw new CheckstyleException("cannot initialize module " + name 473 + " - " + ex.getMessage(), ex); 474 } 475 if (child instanceof FileSetCheck) { 476 final FileSetCheck fsc = (FileSetCheck) child; 477 fsc.init(); 478 addFileSetCheck(fsc); 479 } 480 else if (child instanceof BeforeExecutionFileFilter) { 481 final BeforeExecutionFileFilter filter = (BeforeExecutionFileFilter) child; 482 addBeforeExecutionFileFilter(filter); 483 } 484 else if (child instanceof Filter) { 485 final Filter filter = (Filter) child; 486 addFilter(filter); 487 } 488 else if (child instanceof AuditListener) { 489 final AuditListener listener = (AuditListener) child; 490 addListener(listener); 491 } 492 else { 493 throw new CheckstyleException(name 494 + " is not allowed as a child in Checker"); 495 } 496 } 497 498 /** 499 * Adds a FileSetCheck to the list of FileSetChecks 500 * that is executed in process(). 501 * @param fileSetCheck the additional FileSetCheck 502 */ 503 public void addFileSetCheck(FileSetCheck fileSetCheck) { 504 fileSetCheck.setMessageDispatcher(this); 505 fileSetChecks.add(fileSetCheck); 506 } 507 508 /** 509 * Adds a before execution file filter to the end of the event chain. 510 * @param filter the additional filter 511 */ 512 public void addBeforeExecutionFileFilter(BeforeExecutionFileFilter filter) { 513 beforeExecutionFileFilters.addBeforeExecutionFileFilter(filter); 514 } 515 516 /** 517 * Adds a filter to the end of the audit event filter chain. 518 * @param filter the additional filter 519 */ 520 public void addFilter(Filter filter) { 521 filters.addFilter(filter); 522 } 523 524 @Override 525 public final void addListener(AuditListener listener) { 526 listeners.add(listener); 527 } 528 529 /** 530 * Sets the file extensions that identify the files that pass the 531 * filter of this FileSetCheck. 532 * @param extensions the set of file extensions. A missing 533 * initial '.' character of an extension is automatically added. 534 */ 535 public final void setFileExtensions(String... extensions) { 536 if (extensions == null) { 537 fileExtensions = null; 538 } 539 else { 540 fileExtensions = new String[extensions.length]; 541 for (int i = 0; i < extensions.length; i++) { 542 final String extension = extensions[i]; 543 if (CommonUtil.startsWithChar(extension, '.')) { 544 fileExtensions[i] = extension; 545 } 546 else { 547 fileExtensions[i] = "." + extension; 548 } 549 } 550 } 551 } 552 553 /** 554 * Sets the factory for creating submodules. 555 * 556 * @param moduleFactory the factory for creating FileSetChecks 557 */ 558 public void setModuleFactory(ModuleFactory moduleFactory) { 559 this.moduleFactory = moduleFactory; 560 } 561 562 /** 563 * Sets locale country. 564 * @param localeCountry the country to report messages 565 */ 566 public void setLocaleCountry(String localeCountry) { 567 this.localeCountry = localeCountry; 568 } 569 570 /** 571 * Sets locale language. 572 * @param localeLanguage the language to report messages 573 */ 574 public void setLocaleLanguage(String localeLanguage) { 575 this.localeLanguage = localeLanguage; 576 } 577 578 /** 579 * Sets the severity level. The string should be one of the names 580 * defined in the {@code SeverityLevel} class. 581 * 582 * @param severity The new severity level 583 * @see SeverityLevel 584 */ 585 public final void setSeverity(String severity) { 586 this.severity = SeverityLevel.getInstance(severity); 587 } 588 589 /** 590 * Sets the classloader that is used to contextualize fileset checks. 591 * Some Check implementations will use that classloader to improve the 592 * quality of their reports, e.g. to load a class and then analyze it via 593 * reflection. 594 * @param classLoader the new classloader 595 */ 596 public final void setClassLoader(ClassLoader classLoader) { 597 this.classLoader = classLoader; 598 } 599 600 @Override 601 public final void setModuleClassLoader(ClassLoader moduleClassLoader) { 602 this.moduleClassLoader = moduleClassLoader; 603 } 604 605 /** 606 * Sets a named charset. 607 * @param charset the name of a charset 608 * @throws UnsupportedEncodingException if charset is unsupported. 609 */ 610 public void setCharset(String charset) 611 throws UnsupportedEncodingException { 612 if (!Charset.isSupported(charset)) { 613 final String message = "unsupported charset: '" + charset + "'"; 614 throw new UnsupportedEncodingException(message); 615 } 616 this.charset = charset; 617 } 618 619 /** 620 * Sets the field haltOnException. 621 * @param haltOnException the new value. 622 */ 623 public void setHaltOnException(boolean haltOnException) { 624 this.haltOnException = haltOnException; 625 } 626 627 /** 628 * Clears the cache. 629 */ 630 public void clearCache() { 631 if (cacheFile != null) { 632 cacheFile.reset(); 633 } 634 } 635 636}