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 contextualize(). 116 * 117 * <p>Note: Since the Checker is merely a container for modules 118 * it does not make sense to implement logging functionality 119 * here. Consequently, Checker does not extend AbstractViolationReporter, 120 * leading to a bit of duplicated code for severity level setting. 121 */ 122 private SeverityLevel severity = SeverityLevel.ERROR; 123 124 /** Name of a charset. */ 125 private String charset = StandardCharsets.UTF_8.name(); 126 127 /** Cache file. */ 128 @XdocsPropertyType(PropertyType.FILE) 129 private PropertyCacheFile cacheFile; 130 131 /** Controls whether exceptions should halt execution or not. */ 132 private boolean haltOnException = true; 133 134 /** The tab width for column reporting. */ 135 private int tabWidth = CommonUtil.DEFAULT_TAB_WIDTH; 136 137 /** 138 * Creates a new {@code Checker} instance. 139 * The instance needs to be contextualized and configured. 140 */ 141 public Checker() { 142 addListener(counter); 143 log = LogFactory.getLog(Checker.class); 144 } 145 146 /** 147 * Sets cache file. 148 * 149 * @param fileName the cache file. 150 * @throws IOException if there are some problems with file loading. 151 */ 152 public void setCacheFile(String fileName) throws IOException { 153 final Configuration configuration = getConfiguration(); 154 cacheFile = new PropertyCacheFile(configuration, fileName); 155 cacheFile.load(); 156 } 157 158 /** 159 * Removes before execution file filter. 160 * 161 * @param filter before execution file filter to remove. 162 */ 163 public void removeBeforeExecutionFileFilter(BeforeExecutionFileFilter filter) { 164 beforeExecutionFileFilters.removeBeforeExecutionFileFilter(filter); 165 } 166 167 /** 168 * Removes filter. 169 * 170 * @param filter filter to remove. 171 */ 172 public void removeFilter(Filter filter) { 173 filters.removeFilter(filter); 174 } 175 176 @Override 177 public void destroy() { 178 listeners.clear(); 179 fileSetChecks.clear(); 180 beforeExecutionFileFilters.clear(); 181 filters.clear(); 182 if (cacheFile != null) { 183 try { 184 cacheFile.persist(); 185 } 186 catch (IOException exc) { 187 throw new IllegalStateException( 188 getLocalizedMessage("Checker.cacheFilesException") + ": " 189 + cacheFile.getFileName(), 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 final int errorCount; 220 try (AuditCompletion auditCompletion = new AuditCompletion()) { 221 for (final FileSetCheck fsc : fileSetChecks) { 222 fsc.beginProcessing(charset); 223 } 224 225 final List<File> targetFiles = files.stream() 226 .filter(file -> CommonUtil.matchesFileExtension(file, fileExtensions)) 227 .toList(); 228 processFiles(targetFiles); 229 230 // Finish up 231 // It may also log!!! 232 fileSetChecks.forEach(FileSetCheck::finishProcessing); 233 234 // It may also log!!! 235 fileSetChecks.forEach(FileSetCheck::destroy); 236 237 errorCount = counter.getCount(); 238 } 239 return errorCount; 240 } 241 242 /** 243 * Returns a set of external configuration resource locations which are used by all file set 244 * checks and filters. 245 * 246 * @return a set of external configuration resource locations which are used by all file set 247 * checks and filters. 248 */ 249 private Set<String> getExternalResourceLocations() { 250 return Stream.concat(fileSetChecks.stream(), filters.getFilters().stream()) 251 .filter(ExternalResourceHolder.class::isInstance) 252 .flatMap(resource -> { 253 return ((ExternalResourceHolder) resource) 254 .getExternalResourceLocations().stream(); 255 }) 256 .collect(Collectors.toUnmodifiableSet()); 257 } 258 259 /** 260 * Processes a list of files with all FileSetChecks. 261 * 262 * @param files a list of files to process. 263 * @throws CheckstyleException if error condition within Checkstyle occurs. 264 * @throws Error wraps any java.lang.Error happened during execution 265 * @noinspection NestedTryStatement, ProhibitedExceptionThrown 266 * @noinspectionreason NestedTryStatement - false positive: the inner try-with-resources 267 * has no catch or finally to merge. Remove this suppression when the inspection is fixed. 268 * See <a href="https://youtrack.jetbrains.com/issue/IDEA-393895">IDEA-393895</a>. 269 * It completes file notifications before the outer handlers remove failed files from 270 * the cache and wrap failures. 271 * @noinspectionreason ProhibitedExceptionThrown - preserve Error propagation while adding 272 * the name of the file that was being processed. 273 */ 274 // -@cs[CyclomaticComplexity] no easy way to split this logic of processing the file 275 private void processFiles(List<File> files) throws CheckstyleException { 276 for (final File file : files) { 277 String fileName = null; 278 final String filePath = file.getPath(); 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 try (FileAuditCompletion completion = new FileAuditCompletion(fileName)) { 290 final SortedSet<Violation> fileMessages = processFile(file); 291 fireErrors(fileName, fileMessages); 292 } 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 exc) { 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( 303 getLocalizedMessage("Checker.processFilesException", filePath), exc); 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(getLocalizedMessage("Checker.error", filePath), error); 312 } 313 } 314 } 315 316 /** 317 * Processes a file with all FileSetChecks. 318 * 319 * @param file a file to process. 320 * @return a sorted set of violations to be logged. 321 * @throws CheckstyleException if error condition within Checkstyle occurs. 322 * @noinspection ProhibitedExceptionThrown 323 * @noinspectionreason ProhibitedExceptionThrown - there is no other way to obey 324 * haltOnException field 325 */ 326 private SortedSet<Violation> processFile(File file) throws CheckstyleException { 327 final SortedSet<Violation> fileMessages = new TreeSet<>(); 328 try { 329 final FileText theText = new FileText(file.getAbsoluteFile(), charset); 330 for (final FileSetCheck fsc : fileSetChecks) { 331 fileMessages.addAll(fsc.process(file, theText)); 332 } 333 } 334 catch (final IOException ioe) { 335 log.debug("IOException occurred.", ioe); 336 fileMessages.add(new Violation(1, 337 Definitions.CHECKSTYLE_BUNDLE, EXCEPTION_MSG, 338 new String[] {ioe.getMessage()}, null, getClass(), null)); 339 } 340 // -@cs[IllegalCatch] There is no other way to obey haltOnException field 341 catch (Exception exc) { 342 if (haltOnException) { 343 throw exc; 344 } 345 346 log.debug("Exception occurred.", exc); 347 348 final StringWriter sw = new StringWriter(); 349 final PrintWriter pw = new PrintWriter(sw, true); 350 351 exc.printStackTrace(pw); 352 353 fileMessages.add(new Violation(1, 354 Definitions.CHECKSTYLE_BUNDLE, EXCEPTION_MSG, 355 new String[] {sw.getBuffer().toString()}, 356 null, getClass(), null)); 357 } 358 return fileMessages; 359 } 360 361 /** 362 * Check if all before execution file filters accept starting the file. 363 * 364 * @param fileName 365 * the file to be audited 366 * @return {@code true} if the file is accepted. 367 */ 368 private boolean acceptFileStarted(String fileName) { 369 final String stripped = relativizePathWithCatch(fileName); 370 return beforeExecutionFileFilters.accept(stripped); 371 } 372 373 /** 374 * Notify all listeners about the beginning of a file audit. 375 * 376 * @param fileName 377 * the file to be audited 378 */ 379 @Override 380 public void fireFileStarted(String fileName) { 381 final String stripped = relativizePathWithCatch(fileName); 382 final AuditEvent event = new AuditEvent(this, stripped); 383 for (final AuditListener listener : listeners) { 384 listener.fileStarted(event); 385 } 386 } 387 388 /** 389 * Notify all listeners about the errors in a file. 390 * 391 * @param fileName the audited file 392 * @param errors the audit errors from the file 393 */ 394 @Override 395 public void fireErrors(String fileName, SortedSet<Violation> errors) { 396 final String stripped = relativizePathWithCatch(fileName); 397 boolean hasNonFilteredViolations = false; 398 for (final Violation element : errors) { 399 final AuditEvent event = new AuditEvent(this, stripped, element); 400 if (filters.accept(event)) { 401 hasNonFilteredViolations = true; 402 for (final AuditListener listener : listeners) { 403 listener.addError(event); 404 } 405 } 406 } 407 if (hasNonFilteredViolations && cacheFile != null) { 408 cacheFile.remove(fileName); 409 } 410 } 411 412 /** 413 * Notify all listeners about the end of a file audit. 414 * 415 * @param fileName 416 * the audited file 417 */ 418 @Override 419 public void fireFileFinished(String fileName) { 420 final String stripped = relativizePathWithCatch(fileName); 421 final AuditEvent event = new AuditEvent(this, stripped); 422 for (final AuditListener listener : listeners) { 423 listener.fileFinished(event); 424 } 425 } 426 427 @Override 428 protected void finishLocalSetup() throws CheckstyleException { 429 final Locale locale = Locale.of(localeLanguage, localeCountry); 430 LocalizedMessage.setLocale(locale); 431 432 if (moduleFactory == null) { 433 if (moduleClassLoader == null) { 434 throw new CheckstyleException(getLocalizedMessage("Checker.finishLocalSetup")); 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("moduleFactory", moduleFactory); 446 context.add("severity", severity.getName()); 447 context.add("basedir", basedir); 448 context.add("tabWidth", String.valueOf(tabWidth)); 449 childContext = context; 450 } 451 452 /** 453 * {@inheritDoc} Creates child module. 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 final AbstractAutomaticBean bean) { 465 bean.contextualize(childContext); 466 bean.configure(childConf); 467 } 468 } 469 catch (final CheckstyleException exc) { 470 throw new CheckstyleException( 471 getLocalizedMessage("Checker.setupChildModule", name), exc); 472 } 473 switch (child) { 474 case FileSetCheck fsc -> { 475 fsc.init(); 476 addFileSetCheck(fsc); 477 } 478 case BeforeExecutionFileFilter filter -> addBeforeExecutionFileFilter(filter); 479 case Filter filter -> addFilter(filter); 480 case AuditListener listener -> addListener(listener); 481 case null, default -> throw new CheckstyleException( 482 getLocalizedMessage("Checker.setupChildNotAllowed", name)); 483 } 484 } 485 486 /** 487 * Adds a FileSetCheck to the list of FileSetChecks that is executed in process(). 488 * 489 * @param fileSetCheck the additional FileSetCheck 490 */ 491 public void addFileSetCheck(FileSetCheck fileSetCheck) { 492 fileSetCheck.setMessageDispatcher(this); 493 fileSetChecks.add(fileSetCheck); 494 } 495 496 /** 497 * Adds a before execution file filter to the end of the event chain. 498 * 499 * @param filter the additional filter 500 */ 501 public void addBeforeExecutionFileFilter(BeforeExecutionFileFilter filter) { 502 beforeExecutionFileFilters.addBeforeExecutionFileFilter(filter); 503 } 504 505 /** 506 * Adds a filter to the end of the audit event filter chain. 507 * 508 * @param filter the additional filter 509 */ 510 public void addFilter(Filter filter) { 511 filters.addFilter(filter); 512 } 513 514 @Override 515 public final void addListener(AuditListener listener) { 516 listeners.add(listener); 517 } 518 519 /** 520 * Sets the file extensions that identify the files that pass the filter of this FileSetCheck. 521 * 522 * @param extensions the set of file extensions. A missing 523 * initial '.' character of an extension is automatically added. 524 */ 525 public final void setFileExtensions(String... extensions) { 526 if (extensions != null) { 527 fileExtensions = new String[extensions.length]; 528 for (int index = 0; index < extensions.length; index++) { 529 final String extension = extensions[index]; 530 if (extension.startsWith(EXTENSION_SEPARATOR)) { 531 fileExtensions[index] = extension; 532 } 533 else { 534 fileExtensions[index] = EXTENSION_SEPARATOR + extension; 535 } 536 } 537 } 538 } 539 540 /** 541 * Sets the factory for creating submodules. 542 * 543 * @param moduleFactory the factory for creating FileSetChecks 544 */ 545 public void setModuleFactory(ModuleFactory moduleFactory) { 546 this.moduleFactory = moduleFactory; 547 } 548 549 /** 550 * Sets locale country. 551 * 552 * @param localeCountry the country to report messages 553 */ 554 public void setLocaleCountry(String localeCountry) { 555 this.localeCountry = localeCountry; 556 } 557 558 /** 559 * Sets locale language. 560 * 561 * @param localeLanguage the language to report messages 562 */ 563 public void setLocaleLanguage(String localeLanguage) { 564 this.localeLanguage = localeLanguage; 565 } 566 567 /** 568 * Sets the severity level. The string should be one of the names 569 * defined in the {@code SeverityLevel} class. 570 * 571 * @param severity The new severity level 572 * @see SeverityLevel 573 */ 574 public final void setSeverity(String severity) { 575 this.severity = SeverityLevel.getInstance(severity); 576 } 577 578 @Override 579 public final void setModuleClassLoader(ClassLoader moduleClassLoader) { 580 this.moduleClassLoader = moduleClassLoader; 581 } 582 583 /** 584 * Sets a named charset. 585 * 586 * @param charset the name of a charset 587 * @throws UnsupportedEncodingException if charset is unsupported. 588 */ 589 public void setCharset(String charset) 590 throws UnsupportedEncodingException { 591 if (!Charset.isSupported(charset)) { 592 throw new UnsupportedEncodingException( 593 getLocalizedMessage("Checker.setCharset", charset)); 594 } 595 this.charset = charset; 596 } 597 598 /** 599 * Sets the field haltOnException. 600 * 601 * @param haltOnException the new value. 602 */ 603 public void setHaltOnException(boolean haltOnException) { 604 this.haltOnException = haltOnException; 605 } 606 607 /** 608 * Set the tab width to report audit events with. 609 * 610 * @param tabWidth an {@code int} value 611 */ 612 public final void setTabWidth(int tabWidth) { 613 this.tabWidth = tabWidth; 614 } 615 616 /** 617 * Clears the cache. 618 */ 619 public void clearCache() { 620 if (cacheFile != null) { 621 cacheFile.reset(); 622 } 623 } 624 625 /** 626 * Extracts localized messages from properties files. 627 * 628 * @param messageKey the key pointing to localized message in respective properties file. 629 * @param args the arguments of message in respective properties file. 630 * @return a string containing extracted localized message 631 */ 632 private String getLocalizedMessage(String messageKey, Object... args) { 633 final LocalizedMessage localizedMessage = new LocalizedMessage( 634 Definitions.CHECKSTYLE_BUNDLE, getClass(), 635 messageKey, args); 636 637 return localizedMessage.getMessage(); 638 } 639 640 /** 641 * Relativizes a path and wraps any exception with a user-friendly localized message. 642 * 643 * @param fileName the file path to relativize 644 * @return the relativized path 645 * @throws IllegalStateException if any exception occurs during relativization 646 */ 647 private String relativizePathWithCatch(String fileName) { 648 try { 649 return CommonUtil.relativizePath(basedir, fileName); 650 } 651 // -@cs[IllegalCatch] Catching generic Exception to include fileName and basedir context 652 catch (Exception exception) { 653 throw new IllegalStateException( 654 getLocalizedMessage("general.relativizePath", 655 fileName, basedir), exception); 656 } 657 } 658 659 /** Starts the audit and completes it when processing ends. */ 660 private final class AuditCompletion implements AutoCloseable { 661 662 /** Starts the audit. */ 663 private AuditCompletion() { 664 final AuditEvent event = new AuditEvent(Checker.this); 665 for (final AuditListener listener : listeners) { 666 listener.auditStarted(event); 667 } 668 } 669 670 /** Completes the audit. */ 671 @Override 672 public void close() { 673 final AuditEvent event = new AuditEvent(Checker.this); 674 for (final AuditListener listener : listeners) { 675 listener.auditFinished(event); 676 } 677 } 678 679 } 680 681 /** Starts the file audit and completes it when processing ends. */ 682 private final class FileAuditCompletion implements AutoCloseable { 683 684 /** Name of the file being audited. */ 685 private final String fileName; 686 687 /** 688 * Starts auditing the file. 689 * 690 * @param fileName the name of the file to audit 691 */ 692 private FileAuditCompletion(String fileName) { 693 this.fileName = fileName; 694 fireFileStarted(fileName); 695 } 696 697 /** Completes the file audit. */ 698 @Override 699 public void close() { 700 fireFileFinished(fileName); 701 } 702 703 } 704 705}