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