001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2020 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.lang.ref.WeakReference; 023import java.util.ArrayList; 024import java.util.Collection; 025import java.util.List; 026import java.util.Objects; 027import java.util.regex.Matcher; 028import java.util.regex.Pattern; 029import java.util.regex.PatternSyntaxException; 030 031import com.puppycrawl.tools.checkstyle.TreeWalkerAuditEvent; 032import com.puppycrawl.tools.checkstyle.TreeWalkerFilter; 033import com.puppycrawl.tools.checkstyle.api.AutomaticBean; 034import com.puppycrawl.tools.checkstyle.api.FileContents; 035import com.puppycrawl.tools.checkstyle.api.TextBlock; 036import com.puppycrawl.tools.checkstyle.utils.CommonUtil; 037 038/** 039 * <p> 040 * Filter {@code SuppressWithNearbyCommentFilter} uses nearby comments to suppress audit events. 041 * </p> 042 * <p> 043 * Rationale: Same as {@code SuppressionCommentFilter}. 044 * Whereas the SuppressionCommentFilter uses matched pairs of filters to turn 045 * on/off comment matching, {@code SuppressWithNearbyCommentFilter} uses single comments. 046 * This requires fewer lines to mark a region, and may be aesthetically preferable in some contexts. 047 * </p> 048 * <p> 049 * Attention: This filter may only be specified within the TreeWalker module 050 * ({@code <module name="TreeWalker"/>}) and only applies to checks which are also 051 * defined within this module. To filter non-TreeWalker checks like {@code RegexpSingleline}, 052 * a <a href="https://checkstyle.org/config_filters.html#SuppressWithPlainTextCommentFilter"> 053 * SuppressWithPlainTextCommentFilter</a> or similar filter must be used. 054 * </p> 055 * <p> 056 * SuppressWithNearbyCommentFilter can suppress Checks that have 057 * Treewalker as parent module. 058 * </p> 059 * <ul> 060 * <li> 061 * Property {@code commentFormat} - Specify comment pattern to trigger filter to begin suppression. 062 * Type is {@code java.util.regex.Pattern}. 063 * Default value is {@code "SUPPRESS CHECKSTYLE (\w+)"}. 064 * </li> 065 * <li> 066 * Property {@code checkFormat} - Specify check pattern to suppress. 067 * Type is {@code java.lang.String}. 068 * Default value is {@code ".*"}. 069 * </li> 070 * <li> 071 * Property {@code messageFormat} - Define message pattern to suppress. 072 * Type is {@code java.lang.String}. 073 * Default value is {@code null}. 074 * </li> 075 * <li> 076 * Property {@code idFormat} - Specify check ID pattern to suppress. 077 * Type is {@code java.lang.String}. 078 * Default value is {@code null}. 079 * </li> 080 * <li> 081 * Property {@code influenceFormat} - Specify negative/zero/positive value that 082 * defines the number of lines preceding/at/following the suppression comment. 083 * Type is {@code java.lang.String}. 084 * Default value is {@code "0"}. 085 * </li> 086 * <li> 087 * Property {@code checkCPP} - Control whether to check C++ style comments ({@code //}). 088 * Type is {@code boolean}. 089 * Default value is {@code true}. 090 * </li> 091 * <li> 092 * Property {@code checkC} - Control whether to check C style comments ({@code /* ... */}). 093 * Type is {@code boolean}. 094 * Default value is {@code true}. 095 * </li> 096 * </ul> 097 * <p> 098 * To configure a filter to suppress audit events for <i>check</i> on any line 099 * with a comment {@code SUPPRESS CHECKSTYLE <i>check</i>}: 100 * </p> 101 * <pre> 102 * <module name="SuppressWithNearbyCommentFilter"/> 103 * </pre> 104 * <pre> 105 * private int [] array; // SUPPRESS CHECKSTYLE 106 * </pre> 107 * <p> 108 * To configure a filter to suppress all audit events on any line containing 109 * the comment {@code CHECKSTYLE IGNORE THIS LINE}: 110 * </p> 111 * <pre> 112 * <module name="SuppressWithNearbyCommentFilter"> 113 * <property name="commentFormat" value="CHECKSTYLE IGNORE THIS LINE"/> 114 * <property name="checkFormat" value=".*"/> 115 * <property name="influenceFormat" value="0"/> 116 * </module> 117 * </pre> 118 * <pre> 119 * public static final int lowerCaseConstant; // CHECKSTYLE IGNORE THIS LINE 120 * </pre> 121 * <p> 122 * To configure a filter so that {@code // OK to catch (Throwable|Exception|RuntimeException) here} 123 * permits the current and previous line to avoid generating an IllegalCatch audit event: 124 * </p> 125 * <pre> 126 * <module name="SuppressWithNearbyCommentFilter"> 127 * <property name="commentFormat" value="OK to catch (\w+) here"/> 128 * <property name="checkFormat" value="IllegalCatchCheck"/> 129 * <property name="messageFormat" value="$1"/> 130 * <property name="influenceFormat" value="-1"/> 131 * </module> 132 * </pre> 133 * <pre> 134 * . . . 135 * catch (RuntimeException re) { 136 * // OK to catch RuntimeException here 137 * } 138 * catch (Throwable th) { ... } 139 * . . . 140 * </pre> 141 * <p> 142 * To configure a filter so that {@code CHECKSTYLE IGNORE <i>check</i> FOR NEXT 143 * <i>var</i> LINES} avoids triggering any audits for the given check for 144 * the current line and the next <i>var</i> lines (for a total of <i>var</i>+1 lines): 145 * </p> 146 * <pre> 147 * <module name="SuppressWithNearbyCommentFilter"> 148 * <property name="commentFormat" 149 * value="CHECKSTYLE IGNORE (\w+) FOR NEXT (\d+) LINES"/> 150 * <property name="checkFormat" value="$1"/> 151 * <property name="influenceFormat" value="$2"/> 152 * </module> 153 * </pre> 154 * <pre> 155 * static final int lowerCaseConstant; // CHECKSTYLE IGNORE ConstantNameCheck FOR NEXT 3 LINES 156 * static final int lowerCaseConstant1; 157 * static final int lowerCaseConstant2; 158 * static final int lowerCaseConstant3; 159 * static final int lowerCaseConstant4; // will warn here 160 * </pre> 161 * <p> 162 * To configure a filter to avoid any audits on code like: 163 * </p> 164 * <pre> 165 * <module name="SuppressWithNearbyCommentFilter"> 166 * <property name="commentFormat" 167 * value="ALLOW (\\w+) ON PREVIOUS LINE"/> 168 * <property name="checkFormat" value="$1"/> 169 * <property name="influenceFormat" value="-1"/> 170 * </module> 171 * </pre> 172 * <pre> 173 * private int D2; 174 * // ALLOW MemberName ON PREVIOUS LINE 175 * . . . 176 * </pre> 177 * <p> 178 * To configure a filter to allow suppress one or more Checks (separated by "|") 179 * and demand comment no less than 14 symbols: 180 * </p> 181 * <pre> 182 * <module name="SuppressWithNearbyCommentFilter"> 183 * <property name="commentFormat" 184 * value="@cs\.suppress \[(\w+(\|\w+)*)\] \w[-\.'`,:;\w ]{14,}"/> 185 * <property name="checkFormat" value="$1"/> 186 * <property name="influenceFormat" value="1"/> 187 * </module> 188 * </pre> 189 * <pre> 190 * public static final int [] array; // @cs.suppress [ConstantName|NoWhitespaceAfter] A comment here 191 * </pre> 192 * <p> 193 * It is possible to specify an ID of checks, so that it can be leveraged by 194 * the SuppressWithNearbyCommentFilter to skip validations. The following examples show how to skip 195 * validations near code that has comment like {@code // @cs-: <ID/> (reason)}, 196 * where ID is the ID of checks you want to suppress. 197 * </p> 198 * <p> 199 * Examples of Checkstyle checks configuration: 200 * </p> 201 * <pre> 202 * <module name="RegexpSinglelineJava"> 203 * <property name="id" value="ignore"/> 204 * <property name="format" value="^.*@Ignore\s*$"/> 205 * <property name="message" value="@Ignore should have a reason."/> 206 * </module> 207 * 208 * <module name="RegexpSinglelineJava"> 209 * <property name="id" value="systemout"/> 210 * <property name="format" value="^.*System\.(out|err).*$"/> 211 * <property name="message" value="Don't use System.out/err, use SLF4J instead."/> 212 * </module> 213 * </pre> 214 * <p> 215 * Example of SuppressWithNearbyCommentFilter configuration (idFormat which is set to 216 * '$1' points that ID of the checks is in the first group of commentFormat regular expressions): 217 * </p> 218 * <pre> 219 * <module name="SuppressWithNearbyCommentFilter"> 220 * <property name="commentFormat" value="@cs-: (\w+) \(.*\)"/> 221 * <property name="idFormat" value="$1"/> 222 * <property name="influenceFormat" value="0"/> 223 * </module> 224 * </pre> 225 * <pre> 226 * @Ignore // @cs-: ignore (test has not been implemented yet) 227 * @Test 228 * public void testMethod() { } 229 * 230 * public static void foo() { 231 * System.out.println("Debug info."); // @cs-: systemout (should not fail RegexpSinglelineJava) 232 * } 233 * </pre> 234 * <p> 235 * Example of how to configure the check to suppress more than one checks. 236 * The influence format format is specified in the second regexp group. 237 * </p> 238 * <pre> 239 * <module name="SuppressWithNearbyCommentFilter"> 240 * <property name="commentFormat" value="@cs-\: ([\w\|]+) influence (\d+)"/> 241 * <property name="checkFormat" value="$1"/> 242 * <property name="influenceFormat" value="$2"/> 243 * </module> 244 * </pre> 245 * <pre> 246 * // @cs-: ClassDataAbstractionCoupling influence 2 247 * // @cs-: MagicNumber influence 4 248 * @Service // no violations from ClassDataAbstractionCoupling here 249 * @Transactional 250 * public class UserService { 251 * private int value = 10022; // no violations from MagicNumber here 252 * } 253 * </pre> 254 * <p> 255 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker} 256 * </p> 257 * 258 * @since 5.0 259 */ 260public class SuppressWithNearbyCommentFilter 261 extends AutomaticBean 262 implements TreeWalkerFilter { 263 264 /** Format to turns checkstyle reporting off. */ 265 private static final String DEFAULT_COMMENT_FORMAT = 266 "SUPPRESS CHECKSTYLE (\\w+)"; 267 268 /** Default regex for checks that should be suppressed. */ 269 private static final String DEFAULT_CHECK_FORMAT = ".*"; 270 271 /** Default regex for lines that should be suppressed. */ 272 private static final String DEFAULT_INFLUENCE_FORMAT = "0"; 273 274 /** Tagged comments. */ 275 private final List<Tag> tags = new ArrayList<>(); 276 277 /** Control whether to check C style comments ({@code /* ... */}). */ 278 private boolean checkC = true; 279 280 /** Control whether to check C++ style comments ({@code //}). */ 281 // -@cs[AbbreviationAsWordInName] We can not change it as, 282 // check's property is a part of API (used in configurations). 283 private boolean checkCPP = true; 284 285 /** Specify comment pattern to trigger filter to begin suppression. */ 286 private Pattern commentFormat = Pattern.compile(DEFAULT_COMMENT_FORMAT); 287 288 /** Specify check pattern to suppress. */ 289 private String checkFormat = DEFAULT_CHECK_FORMAT; 290 291 /** Define message pattern to suppress. */ 292 private String messageFormat; 293 294 /** Specify check ID pattern to suppress. */ 295 private String idFormat; 296 297 /** 298 * Specify negative/zero/positive value that defines the number of lines 299 * preceding/at/following the suppression comment. 300 */ 301 private String influenceFormat = DEFAULT_INFLUENCE_FORMAT; 302 303 /** 304 * References the current FileContents for this filter. 305 * Since this is a weak reference to the FileContents, the FileContents 306 * can be reclaimed as soon as the strong references in TreeWalker 307 * are reassigned to the next FileContents, at which time filtering for 308 * the current FileContents is finished. 309 */ 310 private WeakReference<FileContents> fileContentsReference = new WeakReference<>(null); 311 312 /** 313 * Setter to specify comment pattern to trigger filter to begin suppression. 314 * 315 * @param pattern a pattern. 316 */ 317 public final void setCommentFormat(Pattern pattern) { 318 commentFormat = pattern; 319 } 320 321 /** 322 * Returns FileContents for this filter. 323 * 324 * @return the FileContents for this filter. 325 */ 326 private FileContents getFileContents() { 327 return fileContentsReference.get(); 328 } 329 330 /** 331 * Set the FileContents for this filter. 332 * 333 * @param fileContents the FileContents for this filter. 334 * @noinspection WeakerAccess 335 */ 336 public void setFileContents(FileContents fileContents) { 337 fileContentsReference = new WeakReference<>(fileContents); 338 } 339 340 /** 341 * Setter to specify check pattern to suppress. 342 * 343 * @param format a {@code String} value 344 */ 345 public final void setCheckFormat(String format) { 346 checkFormat = format; 347 } 348 349 /** 350 * Setter to define message pattern to suppress. 351 * 352 * @param format a {@code String} value 353 */ 354 public void setMessageFormat(String format) { 355 messageFormat = format; 356 } 357 358 /** 359 * Setter to specify check ID pattern to suppress. 360 * 361 * @param format a {@code String} value 362 */ 363 public void setIdFormat(String format) { 364 idFormat = format; 365 } 366 367 /** 368 * Setter to specify negative/zero/positive value that defines the number 369 * of lines preceding/at/following the suppression comment. 370 * 371 * @param format a {@code String} value 372 */ 373 public final void setInfluenceFormat(String format) { 374 influenceFormat = format; 375 } 376 377 /** 378 * Setter to control whether to check C++ style comments ({@code //}). 379 * 380 * @param checkCpp {@code true} if C++ comments are checked. 381 */ 382 // -@cs[AbbreviationAsWordInName] We can not change it as, 383 // check's property is a part of API (used in configurations). 384 public void setCheckCPP(boolean checkCpp) { 385 checkCPP = checkCpp; 386 } 387 388 /** 389 * Setter to control whether to check C style comments ({@code /* ... */}). 390 * 391 * @param checkC {@code true} if C comments are checked. 392 */ 393 public void setCheckC(boolean checkC) { 394 this.checkC = checkC; 395 } 396 397 @Override 398 protected void finishLocalSetup() { 399 // No code by default 400 } 401 402 @Override 403 public boolean accept(TreeWalkerAuditEvent event) { 404 boolean accepted = true; 405 406 if (event.getLocalizedMessage() != null) { 407 // Lazy update. If the first event for the current file, update file 408 // contents and tag suppressions 409 final FileContents currentContents = event.getFileContents(); 410 411 if (getFileContents() != currentContents) { 412 setFileContents(currentContents); 413 tagSuppressions(); 414 } 415 if (matchesTag(event)) { 416 accepted = false; 417 } 418 } 419 return accepted; 420 } 421 422 /** 423 * Whether current event matches any tag from {@link #tags}. 424 * 425 * @param event TreeWalkerAuditEvent to test match on {@link #tags}. 426 * @return true if event matches any tag from {@link #tags}, false otherwise. 427 */ 428 private boolean matchesTag(TreeWalkerAuditEvent event) { 429 boolean result = false; 430 for (final Tag tag : tags) { 431 if (tag.isMatch(event)) { 432 result = true; 433 break; 434 } 435 } 436 return result; 437 } 438 439 /** 440 * Collects all the suppression tags for all comments into a list and 441 * sorts the list. 442 */ 443 private void tagSuppressions() { 444 tags.clear(); 445 final FileContents contents = getFileContents(); 446 if (checkCPP) { 447 tagSuppressions(contents.getSingleLineComments().values()); 448 } 449 if (checkC) { 450 final Collection<List<TextBlock>> cComments = 451 contents.getBlockComments().values(); 452 cComments.forEach(this::tagSuppressions); 453 } 454 } 455 456 /** 457 * Appends the suppressions in a collection of comments to the full 458 * set of suppression tags. 459 * 460 * @param comments the set of comments. 461 */ 462 private void tagSuppressions(Collection<TextBlock> comments) { 463 for (final TextBlock comment : comments) { 464 final int startLineNo = comment.getStartLineNo(); 465 final String[] text = comment.getText(); 466 tagCommentLine(text[0], startLineNo); 467 for (int i = 1; i < text.length; i++) { 468 tagCommentLine(text[i], startLineNo + i); 469 } 470 } 471 } 472 473 /** 474 * Tags a string if it matches the format for turning 475 * checkstyle reporting on or the format for turning reporting off. 476 * 477 * @param text the string to tag. 478 * @param line the line number of text. 479 */ 480 private void tagCommentLine(String text, int line) { 481 final Matcher matcher = commentFormat.matcher(text); 482 if (matcher.find()) { 483 addTag(matcher.group(0), line); 484 } 485 } 486 487 /** 488 * Adds a comment suppression {@code Tag} to the list of all tags. 489 * 490 * @param text the text of the tag. 491 * @param line the line number of the tag. 492 */ 493 private void addTag(String text, int line) { 494 final Tag tag = new Tag(text, line, this); 495 tags.add(tag); 496 } 497 498 /** 499 * A Tag holds a suppression comment and its location. 500 */ 501 private static final class Tag { 502 503 /** The text of the tag. */ 504 private final String text; 505 506 /** The first line where warnings may be suppressed. */ 507 private final int firstLine; 508 509 /** The last line where warnings may be suppressed. */ 510 private final int lastLine; 511 512 /** The parsed check regexp, expanded for the text of this tag. */ 513 private final Pattern tagCheckRegexp; 514 515 /** The parsed message regexp, expanded for the text of this tag. */ 516 private final Pattern tagMessageRegexp; 517 518 /** The parsed check ID regexp, expanded for the text of this tag. */ 519 private final Pattern tagIdRegexp; 520 521 /** 522 * Constructs a tag. 523 * 524 * @param text the text of the suppression. 525 * @param line the line number. 526 * @param filter the {@code SuppressWithNearbyCommentFilter} with the context 527 * @throws IllegalArgumentException if unable to parse expanded text. 528 */ 529 /* package */ Tag(String text, int line, SuppressWithNearbyCommentFilter filter) { 530 this.text = text; 531 532 // Expand regexp for check and message 533 // Does not intern Patterns with Utils.getPattern() 534 String format = ""; 535 try { 536 format = CommonUtil.fillTemplateWithStringsByRegexp( 537 filter.checkFormat, text, filter.commentFormat); 538 tagCheckRegexp = Pattern.compile(format); 539 if (filter.messageFormat == null) { 540 tagMessageRegexp = null; 541 } 542 else { 543 format = CommonUtil.fillTemplateWithStringsByRegexp( 544 filter.messageFormat, text, filter.commentFormat); 545 tagMessageRegexp = Pattern.compile(format); 546 } 547 if (filter.idFormat == null) { 548 tagIdRegexp = null; 549 } 550 else { 551 format = CommonUtil.fillTemplateWithStringsByRegexp( 552 filter.idFormat, text, filter.commentFormat); 553 tagIdRegexp = Pattern.compile(format); 554 } 555 format = CommonUtil.fillTemplateWithStringsByRegexp( 556 filter.influenceFormat, text, filter.commentFormat); 557 558 final int influence = parseInfluence(format, filter.influenceFormat, text); 559 560 if (influence >= 1) { 561 firstLine = line; 562 lastLine = line + influence; 563 } 564 else { 565 firstLine = line + influence; 566 lastLine = line; 567 } 568 } 569 catch (final PatternSyntaxException ex) { 570 throw new IllegalArgumentException( 571 "unable to parse expanded comment " + format, ex); 572 } 573 } 574 575 /** 576 * Gets influence from suppress filter influence format param. 577 * 578 * @param format influence format to parse 579 * @param influenceFormat raw influence format 580 * @param text text of the suppression 581 * @return parsed influence 582 * @throws IllegalArgumentException when unbale to parse int in format 583 */ 584 private static int parseInfluence(String format, String influenceFormat, String text) { 585 try { 586 return Integer.parseInt(format); 587 } 588 catch (final NumberFormatException ex) { 589 throw new IllegalArgumentException("unable to parse influence from '" + text 590 + "' using " + influenceFormat, ex); 591 } 592 } 593 594 @Override 595 public boolean equals(Object other) { 596 if (this == other) { 597 return true; 598 } 599 if (other == null || getClass() != other.getClass()) { 600 return false; 601 } 602 final Tag tag = (Tag) other; 603 return Objects.equals(firstLine, tag.firstLine) 604 && Objects.equals(lastLine, tag.lastLine) 605 && Objects.equals(text, tag.text) 606 && Objects.equals(tagCheckRegexp, tag.tagCheckRegexp) 607 && Objects.equals(tagMessageRegexp, tag.tagMessageRegexp) 608 && Objects.equals(tagIdRegexp, tag.tagIdRegexp); 609 } 610 611 @Override 612 public int hashCode() { 613 return Objects.hash(text, firstLine, lastLine, tagCheckRegexp, tagMessageRegexp, 614 tagIdRegexp); 615 } 616 617 /** 618 * Determines whether the source of an audit event 619 * matches the text of this tag. 620 * 621 * @param event the {@code TreeWalkerAuditEvent} to check. 622 * @return true if the source of event matches the text of this tag. 623 */ 624 public boolean isMatch(TreeWalkerAuditEvent event) { 625 return isInScopeOfSuppression(event) 626 && isCheckMatch(event) 627 && isIdMatch(event) 628 && isMessageMatch(event); 629 } 630 631 /** 632 * Checks whether the {@link TreeWalkerAuditEvent} is in the scope of the suppression. 633 * 634 * @param event {@link TreeWalkerAuditEvent} instance. 635 * @return true if the {@link TreeWalkerAuditEvent} is in the scope of the suppression. 636 */ 637 private boolean isInScopeOfSuppression(TreeWalkerAuditEvent event) { 638 final int line = event.getLine(); 639 return line >= firstLine && line <= lastLine; 640 } 641 642 /** 643 * Checks whether {@link TreeWalkerAuditEvent} source name matches the check format. 644 * 645 * @param event {@link TreeWalkerAuditEvent} instance. 646 * @return true if the {@link TreeWalkerAuditEvent} source name matches the check format. 647 */ 648 private boolean isCheckMatch(TreeWalkerAuditEvent event) { 649 final Matcher checkMatcher = tagCheckRegexp.matcher(event.getSourceName()); 650 return checkMatcher.find(); 651 } 652 653 /** 654 * Checks whether the {@link TreeWalkerAuditEvent} module ID matches the ID format. 655 * 656 * @param event {@link TreeWalkerAuditEvent} instance. 657 * @return true if the {@link TreeWalkerAuditEvent} module ID matches the ID format. 658 */ 659 private boolean isIdMatch(TreeWalkerAuditEvent event) { 660 boolean match = true; 661 if (tagIdRegexp != null) { 662 if (event.getModuleId() == null) { 663 match = false; 664 } 665 else { 666 final Matcher idMatcher = tagIdRegexp.matcher(event.getModuleId()); 667 match = idMatcher.find(); 668 } 669 } 670 return match; 671 } 672 673 /** 674 * Checks whether the {@link TreeWalkerAuditEvent} message matches the message format. 675 * 676 * @param event {@link TreeWalkerAuditEvent} instance. 677 * @return true if the {@link TreeWalkerAuditEvent} message matches the message format. 678 */ 679 private boolean isMessageMatch(TreeWalkerAuditEvent event) { 680 boolean match = true; 681 if (tagMessageRegexp != null) { 682 final Matcher messageMatcher = tagMessageRegexp.matcher(event.getMessage()); 683 match = messageMatcher.find(); 684 } 685 return match; 686 } 687 688 @Override 689 public String toString() { 690 return "Tag[text='" + text + '\'' 691 + ", firstLine=" + firstLine 692 + ", lastLine=" + lastLine 693 + ", tagCheckRegexp=" + tagCheckRegexp 694 + ", tagMessageRegexp=" + tagMessageRegexp 695 + ", tagIdRegexp=" + tagIdRegexp 696 + ']'; 697 } 698 699 } 700 701}