Coverage Report - com.puppycrawl.tools.checkstyle.filters.SuppressWithPlainTextCommentFilter
 
Classes in this File Line Coverage Branch Coverage Complexity
SuppressWithPlainTextCommentFilter
100%
50/50
100%
16/16
0
SuppressWithPlainTextCommentFilter$Suppression
100%
51/51
100%
38/38
0
SuppressWithPlainTextCommentFilter$SuppressionType
100%
3/3
N/A
0
 
 1  
 ////////////////////////////////////////////////////////////////////////////////
 2  
 // checkstyle: Checks Java source code for adherence to a set of rules.
 3  
 // Copyright (C) 2001-2017 the original author or authors.
 4  
 //
 5  
 // This library is free software; you can redistribute it and/or
 6  
 // modify it under the terms of the GNU Lesser General Public
 7  
 // License as published by the Free Software Foundation; either
 8  
 // version 2.1 of the License, or (at your option) any later version.
 9  
 //
 10  
 // This library is distributed in the hope that it will be useful,
 11  
 // but WITHOUT ANY WARRANTY; without even the implied warranty of
 12  
 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 13  
 // Lesser General Public License for more details.
 14  
 //
 15  
 // You should have received a copy of the GNU Lesser General Public
 16  
 // License along with this library; if not, write to the Free Software
 17  
 // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 18  
 ////////////////////////////////////////////////////////////////////////////////
 19  
 
 20  
 package com.puppycrawl.tools.checkstyle.filters;
 21  
 
 22  
 import java.io.File;
 23  
 import java.io.IOException;
 24  
 import java.nio.charset.StandardCharsets;
 25  
 import java.util.ArrayList;
 26  
 import java.util.List;
 27  
 import java.util.Objects;
 28  
 import java.util.Optional;
 29  
 import java.util.regex.Matcher;
 30  
 import java.util.regex.Pattern;
 31  
 import java.util.regex.PatternSyntaxException;
 32  
 
 33  
 import com.puppycrawl.tools.checkstyle.api.AuditEvent;
 34  
 import com.puppycrawl.tools.checkstyle.api.AutomaticBean;
 35  
 import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
 36  
 import com.puppycrawl.tools.checkstyle.api.FileText;
 37  
 import com.puppycrawl.tools.checkstyle.api.Filter;
 38  
 import com.puppycrawl.tools.checkstyle.utils.CommonUtils;
 39  
 
 40  
 /**
 41  
  * <p>
 42  
  *     A filter that uses comments to suppress audit events.
 43  
  *     The filter can be used only to suppress audit events received from
 44  
  *     {@link com.puppycrawl.tools.checkstyle.api.FileSetCheck} checks.
 45  
  *     SuppressWithPlainTextCommentFilter knows nothing about AST,
 46  
  *     it treats only plain text comments and extracts the information required for suppression from
 47  
  *     the plain text comments. Currently the filter supports only single line comments.
 48  
  * </p>
 49  
  * <p>
 50  
  *     Rationale:
 51  
  *     Sometimes there are legitimate reasons for violating a check. When
 52  
  *     this is a matter of the code in question and not personal
 53  
  *     preference, the best place to override the policy is in the code
 54  
  *     itself.  Semi-structured comments can be associated with the check.
 55  
  *     This is sometimes superior to a separate suppressions file, which
 56  
  *     must be kept up-to-date as the source file is edited.
 57  
  * </p>
 58  
  * @author Andrei Selkin
 59  
  */
 60  273
 public class SuppressWithPlainTextCommentFilter extends AutomaticBean implements Filter {
 61  
 
 62  
     /** Comment format which turns checkstyle reporting off. */
 63  
     private static final String DEFAULT_OFF_FORMAT = "// CHECKSTYLE:OFF";
 64  
 
 65  
     /** Comment format which turns checkstyle reporting on. */
 66  
     private static final String DEFAULT_ON_FORMAT = "// CHECKSTYLE:ON";
 67  
 
 68  
     /** Default check format to suppress. By default the filter suppress all checks. */
 69  
     private static final String DEFAULT_CHECK_FORMAT = ".*";
 70  
 
 71  
     /** Regexp which turns checkstyle reporting off. */
 72  24
     private Pattern offCommentFormat = CommonUtils.createPattern(DEFAULT_OFF_FORMAT);
 73  
 
 74  
     /** Regexp which turns checkstyle reporting on. */
 75  24
     private Pattern onCommentFormat = CommonUtils.createPattern(DEFAULT_ON_FORMAT);
 76  
 
 77  
     /** The check format to suppress. */
 78  24
     private String checkFormat = DEFAULT_CHECK_FORMAT;
 79  
 
 80  
     /** The message format to suppress.*/
 81  
     private String messageFormat;
 82  
 
 83  
     /**
 84  
      * Sets an off comment format pattern.
 85  
      * @param pattern off comment format pattern.
 86  
      */
 87  
     public final void setOffCommentFormat(Pattern pattern) {
 88  16
         offCommentFormat = pattern;
 89  16
     }
 90  
 
 91  
     /**
 92  
      * Sets an on comment format pattern.
 93  
      * @param pattern  on comment format pattern.
 94  
      */
 95  
     public final void setOnCommentFormat(Pattern pattern) {
 96  15
         onCommentFormat = pattern;
 97  15
     }
 98  
 
 99  
     /**
 100  
      * Sets a pattern for check format.
 101  
      * @param format pattern for check format.
 102  
      */
 103  
     public final void setCheckFormat(String format) {
 104  9
         checkFormat = format;
 105  9
     }
 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  3
         messageFormat = format;
 113  3
     }
 114  
 
 115  
     @Override
 116  
     public boolean accept(AuditEvent event) {
 117  35
         boolean accepted = true;
 118  35
         if (event.getLocalizedMessage() != null) {
 119  34
             final FileText fileText = getFileText(event.getFileName());
 120  33
             if (fileText != null) {
 121  32
                 final List<Suppression> suppressions = getSuppressions(fileText);
 122  30
                 accepted = getNearestSuppression(suppressions, event) == null;
 123  
             }
 124  
         }
 125  32
         return accepted;
 126  
     }
 127  
 
 128  
     @Override
 129  
     protected void finishLocalSetup() throws CheckstyleException {
 130  
         // No code by default
 131  20
     }
 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  34
         final File file = new File(fileName);
 140  34
         FileText result = null;
 141  
 
 142  
         // some violations can be on a directory, instead of a file
 143  34
         if (!file.isDirectory()) {
 144  
             try {
 145  33
                 result = new FileText(file, StandardCharsets.UTF_8.name());
 146  
             }
 147  1
             catch (IOException ex) {
 148  1
                 throw new IllegalStateException("Cannot read source file: " + fileName, ex);
 149  32
             }
 150  
         }
 151  
 
 152  33
         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  32
         final List<Suppression> suppressions = new ArrayList<>();
 162  419
         for (int lineNo = 0; lineNo < fileText.size(); lineNo++) {
 163  389
             final Optional<Suppression> suppression = getSuppression(fileText, lineNo);
 164  387
             suppression.ifPresent(suppressions::add);
 165  
         }
 166  30
         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  389
         final String line = fileText.get(lineNo);
 177  389
         final Matcher onCommentMatcher = onCommentFormat.matcher(line);
 178  389
         final Matcher offCommentMatcher = offCommentFormat.matcher(line);
 179  
 
 180  389
         Suppression suppression = null;
 181  389
         if (onCommentMatcher.find()) {
 182  37
             suppression = new Suppression(onCommentMatcher.group(0),
 183  37
                 lineNo + 1, onCommentMatcher.start(), SuppressionType.ON, this);
 184  
         }
 185  389
         if (offCommentMatcher.find()) {
 186  39
             suppression = new Suppression(offCommentMatcher.group(0),
 187  39
                 lineNo + 1, offCommentMatcher.start(), SuppressionType.OFF, this);
 188  
         }
 189  
 
 190  387
         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  60
         return suppressions
 204  30
             .stream()
 205  100
             .filter(suppression -> suppression.isMatch(event))
 206  44
             .reduce((first, second) -> second)
 207  54
             .filter(suppression -> suppression.suppressionType != SuppressionType.ON)
 208  30
             .orElse(null);
 209  
     }
 210  
 
 211  
     /** Enum which represents the type of the suppression. */
 212  4
     private enum SuppressionType {
 213  
 
 214  
         /** On suppression type. */
 215  1
         ON,
 216  
         /** Off suppression type. */
 217  1
         OFF
 218  
 
 219  
     }
 220  
 
 221  
     /** The class which represents the suppression. */
 222  94
     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  76
         ) {
 253  76
             this.text = text;
 254  76
             this.lineNo = lineNo;
 255  76
             this.columnNo = columnNo;
 256  76
             this.suppressionType = suppressionType;
 257  
 
 258  
             //Expand regexp for check and message
 259  
             //Does not intern Patterns with Utils.getPattern()
 260  76
             String format = "";
 261  
             try {
 262  76
                 if (this.suppressionType == SuppressionType.ON) {
 263  74
                     format = CommonUtils.fillTemplateWithStringsByRegexp(
 264  37
                             filter.checkFormat, text, filter.onCommentFormat);
 265  37
                     eventSourceRegexp = Pattern.compile(format);
 266  37
                     if (filter.messageFormat == null) {
 267  32
                         eventMessageRegexp = null;
 268  
                     }
 269  
                     else {
 270  10
                         format = CommonUtils.fillTemplateWithStringsByRegexp(
 271  5
                                 filter.messageFormat, text, filter.onCommentFormat);
 272  5
                         eventMessageRegexp = Pattern.compile(format);
 273  
                     }
 274  
                 }
 275  
                 else {
 276  78
                     format = CommonUtils.fillTemplateWithStringsByRegexp(
 277  39
                             filter.checkFormat, text, filter.offCommentFormat);
 278  39
                     eventSourceRegexp = Pattern.compile(format);
 279  38
                     if (filter.messageFormat == null) {
 280  32
                         eventMessageRegexp = null;
 281  
                     }
 282  
                     else {
 283  12
                         format = CommonUtils.fillTemplateWithStringsByRegexp(
 284  6
                                 filter.messageFormat, text, filter.offCommentFormat);
 285  6
                         eventMessageRegexp = Pattern.compile(format);
 286  
                     }
 287  
                 }
 288  
             }
 289  2
             catch (final PatternSyntaxException ex) {
 290  2
                 throw new IllegalArgumentException(
 291  
                     "unable to parse expanded comment " + format, ex);
 292  74
             }
 293  74
         }
 294  
 
 295  
         @Override
 296  
         public boolean equals(Object other) {
 297  121
             if (this == other) {
 298  2
                 return true;
 299  
             }
 300  119
             if (other == null || getClass() != other.getClass()) {
 301  5
                 return false;
 302  
             }
 303  114
             final Suppression suppression = (Suppression) other;
 304  228
             return Objects.equals(lineNo, suppression.lineNo)
 305  95
                     && Objects.equals(columnNo, suppression.columnNo)
 306  87
                     && Objects.equals(suppressionType, suppression.suppressionType)
 307  78
                     && Objects.equals(text, suppression.text)
 308  69
                     && Objects.equals(eventSourceRegexp, suppression.eventSourceRegexp)
 309  60
                     && Objects.equals(eventMessageRegexp, suppression.eventMessageRegexp);
 310  
         }
 311  
 
 312  
         @Override
 313  
         public int hashCode() {
 314  70
             return Objects.hash(
 315  35
                 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  70
             boolean match = false;
 325  70
             if (isInScopeOfSuppression(event)) {
 326  45
                 final Matcher sourceNameMatcher = eventSourceRegexp.matcher(event.getSourceName());
 327  45
                 if (sourceNameMatcher.find()) {
 328  33
                     match = eventMessageRegexp == null
 329  6
                         || eventMessageRegexp.matcher(event.getMessage()).find();
 330  
                 }
 331  
                 else {
 332  12
                     match = event.getModuleId() != null
 333  11
                         && eventSourceRegexp.matcher(event.getModuleId()).find();
 334  
                 }
 335  
             }
 336  70
             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  70
             return lineNo <= event.getLine();
 346  
         }
 347  
     }
 348  
 
 349  
 }