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