Coverage Report - com.puppycrawl.tools.checkstyle.checks.SuppressWarningsHolder
 
Classes in this File Line Coverage Branch Coverage Complexity
SuppressWarningsHolder
100%
143/143
100%
87/87
0
SuppressWarningsHolder$Entry
100%
12/12
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.checks;
 21  
 
 22  
 import java.util.Collections;
 23  
 import java.util.HashMap;
 24  
 import java.util.LinkedList;
 25  
 import java.util.List;
 26  
 import java.util.Locale;
 27  
 import java.util.Map;
 28  
 
 29  
 import com.puppycrawl.tools.checkstyle.StatelessCheck;
 30  
 import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
 31  
 import com.puppycrawl.tools.checkstyle.api.AuditEvent;
 32  
 import com.puppycrawl.tools.checkstyle.api.DetailAST;
 33  
 import com.puppycrawl.tools.checkstyle.api.TokenTypes;
 34  
 
 35  
 /**
 36  
  * Maintains a set of check suppressions from {@link SuppressWarnings}
 37  
  * annotations.
 38  
  * @author Trevor Robinson
 39  
  * @author Stéphane Galland
 40  
  */
 41  
 @StatelessCheck
 42  30
 public class SuppressWarningsHolder
 43  
     extends AbstractCheck {
 44  
 
 45  
     /**
 46  
      * A key is pointing to the warning message text in "messages.properties"
 47  
      * file.
 48  
      */
 49  
     public static final String MSG_KEY = "suppress.warnings.invalid.target";
 50  
 
 51  
     /**
 52  
      * Optional prefix for warning suppressions that are only intended to be
 53  
      * recognized by checkstyle. For instance, to suppress {@code
 54  
      * FallThroughCheck} only in checkstyle (and not in javac), use the
 55  
      * suppression {@code "checkstyle:fallthrough"} or {@code "checkstyle:FallThrough"}.
 56  
      * To suppress the warning in both tools, just use {@code "fallthrough"}.
 57  
      */
 58  
     private static final String CHECKSTYLE_PREFIX = "checkstyle:";
 59  
 
 60  
     /** Java.lang namespace prefix, which is stripped from SuppressWarnings */
 61  
     private static final String JAVA_LANG_PREFIX = "java.lang.";
 62  
 
 63  
     /** Suffix to be removed from subclasses of Check. */
 64  
     private static final String CHECK_SUFFIX = "Check";
 65  
 
 66  
     /** Special warning id for matching all the warnings. */
 67  
     private static final String ALL_WARNING_MATCHING_ID = "all";
 68  
 
 69  
     /** A map from check source names to suppression aliases. */
 70  2
     private static final Map<String, String> CHECK_ALIAS_MAP = new HashMap<>();
 71  
 
 72  
     /**
 73  
      * A thread-local holder for the list of suppression entries for the last
 74  
      * file parsed.
 75  
      */
 76  4
     private static final ThreadLocal<List<Entry>> ENTRIES =
 77  2
             ThreadLocal.withInitial(LinkedList::new);
 78  
 
 79  
     /**
 80  
      * Returns the default alias for the source name of a check, which is the
 81  
      * source name in lower case with any dotted prefix or "Check" suffix
 82  
      * removed.
 83  
      * @param sourceName the source name of the check (generally the class
 84  
      *        name)
 85  
      * @return the default alias for the given check
 86  
      */
 87  
     public static String getDefaultAlias(String sourceName) {
 88  28
         int endIndex = sourceName.length();
 89  28
         if (sourceName.endsWith(CHECK_SUFFIX)) {
 90  26
             endIndex -= CHECK_SUFFIX.length();
 91  
         }
 92  28
         final int startIndex = sourceName.lastIndexOf('.') + 1;
 93  28
         return sourceName.substring(startIndex, endIndex).toLowerCase(Locale.ENGLISH);
 94  
     }
 95  
 
 96  
     /**
 97  
      * Returns the alias for the source name of a check. If an alias has been
 98  
      * explicitly registered via {@link #registerAlias(String, String)}, that
 99  
      * alias is returned; otherwise, the default alias is used.
 100  
      * @param sourceName the source name of the check (generally the class
 101  
      *        name)
 102  
      * @return the current alias for the given check
 103  
      */
 104  
     public static String getAlias(String sourceName) {
 105  35
         String checkAlias = CHECK_ALIAS_MAP.get(sourceName);
 106  35
         if (checkAlias == null) {
 107  26
             checkAlias = getDefaultAlias(sourceName);
 108  
         }
 109  35
         return checkAlias;
 110  
     }
 111  
 
 112  
     /**
 113  
      * Registers an alias for the source name of a check.
 114  
      * @param sourceName the source name of the check (generally the class
 115  
      *        name)
 116  
      * @param checkAlias the alias used in {@link SuppressWarnings} annotations
 117  
      */
 118  
     private static void registerAlias(String sourceName, String checkAlias) {
 119  5
         CHECK_ALIAS_MAP.put(sourceName, checkAlias);
 120  5
     }
 121  
 
 122  
     /**
 123  
      * Registers a list of source name aliases based on a comma-separated list
 124  
      * of {@code source=alias} items, such as {@code
 125  
      * com.puppycrawl.tools.checkstyle.checks.sizes.ParameterNumberCheck=
 126  
      * paramnum}.
 127  
      * @param aliasList the list of comma-separated alias assignments
 128  
      */
 129  
     public void setAliasList(String... aliasList) {
 130  13
         for (String sourceAlias : aliasList) {
 131  7
             final int index = sourceAlias.indexOf('=');
 132  7
             if (index > 0) {
 133  10
                 registerAlias(sourceAlias.substring(0, index), sourceAlias
 134  5
                     .substring(index + 1));
 135  
             }
 136  2
             else if (!sourceAlias.isEmpty()) {
 137  1
                 throw new IllegalArgumentException(
 138  
                     "'=' expected in alias list item: " + sourceAlias);
 139  
             }
 140  
         }
 141  6
     }
 142  
 
 143  
     /**
 144  
      * Checks for a suppression of a check with the given source name and
 145  
      * location in the last file processed.
 146  
      * @param event audit event.
 147  
      * @return whether the check with the given name is suppressed at the given
 148  
      *         source location
 149  
      */
 150  
     public static boolean isSuppressed(AuditEvent event) {
 151  33
         final List<Entry> entries = ENTRIES.get();
 152  33
         final String sourceName = event.getSourceName();
 153  33
         final String checkAlias = getAlias(sourceName);
 154  33
         final int line = event.getLine();
 155  33
         final int column = event.getColumn();
 156  33
         boolean suppressed = false;
 157  33
         for (Entry entry : entries) {
 158  389
             final boolean afterStart = isSuppressedAfterEventStart(line, column, entry);
 159  389
             final boolean beforeEnd = isSuppressedBeforeEventEnd(line, column, entry);
 160  389
             final boolean nameMatches =
 161  389
                 ALL_WARNING_MATCHING_ID.equals(entry.getCheckName())
 162  386
                     || entry.getCheckName().equalsIgnoreCase(checkAlias);
 163  389
             final boolean idMatches = event.getModuleId() != null
 164  184
                 && event.getModuleId().equals(entry.getCheckName());
 165  389
             if (afterStart && beforeEnd && (nameMatches || idMatches)) {
 166  17
                 suppressed = true;
 167  17
                 break;
 168  
             }
 169  372
         }
 170  33
         return suppressed;
 171  
     }
 172  
 
 173  
     /**
 174  
      * Checks whether suppression entry position is after the audit event occurrence position
 175  
      * in the source file.
 176  
      * @param line the line number in the source file where the event occurred.
 177  
      * @param column the column number in the source file where the event occurred.
 178  
      * @param entry suppression entry.
 179  
      * @return true if suppression entry position is after the audit event occurrence position
 180  
      *         in the source file.
 181  
      */
 182  
     private static boolean isSuppressedAfterEventStart(int line, int column, Entry entry) {
 183  778
         return entry.getFirstLine() < line
 184  139
             || entry.getFirstLine() == line
 185  5
             && (column == 0 || entry.getFirstColumn() <= column);
 186  
     }
 187  
 
 188  
     /**
 189  
      * Checks whether suppression entry position is before the audit event occurrence position
 190  
      * in the source file.
 191  
      * @param line the line number in the source file where the event occurred.
 192  
      * @param column the column number in the source file where the event occurred.
 193  
      * @param entry suppression entry.
 194  
      * @return true if suppression entry position is before the audit event occurrence position
 195  
      *         in the source file.
 196  
      */
 197  
     private static boolean isSuppressedBeforeEventEnd(int line, int column, Entry entry) {
 198  778
         return entry.getLastLine() > line
 199  220
             || entry.getLastLine() == line && entry
 200  13
                 .getLastColumn() >= column;
 201  
     }
 202  
 
 203  
     @Override
 204  
     public int[] getDefaultTokens() {
 205  26
         return getRequiredTokens();
 206  
     }
 207  
 
 208  
     @Override
 209  
     public int[] getAcceptableTokens() {
 210  4
         return getRequiredTokens();
 211  
     }
 212  
 
 213  
     @Override
 214  
     public int[] getRequiredTokens() {
 215  57
         return new int[] {TokenTypes.ANNOTATION};
 216  
     }
 217  
 
 218  
     @Override
 219  
     public void beginTree(DetailAST rootAST) {
 220  9
         ENTRIES.get().clear();
 221  9
     }
 222  
 
 223  
     @Override
 224  
     public void visitToken(DetailAST ast) {
 225  
         // check whether annotation is SuppressWarnings
 226  
         // expected children: AT ( IDENT | DOT ) LPAREN <values> RPAREN
 227  72
         String identifier = getIdentifier(getNthChild(ast, 1));
 228  71
         if (identifier.startsWith(JAVA_LANG_PREFIX)) {
 229  2
             identifier = identifier.substring(JAVA_LANG_PREFIX.length());
 230  
         }
 231  71
         if ("SuppressWarnings".equals(identifier)) {
 232  
 
 233  68
             final List<String> values = getAllAnnotationValues(ast);
 234  68
             if (!isAnnotationEmpty(values)) {
 235  66
                 final DetailAST targetAST = getAnnotationTarget(ast);
 236  
 
 237  66
                 if (targetAST == null) {
 238  1
                     log(ast.getLineNo(), MSG_KEY);
 239  
                 }
 240  
                 else {
 241  
                     // get text range of target
 242  65
                     final int firstLine = targetAST.getLineNo();
 243  65
                     final int firstColumn = targetAST.getColumnNo();
 244  65
                     final DetailAST nextAST = targetAST.getNextSibling();
 245  
                     final int lastLine;
 246  
                     final int lastColumn;
 247  65
                     if (nextAST == null) {
 248  6
                         lastLine = Integer.MAX_VALUE;
 249  6
                         lastColumn = Integer.MAX_VALUE;
 250  
                     }
 251  
                     else {
 252  59
                         lastLine = nextAST.getLineNo();
 253  59
                         lastColumn = nextAST.getColumnNo() - 1;
 254  
                     }
 255  
 
 256  
                     // add suppression entries for listed checks
 257  65
                     final List<Entry> entries = ENTRIES.get();
 258  65
                     for (String value : values) {
 259  65
                         String checkName = value;
 260  
                         // strip off the checkstyle-only prefix if present
 261  65
                         checkName = removeCheckstylePrefixIfExists(checkName);
 262  65
                         entries.add(new Entry(checkName, firstLine, firstColumn,
 263  
                                 lastLine, lastColumn));
 264  65
                     }
 265  
                 }
 266  
             }
 267  
         }
 268  71
     }
 269  
 
 270  
     /**
 271  
      * Method removes checkstyle prefix (checkstyle:) from check name if exists.
 272  
      *
 273  
      * @param checkName
 274  
      *            - name of the check
 275  
      * @return check name without prefix
 276  
      */
 277  
     private static String removeCheckstylePrefixIfExists(String checkName) {
 278  65
         String result = checkName;
 279  65
         if (checkName.startsWith(CHECKSTYLE_PREFIX)) {
 280  10
             result = checkName.substring(CHECKSTYLE_PREFIX.length());
 281  
         }
 282  65
         return result;
 283  
     }
 284  
 
 285  
     /**
 286  
      * Get all annotation values.
 287  
      * @param ast annotation token
 288  
      * @return list values
 289  
      */
 290  
     private static List<String> getAllAnnotationValues(DetailAST ast) {
 291  
         // get values of annotation
 292  69
         List<String> values = null;
 293  69
         final DetailAST lparenAST = ast.findFirstToken(TokenTypes.LPAREN);
 294  69
         if (lparenAST != null) {
 295  68
             final DetailAST nextAST = lparenAST.getNextSibling();
 296  68
             final int nextType = nextAST.getType();
 297  68
             switch (nextType) {
 298  
                 case TokenTypes.EXPR:
 299  
                 case TokenTypes.ANNOTATION_ARRAY_INIT:
 300  61
                     values = getAnnotationValues(nextAST);
 301  61
                     break;
 302  
 
 303  
                 case TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR:
 304  
                     // expected children: IDENT ASSIGN ( EXPR |
 305  
                     // ANNOTATION_ARRAY_INIT )
 306  5
                     values = getAnnotationValues(getNthChild(nextAST, 2));
 307  5
                     break;
 308  
 
 309  
                 case TokenTypes.RPAREN:
 310  
                     // no value present (not valid Java)
 311  1
                     break;
 312  
 
 313  
                 default:
 314  
                     // unknown annotation value type (new syntax?)
 315  1
                     throw new IllegalArgumentException("Unexpected AST: " + nextAST);
 316  
             }
 317  
         }
 318  68
         return values;
 319  
     }
 320  
 
 321  
     /**
 322  
      * Checks that annotation is empty.
 323  
      * @param values list of values in the annotation
 324  
      * @return whether annotation is empty or contains some values
 325  
      */
 326  
     private static boolean isAnnotationEmpty(List<String> values) {
 327  68
         return values == null;
 328  
     }
 329  
 
 330  
     /**
 331  
      * Get target of annotation.
 332  
      * @param ast the AST node to get the child of
 333  
      * @return get target of annotation
 334  
      */
 335  
     private static DetailAST getAnnotationTarget(DetailAST ast) {
 336  
         final DetailAST targetAST;
 337  67
         final DetailAST parentAST = ast.getParent();
 338  67
         switch (parentAST.getType()) {
 339  
             case TokenTypes.MODIFIERS:
 340  
             case TokenTypes.ANNOTATIONS:
 341  66
                 targetAST = getAcceptableParent(parentAST);
 342  66
                 break;
 343  
             default:
 344  
                 // unexpected container type
 345  1
                 throw new IllegalArgumentException("Unexpected container AST: " + parentAST);
 346  
         }
 347  66
         return targetAST;
 348  
     }
 349  
 
 350  
     /**
 351  
      * Returns parent of given ast if parent has one of the following types:
 352  
      * ANNOTATION_DEF, PACKAGE_DEF, CLASS_DEF, ENUM_DEF, ENUM_CONSTANT_DEF, CTOR_DEF,
 353  
      * METHOD_DEF, PARAMETER_DEF, VARIABLE_DEF, ANNOTATION_FIELD_DEF, TYPE, LITERAL_NEW,
 354  
      * LITERAL_THROWS, TYPE_ARGUMENT, IMPLEMENTS_CLAUSE, DOT.
 355  
      * @param child an ast
 356  
      * @return returns ast - parent of given
 357  
      */
 358  
     private static DetailAST getAcceptableParent(DetailAST child) {
 359  
         final DetailAST result;
 360  66
         final DetailAST parent = child.getParent();
 361  66
         switch (parent.getType()) {
 362  
             case TokenTypes.ANNOTATION_DEF:
 363  
             case TokenTypes.PACKAGE_DEF:
 364  
             case TokenTypes.CLASS_DEF:
 365  
             case TokenTypes.INTERFACE_DEF:
 366  
             case TokenTypes.ENUM_DEF:
 367  
             case TokenTypes.ENUM_CONSTANT_DEF:
 368  
             case TokenTypes.CTOR_DEF:
 369  
             case TokenTypes.METHOD_DEF:
 370  
             case TokenTypes.PARAMETER_DEF:
 371  
             case TokenTypes.VARIABLE_DEF:
 372  
             case TokenTypes.ANNOTATION_FIELD_DEF:
 373  
             case TokenTypes.TYPE:
 374  
             case TokenTypes.LITERAL_NEW:
 375  
             case TokenTypes.LITERAL_THROWS:
 376  
             case TokenTypes.TYPE_ARGUMENT:
 377  
             case TokenTypes.IMPLEMENTS_CLAUSE:
 378  
             case TokenTypes.DOT:
 379  65
                 result = parent;
 380  65
                 break;
 381  
             default:
 382  
                 // it's possible case, but shouldn't be processed here
 383  1
                 result = null;
 384  
         }
 385  66
         return result;
 386  
     }
 387  
 
 388  
     /**
 389  
      * Returns the n'th child of an AST node.
 390  
      * @param ast the AST node to get the child of
 391  
      * @param index the index of the child to get
 392  
      * @return the n'th child of the given AST node, or {@code null} if none
 393  
      */
 394  
     private static DetailAST getNthChild(DetailAST ast, int index) {
 395  77
         DetailAST child = ast.getFirstChild();
 396  158
         for (int i = 0; i < index && child != null; ++i) {
 397  81
             child = child.getNextSibling();
 398  
         }
 399  77
         return child;
 400  
     }
 401  
 
 402  
     /**
 403  
      * Returns the Java identifier represented by an AST.
 404  
      * @param ast an AST node for an IDENT or DOT
 405  
      * @return the Java identifier represented by the given AST subtree
 406  
      * @throws IllegalArgumentException if the AST is invalid
 407  
      */
 408  
     private static String getIdentifier(DetailAST ast) {
 409  80
         if (ast == null) {
 410  1
             throw new IllegalArgumentException("Identifier AST expected, but get null.");
 411  
         }
 412  
         final String identifier;
 413  79
         if (ast.getType() == TokenTypes.IDENT) {
 414  75
             identifier = ast.getText();
 415  
         }
 416  
         else {
 417  4
             identifier = getIdentifier(ast.getFirstChild()) + "."
 418  4
                 + getIdentifier(ast.getLastChild());
 419  
         }
 420  79
         return identifier;
 421  
     }
 422  
 
 423  
     /**
 424  
      * Returns the literal string expression represented by an AST.
 425  
      * @param ast an AST node for an EXPR
 426  
      * @return the Java string represented by the given AST expression
 427  
      *         or empty string if expression is too complex
 428  
      * @throws IllegalArgumentException if the AST is invalid
 429  
      */
 430  
     private static String getStringExpr(DetailAST ast) {
 431  66
         final DetailAST firstChild = ast.getFirstChild();
 432  66
         String expr = "";
 433  
 
 434  66
         switch (firstChild.getType()) {
 435  
             case TokenTypes.STRING_LITERAL:
 436  
                 // NOTE: escaped characters are not unescaped
 437  53
                 final String quotedText = firstChild.getText();
 438  53
                 expr = quotedText.substring(1, quotedText.length() - 1);
 439  53
                 break;
 440  
             case TokenTypes.IDENT:
 441  5
                 expr = firstChild.getText();
 442  5
                 break;
 443  
             case TokenTypes.DOT:
 444  4
                 expr = firstChild.getLastChild().getText();
 445  4
                 break;
 446  
             default:
 447  
                 // annotations with complex expressions cannot suppress warnings
 448  
         }
 449  66
         return expr;
 450  
     }
 451  
 
 452  
     /**
 453  
      * Returns the annotation values represented by an AST.
 454  
      * @param ast an AST node for an EXPR or ANNOTATION_ARRAY_INIT
 455  
      * @return the list of Java string represented by the given AST for an
 456  
      *         expression or annotation array initializer
 457  
      * @throws IllegalArgumentException if the AST is invalid
 458  
      */
 459  
     private static List<String> getAnnotationValues(DetailAST ast) {
 460  
         final List<String> annotationValues;
 461  67
         switch (ast.getType()) {
 462  
             case TokenTypes.EXPR:
 463  61
                 annotationValues = Collections.singletonList(getStringExpr(ast));
 464  61
                 break;
 465  
             case TokenTypes.ANNOTATION_ARRAY_INIT:
 466  5
                 annotationValues = findAllExpressionsInChildren(ast);
 467  5
                 break;
 468  
             default:
 469  1
                 throw new IllegalArgumentException(
 470  
                         "Expression or annotation array initializer AST expected: " + ast);
 471  
         }
 472  66
         return annotationValues;
 473  
     }
 474  
 
 475  
     /**
 476  
      * Method looks at children and returns list of expressions in strings.
 477  
      * @param parent ast, that contains children
 478  
      * @return list of expressions in strings
 479  
      */
 480  
     private static List<String> findAllExpressionsInChildren(DetailAST parent) {
 481  5
         final List<String> valueList = new LinkedList<>();
 482  5
         DetailAST childAST = parent.getFirstChild();
 483  16
         while (childAST != null) {
 484  11
             if (childAST.getType() == TokenTypes.EXPR) {
 485  5
                 valueList.add(getStringExpr(childAST));
 486  
             }
 487  11
             childAST = childAST.getNextSibling();
 488  
         }
 489  5
         return valueList;
 490  
     }
 491  
 
 492  
     /** Records a particular suppression for a region of a file. */
 493  
     private static class Entry {
 494  
         /** The source name of the suppressed check. */
 495  
         private final String checkName;
 496  
         /** The suppression region for the check - first line. */
 497  
         private final int firstLine;
 498  
         /** The suppression region for the check - first column. */
 499  
         private final int firstColumn;
 500  
         /** The suppression region for the check - last line. */
 501  
         private final int lastLine;
 502  
         /** The suppression region for the check - last column. */
 503  
         private final int lastColumn;
 504  
 
 505  
         /**
 506  
          * Constructs a new suppression region entry.
 507  
          * @param checkName the source name of the suppressed check
 508  
          * @param firstLine the first line of the suppression region
 509  
          * @param firstColumn the first column of the suppression region
 510  
          * @param lastLine the last line of the suppression region
 511  
          * @param lastColumn the last column of the suppression region
 512  
          */
 513  
         Entry(String checkName, int firstLine, int firstColumn,
 514  71
             int lastLine, int lastColumn) {
 515  71
             this.checkName = checkName;
 516  71
             this.firstLine = firstLine;
 517  71
             this.firstColumn = firstColumn;
 518  71
             this.lastLine = lastLine;
 519  71
             this.lastColumn = lastColumn;
 520  71
         }
 521  
 
 522  
         /**
 523  
          * Gets he source name of the suppressed check.
 524  
          * @return the source name of the suppressed check
 525  
          */
 526  
         public String getCheckName() {
 527  959
             return checkName;
 528  
         }
 529  
 
 530  
         /**
 531  
          * Gets the first line of the suppression region.
 532  
          * @return the first line of the suppression region
 533  
          */
 534  
         public int getFirstLine() {
 535  528
             return firstLine;
 536  
         }
 537  
 
 538  
         /**
 539  
          * Gets the first column of the suppression region.
 540  
          * @return the first column of the suppression region
 541  
          */
 542  
         public int getFirstColumn() {
 543  5
             return firstColumn;
 544  
         }
 545  
 
 546  
         /**
 547  
          * Gets the last line of the suppression region.
 548  
          * @return the last line of the suppression region
 549  
          */
 550  
         public int getLastLine() {
 551  609
             return lastLine;
 552  
         }
 553  
 
 554  
         /**
 555  
          * Gets the last column of the suppression region.
 556  
          * @return the last column of the suppression region
 557  
          */
 558  
         public int getLastColumn() {
 559  13
             return lastColumn;
 560  
         }
 561  
     }
 562  
 }