001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2020 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 /** The basedir to strip off in file names. */ 090 private String basedir; 091 092 /** Locale country to report messages . **/ 093 private String localeCountry = Locale.getDefault().getCountry(); 094 /** Locale language to report messages . **/ 095 private String localeLanguage = Locale.getDefault().getLanguage(); 096 097 /** The factory for instantiating submodules. */ 098 private ModuleFactory moduleFactory; 099 100 /** The classloader used for loading Checkstyle module classes. */ 101 private ClassLoader moduleClassLoader; 102 103 /** The context of all child components. */ 104 private Context childContext; 105 106 /** The file extensions that are accepted. */ 107 private String[] fileExtensions = CommonUtil.EMPTY_STRING_ARRAY; 108 109 /** 110 * The severity level of any violations found by submodules. 111 * The value of this property is passed to submodules via 112 * contextualize(). 113 * 114 * <p>Note: Since the Checker is merely a container for modules 115 * it does not make sense to implement logging functionality 116 * here. Consequently Checker does not extend AbstractViolationReporter, 117 * leading to a bit of duplicated code for severity level setting. 118 */ 119 private SeverityLevel severity = SeverityLevel.ERROR; 120 121 /** Name of a charset. */ 122 private String charset = StandardCharsets.UTF_8.name(); 123 124 /** Cache file. **/ 125 private PropertyCacheFile cacheFile; 126 127 /** Controls whether exceptions should halt execution or not. */ 128 private boolean haltOnException = true; 129 130 /** The tab width for column reporting. */ 131 private int tabWidth = CommonUtil.DEFAULT_TAB_WIDTH; 132 133 /** 134 * Creates a new {@code Checker} instance. 135 * The instance needs to be contextualized and configured. 136 */ 137 public Checker() { 138 addListener(counter); 139 log = LogFactory.getLog(Checker.class); 140 } 141 142 /** 143 * Sets cache file. 144 * 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 * 157 * @param filter before execution file filter to remove. 158 */ 159 public void removeBeforeExecutionFileFilter(BeforeExecutionFileFilter filter) { 160 beforeExecutionFileFilters.removeBeforeExecutionFileFilter(filter); 161 } 162 163 /** 164 * Removes filter. 165 * 166 * @param filter filter to remove. 167 */ 168 public void removeFilter(Filter filter) { 169 filters.removeFilter(filter); 170 } 171 172 @Override 173 public void destroy() { 174 listeners.clear(); 175 fileSetChecks.clear(); 176 beforeExecutionFileFilters.clear(); 177 filters.clear(); 178 if (cacheFile != null) { 179 try { 180 cacheFile.persist(); 181 } 182 catch (IOException ex) { 183 throw new IllegalStateException("Unable to persist cache file.", ex); 184 } 185 } 186 } 187 188 /** 189 * Removes a given listener. 190 * 191 * @param listener a listener to remove 192 */ 193 public void removeListener(AuditListener listener) { 194 listeners.remove(listener); 195 } 196 197 /** 198 * Sets base directory. 199 * 200 * @param basedir the base directory to strip off in file names 201 */ 202 public void setBasedir(String basedir) { 203 this.basedir = basedir; 204 } 205 206 @Override 207 public int process(List<File> files) throws CheckstyleException { 208 if (cacheFile != null) { 209 cacheFile.putExternalResources(getExternalResourceLocations()); 210 } 211 212 // Prepare to start 213 fireAuditStarted(); 214 for (final FileSetCheck fsc : fileSetChecks) { 215 fsc.beginProcessing(charset); 216 } 217 218 final List<File> targetFiles = files.stream() 219 .filter(file -> CommonUtil.matchesFileExtension(file, fileExtensions)) 220 .collect(Collectors.toList()); 221 processFiles(targetFiles); 222 223 // Finish up 224 // It may also log!!! 225 fileSetChecks.forEach(FileSetCheck::finishProcessing); 226 227 // It may also log!!! 228 fileSetChecks.forEach(FileSetCheck::destroy); 229 230 final int errorCount = counter.getCount(); 231 fireAuditFinished(); 232 return errorCount; 233 } 234 235 /** 236 * Returns a set of external configuration resource locations which are used by all file set 237 * checks and filters. 238 * 239 * @return a set of external configuration resource locations which are used by all file set 240 * checks and filters. 241 */ 242 private Set<String> getExternalResourceLocations() { 243 final Set<String> externalResources = new HashSet<>(); 244 fileSetChecks.stream().filter(check -> check instanceof ExternalResourceHolder) 245 .forEach(check -> { 246 final Set<String> locations = 247 ((ExternalResourceHolder) check).getExternalResourceLocations(); 248 externalResources.addAll(locations); 249 }); 250 filters.getFilters().stream().filter(filter -> filter instanceof ExternalResourceHolder) 251 .forEach(filter -> { 252 final Set<String> locations = 253 ((ExternalResourceHolder) filter).getExternalResourceLocations(); 254 externalResources.addAll(locations); 255 }); 256 return externalResources; 257 } 258 259 /** Notify all listeners about the audit start. */ 260 private void fireAuditStarted() { 261 final AuditEvent event = new AuditEvent(this); 262 for (final AuditListener listener : listeners) { 263 listener.auditStarted(event); 264 } 265 } 266 267 /** Notify all listeners about the audit end. */ 268 private void fireAuditFinished() { 269 final AuditEvent event = new AuditEvent(this); 270 for (final AuditListener listener : listeners) { 271 listener.auditFinished(event); 272 } 273 } 274 275 /** 276 * Processes a list of files with all FileSetChecks. 277 * 278 * @param files a list of files to process. 279 * @throws CheckstyleException if error condition within Checkstyle occurs. 280 * @throws Error wraps any java.lang.Error happened during execution 281 * @noinspection ProhibitedExceptionThrown 282 */ 283 // -@cs[CyclomaticComplexity] no easy way to split this logic of processing the file 284 private void processFiles(List<File> files) throws CheckstyleException { 285 for (final File file : files) { 286 String fileName = null; 287 try { 288 fileName = file.getAbsolutePath(); 289 final long timestamp = file.lastModified(); 290 if (cacheFile != null && cacheFile.isInCache(fileName, timestamp) 291 || !acceptFileStarted(fileName)) { 292 continue; 293 } 294 if (cacheFile != null) { 295 cacheFile.put(fileName, timestamp); 296 } 297 fireFileStarted(fileName); 298 final SortedSet<LocalizedMessage> fileMessages = processFile(file); 299 fireErrors(fileName, fileMessages); 300 fireFileFinished(fileName); 301 } 302 // -@cs[IllegalCatch] There is no other way to deliver filename that was under 303 // processing. See https://github.com/checkstyle/checkstyle/issues/2285 304 catch (Exception ex) { 305 if (fileName != null && cacheFile != null) { 306 cacheFile.remove(fileName); 307 } 308 309 // We need to catch all exceptions to put a reason failure (file name) in exception 310 throw new CheckstyleException("Exception was thrown while processing " 311 + file.getPath(), ex); 312 } 313 catch (Error error) { 314 if (fileName != null && cacheFile != null) { 315 cacheFile.remove(fileName); 316 } 317 318 // We need to catch all errors to put a reason failure (file name) in error 319 throw new Error("Error was thrown while processing " + file.getPath(), error); 320 } 321 } 322 } 323 324 /** 325 * Processes a file with all FileSetChecks. 326 * 327 * @param file a file to process. 328 * @return a sorted set of messages to be logged. 329 * @throws CheckstyleException if error condition within Checkstyle occurs. 330 * @noinspection ProhibitedExceptionThrown 331 */ 332 private SortedSet<LocalizedMessage> processFile(File file) throws CheckstyleException { 333 final SortedSet<LocalizedMessage> fileMessages = new TreeSet<>(); 334 try { 335 final FileText theText = new FileText(file.getAbsoluteFile(), charset); 336 for (final FileSetCheck fsc : fileSetChecks) { 337 fileMessages.addAll(fsc.process(file, theText)); 338 } 339 } 340 catch (final IOException ioe) { 341 log.debug("IOException occurred.", ioe); 342 fileMessages.add(new LocalizedMessage(1, 343 Definitions.CHECKSTYLE_BUNDLE, EXCEPTION_MSG, 344 new String[] {ioe.getMessage()}, null, getClass(), null)); 345 } 346 // -@cs[IllegalCatch] There is no other way to obey haltOnException field 347 catch (Exception ex) { 348 if (haltOnException) { 349 throw ex; 350 } 351 352 log.debug("Exception occurred.", ex); 353 354 final StringWriter sw = new StringWriter(); 355 final PrintWriter pw = new PrintWriter(sw, true); 356 357 ex.printStackTrace(pw); 358 359 fileMessages.add(new LocalizedMessage(1, 360 Definitions.CHECKSTYLE_BUNDLE, EXCEPTION_MSG, 361 new String[] {sw.getBuffer().toString()}, 362 null, getClass(), null)); 363 } 364 return fileMessages; 365 } 366 367 /** 368 * Check if all before execution file filters accept starting the file. 369 * 370 * @param fileName 371 * the file to be audited 372 * @return {@code true} if the file is accepted. 373 */ 374 private boolean acceptFileStarted(String fileName) { 375 final String stripped = CommonUtil.relativizeAndNormalizePath(basedir, fileName); 376 return beforeExecutionFileFilters.accept(stripped); 377 } 378 379 /** 380 * Notify all listeners about the beginning of a file audit. 381 * 382 * @param fileName 383 * the file to be audited 384 */ 385 @Override 386 public void fireFileStarted(String fileName) { 387 final String stripped = CommonUtil.relativizeAndNormalizePath(basedir, fileName); 388 final AuditEvent event = new AuditEvent(this, stripped); 389 for (final AuditListener listener : listeners) { 390 listener.fileStarted(event); 391 } 392 } 393 394 /** 395 * Notify all listeners about the errors in a file. 396 * 397 * @param fileName the audited file 398 * @param errors the audit errors from the file 399 */ 400 @Override 401 public void fireErrors(String fileName, SortedSet<LocalizedMessage> errors) { 402 final String stripped = CommonUtil.relativizeAndNormalizePath(basedir, fileName); 403 boolean hasNonFilteredViolations = false; 404 for (final LocalizedMessage element : errors) { 405 final AuditEvent event = new AuditEvent(this, stripped, element); 406 if (filters.accept(event)) { 407 hasNonFilteredViolations = true; 408 for (final AuditListener listener : listeners) { 409 listener.addError(event); 410 } 411 } 412 } 413 if (hasNonFilteredViolations && cacheFile != null) { 414 cacheFile.remove(fileName); 415 } 416 } 417 418 /** 419 * Notify all listeners about the end of a file audit. 420 * 421 * @param fileName 422 * the audited file 423 */ 424 @Override 425 public void fireFileFinished(String fileName) { 426 final String stripped = CommonUtil.relativizeAndNormalizePath(basedir, fileName); 427 final AuditEvent event = new AuditEvent(this, stripped); 428 for (final AuditListener listener : listeners) { 429 listener.fileFinished(event); 430 } 431 } 432 433 @Override 434 protected void finishLocalSetup() throws CheckstyleException { 435 final Locale locale = new Locale(localeLanguage, localeCountry); 436 LocalizedMessage.setLocale(locale); 437 438 if (moduleFactory == null) { 439 if (moduleClassLoader == null) { 440 throw new CheckstyleException( 441 "if no custom moduleFactory is set, " 442 + "moduleClassLoader must be specified"); 443 } 444 445 final Set<String> packageNames = PackageNamesLoader 446 .getPackageNames(moduleClassLoader); 447 moduleFactory = new PackageObjectFactory(packageNames, 448 moduleClassLoader); 449 } 450 451 final DefaultContext context = new DefaultContext(); 452 context.add("charset", charset); 453 context.add("moduleFactory", moduleFactory); 454 context.add("severity", severity.getName()); 455 context.add("basedir", basedir); 456 context.add("tabWidth", String.valueOf(tabWidth)); 457 childContext = context; 458 } 459 460 /** 461 * {@inheritDoc} Creates child module. 462 * 463 * @noinspection ChainOfInstanceofChecks 464 */ 465 @Override 466 protected void setupChild(Configuration childConf) 467 throws CheckstyleException { 468 final String name = childConf.getName(); 469 final Object child; 470 471 try { 472 child = moduleFactory.createModule(name); 473 474 if (child instanceof AutomaticBean) { 475 final AutomaticBean bean = (AutomaticBean) child; 476 bean.contextualize(childContext); 477 bean.configure(childConf); 478 } 479 } 480 catch (final CheckstyleException ex) { 481 throw new CheckstyleException("cannot initialize module " + name 482 + " - " + ex.getMessage(), ex); 483 } 484 if (child instanceof FileSetCheck) { 485 final FileSetCheck fsc = (FileSetCheck) child; 486 fsc.init(); 487 addFileSetCheck(fsc); 488 } 489 else if (child instanceof BeforeExecutionFileFilter) { 490 final BeforeExecutionFileFilter filter = (BeforeExecutionFileFilter) child; 491 addBeforeExecutionFileFilter(filter); 492 } 493 else if (child instanceof Filter) { 494 final Filter filter = (Filter) child; 495 addFilter(filter); 496 } 497 else if (child instanceof AuditListener) { 498 final AuditListener listener = (AuditListener) child; 499 addListener(listener); 500 } 501 else { 502 throw new CheckstyleException(name 503 + " is not allowed as a child in Checker"); 504 } 505 } 506 507 /** 508 * Adds a FileSetCheck to the list of FileSetChecks 509 * that is executed in process(). 510 * 511 * @param fileSetCheck the additional FileSetCheck 512 */ 513 public void addFileSetCheck(FileSetCheck fileSetCheck) { 514 fileSetCheck.setMessageDispatcher(this); 515 fileSetChecks.add(fileSetCheck); 516 } 517 518 /** 519 * Adds a before execution file filter to the end of the event chain. 520 * 521 * @param filter the additional filter 522 */ 523 public void addBeforeExecutionFileFilter(BeforeExecutionFileFilter filter) { 524 beforeExecutionFileFilters.addBeforeExecutionFileFilter(filter); 525 } 526 527 /** 528 * Adds a filter to the end of the audit event filter chain. 529 * 530 * @param filter the additional filter 531 */ 532 public void addFilter(Filter filter) { 533 filters.addFilter(filter); 534 } 535 536 @Override 537 public final void addListener(AuditListener listener) { 538 listeners.add(listener); 539 } 540 541 /** 542 * Sets the file extensions that identify the files that pass the 543 * filter of this FileSetCheck. 544 * 545 * @param extensions the set of file extensions. A missing 546 * initial '.' character of an extension is automatically added. 547 */ 548 public final void setFileExtensions(String... extensions) { 549 if (extensions == null) { 550 fileExtensions = null; 551 } 552 else { 553 fileExtensions = new String[extensions.length]; 554 for (int i = 0; i < extensions.length; i++) { 555 final String extension = extensions[i]; 556 if (CommonUtil.startsWithChar(extension, '.')) { 557 fileExtensions[i] = extension; 558 } 559 else { 560 fileExtensions[i] = "." + extension; 561 } 562 } 563 } 564 } 565 566 /** 567 * Sets the factory for creating submodules. 568 * 569 * @param moduleFactory the factory for creating FileSetChecks 570 */ 571 public void setModuleFactory(ModuleFactory moduleFactory) { 572 this.moduleFactory = moduleFactory; 573 } 574 575 /** 576 * Sets locale country. 577 * 578 * @param localeCountry the country to report messages 579 */ 580 public void setLocaleCountry(String localeCountry) { 581 this.localeCountry = localeCountry; 582 } 583 584 /** 585 * Sets locale language. 586 * 587 * @param localeLanguage the language to report messages 588 */ 589 public void setLocaleLanguage(String localeLanguage) { 590 this.localeLanguage = localeLanguage; 591 } 592 593 /** 594 * Sets the severity level. The string should be one of the names 595 * defined in the {@code SeverityLevel} class. 596 * 597 * @param severity The new severity level 598 * @see SeverityLevel 599 */ 600 public final void setSeverity(String severity) { 601 this.severity = SeverityLevel.getInstance(severity); 602 } 603 604 @Override 605 public final void setModuleClassLoader(ClassLoader moduleClassLoader) { 606 this.moduleClassLoader = moduleClassLoader; 607 } 608 609 /** 610 * Sets a named charset. 611 * 612 * @param charset the name of a charset 613 * @throws UnsupportedEncodingException if charset is unsupported. 614 */ 615 public void setCharset(String charset) 616 throws UnsupportedEncodingException { 617 if (!Charset.isSupported(charset)) { 618 final String message = "unsupported charset: '" + charset + "'"; 619 throw new UnsupportedEncodingException(message); 620 } 621 this.charset = charset; 622 } 623 624 /** 625 * Sets the field haltOnException. 626 * 627 * @param haltOnException the new value. 628 */ 629 public void setHaltOnException(boolean haltOnException) { 630 this.haltOnException = haltOnException; 631 } 632 633 /** 634 * Set the tab width to report audit events with. 635 * 636 * @param tabWidth an {@code int} value 637 */ 638 public final void setTabWidth(int tabWidth) { 639 this.tabWidth = tabWidth; 640 } 641 642 /** 643 * Clears the cache. 644 */ 645 public void clearCache() { 646 if (cacheFile != null) { 647 cacheFile.reset(); 648 } 649 } 650 651}