Coverage Report - com.puppycrawl.tools.checkstyle.filters.SuppressionCommentFilter
 
Classes in this File Line Coverage Branch Coverage Complexity
SuppressionCommentFilter
87%
75/86
85%
29/34
2.75
SuppressionCommentFilter$Tag
84%
38/45
87%
14/16
2.75
 
 1  
 ////////////////////////////////////////////////////////////////////////////////
 2  
 // checkstyle: Checks Java source code for adherence to a set of rules.
 3  
 // Copyright (C) 2001-2014  Oliver Burn
 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  
 package com.puppycrawl.tools.checkstyle.filters;
 20  
 
 21  
 import com.google.common.collect.Lists;
 22  
 import com.puppycrawl.tools.checkstyle.api.AuditEvent;
 23  
 import com.puppycrawl.tools.checkstyle.api.AutomaticBean;
 24  
 import com.puppycrawl.tools.checkstyle.api.FileContents;
 25  
 import com.puppycrawl.tools.checkstyle.api.Filter;
 26  
 import com.puppycrawl.tools.checkstyle.api.TextBlock;
 27  
 import com.puppycrawl.tools.checkstyle.api.Utils;
 28  
 import com.puppycrawl.tools.checkstyle.checks.FileContentsHolder;
 29  
 import java.lang.ref.WeakReference;
 30  
 import java.util.Collection;
 31  
 import java.util.Collections;
 32  
 import java.util.List;
 33  
 import java.util.regex.Matcher;
 34  
 import java.util.regex.Pattern;
 35  
 import java.util.regex.PatternSyntaxException;
 36  
 import org.apache.commons.beanutils.ConversionException;
 37  
 
 38  
 /**
 39  
  * <p>
 40  
  * A filter that uses comments to suppress audit events.
 41  
  * </p>
 42  
  * <p>
 43  
  * Rationale:
 44  
  * Sometimes there are legitimate reasons for violating a check.  When
 45  
  * this is a matter of the code in question and not personal
 46  
  * preference, the best place to override the policy is in the code
 47  
  * itself.  Semi-structured comments can be associated with the check.
 48  
  * This is sometimes superior to a separate suppressions file, which
 49  
  * must be kept up-to-date as the source file is edited.
 50  
  * </p>
 51  
  * <p>
 52  
  * Usage:
 53  
  * This check only works in conjunction with the FileContentsHolder module
 54  
  * since that module makes the suppression comments in the .java
 55  
  * files available <i>sub rosa</i>.
 56  
  * </p>
 57  
  * @see FileContentsHolder
 58  
  * @author Mike McMahon
 59  
  * @author Rick Giles
 60  
  */
 61  132
 public class SuppressionCommentFilter
 62  
     extends AutomaticBean
 63  
     implements Filter
 64  
 {
 65  
     /**
 66  
      * A Tag holds a suppression comment and its location, and determines
 67  
      * whether the supression turns checkstyle reporting on or off.
 68  
      * @author Rick Giles
 69  
      */
 70  44
     public class Tag
 71  
         implements Comparable<Tag>
 72  
     {
 73  
         /** The text of the tag. */
 74  
         private final String mText;
 75  
 
 76  
         /** The line number of the tag. */
 77  
         private final int mLine;
 78  
 
 79  
         /** The column number of the tag. */
 80  
         private final int mColumn;
 81  
 
 82  
         /** Determines whether the suppression turns checkstyle reporting on. */
 83  
         private final boolean mOn;
 84  
 
 85  
         /** The parsed check regexp, expanded for the text of this tag. */
 86  
         private Pattern mTagCheckRegexp;
 87  
 
 88  
         /** The parsed message regexp, expanded for the text of this tag. */
 89  
         private Pattern mTagMessageRegexp;
 90  
 
 91  
         /**
 92  
          * Constructs a tag.
 93  
          * @param aLine the line number.
 94  
          * @param aColumn the column number.
 95  
          * @param aText the text of the suppression.
 96  
          * @param aOn <code>true</code> if the tag turns checkstyle reporting.
 97  
          * @throws ConversionException if unable to parse expanded aText.
 98  
          * on.
 99  
          */
 100  
         public Tag(int aLine, int aColumn, String aText, boolean aOn)
 101  
             throws ConversionException
 102  31
         {
 103  31
             mLine = aLine;
 104  31
             mColumn = aColumn;
 105  31
             mText = aText;
 106  31
             mOn = aOn;
 107  
 
 108  31
             mTagCheckRegexp = mCheckRegexp;
 109  
             //Expand regexp for check and message
 110  
             //Does not intern Patterns with Utils.getPattern()
 111  31
             String format = "";
 112  
             try {
 113  31
                 if (aOn) {
 114  16
                     format =
 115  
                         expandFromComment(aText, mCheckFormat, mOnRegexp);
 116  16
                     mTagCheckRegexp = Pattern.compile(format);
 117  16
                     if (mMessageFormat != null) {
 118  2
                         format =
 119  
                             expandFromComment(aText, mMessageFormat, mOnRegexp);
 120  2
                         mTagMessageRegexp = Pattern.compile(format);
 121  
                     }
 122  
                 }
 123  
                 else {
 124  15
                     format =
 125  
                         expandFromComment(aText, mCheckFormat, mOffRegexp);
 126  15
                     mTagCheckRegexp = Pattern.compile(format);
 127  15
                     if (mMessageFormat != null) {
 128  2
                         format =
 129  
                             expandFromComment(
 130  
                                 aText,
 131  
                                 mMessageFormat,
 132  
                                 mOffRegexp);
 133  2
                         mTagMessageRegexp = Pattern.compile(format);
 134  
                     }
 135  
                 }
 136  
             }
 137  0
             catch (final PatternSyntaxException e) {
 138  0
                 throw new ConversionException(
 139  
                     "unable to parse expanded comment " + format,
 140  
                     e);
 141  31
             }
 142  31
         }
 143  
 
 144  
         /** @return the text of the tag. */
 145  
         public String getText()
 146  
         {
 147  0
             return mText;
 148  
         }
 149  
 
 150  
         /** @return the line number of the tag in the source file. */
 151  
         public int getLine()
 152  
         {
 153  489
             return mLine;
 154  
         }
 155  
 
 156  
         /**
 157  
          * Determines the column number of the tag in the source file.
 158  
          * Will be 0 for all lines of multiline comment, except the
 159  
          * first line.
 160  
          * @return the column number of the tag in the source file.
 161  
          */
 162  
         public int getColumn()
 163  
         {
 164  0
             return mColumn;
 165  
         }
 166  
 
 167  
         /**
 168  
          * Determines whether the suppression turns checkstyle reporting on or
 169  
          * off.
 170  
          * @return <code>true</code>if the suppression turns reporting on.
 171  
          */
 172  
         public boolean isOn()
 173  
         {
 174  55
             return mOn;
 175  
         }
 176  
 
 177  
         /**
 178  
          * Compares the position of this tag in the file
 179  
          * with the position of another tag.
 180  
          * @param aObject the tag to compare with this one.
 181  
          * @return a negative number if this tag is before the other tag,
 182  
          * 0 if they are at the same position, and a positive number if this
 183  
          * tag is after the other tag.
 184  
          * @see java.lang.Comparable#compareTo(java.lang.Object)
 185  
          */
 186  
         public int compareTo(Tag aObject)
 187  
         {
 188  44
             if (mLine == aObject.mLine) {
 189  0
                 return mColumn - aObject.mColumn;
 190  
             }
 191  
 
 192  44
             return (mLine - aObject.mLine);
 193  
         }
 194  
 
 195  
         /**
 196  
          * Determines whether the source of an audit event
 197  
          * matches the text of this tag.
 198  
          * @param aEvent the <code>AuditEvent</code> to check.
 199  
          * @return true if the source of aEvent matches the text of this tag.
 200  
          */
 201  
         public boolean isMatch(AuditEvent aEvent)
 202  
         {
 203  201
             final Matcher tagMatcher =
 204  
                 mTagCheckRegexp.matcher(aEvent.getSourceName());
 205  201
             if (tagMatcher.find()) {
 206  149
                 if (mTagMessageRegexp != null) {
 207  2
                     final Matcher messageMatcher =
 208  
                             mTagMessageRegexp.matcher(aEvent.getMessage());
 209  2
                     return messageMatcher.find();
 210  
                 }
 211  147
                 return true;
 212  
             }
 213  52
             return false;
 214  
         }
 215  
 
 216  
         /**
 217  
          * Expand based on a matching comment.
 218  
          * @param aComment the comment.
 219  
          * @param aString the string to expand.
 220  
          * @param aRegexp the parsed expander.
 221  
          * @return the expanded string
 222  
          */
 223  
         private String expandFromComment(
 224  
             String aComment,
 225  
             String aString,
 226  
             Pattern aRegexp)
 227  
         {
 228  35
             final Matcher matcher = aRegexp.matcher(aComment);
 229  
             // Match primarily for effect.
 230  35
             if (!matcher.find()) {
 231  
                 ///CLOVER:OFF
 232  0
                 return aString;
 233  
                 ///CLOVER:ON
 234  
             }
 235  35
             String result = aString;
 236  81
             for (int i = 0; i <= matcher.groupCount(); i++) {
 237  
                 // $n expands comment match like in Pattern.subst().
 238  46
                 result = result.replaceAll("\\$" + i, matcher.group(i));
 239  
             }
 240  35
             return result;
 241  
         }
 242  
 
 243  
         @Override
 244  
         public final String toString()
 245  
         {
 246  0
             return "Tag[line=" + getLine() + "; col=" + getColumn()
 247  
                 + "; on=" + isOn() + "; text='" + getText() + "']";
 248  
         }
 249  
     }
 250  
 
 251  
     /** Turns checkstyle reporting off. */
 252  
     private static final String DEFAULT_OFF_FORMAT = "CHECKSTYLE\\:OFF";
 253  
 
 254  
     /** Turns checkstyle reporting on. */
 255  
     private static final String DEFAULT_ON_FORMAT = "CHECKSTYLE\\:ON";
 256  
 
 257  
     /** Control all checks */
 258  
     private static final String DEFAULT_CHECK_FORMAT = ".*";
 259  
 
 260  
     /** Whether to look in comments of the C type. */
 261  8
     private boolean mCheckC = true;
 262  
 
 263  
     /** Whether to look in comments of the C++ type. */
 264  8
     private boolean mCheckCPP = true;
 265  
 
 266  
     /** Parsed comment regexp that turns checkstyle reporting off. */
 267  
     private Pattern mOffRegexp;
 268  
 
 269  
     /** Parsed comment regexp that turns checkstyle reporting on. */
 270  
     private Pattern mOnRegexp;
 271  
 
 272  
     /** The check format to suppress. */
 273  
     private String mCheckFormat;
 274  
 
 275  
     /** The parsed check regexp. */
 276  
     private Pattern mCheckRegexp;
 277  
 
 278  
     /** The message format to suppress. */
 279  
     private String mMessageFormat;
 280  
 
 281  
     //TODO: Investigate performance improvement with array
 282  
     /** Tagged comments */
 283  8
     private final List<Tag> mTags = Lists.newArrayList();
 284  
 
 285  
     /**
 286  
      * References the current FileContents for this filter.
 287  
      * Since this is a weak reference to the FileContents, the FileContents
 288  
      * can be reclaimed as soon as the strong references in TreeWalker
 289  
      * and FileContentsHolder are reassigned to the next FileContents,
 290  
      * at which time filtering for the current FileContents is finished.
 291  
      */
 292  8
     private WeakReference<FileContents> mFileContentsReference =
 293  
         new WeakReference<FileContents>(null);
 294  
 
 295  
     /**
 296  
      * Constructs a SuppressionCommentFilter.
 297  
      * Initializes comment on, comment off, and check formats
 298  
      * to defaults.
 299  
      */
 300  
     public SuppressionCommentFilter()
 301  8
     {
 302  8
         setOnCommentFormat(DEFAULT_ON_FORMAT);
 303  8
         setOffCommentFormat(DEFAULT_OFF_FORMAT);
 304  8
         setCheckFormat(DEFAULT_CHECK_FORMAT);
 305  8
     }
 306  
 
 307  
     /**
 308  
      * Set the format for a comment that turns off reporting.
 309  
      * @param aFormat a <code>String</code> value.
 310  
      * @throws ConversionException unable to parse aFormat.
 311  
      */
 312  
     public void setOffCommentFormat(String aFormat)
 313  
         throws ConversionException
 314  
     {
 315  
         try {
 316  13
             mOffRegexp = Utils.getPattern(aFormat);
 317  
         }
 318  0
         catch (final PatternSyntaxException e) {
 319  0
             throw new ConversionException("unable to parse " + aFormat, e);
 320  13
         }
 321  13
     }
 322  
 
 323  
     /**
 324  
      * Set the format for a comment that turns on reporting.
 325  
      * @param aFormat a <code>String</code> value
 326  
      * @throws ConversionException unable to parse aFormat
 327  
      */
 328  
     public void setOnCommentFormat(String aFormat)
 329  
         throws ConversionException
 330  
     {
 331  
         try {
 332  13
             mOnRegexp = Utils.getPattern(aFormat);
 333  
         }
 334  0
         catch (final PatternSyntaxException e) {
 335  0
             throw new ConversionException("unable to parse " + aFormat, e);
 336  13
         }
 337  13
     }
 338  
 
 339  
     /** @return the FileContents for this filter. */
 340  
     public FileContents getFileContents()
 341  
     {
 342  136
         return mFileContentsReference.get();
 343  
     }
 344  
 
 345  
     /**
 346  
      * Set the FileContents for this filter.
 347  
      * @param aFileContents the FileContents for this filter.
 348  
      */
 349  
     public void setFileContents(FileContents aFileContents)
 350  
     {
 351  8
         mFileContentsReference = new WeakReference<FileContents>(aFileContents);
 352  8
     }
 353  
 
 354  
     /**
 355  
      * Set the format for a check.
 356  
      * @param aFormat a <code>String</code> value
 357  
      * @throws ConversionException unable to parse aFormat
 358  
      */
 359  
     public void setCheckFormat(String aFormat)
 360  
         throws ConversionException
 361  
     {
 362  
         try {
 363  12
             mCheckRegexp = Utils.getPattern(aFormat);
 364  12
             mCheckFormat = aFormat;
 365  
         }
 366  0
         catch (final PatternSyntaxException e) {
 367  0
             throw new ConversionException("unable to parse " + aFormat, e);
 368  12
         }
 369  12
     }
 370  
 
 371  
     /**
 372  
      * Set the format for a message.
 373  
      * @param aFormat a <code>String</code> value
 374  
      * @throws ConversionException unable to parse aFormat
 375  
      */
 376  
     public void setMessageFormat(String aFormat)
 377  
         throws ConversionException
 378  
     {
 379  
         // check that aFormat parses
 380  
         try {
 381  2
             Utils.getPattern(aFormat);
 382  
         }
 383  0
         catch (final PatternSyntaxException e) {
 384  0
             throw new ConversionException("unable to parse " + aFormat, e);
 385  2
         }
 386  2
         mMessageFormat = aFormat;
 387  2
     }
 388  
 
 389  
 
 390  
     /**
 391  
      * Set whether to look in C++ comments.
 392  
      * @param aCheckCPP <code>true</code> if C++ comments are checked.
 393  
      */
 394  
     public void setCheckCPP(boolean aCheckCPP)
 395  
     {
 396  1
         mCheckCPP = aCheckCPP;
 397  1
     }
 398  
 
 399  
     /**
 400  
      * Set whether to look in C comments.
 401  
      * @param aCheckC <code>true</code> if C comments are checked.
 402  
      */
 403  
     public void setCheckC(boolean aCheckC)
 404  
     {
 405  1
         mCheckC = aCheckC;
 406  1
     }
 407  
 
 408  
     /** {@inheritDoc} */
 409  
     public boolean accept(AuditEvent aEvent)
 410  
     {
 411  128
         if (aEvent.getLocalizedMessage() == null) {
 412  0
             return true;        // A special event.
 413  
         }
 414  
 
 415  
         // Lazy update. If the first event for the current file, update file
 416  
         // contents and tag suppressions
 417  128
         final FileContents currentContents = FileContentsHolder.getContents();
 418  128
         if (currentContents == null) {
 419  
             // we have no contents, so we can not filter.
 420  
             // TODO: perhaps we should notify user somehow?
 421  0
             return true;
 422  
         }
 423  128
         if (getFileContents() != currentContents) {
 424  8
             setFileContents(currentContents);
 425  8
             tagSuppressions();
 426  
         }
 427  128
         final Tag matchTag = findNearestMatch(aEvent);
 428  128
         if ((matchTag != null) && !matchTag.isOn()) {
 429  16
             return false;
 430  
         }
 431  112
         return true;
 432  
     }
 433  
 
 434  
     /**
 435  
      * Finds the nearest comment text tag that matches an audit event.
 436  
      * The nearest tag is before the line and column of the event.
 437  
      * @param aEvent the <code>AuditEvent</code> to match.
 438  
      * @return The <code>Tag</code> nearest aEvent.
 439  
      */
 440  
     private Tag findNearestMatch(AuditEvent aEvent)
 441  
     {
 442  128
         Tag result = null;
 443  
         // TODO: try binary search if sequential search becomes a performance
 444  
         // problem.
 445  128
         for (Tag tag : mTags) {
 446  288
             if ((tag.getLine() > aEvent.getLine())
 447  
                 || ((tag.getLine() == aEvent.getLine())
 448  
                     && (tag.getColumn() > aEvent.getColumn())))
 449  
             {
 450  0
                 break;
 451  
             }
 452  201
             if (tag.isMatch(aEvent)) {
 453  148
                 result = tag;
 454  
             }
 455  
         };
 456  128
         return result;
 457  
     }
 458  
 
 459  
     /**
 460  
      * Collects all the suppression tags for all comments into a list and
 461  
      * sorts the list.
 462  
      */
 463  
     private void tagSuppressions()
 464  
     {
 465  8
         mTags.clear();
 466  8
         final FileContents contents = getFileContents();
 467  8
         if (mCheckCPP) {
 468  7
             tagSuppressions(contents.getCppComments().values());
 469  
         }
 470  8
         if (mCheckC) {
 471  7
             final Collection<List<TextBlock>> cComments = contents
 472  
                     .getCComments().values();
 473  7
             for (List<TextBlock> element : cComments) {
 474  28
                 tagSuppressions(element);
 475  
             }
 476  
         }
 477  8
         Collections.sort(mTags);
 478  8
     }
 479  
 
 480  
     /**
 481  
      * Appends the suppressions in a collection of comments to the full
 482  
      * set of suppression tags.
 483  
      * @param aComments the set of comments.
 484  
      */
 485  
     private void tagSuppressions(Collection<TextBlock> aComments)
 486  
     {
 487  35
         for (TextBlock comment : aComments) {
 488  182
             final int startLineNo = comment.getStartLineNo();
 489  182
             final String[] text = comment.getText();
 490  182
             tagCommentLine(text[0], startLineNo, comment.getStartColNo());
 491  217
             for (int i = 1; i < text.length; i++) {
 492  35
                 tagCommentLine(text[i], startLineNo + i, 0);
 493  
             }
 494  182
         }
 495  35
     }
 496  
 
 497  
     /**
 498  
      * Tags a string if it matches the format for turning
 499  
      * checkstyle reporting on or the format for turning reporting off.
 500  
      * @param aText the string to tag.
 501  
      * @param aLine the line number of aText.
 502  
      * @param aColumn the column number of aText.
 503  
      */
 504  
     private void tagCommentLine(String aText, int aLine, int aColumn)
 505  
     {
 506  217
         final Matcher offMatcher = mOffRegexp.matcher(aText);
 507  217
         if (offMatcher.find()) {
 508  15
             addTag(offMatcher.group(0), aLine, aColumn, false);
 509  
         }
 510  
         else {
 511  202
             final Matcher onMatcher = mOnRegexp.matcher(aText);
 512  202
             if (onMatcher.find()) {
 513  16
                 addTag(onMatcher.group(0), aLine, aColumn, true);
 514  
             }
 515  
         }
 516  217
     }
 517  
 
 518  
     /**
 519  
      * Adds a <code>Tag</code> to the list of all tags.
 520  
      * @param aText the text of the tag.
 521  
      * @param aLine the line number of the tag.
 522  
      * @param aColumn the column number of the tag.
 523  
      * @param aOn <code>true</code> if the tag turns checkstyle reporting on.
 524  
      */
 525  
     private void addTag(String aText, int aLine, int aColumn, boolean aOn)
 526  
     {
 527  31
         final Tag tag = new Tag(aLine, aColumn, aText, aOn);
 528  31
         mTags.add(tag);
 529  31
     }
 530  
 }