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.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 = CommonUtil.EMPTY_STRING_ARRAY; 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 ex) { 188 throw new IllegalStateException("Unable to persist cache file.", ex); 189 } 190 } 191 } 192 193 /** 194 * Removes a given listener. 195 * 196 * @param listener a listener to remove 197 */ 198 public void removeListener(AuditListener listener) { 199 listeners.remove(listener); 200 } 201 202 /** 203 * Sets base directory. 204 * 205 * @param basedir the base directory to strip off in file names 206 */ 207 public void setBasedir(String basedir) { 208 this.basedir = basedir; 209 } 210 211 @Override 212 public int process(List<File> files) throws CheckstyleException { 213 if (cacheFile != null) { 214 cacheFile.putExternalResources(getExternalResourceLocations()); 215 } 216 217 // Prepare to start 218 fireAuditStarted(); 219 for (final FileSetCheck fsc : fileSetChecks) { 220 fsc.beginProcessing(charset); 221 } 222 223 final List<File> targetFiles = files.stream() 224 .filter(file -> CommonUtil.matchesFileExtension(file, fileExtensions)) 225 .collect(Collectors.toList()); 226 processFiles(targetFiles); 227 228 // Finish up 229 // It may also log!!! 230 fileSetChecks.forEach(FileSetCheck::finishProcessing); 231 232 // It may also log!!! 233 fileSetChecks.forEach(FileSetCheck::destroy); 234 235 final int errorCount = counter.getCount(); 236 fireAuditFinished(); 237 return errorCount; 238 } 239 240 /** 241 * Returns a set of external configuration resource locations which are used by all file set 242 * checks and filters. 243 * 244 * @return a set of external configuration resource locations which are used by all file set 245 * checks and filters. 246 */ 247 private Set<String> getExternalResourceLocations() { 248 return Stream.concat(fileSetChecks.stream(), filters.getFilters().stream()) 249 .filter(ExternalResourceHolder.class::isInstance) 250 .map(ExternalResourceHolder.class::cast) 251 .flatMap(resource -> resource.getExternalResourceLocations().stream()) 252 .collect(Collectors.toSet()); 253 } 254 255 /** Notify all listeners about the audit start. */ 256 private void fireAuditStarted() { 257 final AuditEvent event = new AuditEvent(this); 258 for (final AuditListener listener : listeners) { 259 listener.auditStarted(event); 260 } 261 } 262 263 /** Notify all listeners about the audit end. */ 264 private void fireAuditFinished() { 265 final AuditEvent event = new AuditEvent(this); 266 for (final AuditListener listener : listeners) { 267 listener.auditFinished(event); 268 } 269 } 270 271 /** 272 * Processes a list of files with all FileSetChecks. 273 * 274 * @param files a list of files to process. 275 * @throws CheckstyleException if error condition within Checkstyle occurs. 276 * @throws Error wraps any java.lang.Error happened during execution 277 * @noinspection ProhibitedExceptionThrown 278 * @noinspectionreason ProhibitedExceptionThrown - There is no other way to 279 * deliver filename that was under processing. 280 */ 281 // -@cs[CyclomaticComplexity] no easy way to split this logic of processing the file 282 private void processFiles(List<File> files) throws CheckstyleException { 283 for (final File file : files) { 284 String fileName = null; 285 try { 286 fileName = file.getAbsolutePath(); 287 final long timestamp = file.lastModified(); 288 if (cacheFile != null && cacheFile.isInCache(fileName, timestamp) 289 || !acceptFileStarted(fileName)) { 290 continue; 291 } 292 if (cacheFile != null) { 293 cacheFile.put(fileName, timestamp); 294 } 295 fireFileStarted(fileName); 296 final SortedSet<Violation> fileMessages = processFile(file); 297 fireErrors(fileName, fileMessages); 298 fireFileFinished(fileName); 299 } 300 // -@cs[IllegalCatch] There is no other way to deliver filename that was under 301 // processing. See https://github.com/checkstyle/checkstyle/issues/2285 302 catch (Exception ex) { 303 if (fileName != null && cacheFile != null) { 304 cacheFile.remove(fileName); 305 } 306 307 // We need to catch all exceptions to put a reason failure (file name) in exception 308 throw new CheckstyleException("Exception was thrown while processing " 309 + file.getPath(), ex); 310 } 311 catch (Error error) { 312 if (fileName != null && cacheFile != null) { 313 cacheFile.remove(fileName); 314 } 315 316 // We need to catch all errors to put a reason failure (file name) in error 317 throw new Error("Error was thrown while processing " + file.getPath(), error); 318 } 319 } 320 } 321 322 /** 323 * Processes a file with all FileSetChecks. 324 * 325 * @param file a file to process. 326 * @return a sorted set of violations to be logged. 327 * @throws CheckstyleException if error condition within Checkstyle occurs. 328 * @noinspection ProhibitedExceptionThrown 329 * @noinspectionreason ProhibitedExceptionThrown - there is no other way to obey 330 * haltOnException field 331 */ 332 private SortedSet<Violation> processFile(File file) throws CheckstyleException { 333 final SortedSet<Violation> 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 Violation(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 Violation(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<Violation> errors) { 402 final String stripped = CommonUtil.relativizeAndNormalizePath(basedir, fileName); 403 boolean hasNonFilteredViolations = false; 404 for (final Violation 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 * @noinspectionreason ChainOfInstanceofChecks - we treat checks and filters differently 465 */ 466 @Override 467 protected void setupChild(Configuration childConf) 468 throws CheckstyleException { 469 final String name = childConf.getName(); 470 final Object child; 471 472 try { 473 child = moduleFactory.createModule(name); 474 475 if (child instanceof AbstractAutomaticBean) { 476 final AbstractAutomaticBean bean = (AbstractAutomaticBean) child; 477 bean.contextualize(childContext); 478 bean.configure(childConf); 479 } 480 } 481 catch (final CheckstyleException ex) { 482 throw new CheckstyleException("cannot initialize module " + name 483 + " - " + ex.getMessage(), ex); 484 } 485 if (child instanceof FileSetCheck) { 486 final FileSetCheck fsc = (FileSetCheck) child; 487 fsc.init(); 488 addFileSetCheck(fsc); 489 } 490 else if (child instanceof BeforeExecutionFileFilter) { 491 final BeforeExecutionFileFilter filter = (BeforeExecutionFileFilter) child; 492 addBeforeExecutionFileFilter(filter); 493 } 494 else if (child instanceof Filter) { 495 final Filter filter = (Filter) child; 496 addFilter(filter); 497 } 498 else if (child instanceof AuditListener) { 499 final AuditListener listener = (AuditListener) child; 500 addListener(listener); 501 } 502 else { 503 throw new CheckstyleException(name 504 + " is not allowed as a child in Checker"); 505 } 506 } 507 508 /** 509 * Adds a FileSetCheck to the list of FileSetChecks 510 * that is executed in process(). 511 * 512 * @param fileSetCheck the additional FileSetCheck 513 */ 514 public void addFileSetCheck(FileSetCheck fileSetCheck) { 515 fileSetCheck.setMessageDispatcher(this); 516 fileSetChecks.add(fileSetCheck); 517 } 518 519 /** 520 * Adds a before execution file filter to the end of the event chain. 521 * 522 * @param filter the additional filter 523 */ 524 public void addBeforeExecutionFileFilter(BeforeExecutionFileFilter filter) { 525 beforeExecutionFileFilters.addBeforeExecutionFileFilter(filter); 526 } 527 528 /** 529 * Adds a filter to the end of the audit event filter chain. 530 * 531 * @param filter the additional filter 532 */ 533 public void addFilter(Filter filter) { 534 filters.addFilter(filter); 535 } 536 537 @Override 538 public final void addListener(AuditListener listener) { 539 listeners.add(listener); 540 } 541 542 /** 543 * Sets the file extensions that identify the files that pass the 544 * filter of this FileSetCheck. 545 * 546 * @param extensions the set of file extensions. A missing 547 * initial '.' character of an extension is automatically added. 548 */ 549 public final void setFileExtensions(String... extensions) { 550 if (extensions == null) { 551 fileExtensions = null; 552 } 553 else { 554 fileExtensions = new String[extensions.length]; 555 for (int i = 0; i < extensions.length; i++) { 556 final String extension = extensions[i]; 557 if (extension.startsWith(EXTENSION_SEPARATOR)) { 558 fileExtensions[i] = extension; 559 } 560 else { 561 fileExtensions[i] = EXTENSION_SEPARATOR + extension; 562 } 563 } 564 } 565 } 566 567 /** 568 * Sets the factory for creating submodules. 569 * 570 * @param moduleFactory the factory for creating FileSetChecks 571 */ 572 public void setModuleFactory(ModuleFactory moduleFactory) { 573 this.moduleFactory = moduleFactory; 574 } 575 576 /** 577 * Sets locale country. 578 * 579 * @param localeCountry the country to report messages 580 */ 581 public void setLocaleCountry(String localeCountry) { 582 this.localeCountry = localeCountry; 583 } 584 585 /** 586 * Sets locale language. 587 * 588 * @param localeLanguage the language to report messages 589 */ 590 public void setLocaleLanguage(String localeLanguage) { 591 this.localeLanguage = localeLanguage; 592 } 593 594 /** 595 * Sets the severity level. The string should be one of the names 596 * defined in the {@code SeverityLevel} class. 597 * 598 * @param severity The new severity level 599 * @see SeverityLevel 600 */ 601 public final void setSeverity(String severity) { 602 this.severity = SeverityLevel.getInstance(severity); 603 } 604 605 @Override 606 public final void setModuleClassLoader(ClassLoader moduleClassLoader) { 607 this.moduleClassLoader = moduleClassLoader; 608 } 609 610 /** 611 * Sets a named charset. 612 * 613 * @param charset the name of a charset 614 * @throws UnsupportedEncodingException if charset is unsupported. 615 */ 616 public void setCharset(String charset) 617 throws UnsupportedEncodingException { 618 if (!Charset.isSupported(charset)) { 619 final String message = "unsupported charset: '" + charset + "'"; 620 throw new UnsupportedEncodingException(message); 621 } 622 this.charset = charset; 623 } 624 625 /** 626 * Sets the field haltOnException. 627 * 628 * @param haltOnException the new value. 629 */ 630 public void setHaltOnException(boolean haltOnException) { 631 this.haltOnException = haltOnException; 632 } 633 634 /** 635 * Set the tab width to report audit events with. 636 * 637 * @param tabWidth an {@code int} value 638 */ 639 public final void setTabWidth(int tabWidth) { 640 this.tabWidth = tabWidth; 641 } 642 643 /** 644 * Clears the cache. 645 */ 646 public void clearCache() { 647 if (cacheFile != null) { 648 cacheFile.reset(); 649 } 650 } 651 652}