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.filters; 021 022import java.io.File; 023import java.io.IOException; 024import java.nio.charset.StandardCharsets; 025import java.util.ArrayList; 026import java.util.List; 027import java.util.Objects; 028import java.util.Optional; 029import java.util.regex.Matcher; 030import java.util.regex.Pattern; 031import java.util.regex.PatternSyntaxException; 032 033import com.puppycrawl.tools.checkstyle.api.AuditEvent; 034import com.puppycrawl.tools.checkstyle.api.AutomaticBean; 035import com.puppycrawl.tools.checkstyle.api.CheckstyleException; 036import com.puppycrawl.tools.checkstyle.api.FileText; 037import com.puppycrawl.tools.checkstyle.api.Filter; 038import com.puppycrawl.tools.checkstyle.utils.CommonUtils; 039 040/** 041 * <p> 042 * A filter that uses comments to suppress audit events. 043 * The filter can be used only to suppress audit events received from 044 * {@link com.puppycrawl.tools.checkstyle.api.FileSetCheck} checks. 045 * SuppressWithPlainTextCommentFilter knows nothing about AST, 046 * it treats only plain text comments and extracts the information required for suppression from 047 * the plain text comments. Currently the filter supports only single line comments. 048 * </p> 049 * <p> 050 * Rationale: 051 * Sometimes there are legitimate reasons for violating a check. When 052 * this is a matter of the code in question and not personal 053 * preference, the best place to override the policy is in the code 054 * itself. Semi-structured comments can be associated with the check. 055 * This is sometimes superior to a separate suppressions file, which 056 * must be kept up-to-date as the source file is edited. 057 * </p> 058 * @author Andrei Selkin 059 */ 060public class SuppressWithPlainTextCommentFilter extends AutomaticBean implements Filter { 061 062 /** Comment format which turns checkstyle reporting off. */ 063 private static final String DEFAULT_OFF_FORMAT = "// CHECKSTYLE:OFF"; 064 065 /** Comment format which turns checkstyle reporting on. */ 066 private static final String DEFAULT_ON_FORMAT = "// CHECKSTYLE:ON"; 067 068 /** Default check format to suppress. By default the filter suppress all checks. */ 069 private static final String DEFAULT_CHECK_FORMAT = ".*"; 070 071 /** Regexp which turns checkstyle reporting off. */ 072 private Pattern offCommentFormat = CommonUtils.createPattern(DEFAULT_OFF_FORMAT); 073 074 /** Regexp which turns checkstyle reporting on. */ 075 private Pattern onCommentFormat = CommonUtils.createPattern(DEFAULT_ON_FORMAT); 076 077 /** The check format to suppress. */ 078 private String checkFormat = DEFAULT_CHECK_FORMAT; 079 080 /** The message format to suppress.*/ 081 private String messageFormat; 082 083 /** 084 * Sets an off comment format pattern. 085 * @param pattern off comment format pattern. 086 */ 087 public final void setOffCommentFormat(Pattern pattern) { 088 offCommentFormat = pattern; 089 } 090 091 /** 092 * Sets an on comment format pattern. 093 * @param pattern on comment format pattern. 094 */ 095 public final void setOnCommentFormat(Pattern pattern) { 096 onCommentFormat = pattern; 097 } 098 099 /** 100 * Sets a pattern for check format. 101 * @param format pattern for check format. 102 */ 103 public final void setCheckFormat(String format) { 104 checkFormat = format; 105 } 106 107 /** 108 * Sets a pattern for message format. 109 * @param format pattern for message format. 110 */ 111 public final void setMessageFormat(String format) { 112 messageFormat = format; 113 } 114 115 @Override 116 public boolean accept(AuditEvent event) { 117 boolean accepted = true; 118 if (event.getLocalizedMessage() != null) { 119 final FileText fileText = getFileText(event.getFileName()); 120 if (fileText != null) { 121 final List<Suppression> suppressions = getSuppressions(fileText); 122 accepted = getNearestSuppression(suppressions, event) == null; 123 } 124 } 125 return accepted; 126 } 127 128 @Override 129 protected void finishLocalSetup() throws CheckstyleException { 130 // No code by default 131 } 132 133 /** 134 * Returns {@link FileText} instance created based on the given file name. 135 * @param fileName the name of the file. 136 * @return {@link FileText} instance. 137 */ 138 private static FileText getFileText(String fileName) { 139 final File file = new File(fileName); 140 FileText result = null; 141 142 // some violations can be on a directory, instead of a file 143 if (!file.isDirectory()) { 144 try { 145 result = new FileText(file, StandardCharsets.UTF_8.name()); 146 } 147 catch (IOException ex) { 148 throw new IllegalStateException("Cannot read source file: " + fileName, ex); 149 } 150 } 151 152 return result; 153 } 154 155 /** 156 * Returns the list of {@link Suppression} instances retrieved from the given {@link FileText}. 157 * @param fileText {@link FileText} instance. 158 * @return list of {@link Suppression} instances. 159 */ 160 private List<Suppression> getSuppressions(FileText fileText) { 161 final List<Suppression> suppressions = new ArrayList<>(); 162 for (int lineNo = 0; lineNo < fileText.size(); lineNo++) { 163 final Optional<Suppression> suppression = getSuppression(fileText, lineNo); 164 suppression.ifPresent(suppressions::add); 165 } 166 return suppressions; 167 } 168 169 /** 170 * Tries to extract the suppression from the given line. 171 * @param fileText {@link FileText} instance. 172 * @param lineNo line number. 173 * @return {@link Optional} of {@link Suppression}. 174 */ 175 private Optional<Suppression> getSuppression(FileText fileText, int lineNo) { 176 final String line = fileText.get(lineNo); 177 final Matcher onCommentMatcher = onCommentFormat.matcher(line); 178 final Matcher offCommentMatcher = offCommentFormat.matcher(line); 179 180 Suppression suppression = null; 181 if (onCommentMatcher.find()) { 182 suppression = new Suppression(onCommentMatcher.group(0), 183 lineNo + 1, onCommentMatcher.start(), SuppressionType.ON, this); 184 } 185 if (offCommentMatcher.find()) { 186 suppression = new Suppression(offCommentMatcher.group(0), 187 lineNo + 1, offCommentMatcher.start(), SuppressionType.OFF, this); 188 } 189 190 return Optional.ofNullable(suppression); 191 } 192 193 /** 194 * Finds the nearest {@link Suppression} instance which can suppress 195 * the given {@link AuditEvent}. The nearest suppression is the suppression which scope 196 * is before the line and column of the event. 197 * @param suppressions {@link Suppression} instance. 198 * @param event {@link AuditEvent} instance. 199 * @return {@link Suppression} instance. 200 */ 201 private static Suppression getNearestSuppression(List<Suppression> suppressions, 202 AuditEvent event) { 203 return suppressions 204 .stream() 205 .filter(suppression -> suppression.isMatch(event)) 206 .reduce((first, second) -> second) 207 .filter(suppression -> suppression.suppressionType != SuppressionType.ON) 208 .orElse(null); 209 } 210 211 /** Enum which represents the type of the suppression. */ 212 private enum SuppressionType { 213 214 /** On suppression type. */ 215 ON, 216 /** Off suppression type. */ 217 OFF 218 219 } 220 221 /** The class which represents the suppression. */ 222 public static class Suppression { 223 224 /** The regexp which is used to match the event source.*/ 225 private final Pattern eventSourceRegexp; 226 /** The regexp which is used to match the event message.*/ 227 private final Pattern eventMessageRegexp; 228 229 /** Suppression text.*/ 230 private final String text; 231 /** Suppression line.*/ 232 private final int lineNo; 233 /** Suppression column number.*/ 234 private final int columnNo; 235 /** Suppression type. */ 236 private final SuppressionType suppressionType; 237 238 /** 239 * Creates new suppression instance. 240 * @param text suppression text. 241 * @param lineNo suppression line number. 242 * @param columnNo suppression column number. 243 * @param suppressionType suppression type. 244 * @param filter the {@link SuppressWithPlainTextCommentFilter} with the context. 245 */ 246 protected Suppression( 247 String text, 248 int lineNo, 249 int columnNo, 250 SuppressionType suppressionType, 251 SuppressWithPlainTextCommentFilter filter 252 ) { 253 this.text = text; 254 this.lineNo = lineNo; 255 this.columnNo = columnNo; 256 this.suppressionType = suppressionType; 257 258 //Expand regexp for check and message 259 //Does not intern Patterns with Utils.getPattern() 260 String format = ""; 261 try { 262 if (this.suppressionType == SuppressionType.ON) { 263 format = CommonUtils.fillTemplateWithStringsByRegexp( 264 filter.checkFormat, text, filter.onCommentFormat); 265 eventSourceRegexp = Pattern.compile(format); 266 if (filter.messageFormat == null) { 267 eventMessageRegexp = null; 268 } 269 else { 270 format = CommonUtils.fillTemplateWithStringsByRegexp( 271 filter.messageFormat, text, filter.onCommentFormat); 272 eventMessageRegexp = Pattern.compile(format); 273 } 274 } 275 else { 276 format = CommonUtils.fillTemplateWithStringsByRegexp( 277 filter.checkFormat, text, filter.offCommentFormat); 278 eventSourceRegexp = Pattern.compile(format); 279 if (filter.messageFormat == null) { 280 eventMessageRegexp = null; 281 } 282 else { 283 format = CommonUtils.fillTemplateWithStringsByRegexp( 284 filter.messageFormat, text, filter.offCommentFormat); 285 eventMessageRegexp = Pattern.compile(format); 286 } 287 } 288 } 289 catch (final PatternSyntaxException ex) { 290 throw new IllegalArgumentException( 291 "unable to parse expanded comment " + format, ex); 292 } 293 } 294 295 @Override 296 public boolean equals(Object other) { 297 if (this == other) { 298 return true; 299 } 300 if (other == null || getClass() != other.getClass()) { 301 return false; 302 } 303 final Suppression suppression = (Suppression) other; 304 return Objects.equals(lineNo, suppression.lineNo) 305 && Objects.equals(columnNo, suppression.columnNo) 306 && Objects.equals(suppressionType, suppression.suppressionType) 307 && Objects.equals(text, suppression.text) 308 && Objects.equals(eventSourceRegexp, suppression.eventSourceRegexp) 309 && Objects.equals(eventMessageRegexp, suppression.eventMessageRegexp); 310 } 311 312 @Override 313 public int hashCode() { 314 return Objects.hash( 315 text, lineNo, columnNo, suppressionType, eventSourceRegexp, eventMessageRegexp); 316 } 317 318 /** 319 * Checks whether the suppression matches the given {@link AuditEvent}. 320 * @param event {@link AuditEvent} instance. 321 * @return true if the suppression matches {@link AuditEvent}. 322 */ 323 private boolean isMatch(AuditEvent event) { 324 boolean match = false; 325 if (isInScopeOfSuppression(event)) { 326 final Matcher sourceNameMatcher = eventSourceRegexp.matcher(event.getSourceName()); 327 if (sourceNameMatcher.find()) { 328 match = eventMessageRegexp == null 329 || eventMessageRegexp.matcher(event.getMessage()).find(); 330 } 331 else { 332 match = event.getModuleId() != null 333 && eventSourceRegexp.matcher(event.getModuleId()).find(); 334 } 335 } 336 return match; 337 } 338 339 /** 340 * Checks whether {@link AuditEvent} is in the scope of the suppression. 341 * @param event {@link AuditEvent} instance. 342 * @return true if {@link AuditEvent} is in the scope of the suppression. 343 */ 344 private boolean isInScopeOfSuppression(AuditEvent event) { 345 return lineNo <= event.getLine(); 346 } 347 348 } 349 350}