001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2018 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; 036 037import org.apache.commons.logging.Log; 038import org.apache.commons.logging.LogFactory; 039 040import com.puppycrawl.tools.checkstyle.api.AuditEvent; 041import com.puppycrawl.tools.checkstyle.api.AuditListener; 042import com.puppycrawl.tools.checkstyle.api.AutomaticBean; 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.LocalizedMessage; 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.utils.CommonUtils; 059 060/** 061 * This class provides the functionality to check a set of files. 062 * @author Oliver Burn 063 * @author <a href="mailto:stephane.bailliez@wanadoo.fr">Stephane Bailliez</a> 064 * @author lkuehne 065 * @author Andrei Selkin 066 */ 067public class Checker extends AutomaticBean implements MessageDispatcher, RootModule { 068 069 /** Message to use when an exception occurs and should be printed as a violation. */ 070 public static final String EXCEPTION_MSG = "general.exception"; 071 072 /** Logger for Checker. */ 073 private final Log log; 074 075 /** Maintains error count. */ 076 private final SeverityLevelCounter counter = new SeverityLevelCounter( 077 SeverityLevel.ERROR); 078 079 /** Vector of listeners. */ 080 private final List<AuditListener> listeners = new ArrayList<>(); 081 082 /** Vector of fileset checks. */ 083 private final List<FileSetCheck> fileSetChecks = new ArrayList<>(); 084 085 /** The audit event before execution file filters. */ 086 private final BeforeExecutionFileFilterSet beforeExecutionFileFilters = 087 new BeforeExecutionFileFilterSet(); 088 089 /** The audit event filters. */ 090 private final FilterSet filters = new FilterSet(); 091 092 /** Class loader to resolve classes with. **/ 093 private ClassLoader classLoader = Thread.currentThread() 094 .getContextClassLoader(); 095 096 /** The basedir to strip off in file names. */ 097 private String basedir; 098 099 /** Locale country to report messages . **/ 100 private String localeCountry = Locale.getDefault().getCountry(); 101 /** Locale language to report messages . **/ 102 private String localeLanguage = Locale.getDefault().getLanguage(); 103 104 /** The factory for instantiating submodules. */ 105 private ModuleFactory moduleFactory; 106 107 /** The classloader used for loading Checkstyle module classes. */ 108 private ClassLoader moduleClassLoader; 109 110 /** The context of all child components. */ 111 private Context childContext; 112 113 /** The file extensions that are accepted. */ 114 private String[] fileExtensions = CommonUtils.EMPTY_STRING_ARRAY; 115 116 /** 117 * The severity level of any violations found by submodules. 118 * The value of this property is passed to submodules via 119 * contextualize(). 120 * 121 * <p>Note: Since the Checker is merely a container for modules 122 * it does not make sense to implement logging functionality 123 * here. Consequently Checker does not extend AbstractViolationReporter, 124 * leading to a bit of duplicated code for severity level setting. 125 */ 126 private SeverityLevel severity = SeverityLevel.ERROR; 127 128 /** Name of a charset. */ 129 private String charset = System.getProperty("file.encoding", StandardCharsets.UTF_8.name()); 130 131 /** Cache file. **/ 132 private PropertyCacheFile cacheFile; 133 134 /** Controls whether exceptions should halt execution or not. */ 135 private boolean haltOnException = true; 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 * @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 * @param filter before execution file filter to remove. 160 */ 161 public void removeBeforeExecutionFileFilter(BeforeExecutionFileFilter filter) { 162 beforeExecutionFileFilters.removeBeforeExecutionFileFilter(filter); 163 } 164 165 /** 166 * Removes filter. 167 * @param filter filter to remove. 168 */ 169 public void removeFilter(Filter filter) { 170 filters.removeFilter(filter); 171 } 172 173 @Override 174 public void destroy() { 175 listeners.clear(); 176 fileSetChecks.clear(); 177 beforeExecutionFileFilters.clear(); 178 filters.clear(); 179 if (cacheFile != null) { 180 try { 181 cacheFile.persist(); 182 } 183 catch (IOException ex) { 184 throw new IllegalStateException("Unable to persist cache file.", ex); 185 } 186 } 187 } 188 189 /** 190 * Removes a given listener. 191 * @param listener a listener to remove 192 */ 193 public void removeListener(AuditListener listener) { 194 listeners.remove(listener); 195 } 196 197 /** 198 * Sets base directory. 199 * @param basedir the base directory to strip off in file names 200 */ 201 public void setBasedir(String basedir) { 202 this.basedir = basedir; 203 } 204 205 @Override 206 public int process(List<File> files) throws CheckstyleException { 207 if (cacheFile != null) { 208 cacheFile.putExternalResources(getExternalResourceLocations()); 209 } 210 211 // Prepare to start 212 fireAuditStarted(); 213 for (final FileSetCheck fsc : fileSetChecks) { 214 fsc.beginProcessing(charset); 215 } 216 217 processFiles(files); 218 219 // Finish up 220 // It may also log!!! 221 fileSetChecks.forEach(FileSetCheck::finishProcessing); 222 223 // It may also log!!! 224 fileSetChecks.forEach(FileSetCheck::destroy); 225 226 final int errorCount = counter.getCount(); 227 fireAuditFinished(); 228 return errorCount; 229 } 230 231 /** 232 * Returns a set of external configuration resource locations which are used by all file set 233 * checks and filters. 234 * @return a set of external configuration resource locations which are used by all file set 235 * checks and filters. 236 */ 237 private Set<String> getExternalResourceLocations() { 238 final Set<String> externalResources = new HashSet<>(); 239 fileSetChecks.stream().filter(check -> check instanceof ExternalResourceHolder) 240 .forEach(check -> { 241 final Set<String> locations = 242 ((ExternalResourceHolder) check).getExternalResourceLocations(); 243 externalResources.addAll(locations); 244 }); 245 filters.getFilters().stream().filter(filter -> filter instanceof ExternalResourceHolder) 246 .forEach(filter -> { 247 final Set<String> locations = 248 ((ExternalResourceHolder) filter).getExternalResourceLocations(); 249 externalResources.addAll(locations); 250 }); 251 return externalResources; 252 } 253 254 /** Notify all listeners about the audit start. */ 255 private void fireAuditStarted() { 256 final AuditEvent event = new AuditEvent(this); 257 for (final AuditListener listener : listeners) { 258 listener.auditStarted(event); 259 } 260 } 261 262 /** Notify all listeners about the audit end. */ 263 private void fireAuditFinished() { 264 final AuditEvent event = new AuditEvent(this); 265 for (final AuditListener listener : listeners) { 266 listener.auditFinished(event); 267 } 268 } 269 270 /** 271 * Processes a list of files with all FileSetChecks. 272 * @param files a list of files to process. 273 * @throws CheckstyleException if error condition within Checkstyle occurs. 274 * @noinspection ProhibitedExceptionThrown 275 */ 276 private void processFiles(List<File> files) throws CheckstyleException { 277 for (final File file : files) { 278 try { 279 final String fileName = file.getAbsolutePath(); 280 final long timestamp = file.lastModified(); 281 if (cacheFile != null && cacheFile.isInCache(fileName, timestamp) 282 || !CommonUtils.matchesFileExtension(file, fileExtensions) 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 // We need to catch all exceptions to put a reason failure (file name) in exception 298 throw new CheckstyleException("Exception was thrown while processing " 299 + file.getPath(), ex); 300 } 301 catch (Error error) { 302 // We need to catch all errors to put a reason failure (file name) in error 303 throw new Error("Error was thrown while processing " + file.getPath(), error); 304 } 305 } 306 } 307 308 /** 309 * Processes a file with all FileSetChecks. 310 * @param file a file to process. 311 * @return a sorted set of messages to be logged. 312 * @throws CheckstyleException if error condition within Checkstyle occurs. 313 * @noinspection ProhibitedExceptionThrown 314 */ 315 private SortedSet<LocalizedMessage> processFile(File file) throws CheckstyleException { 316 final SortedSet<LocalizedMessage> fileMessages = new TreeSet<>(); 317 try { 318 final FileText theText = new FileText(file.getAbsoluteFile(), charset); 319 for (final FileSetCheck fsc : fileSetChecks) { 320 fileMessages.addAll(fsc.process(file, theText)); 321 } 322 } 323 catch (final IOException ioe) { 324 log.debug("IOException occurred.", ioe); 325 fileMessages.add(new LocalizedMessage(0, 326 Definitions.CHECKSTYLE_BUNDLE, EXCEPTION_MSG, 327 new String[] {ioe.getMessage()}, null, getClass(), null)); 328 } 329 // -@cs[IllegalCatch] There is no other way to obey haltOnException field 330 catch (Exception ex) { 331 if (haltOnException) { 332 throw ex; 333 } 334 335 log.debug("Exception occurred.", ex); 336 337 final StringWriter sw = new StringWriter(); 338 final PrintWriter pw = new PrintWriter(sw, true); 339 340 ex.printStackTrace(pw); 341 342 fileMessages.add(new LocalizedMessage(0, 343 Definitions.CHECKSTYLE_BUNDLE, EXCEPTION_MSG, 344 new String[] {sw.getBuffer().toString()}, 345 null, getClass(), null)); 346 } 347 return fileMessages; 348 } 349 350 /** 351 * Check if all before execution file filters accept starting the file. 352 * 353 * @param fileName 354 * the file to be audited 355 * @return {@code true} if the file is accepted. 356 */ 357 private boolean acceptFileStarted(String fileName) { 358 final String stripped = CommonUtils.relativizeAndNormalizePath(basedir, fileName); 359 return beforeExecutionFileFilters.accept(stripped); 360 } 361 362 /** 363 * Notify all listeners about the beginning of a file audit. 364 * 365 * @param fileName 366 * the file to be audited 367 */ 368 @Override 369 public void fireFileStarted(String fileName) { 370 final String stripped = CommonUtils.relativizeAndNormalizePath(basedir, fileName); 371 final AuditEvent event = new AuditEvent(this, stripped); 372 for (final AuditListener listener : listeners) { 373 listener.fileStarted(event); 374 } 375 } 376 377 /** 378 * Notify all listeners about the errors in a file. 379 * 380 * @param fileName the audited file 381 * @param errors the audit errors from the file 382 */ 383 @Override 384 public void fireErrors(String fileName, SortedSet<LocalizedMessage> errors) { 385 final String stripped = CommonUtils.relativizeAndNormalizePath(basedir, fileName); 386 boolean hasNonFilteredViolations = false; 387 for (final LocalizedMessage element : errors) { 388 final AuditEvent event = new AuditEvent(this, stripped, element); 389 if (filters.accept(event)) { 390 hasNonFilteredViolations = true; 391 for (final AuditListener listener : listeners) { 392 listener.addError(event); 393 } 394 } 395 } 396 if (hasNonFilteredViolations && cacheFile != null) { 397 cacheFile.remove(fileName); 398 } 399 } 400 401 /** 402 * Notify all listeners about the end of a file audit. 403 * 404 * @param fileName 405 * the audited file 406 */ 407 @Override 408 public void fireFileFinished(String fileName) { 409 final String stripped = CommonUtils.relativizeAndNormalizePath(basedir, fileName); 410 final AuditEvent event = new AuditEvent(this, stripped); 411 for (final AuditListener listener : listeners) { 412 listener.fileFinished(event); 413 } 414 } 415 416 @Override 417 protected void finishLocalSetup() throws CheckstyleException { 418 final Locale locale = new Locale(localeLanguage, localeCountry); 419 LocalizedMessage.setLocale(locale); 420 421 if (moduleFactory == null) { 422 if (moduleClassLoader == null) { 423 throw new CheckstyleException( 424 "if no custom moduleFactory is set, " 425 + "moduleClassLoader must be specified"); 426 } 427 428 final Set<String> packageNames = PackageNamesLoader 429 .getPackageNames(moduleClassLoader); 430 moduleFactory = new PackageObjectFactory(packageNames, 431 moduleClassLoader); 432 } 433 434 final DefaultContext context = new DefaultContext(); 435 context.add("charset", charset); 436 context.add("classLoader", classLoader); 437 context.add("moduleFactory", moduleFactory); 438 context.add("severity", severity.getName()); 439 context.add("basedir", basedir); 440 childContext = context; 441 } 442 443 /** 444 * {@inheritDoc} Creates child module. 445 * @noinspection ChainOfInstanceofChecks 446 */ 447 @Override 448 protected void setupChild(Configuration childConf) 449 throws CheckstyleException { 450 final String name = childConf.getName(); 451 final Object child; 452 453 try { 454 child = moduleFactory.createModule(name); 455 456 if (child instanceof AutomaticBean) { 457 final AutomaticBean bean = (AutomaticBean) child; 458 bean.contextualize(childContext); 459 bean.configure(childConf); 460 } 461 } 462 catch (final CheckstyleException ex) { 463 throw new CheckstyleException("cannot initialize module " + name 464 + " - " + ex.getMessage(), ex); 465 } 466 if (child instanceof FileSetCheck) { 467 final FileSetCheck fsc = (FileSetCheck) child; 468 fsc.init(); 469 addFileSetCheck(fsc); 470 } 471 else if (child instanceof BeforeExecutionFileFilter) { 472 final BeforeExecutionFileFilter filter = (BeforeExecutionFileFilter) child; 473 addBeforeExecutionFileFilter(filter); 474 } 475 else if (child instanceof Filter) { 476 final Filter filter = (Filter) child; 477 addFilter(filter); 478 } 479 else if (child instanceof AuditListener) { 480 final AuditListener listener = (AuditListener) child; 481 addListener(listener); 482 } 483 else { 484 throw new CheckstyleException(name 485 + " is not allowed as a child in Checker"); 486 } 487 } 488 489 /** 490 * Adds a FileSetCheck to the list of FileSetChecks 491 * that is executed in process(). 492 * @param fileSetCheck the additional FileSetCheck 493 */ 494 public void addFileSetCheck(FileSetCheck fileSetCheck) { 495 fileSetCheck.setMessageDispatcher(this); 496 fileSetChecks.add(fileSetCheck); 497 } 498 499 /** 500 * Adds a before execution file filter to the end of the event chain. 501 * @param filter the additional filter 502 */ 503 public void addBeforeExecutionFileFilter(BeforeExecutionFileFilter filter) { 504 beforeExecutionFileFilters.addBeforeExecutionFileFilter(filter); 505 } 506 507 /** 508 * Adds a filter to the end of the audit event filter chain. 509 * @param filter the additional filter 510 */ 511 public void addFilter(Filter filter) { 512 filters.addFilter(filter); 513 } 514 515 @Override 516 public final void addListener(AuditListener listener) { 517 listeners.add(listener); 518 } 519 520 /** 521 * Sets the file extensions that identify the files that pass the 522 * filter of this FileSetCheck. 523 * @param extensions the set of file extensions. A missing 524 * initial '.' character of an extension is automatically added. 525 */ 526 public final void setFileExtensions(String... extensions) { 527 if (extensions == null) { 528 fileExtensions = null; 529 } 530 else { 531 fileExtensions = new String[extensions.length]; 532 for (int i = 0; i < extensions.length; i++) { 533 final String extension = extensions[i]; 534 if (CommonUtils.startsWithChar(extension, '.')) { 535 fileExtensions[i] = extension; 536 } 537 else { 538 fileExtensions[i] = "." + extension; 539 } 540 } 541 } 542 } 543 544 /** 545 * Sets the factory for creating submodules. 546 * 547 * @param moduleFactory the factory for creating FileSetChecks 548 */ 549 public void setModuleFactory(ModuleFactory moduleFactory) { 550 this.moduleFactory = moduleFactory; 551 } 552 553 /** 554 * Sets locale country. 555 * @param localeCountry the country to report messages 556 */ 557 public void setLocaleCountry(String localeCountry) { 558 this.localeCountry = localeCountry; 559 } 560 561 /** 562 * Sets locale language. 563 * @param localeLanguage the language to report messages 564 */ 565 public void setLocaleLanguage(String localeLanguage) { 566 this.localeLanguage = localeLanguage; 567 } 568 569 /** 570 * Sets the severity level. The string should be one of the names 571 * defined in the {@code SeverityLevel} class. 572 * 573 * @param severity The new severity level 574 * @see SeverityLevel 575 */ 576 public final void setSeverity(String severity) { 577 this.severity = SeverityLevel.getInstance(severity); 578 } 579 580 /** 581 * Sets the classloader that is used to contextualize fileset checks. 582 * Some Check implementations will use that classloader to improve the 583 * quality of their reports, e.g. to load a class and then analyze it via 584 * reflection. 585 * @param classLoader the new classloader 586 */ 587 public final void setClassLoader(ClassLoader classLoader) { 588 this.classLoader = classLoader; 589 } 590 591 @Override 592 public final void setModuleClassLoader(ClassLoader moduleClassLoader) { 593 this.moduleClassLoader = moduleClassLoader; 594 } 595 596 /** 597 * Sets a named charset. 598 * @param charset the name of a charset 599 * @throws UnsupportedEncodingException if charset is unsupported. 600 */ 601 public void setCharset(String charset) 602 throws UnsupportedEncodingException { 603 if (!Charset.isSupported(charset)) { 604 final String message = "unsupported charset: '" + charset + "'"; 605 throw new UnsupportedEncodingException(message); 606 } 607 this.charset = charset; 608 } 609 610 /** 611 * Sets the field haltOnException. 612 * @param haltOnException the new value. 613 */ 614 public void setHaltOnException(boolean haltOnException) { 615 this.haltOnException = haltOnException; 616 } 617 618 /** 619 * Clears the cache. 620 */ 621 public void clearCache() { 622 if (cacheFile != null) { 623 cacheFile.reset(); 624 } 625 } 626 627}