001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2026 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.BeforeExecutionFileFilter; 044import com.puppycrawl.tools.checkstyle.api.BeforeExecutionFileFilterSet; 045import com.puppycrawl.tools.checkstyle.api.CheckstyleException; 046import com.puppycrawl.tools.checkstyle.api.Configuration; 047import com.puppycrawl.tools.checkstyle.api.Context; 048import com.puppycrawl.tools.checkstyle.api.ExternalResourceHolder; 049import com.puppycrawl.tools.checkstyle.api.FileSetCheck; 050import com.puppycrawl.tools.checkstyle.api.FileText; 051import com.puppycrawl.tools.checkstyle.api.Filter; 052import com.puppycrawl.tools.checkstyle.api.FilterSet; 053import com.puppycrawl.tools.checkstyle.api.MessageDispatcher; 054import com.puppycrawl.tools.checkstyle.api.RootModule; 055import com.puppycrawl.tools.checkstyle.api.SeverityLevel; 056import com.puppycrawl.tools.checkstyle.api.SeverityLevelCounter; 057import com.puppycrawl.tools.checkstyle.api.Violation; 058import com.puppycrawl.tools.checkstyle.utils.CommonUtil; 059 060/** 061 * This class provides the functionality to check a set of files. 062 */ 063public class Checker extends AbstractAutomaticBean implements MessageDispatcher, RootModule { 064 065 /** Message to use when an exception occurs and should be printed as a violation. */ 066 public static final String EXCEPTION_MSG = "general.exception"; 067 068 /** The extension separator. */ 069 private static final String EXTENSION_SEPARATOR = "."; 070 071 /** Logger for Checker. */ 072 private final Log log; 073 074 /** Maintains error count. */ 075 private final SeverityLevelCounter counter = new SeverityLevelCounter( 076 SeverityLevel.ERROR); 077 078 /** Vector of listeners. */ 079 private final List<AuditListener> listeners = new ArrayList<>(); 080 081 /** Vector of fileset checks. */ 082 private final List<FileSetCheck> fileSetChecks = new ArrayList<>(); 083 084 /** The audit event before execution file filters. */ 085 private final BeforeExecutionFileFilterSet beforeExecutionFileFilters = 086 new BeforeExecutionFileFilterSet(); 087 088 /** The audit event filters. */ 089 private final FilterSet filters = new FilterSet(); 090 091 /** The basedir to strip off in file names. */ 092 private String basedir; 093 094 /** Locale country to report messages . **/ 095 @XdocsPropertyType(PropertyType.LOCALE_COUNTRY) 096 private String localeCountry = Locale.getDefault().getCountry(); 097 /** Locale language to report messages . **/ 098 @XdocsPropertyType(PropertyType.LOCALE_LANGUAGE) 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; 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 = StandardCharsets.UTF_8.name(); 127 128 /** Cache file. **/ 129 @XdocsPropertyType(PropertyType.FILE) 130 private PropertyCacheFile cacheFile; 131 132 /** Controls whether exceptions should halt execution or not. */ 133 private boolean haltOnException = true; 134 135 /** The tab width for column reporting. */ 136 private int tabWidth = CommonUtil.DEFAULT_TAB_WIDTH; 137 138 /** 139 * Creates a new {@code Checker} instance. 140 * The instance needs to be contextualized and configured. 141 */ 142 public Checker() { 143 addListener(counter); 144 log = LogFactory.getLog(Checker.class); 145 } 146 147 /** 148 * Sets cache file. 149 * 150 * @param fileName the cache file. 151 * @throws IOException if there are some problems with file loading. 152 */ 153 public void setCacheFile(String fileName) throws IOException { 154 final Configuration configuration = getConfiguration(); 155 cacheFile = new PropertyCacheFile(configuration, fileName); 156 cacheFile.load(); 157 } 158 159 /** 160 * Removes before execution file filter. 161 * 162 * @param filter before execution file filter to remove. 163 */ 164 public void removeBeforeExecutionFileFilter(BeforeExecutionFileFilter filter) { 165 beforeExecutionFileFilters.removeBeforeExecutionFileFilter(filter); 166 } 167 168 /** 169 * Removes filter. 170 * 171 * @param filter filter to remove. 172 */ 173 public void removeFilter(Filter filter) { 174 filters.removeFilter(filter); 175 } 176 177 @Override 178 public void destroy() { 179 listeners.clear(); 180 fileSetChecks.clear(); 181 beforeExecutionFileFilters.clear(); 182 filters.clear(); 183 if (cacheFile != null) { 184 try { 185 cacheFile.persist(); 186 } 187 catch (IOException exc) { 188 throw new IllegalStateException( 189 getLocalizedMessage("Checker.cacheFilesException"), exc); 190 } 191 } 192 } 193 194 /** 195 * Removes a given listener. 196 * 197 * @param listener a listener to remove 198 */ 199 public void removeListener(AuditListener listener) { 200 listeners.remove(listener); 201 } 202 203 /** 204 * Sets base directory. 205 * 206 * @param basedir the base directory to strip off in file names 207 */ 208 public void setBasedir(String basedir) { 209 this.basedir = basedir; 210 } 211 212 @Override 213 public int process(List<File> files) throws CheckstyleException { 214 if (cacheFile != null) { 215 cacheFile.putExternalResources(getExternalResourceLocations()); 216 } 217 218 // Prepare to start 219 fireAuditStarted(); 220 for (final FileSetCheck fsc : fileSetChecks) { 221 fsc.beginProcessing(charset); 222 } 223 224 final List<File> targetFiles = files.stream() 225 .filter(file -> CommonUtil.matchesFileExtension(file, fileExtensions)) 226 .toList(); 227 processFiles(targetFiles); 228 229 // Finish up 230 // It may also log!!! 231 fileSetChecks.forEach(FileSetCheck::finishProcessing); 232 233 // It may also log!!! 234 fileSetChecks.forEach(FileSetCheck::destroy); 235 236 final int errorCount = counter.getCount(); 237 fireAuditFinished(); 238 return errorCount; 239 } 240 241 /** 242 * Returns a set of external configuration resource locations which are used by all file set 243 * checks and filters. 244 * 245 * @return a set of external configuration resource locations which are used by all file set 246 * checks and filters. 247 */ 248 private Set<String> getExternalResourceLocations() { 249 return Stream.concat(fileSetChecks.stream(), filters.getFilters().stream()) 250 .filter(ExternalResourceHolder.class::isInstance) 251 .flatMap(resource -> { 252 return ((ExternalResourceHolder) resource) 253 .getExternalResourceLocations().stream(); 254 }) 255 .collect(Collectors.toUnmodifiableSet()); 256 } 257 258 /** Notify all listeners about the audit start. */ 259 private void fireAuditStarted() { 260 final AuditEvent event = new AuditEvent(this); 261 for (final AuditListener listener : listeners) { 262 listener.auditStarted(event); 263 } 264 } 265 266 /** Notify all listeners about the audit end. */ 267 private void fireAuditFinished() { 268 final AuditEvent event = new AuditEvent(this); 269 for (final AuditListener listener : listeners) { 270 listener.auditFinished(event); 271 } 272 } 273 274 /** 275 * Processes a list of files with all FileSetChecks. 276 * 277 * @param files a list of files to process. 278 * @throws CheckstyleException if error condition within Checkstyle occurs. 279 * @throws Error wraps any java.lang.Error happened during execution 280 * @noinspection ProhibitedExceptionThrown 281 * @noinspectionreason ProhibitedExceptionThrown - There is no other way to 282 * deliver filename that was under processing. 283 */ 284 // -@cs[CyclomaticComplexity] no easy way to split this logic of processing the file 285 private void processFiles(List<File> files) throws CheckstyleException { 286 for (final File file : files) { 287 String fileName = null; 288 final String filePath = file.getPath(); 289 try { 290 fileName = file.getAbsolutePath(); 291 final long timestamp = file.lastModified(); 292 if (cacheFile != null && cacheFile.isInCache(fileName, timestamp) 293 || !acceptFileStarted(fileName)) { 294 continue; 295 } 296 if (cacheFile != null) { 297 cacheFile.put(fileName, timestamp); 298 } 299 fireFileStarted(fileName); 300 final SortedSet<Violation> fileMessages = processFile(file); 301 fireErrors(fileName, fileMessages); 302 fireFileFinished(fileName); 303 } 304 // -@cs[IllegalCatch] There is no other way to deliver filename that was under 305 // processing. See https://github.com/checkstyle/checkstyle/issues/2285 306 catch (Exception exc) { 307 if (fileName != null && cacheFile != null) { 308 cacheFile.remove(fileName); 309 } 310 311 // We need to catch all exceptions to put a reason failure (file name) in exception 312 throw new CheckstyleException( 313 getLocalizedMessage("Checker.processFilesException", filePath), exc); 314 } 315 catch (Error error) { 316 if (fileName != null && cacheFile != null) { 317 cacheFile.remove(fileName); 318 } 319 320 // We need to catch all errors to put a reason failure (file name) in error 321 throw new Error(getLocalizedMessage("Checker.error", filePath), error); 322 } 323 } 324 } 325 326 /** 327 * Processes a file with all FileSetChecks. 328 * 329 * @param file a file to process. 330 * @return a sorted set of violations to be logged. 331 * @throws CheckstyleException if error condition within Checkstyle occurs. 332 * @noinspection ProhibitedExceptionThrown 333 * @noinspectionreason ProhibitedExceptionThrown - there is no other way to obey 334 * haltOnException field 335 */ 336 private SortedSet<Violation> processFile(File file) throws CheckstyleException { 337 final SortedSet<Violation> fileMessages = new TreeSet<>(); 338 try { 339 final FileText theText = new FileText(file.getAbsoluteFile(), charset); 340 for (final FileSetCheck fsc : fileSetChecks) { 341 fileMessages.addAll(fsc.process(file, theText)); 342 } 343 } 344 catch (final IOException ioe) { 345 log.debug("IOException occurred.", ioe); 346 fileMessages.add(new Violation(1, 347 Definitions.CHECKSTYLE_BUNDLE, EXCEPTION_MSG, 348 new String[] {ioe.getMessage()}, null, getClass(), null)); 349 } 350 // -@cs[IllegalCatch] There is no other way to obey haltOnException field 351 catch (Exception exc) { 352 if (haltOnException) { 353 throw exc; 354 } 355 356 log.debug("Exception occurred.", exc); 357 358 final StringWriter sw = new StringWriter(); 359 final PrintWriter pw = new PrintWriter(sw, true); 360 361 exc.printStackTrace(pw); 362 363 fileMessages.add(new Violation(1, 364 Definitions.CHECKSTYLE_BUNDLE, EXCEPTION_MSG, 365 new String[] {sw.getBuffer().toString()}, 366 null, getClass(), null)); 367 } 368 return fileMessages; 369 } 370 371 /** 372 * Check if all before execution file filters accept starting the file. 373 * 374 * @param fileName 375 * the file to be audited 376 * @return {@code true} if the file is accepted. 377 */ 378 private boolean acceptFileStarted(String fileName) { 379 final String stripped = relativizePathWithCatch(fileName); 380 return beforeExecutionFileFilters.accept(stripped); 381 } 382 383 /** 384 * Notify all listeners about the beginning of a file audit. 385 * 386 * @param fileName 387 * the file to be audited 388 */ 389 @Override 390 public void fireFileStarted(String fileName) { 391 final String stripped = relativizePathWithCatch(fileName); 392 final AuditEvent event = new AuditEvent(this, stripped); 393 for (final AuditListener listener : listeners) { 394 listener.fileStarted(event); 395 } 396 } 397 398 /** 399 * Notify all listeners about the errors in a file. 400 * 401 * @param fileName the audited file 402 * @param errors the audit errors from the file 403 */ 404 @Override 405 public void fireErrors(String fileName, SortedSet<Violation> errors) { 406 final String stripped = relativizePathWithCatch(fileName); 407 boolean hasNonFilteredViolations = false; 408 for (final Violation element : errors) { 409 final AuditEvent event = new AuditEvent(this, stripped, element); 410 if (filters.accept(event)) { 411 hasNonFilteredViolations = true; 412 for (final AuditListener listener : listeners) { 413 listener.addError(event); 414 } 415 } 416 } 417 if (hasNonFilteredViolations && cacheFile != null) { 418 cacheFile.remove(fileName); 419 } 420 } 421 422 /** 423 * Notify all listeners about the end of a file audit. 424 * 425 * @param fileName 426 * the audited file 427 */ 428 @Override 429 public void fireFileFinished(String fileName) { 430 final String stripped = relativizePathWithCatch(fileName); 431 final AuditEvent event = new AuditEvent(this, stripped); 432 for (final AuditListener listener : listeners) { 433 listener.fileFinished(event); 434 } 435 } 436 437 @Override 438 protected void finishLocalSetup() throws CheckstyleException { 439 final Locale locale = Locale.of(localeLanguage, localeCountry); 440 LocalizedMessage.setLocale(locale); 441 442 if (moduleFactory == null) { 443 if (moduleClassLoader == null) { 444 throw new CheckstyleException(getLocalizedMessage("Checker.finishLocalSetup")); 445 } 446 447 final Set<String> packageNames = PackageNamesLoader 448 .getPackageNames(moduleClassLoader); 449 moduleFactory = new PackageObjectFactory(packageNames, 450 moduleClassLoader); 451 } 452 453 final DefaultContext context = new DefaultContext(); 454 context.add("charset", charset); 455 context.add("moduleFactory", moduleFactory); 456 context.add("severity", severity.getName()); 457 context.add("basedir", basedir); 458 context.add("tabWidth", String.valueOf(tabWidth)); 459 childContext = context; 460 } 461 462 /** 463 * {@inheritDoc} Creates child module. 464 * 465 * @noinspection ChainOfInstanceofChecks 466 * @noinspectionreason ChainOfInstanceofChecks - we treat checks and filters differently 467 */ 468 @Override 469 protected void setupChild(Configuration childConf) 470 throws CheckstyleException { 471 final String name = childConf.getName(); 472 final Object child; 473 474 try { 475 child = moduleFactory.createModule(name); 476 477 if (child instanceof AbstractAutomaticBean bean) { 478 bean.contextualize(childContext); 479 bean.configure(childConf); 480 } 481 } 482 catch (final CheckstyleException exc) { 483 throw new CheckstyleException( 484 getLocalizedMessage("Checker.setupChildModule", name, exc.getMessage()), exc); 485 } 486 if (child instanceof FileSetCheck fsc) { 487 fsc.init(); 488 addFileSetCheck(fsc); 489 } 490 else if (child instanceof BeforeExecutionFileFilter filter) { 491 addBeforeExecutionFileFilter(filter); 492 } 493 else if (child instanceof Filter filter) { 494 addFilter(filter); 495 } 496 else if (child instanceof AuditListener listener) { 497 addListener(listener); 498 } 499 else { 500 throw new CheckstyleException( 501 getLocalizedMessage("Checker.setupChildNotAllowed", name)); 502 } 503 } 504 505 /** 506 * Adds a FileSetCheck to the list of FileSetChecks 507 * that is executed in process(). 508 * 509 * @param fileSetCheck the additional FileSetCheck 510 */ 511 public void addFileSetCheck(FileSetCheck fileSetCheck) { 512 fileSetCheck.setMessageDispatcher(this); 513 fileSetChecks.add(fileSetCheck); 514 } 515 516 /** 517 * Adds a before execution file filter to the end of the event chain. 518 * 519 * @param filter the additional filter 520 */ 521 public void addBeforeExecutionFileFilter(BeforeExecutionFileFilter filter) { 522 beforeExecutionFileFilters.addBeforeExecutionFileFilter(filter); 523 } 524 525 /** 526 * Adds a filter to the end of the audit event filter chain. 527 * 528 * @param filter the additional filter 529 */ 530 public void addFilter(Filter filter) { 531 filters.addFilter(filter); 532 } 533 534 @Override 535 public final void addListener(AuditListener listener) { 536 listeners.add(listener); 537 } 538 539 /** 540 * Sets the file extensions that identify the files that pass the 541 * filter of this FileSetCheck. 542 * 543 * @param extensions the set of file extensions. A missing 544 * initial '.' character of an extension is automatically added. 545 */ 546 public final void setFileExtensions(String... extensions) { 547 if (extensions != null) { 548 fileExtensions = new String[extensions.length]; 549 for (int i = 0; i < extensions.length; i++) { 550 final String extension = extensions[i]; 551 if (extension.startsWith(EXTENSION_SEPARATOR)) { 552 fileExtensions[i] = extension; 553 } 554 else { 555 fileExtensions[i] = EXTENSION_SEPARATOR + extension; 556 } 557 } 558 } 559 } 560 561 /** 562 * Sets the factory for creating submodules. 563 * 564 * @param moduleFactory the factory for creating FileSetChecks 565 */ 566 public void setModuleFactory(ModuleFactory moduleFactory) { 567 this.moduleFactory = moduleFactory; 568 } 569 570 /** 571 * Sets locale country. 572 * 573 * @param localeCountry the country to report messages 574 */ 575 public void setLocaleCountry(String localeCountry) { 576 this.localeCountry = localeCountry; 577 } 578 579 /** 580 * Sets locale language. 581 * 582 * @param localeLanguage the language to report messages 583 */ 584 public void setLocaleLanguage(String localeLanguage) { 585 this.localeLanguage = localeLanguage; 586 } 587 588 /** 589 * Sets the severity level. The string should be one of the names 590 * defined in the {@code SeverityLevel} class. 591 * 592 * @param severity The new severity level 593 * @see SeverityLevel 594 */ 595 public final void setSeverity(String severity) { 596 this.severity = SeverityLevel.getInstance(severity); 597 } 598 599 @Override 600 public final void setModuleClassLoader(ClassLoader moduleClassLoader) { 601 this.moduleClassLoader = moduleClassLoader; 602 } 603 604 /** 605 * Sets a named charset. 606 * 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 throw new UnsupportedEncodingException( 614 getLocalizedMessage("Checker.setCharset", charset)); 615 } 616 this.charset = charset; 617 } 618 619 /** 620 * Sets the field haltOnException. 621 * 622 * @param haltOnException the new value. 623 */ 624 public void setHaltOnException(boolean haltOnException) { 625 this.haltOnException = haltOnException; 626 } 627 628 /** 629 * Set the tab width to report audit events with. 630 * 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 /** 647 * Extracts localized messages from properties files. 648 * 649 * @param messageKey the key pointing to localized message in respective properties file. 650 * @param args the arguments of message in respective properties file. 651 * @return a string containing extracted localized message 652 */ 653 private String getLocalizedMessage(String messageKey, Object... args) { 654 final LocalizedMessage localizedMessage = new LocalizedMessage( 655 Definitions.CHECKSTYLE_BUNDLE, getClass(), 656 messageKey, args); 657 658 return localizedMessage.getMessage(); 659 } 660 661 /** 662 * Relativizes a path and wraps any exception with a user-friendly localized message. 663 * 664 * @param fileName the file path to relativize 665 * @return the relativized path 666 * @throws IllegalStateException if any exception occurs during relativization 667 */ 668 private String relativizePathWithCatch(String fileName) { 669 try { 670 return CommonUtil.relativizePath(basedir, fileName); 671 } 672 // -@cs[IllegalCatch] Catching generic Exception to include fileName and basedir context 673 catch (Exception exception) { 674 throw new IllegalStateException( 675 getLocalizedMessage("general.relativizePath", 676 fileName, basedir), exception); 677 } 678 } 679 680}