Coverage Report - com.puppycrawl.tools.checkstyle.filters.SuppressWithNearbyCommentFilter
 
Classes in this File Line Coverage Branch Coverage Complexity
SuppressWithNearbyCommentFilter
87%
74/85
80%
24/30
3.136
SuppressWithNearbyCommentFilter$Tag
77%
42/54
81%
18/22
3.136
 
 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.Iterator;
 33  
 import java.util.List;
 34  
 import java.util.regex.Matcher;
 35  
 import java.util.regex.Pattern;
 36  
 import java.util.regex.PatternSyntaxException;
 37  
 import org.apache.commons.beanutils.ConversionException;
 38  
 
 39  
 /**
 40  
  * <p>
 41  
  * A filter that uses nearby comments to suppress audit events.
 42  
  * </p>
 43  
  * <p>
 44  
  * This check is philosophically similar to {@link SuppressionCommentFilter}.
 45  
  * Unlike {@link SuppressionCommentFilter}, this filter does not require
 46  
  * pairs of comments.  This check may be used to suppress warnings in the
 47  
  * current line:
 48  
  * <pre>
 49  
  *    offendingLine(for, whatever, reason); // SUPPRESS ParameterNumberCheck
 50  
  * </pre>
 51  
  * or it may be configured to span multiple lines, either forward:
 52  
  * <pre>
 53  
  *    // PERMIT MultipleVariableDeclarations NEXT 3 LINES
 54  
  *    double x1 = 1.0, y1 = 0.0, z1 = 0.0;
 55  
  *    double x2 = 0.0, y2 = 1.0, z2 = 0.0;
 56  
  *    double x3 = 0.0, y3 = 0.0, z3 = 1.0;
 57  
  * </pre>
 58  
  * or reverse:
 59  
  * <pre>
 60  
  *   try {
 61  
  *     thirdPartyLibrary.method();
 62  
  *   } catch (RuntimeException e) {
 63  
  *     // ALLOW ILLEGAL CATCH BECAUSE third party API wraps everything
 64  
  *     // in RuntimeExceptions.
 65  
  *     ...
 66  
  *   }
 67  
  * </pre>
 68  
  * </p>
 69  
  * <p>
 70  
  * See {@link SuppressionCommentFilter} for usage notes.
 71  
  * </p>
 72  
  *
 73  
  * @author Mick Killianey
 74  
  */
 75  137
 public class SuppressWithNearbyCommentFilter
 76  
     extends AutomaticBean
 77  
     implements Filter
 78  
 {
 79  
     /**
 80  
      * A Tag holds a suppression comment and its location.
 81  
      */
 82  20
     public class Tag implements Comparable<Tag>
 83  
     {
 84  
         /** The text of the tag. */
 85  
         private final String mText;
 86  
 
 87  
         /** The first line where warnings may be suppressed. */
 88  
         private int mFirstLine;
 89  
 
 90  
         /** The last line where warnings may be suppressed. */
 91  
         private int mLastLine;
 92  
 
 93  
         /** The parsed check regexp, expanded for the text of this tag. */
 94  
         private Pattern mTagCheckRegexp;
 95  
 
 96  
         /** The parsed message regexp, expanded for the text of this tag. */
 97  
         private Pattern mTagMessageRegexp;
 98  
 
 99  
         /**
 100  
          * Constructs a tag.
 101  
          * @param aText the text of the suppression.
 102  
          * @param aLine the line number.
 103  
          * @throws ConversionException if unable to parse expanded aText.
 104  
          * on.
 105  
          */
 106  
         public Tag(String aText, int aLine)
 107  
             throws ConversionException
 108  19
         {
 109  19
             mText = aText;
 110  
 
 111  19
             mTagCheckRegexp = mCheckRegexp;
 112  
             //Expand regexp for check and message
 113  
             //Does not intern Patterns with Utils.getPattern()
 114  19
             String format = "";
 115  
             try {
 116  19
                 format = expandFromComment(aText, mCheckFormat, mCommentRegexp);
 117  19
                 mTagCheckRegexp = Pattern.compile(format);
 118  19
                 if (mMessageFormat != null) {
 119  2
                     format = expandFromComment(
 120  
                          aText, mMessageFormat, mCommentRegexp);
 121  2
                     mTagMessageRegexp = Pattern.compile(format);
 122  
                 }
 123  19
                 int influence = 0;
 124  19
                 if (mInfluenceFormat != null) {
 125  19
                     format = expandFromComment(
 126  
                         aText, mInfluenceFormat, mCommentRegexp);
 127  
                     try {
 128  19
                         if (format.startsWith("+")) {
 129  2
                             format = format.substring(1);
 130  
                         }
 131  19
                         influence = Integer.parseInt(format);
 132  
                     }
 133  0
                     catch (final NumberFormatException e) {
 134  0
                         throw new ConversionException(
 135  
                             "unable to parse influence from '" + aText
 136  
                                 + "' using " + mInfluenceFormat, e);
 137  19
                     }
 138  
                 }
 139  19
                 if (influence >= 0) {
 140  15
                     mFirstLine = aLine;
 141  15
                     mLastLine = aLine + influence;
 142  
                 }
 143  
                 else {
 144  4
                     mFirstLine = aLine + influence;
 145  4
                     mLastLine = aLine;
 146  
                 }
 147  
             }
 148  0
             catch (final PatternSyntaxException e) {
 149  0
                 throw new ConversionException(
 150  
                     "unable to parse expanded comment " + format,
 151  
                     e);
 152  19
             }
 153  19
         }
 154  
 
 155  
         /** @return the text of the tag. */
 156  
         public String getText()
 157  
         {
 158  0
             return mText;
 159  
         }
 160  
 
 161  
         /** @return the line number of the first suppressed line. */
 162  
         public int getFirstLine()
 163  
         {
 164  0
             return mFirstLine;
 165  
         }
 166  
 
 167  
         /** @return the line number of the last suppressed line. */
 168  
         public int getLastLine()
 169  
         {
 170  0
             return mLastLine;
 171  
         }
 172  
 
 173  
         /**
 174  
          * Compares the position of this tag in the file
 175  
          * with the position of another tag.
 176  
          * @param aOther the tag to compare with this one.
 177  
          * @return a negative number if this tag is before the other tag,
 178  
          * 0 if they are at the same position, and a positive number if this
 179  
          * tag is after the other tag.
 180  
          * @see java.lang.Comparable#compareTo(java.lang.Object)
 181  
          */
 182  
         public int compareTo(Tag aOther)
 183  
         {
 184  20
             if (mFirstLine == aOther.mFirstLine) {
 185  0
                 return mLastLine - aOther.mLastLine;
 186  
             }
 187  
 
 188  20
             return (mFirstLine - aOther.mFirstLine);
 189  
         }
 190  
 
 191  
         /**
 192  
          * Determines whether the source of an audit event
 193  
          * matches the text of this tag.
 194  
          * @param aEvent the <code>AuditEvent</code> to check.
 195  
          * @return true if the source of aEvent matches the text of this tag.
 196  
          */
 197  
         public boolean isMatch(AuditEvent aEvent)
 198  
         {
 199  428
             final int line = aEvent.getLine();
 200  428
             if (line < mFirstLine) {
 201  115
                 return false;
 202  
             }
 203  313
             if (line > mLastLine) {
 204  290
                 return false;
 205  
             }
 206  23
             final Matcher tagMatcher =
 207  
                 mTagCheckRegexp.matcher(aEvent.getSourceName());
 208  23
             if (tagMatcher.find()) {
 209  20
                 return true;
 210  
             }
 211  3
             if (mTagMessageRegexp != null) {
 212  0
                 final Matcher messageMatcher =
 213  
                     mTagMessageRegexp.matcher(aEvent.getMessage());
 214  0
                 return messageMatcher.find();
 215  
             }
 216  3
             return false;
 217  
         }
 218  
 
 219  
         /**
 220  
          * Expand based on a matching comment.
 221  
          * @param aComment the comment.
 222  
          * @param aString the string to expand.
 223  
          * @param aRegexp the parsed expander.
 224  
          * @return the expanded string
 225  
          */
 226  
         private String expandFromComment(
 227  
             String aComment,
 228  
             String aString,
 229  
             Pattern aRegexp)
 230  
         {
 231  40
             final Matcher matcher = aRegexp.matcher(aComment);
 232  
             // Match primarily for effect.
 233  40
             if (!matcher.find()) {
 234  
                 ///CLOVER:OFF
 235  0
                 return aString;
 236  
                 ///CLOVER:ON
 237  
             }
 238  40
             String result = aString;
 239  126
             for (int i = 0; i <= matcher.groupCount(); i++) {
 240  
                 // $n expands comment match like in Pattern.subst().
 241  86
                 result = result.replaceAll("\\$" + i, matcher.group(i));
 242  
             }
 243  40
             return result;
 244  
         }
 245  
 
 246  
         /** {@inheritDoc} */
 247  
         @Override
 248  
         public final String toString()
 249  
         {
 250  0
             return "Tag[lines=[" + getFirstLine() + " to " + getLastLine()
 251  
                 + "]; text='" + getText() + "']";
 252  
         }
 253  
     }
 254  
 
 255  
     /** Format to turns checkstyle reporting off. */
 256  
     private static final String DEFAULT_COMMENT_FORMAT =
 257  
         "SUPPRESS CHECKSTYLE (\\w+)";
 258  
 
 259  
     /** Default regex for checks that should be suppressed. */
 260  
     private static final String DEFAULT_CHECK_FORMAT = ".*";
 261  
 
 262  
     /** Default regex for messages that should be suppressed. */
 263  1
     private static final String DEFAULT_MESSAGE_FORMAT = null;
 264  
 
 265  
     /** Default regex for lines that should be suppressed. */
 266  
     private static final String DEFAULT_INFLUENCE_FORMAT = "0";
 267  
 
 268  
     /** Whether to look for trigger in C-style comments. */
 269  7
     private boolean mCheckC = true;
 270  
 
 271  
     /** Whether to look for trigger in C++-style comments. */
 272  7
     private boolean mCheckCPP = true;
 273  
 
 274  
     /** Parsed comment regexp that marks checkstyle suppression region. */
 275  
     private Pattern mCommentRegexp;
 276  
 
 277  
     /** The comment pattern that triggers suppression. */
 278  
     private String mCheckFormat;
 279  
 
 280  
     /** The parsed check regexp. */
 281  
     private Pattern mCheckRegexp;
 282  
 
 283  
     /** The message format to suppress. */
 284  
     private String mMessageFormat;
 285  
 
 286  
     /** The influence of the suppression comment. */
 287  
     private String mInfluenceFormat;
 288  
 
 289  
 
 290  
     //TODO: Investigate performance improvement with array
 291  
     /** Tagged comments */
 292  7
     private final List<Tag> mTags = Lists.newArrayList();
 293  
 
 294  
     /**
 295  
      * References the current FileContents for this filter.
 296  
      * Since this is a weak reference to the FileContents, the FileContents
 297  
      * can be reclaimed as soon as the strong references in TreeWalker
 298  
      * and FileContentsHolder are reassigned to the next FileContents,
 299  
      * at which time filtering for the current FileContents is finished.
 300  
      */
 301  7
     private WeakReference<FileContents> mFileContentsReference =
 302  
         new WeakReference<FileContents>(null);
 303  
 
 304  
     /**
 305  
      * Constructs a SuppressionCommentFilter.
 306  
      * Initializes comment on, comment off, and check formats
 307  
      * to defaults.
 308  
      */
 309  
     public SuppressWithNearbyCommentFilter()
 310  7
     {
 311  7
         if (DEFAULT_COMMENT_FORMAT != null) {
 312  7
             setCommentFormat(DEFAULT_COMMENT_FORMAT);
 313  
         }
 314  7
         if (DEFAULT_CHECK_FORMAT != null) {
 315  7
             setCheckFormat(DEFAULT_CHECK_FORMAT);
 316  
         }
 317  7
         if (DEFAULT_MESSAGE_FORMAT != null) {
 318  0
             setMessageFormat(DEFAULT_MESSAGE_FORMAT);
 319  
         }
 320  7
         if (DEFAULT_INFLUENCE_FORMAT != null) {
 321  7
             setInfluenceFormat(DEFAULT_INFLUENCE_FORMAT);
 322  
         }
 323  7
     }
 324  
 
 325  
     /**
 326  
      * Set the format for a comment that turns off reporting.
 327  
      * @param aFormat a <code>String</code> value.
 328  
      * @throws ConversionException unable to parse aFormat.
 329  
      */
 330  
     public void setCommentFormat(String aFormat)
 331  
         throws ConversionException
 332  
     {
 333  
         try {
 334  11
             mCommentRegexp = Utils.getPattern(aFormat);
 335  
         }
 336  0
         catch (final PatternSyntaxException e) {
 337  0
             throw new ConversionException("unable to parse " + aFormat, e);
 338  11
         }
 339  11
     }
 340  
 
 341  
     /** @return the FileContents for this filter. */
 342  
     public FileContents getFileContents()
 343  
     {
 344  175
         return mFileContentsReference.get();
 345  
     }
 346  
 
 347  
     /**
 348  
      * Set the FileContents for this filter.
 349  
      * @param aFileContents the FileContents for this filter.
 350  
      */
 351  
     public void setFileContents(FileContents aFileContents)
 352  
     {
 353  7
         mFileContentsReference = new WeakReference<FileContents>(aFileContents);
 354  7
     }
 355  
 
 356  
     /**
 357  
      * Set the format for a check.
 358  
      * @param aFormat a <code>String</code> value
 359  
      * @throws ConversionException unable to parse aFormat
 360  
      */
 361  
     public void setCheckFormat(String aFormat)
 362  
         throws ConversionException
 363  
     {
 364  
         try {
 365  11
             mCheckRegexp = Utils.getPattern(aFormat);
 366  11
             mCheckFormat = aFormat;
 367  
         }
 368  0
         catch (final PatternSyntaxException e) {
 369  0
             throw new ConversionException("unable to parse " + aFormat, e);
 370  11
         }
 371  11
     }
 372  
 
 373  
     /**
 374  
      * Set the format for a message.
 375  
      * @param aFormat a <code>String</code> value
 376  
      * @throws ConversionException unable to parse aFormat
 377  
      */
 378  
     public void setMessageFormat(String aFormat)
 379  
         throws ConversionException
 380  
     {
 381  
         // check that aFormat parses
 382  
         try {
 383  1
             Utils.getPattern(aFormat);
 384  
         }
 385  0
         catch (final PatternSyntaxException e) {
 386  0
             throw new ConversionException("unable to parse " + aFormat, e);
 387  1
         }
 388  1
         mMessageFormat = aFormat;
 389  1
     }
 390  
 
 391  
     /**
 392  
      * Set the format for the influence of this check.
 393  
      * @param aFormat a <code>String</code> value
 394  
      * @throws ConversionException unable to parse aFormat
 395  
      */
 396  
     public void setInfluenceFormat(String aFormat)
 397  
         throws ConversionException
 398  
     {
 399  
         // check that aFormat parses
 400  
         try {
 401  11
             Utils.getPattern(aFormat);
 402  
         }
 403  0
         catch (final PatternSyntaxException e) {
 404  0
             throw new ConversionException("unable to parse " + aFormat, e);
 405  11
         }
 406  11
         mInfluenceFormat = aFormat;
 407  11
     }
 408  
 
 409  
 
 410  
     /**
 411  
      * Set whether to look in C++ comments.
 412  
      * @param aCheckCPP <code>true</code> if C++ comments are checked.
 413  
      */
 414  
     public void setCheckCPP(boolean aCheckCPP)
 415  
     {
 416  1
         mCheckCPP = aCheckCPP;
 417  1
     }
 418  
 
 419  
     /**
 420  
      * Set whether to look in C comments.
 421  
      * @param aCheckC <code>true</code> if C comments are checked.
 422  
      */
 423  
     public void setCheckC(boolean aCheckC)
 424  
     {
 425  1
         mCheckC = aCheckC;
 426  1
     }
 427  
 
 428  
     /** {@inheritDoc} */
 429  
     public boolean accept(AuditEvent aEvent)
 430  
     {
 431  168
         if (aEvent.getLocalizedMessage() == null) {
 432  0
             return true;        // A special event.
 433  
         }
 434  
 
 435  
         // Lazy update. If the first event for the current file, update file
 436  
         // contents and tag suppressions
 437  168
         final FileContents currentContents = FileContentsHolder.getContents();
 438  168
         if (currentContents == null) {
 439  
             // we have no contents, so we can not filter.
 440  
             // TODO: perhaps we should notify user somehow?
 441  0
             return true;
 442  
         }
 443  168
         if (getFileContents() != currentContents) {
 444  7
             setFileContents(currentContents);
 445  7
             tagSuppressions();
 446  
         }
 447  168
         for (final Iterator<Tag> iter = mTags.iterator(); iter.hasNext();) {
 448  428
             final Tag tag = iter.next();
 449  428
             if (tag.isMatch(aEvent)) {
 450  20
                 return false;
 451  
             }
 452  408
         }
 453  148
         return true;
 454  
     }
 455  
 
 456  
     /**
 457  
      * Collects all the suppression tags for all comments into a list and
 458  
      * sorts the list.
 459  
      */
 460  
     private void tagSuppressions()
 461  
     {
 462  7
         mTags.clear();
 463  7
         final FileContents contents = getFileContents();
 464  7
         if (mCheckCPP) {
 465  6
             tagSuppressions(contents.getCppComments().values());
 466  
         }
 467  7
         if (mCheckC) {
 468  6
             final Collection<List<TextBlock>> cComments =
 469  
                 contents.getCComments().values();
 470  6
             for (final List<TextBlock> element : cComments) {
 471  36
                 tagSuppressions(element);
 472  
             }
 473  
         }
 474  7
         Collections.sort(mTags);
 475  7
     }
 476  
 
 477  
     /**
 478  
      * Appends the suppressions in a collection of comments to the full
 479  
      * set of suppression tags.
 480  
      * @param aComments the set of comments.
 481  
      */
 482  
     private void tagSuppressions(Collection<TextBlock> aComments)
 483  
     {
 484  42
         for (final TextBlock comment : aComments) {
 485  156
             final int startLineNo = comment.getStartLineNo();
 486  156
             final String[] text = comment.getText();
 487  156
             tagCommentLine(text[0], startLineNo);
 488  180
             for (int i = 1; i < text.length; i++) {
 489  24
                 tagCommentLine(text[i], startLineNo + i);
 490  
             }
 491  156
         }
 492  42
     }
 493  
 
 494  
     /**
 495  
      * Tags a string if it matches the format for turning
 496  
      * checkstyle reporting on or the format for turning reporting off.
 497  
      * @param aText the string to tag.
 498  
      * @param aLine the line number of aText.
 499  
      */
 500  
     private void tagCommentLine(String aText, int aLine)
 501  
     {
 502  180
         final Matcher matcher = mCommentRegexp.matcher(aText);
 503  180
         if (matcher.find()) {
 504  19
             addTag(matcher.group(0), aLine);
 505  
         }
 506  180
     }
 507  
 
 508  
     /**
 509  
      * Adds a comment suppression <code>Tag</code> to the list of all tags.
 510  
      * @param aText the text of the tag.
 511  
      * @param aLine the line number of the tag.
 512  
      */
 513  
     private void addTag(String aText, int aLine)
 514  
     {
 515  19
         final Tag tag = new Tag(aText, aLine);
 516  19
         mTags.add(tag);
 517  19
     }
 518  
 }