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