View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2025 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.coding;
21  
22  import java.util.ArrayList;
23  import java.util.Collections;
24  import java.util.List;
25  import java.util.regex.Pattern;
26  
27  import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
28  import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
29  import com.puppycrawl.tools.checkstyle.api.DetailAST;
30  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
31  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
32  import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
33  
34  /**
35   * <div>
36   * Checks if unnecessary parentheses are used in a statement or expression.
37   * The check will flag the following with warnings:
38   * </div>
39   * <div class="wrapper"><pre class="prettyprint"><code class="language-java">
40   * return (x);          // parens around identifier
41   * return (x + 1);      // parens around return value
42   * int x = (y / 2 + 1); // parens around assignment rhs
43   * for (int i = (0); i &lt; 10; i++) {  // parens around literal
44   * t -= (z + 1);                     // parens around assignment rhs
45   * boolean a = (x &gt; 7 &amp;&amp; y &gt; 5)      // parens around expression
46   *             || z &lt; 9;
47   * boolean b = (~a) &gt; -27            // parens around ~a
48   *             &amp;&amp; (a-- &lt; 30);        // parens around expression
49   * </code></pre></div>
50   *
51   * <p>
52   * Notes:
53   * The check is not "type aware", that is to say, it can't tell if parentheses
54   * are unnecessary based on the types in an expression. The check is partially aware about
55   * operator precedence but unaware about operator associativity.
56   * It won't catch cases such as:
57   * </p>
58   * <div class="wrapper"><pre class="prettyprint"><code class="language-java">
59   * int x = (a + b) + c; // 1st Case
60   * boolean p = true; // 2nd Case
61   * int q = 4;
62   * int r = 3;
63   * if (p == (q &lt;= r)) {}
64   * </code></pre></div>
65   *
66   * <p>
67   * In the first case, given that <em>a</em>, <em>b</em>, and <em>c</em> are
68   * all {@code int} variables, the parentheses around {@code a + b}
69   * are not needed.
70   * In the second case, parentheses are required as <em>q</em>, <em>r</em> are
71   * of type {@code int} and <em>p</em> is of type {@code boolean}
72   * and removing parentheses will give a compile-time error. Even if <em>q</em>
73   * and <em>r</em> were {@code boolean} still there will be no violation
74   * raised as check is not "type aware".
75   * </p>
76   *
77   * <p>
78   * The partial support for operator precedence includes cases of the following type:
79   * </p>
80   * <div class="wrapper"><pre class="prettyprint"><code class="language-java">
81   * boolean a = true, b = true;
82   * boolean c = false, d = false;
83   * if ((a &amp;&amp; b) || c) { // violation, unnecessary paren
84   * }
85   * if (a &amp;&amp; (b || c)) { // ok
86   * }
87   * if ((a == b) &amp;&amp; c) { // violation, unnecessary paren
88   * }
89   * String e = &quot;e&quot;;
90   * if ((e instanceof String) &amp;&amp; a || b) { // violation, unnecessary paren
91   * }
92   * int f = 0;
93   * int g = 0;
94   * if (!(f &gt;= g) // ok
95   *         &amp;&amp; (g &gt; f)) { // violation, unnecessary paren
96   * }
97   * if ((++f) &gt; g &amp;&amp; a) { // violation, unnecessary paren
98   * }
99   * </code></pre></div>
100  *
101  * @since 3.4
102  */
103 @FileStatefulCheck
104 public class UnnecessaryParenthesesCheck extends AbstractCheck {
105 
106     /**
107      * A key is pointing to the warning message text in "messages.properties"
108      * file.
109      */
110     public static final String MSG_IDENT = "unnecessary.paren.ident";
111 
112     /**
113      * A key is pointing to the warning message text in "messages.properties"
114      * file.
115      */
116     public static final String MSG_ASSIGN = "unnecessary.paren.assign";
117 
118     /**
119      * A key is pointing to the warning message text in "messages.properties"
120      * file.
121      */
122     public static final String MSG_EXPR = "unnecessary.paren.expr";
123 
124     /**
125      * A key is pointing to the warning message text in "messages.properties"
126      * file.
127      */
128     public static final String MSG_LITERAL = "unnecessary.paren.literal";
129 
130     /**
131      * A key is pointing to the warning message text in "messages.properties"
132      * file.
133      */
134     public static final String MSG_STRING = "unnecessary.paren.string";
135 
136     /**
137      * A key is pointing to the warning message text in "messages.properties"
138      * file.
139      */
140     public static final String MSG_RETURN = "unnecessary.paren.return";
141 
142     /**
143      * A key is pointing to the warning message text in "messages.properties"
144      * file.
145      */
146     public static final String MSG_LAMBDA = "unnecessary.paren.lambda";
147 
148     /**
149      * Compiled pattern used to match newline control characters, for replacement.
150      */
151     private static final Pattern NEWLINE = Pattern.compile("\\R");
152 
153     /**
154      * String used to amend TEXT_BLOCK_CONTENT so that it matches STRING_LITERAL.
155      */
156     private static final String QUOTE = "\"";
157 
158     /** The maximum string length before we chop the string. */
159     private static final int MAX_QUOTED_LENGTH = 25;
160 
161     /** Token types for literals. */
162     private static final int[] LITERALS = {
163         TokenTypes.NUM_DOUBLE,
164         TokenTypes.NUM_FLOAT,
165         TokenTypes.NUM_INT,
166         TokenTypes.NUM_LONG,
167         TokenTypes.STRING_LITERAL,
168         TokenTypes.LITERAL_NULL,
169         TokenTypes.LITERAL_FALSE,
170         TokenTypes.LITERAL_TRUE,
171         TokenTypes.TEXT_BLOCK_LITERAL_BEGIN,
172     };
173 
174     /** Token types for assignment operations. */
175     private static final int[] ASSIGNMENTS = {
176         TokenTypes.ASSIGN,
177         TokenTypes.BAND_ASSIGN,
178         TokenTypes.BOR_ASSIGN,
179         TokenTypes.BSR_ASSIGN,
180         TokenTypes.BXOR_ASSIGN,
181         TokenTypes.DIV_ASSIGN,
182         TokenTypes.MINUS_ASSIGN,
183         TokenTypes.MOD_ASSIGN,
184         TokenTypes.PLUS_ASSIGN,
185         TokenTypes.SL_ASSIGN,
186         TokenTypes.SR_ASSIGN,
187         TokenTypes.STAR_ASSIGN,
188     };
189 
190     /** Token types for conditional operators. */
191     private static final int[] CONDITIONAL_OPERATOR = {
192         TokenTypes.LOR,
193         TokenTypes.LAND,
194     };
195 
196     /** Token types for relation operator. */
197     private static final int[] RELATIONAL_OPERATOR = {
198         TokenTypes.LITERAL_INSTANCEOF,
199         TokenTypes.GT,
200         TokenTypes.LT,
201         TokenTypes.GE,
202         TokenTypes.LE,
203         TokenTypes.EQUAL,
204         TokenTypes.NOT_EQUAL,
205     };
206 
207     /** Token types for unary and postfix operators. */
208     private static final int[] UNARY_AND_POSTFIX = {
209         TokenTypes.UNARY_MINUS,
210         TokenTypes.UNARY_PLUS,
211         TokenTypes.INC,
212         TokenTypes.DEC,
213         TokenTypes.LNOT,
214         TokenTypes.BNOT,
215         TokenTypes.POST_INC,
216         TokenTypes.POST_DEC,
217     };
218 
219     /** Token types for bitwise binary operator. */
220     private static final int[] BITWISE_BINARY_OPERATORS = {
221         TokenTypes.BXOR,
222         TokenTypes.BOR,
223         TokenTypes.BAND,
224     };
225 
226     /**
227      * Used to test if logging a warning in a parent node may be skipped
228      * because a warning was already logged on an immediate child node.
229      */
230     private DetailAST parentToSkip;
231     /** Depth of nested assignments.  Normally this will be 0 or 1. */
232     private int assignDepth;
233 
234     @Override
235     public int[] getDefaultTokens() {
236         return new int[] {
237             TokenTypes.EXPR,
238             TokenTypes.IDENT,
239             TokenTypes.NUM_DOUBLE,
240             TokenTypes.NUM_FLOAT,
241             TokenTypes.NUM_INT,
242             TokenTypes.NUM_LONG,
243             TokenTypes.STRING_LITERAL,
244             TokenTypes.LITERAL_NULL,
245             TokenTypes.LITERAL_FALSE,
246             TokenTypes.LITERAL_TRUE,
247             TokenTypes.ASSIGN,
248             TokenTypes.BAND_ASSIGN,
249             TokenTypes.BOR_ASSIGN,
250             TokenTypes.BSR_ASSIGN,
251             TokenTypes.BXOR_ASSIGN,
252             TokenTypes.DIV_ASSIGN,
253             TokenTypes.MINUS_ASSIGN,
254             TokenTypes.MOD_ASSIGN,
255             TokenTypes.PLUS_ASSIGN,
256             TokenTypes.SL_ASSIGN,
257             TokenTypes.SR_ASSIGN,
258             TokenTypes.STAR_ASSIGN,
259             TokenTypes.LAMBDA,
260             TokenTypes.TEXT_BLOCK_LITERAL_BEGIN,
261             TokenTypes.LAND,
262             TokenTypes.LOR,
263             TokenTypes.LITERAL_INSTANCEOF,
264             TokenTypes.GT,
265             TokenTypes.LT,
266             TokenTypes.GE,
267             TokenTypes.LE,
268             TokenTypes.EQUAL,
269             TokenTypes.NOT_EQUAL,
270             TokenTypes.UNARY_MINUS,
271             TokenTypes.UNARY_PLUS,
272             TokenTypes.INC,
273             TokenTypes.DEC,
274             TokenTypes.LNOT,
275             TokenTypes.BNOT,
276             TokenTypes.POST_INC,
277             TokenTypes.POST_DEC,
278         };
279     }
280 
281     @Override
282     public int[] getAcceptableTokens() {
283         return new int[] {
284             TokenTypes.EXPR,
285             TokenTypes.IDENT,
286             TokenTypes.NUM_DOUBLE,
287             TokenTypes.NUM_FLOAT,
288             TokenTypes.NUM_INT,
289             TokenTypes.NUM_LONG,
290             TokenTypes.STRING_LITERAL,
291             TokenTypes.LITERAL_NULL,
292             TokenTypes.LITERAL_FALSE,
293             TokenTypes.LITERAL_TRUE,
294             TokenTypes.ASSIGN,
295             TokenTypes.BAND_ASSIGN,
296             TokenTypes.BOR_ASSIGN,
297             TokenTypes.BSR_ASSIGN,
298             TokenTypes.BXOR_ASSIGN,
299             TokenTypes.DIV_ASSIGN,
300             TokenTypes.MINUS_ASSIGN,
301             TokenTypes.MOD_ASSIGN,
302             TokenTypes.PLUS_ASSIGN,
303             TokenTypes.SL_ASSIGN,
304             TokenTypes.SR_ASSIGN,
305             TokenTypes.STAR_ASSIGN,
306             TokenTypes.LAMBDA,
307             TokenTypes.TEXT_BLOCK_LITERAL_BEGIN,
308             TokenTypes.LAND,
309             TokenTypes.LOR,
310             TokenTypes.LITERAL_INSTANCEOF,
311             TokenTypes.GT,
312             TokenTypes.LT,
313             TokenTypes.GE,
314             TokenTypes.LE,
315             TokenTypes.EQUAL,
316             TokenTypes.NOT_EQUAL,
317             TokenTypes.UNARY_MINUS,
318             TokenTypes.UNARY_PLUS,
319             TokenTypes.INC,
320             TokenTypes.DEC,
321             TokenTypes.LNOT,
322             TokenTypes.BNOT,
323             TokenTypes.POST_INC,
324             TokenTypes.POST_DEC,
325             TokenTypes.BXOR,
326             TokenTypes.BOR,
327             TokenTypes.BAND,
328             TokenTypes.QUESTION,
329         };
330     }
331 
332     @Override
333     public int[] getRequiredTokens() {
334         // Check can work with any of acceptable tokens
335         return CommonUtil.EMPTY_INT_ARRAY;
336     }
337 
338     // -@cs[CyclomaticComplexity] All logs should be in visit token.
339     @Override
340     public void visitToken(DetailAST ast) {
341         final DetailAST parent = ast.getParent();
342 
343         if (isLambdaSingleParameterSurrounded(ast)) {
344             log(ast, MSG_LAMBDA);
345         }
346         else if (ast.getType() == TokenTypes.QUESTION) {
347             getParenthesesChildrenAroundQuestion(ast)
348                 .forEach(unnecessaryChild -> log(unnecessaryChild, MSG_EXPR));
349         }
350         else if (parent.getType() != TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR) {
351             final int type = ast.getType();
352             final boolean surrounded = isSurrounded(ast);
353             // An identifier surrounded by parentheses.
354             if (surrounded && type == TokenTypes.IDENT) {
355                 parentToSkip = ast.getParent();
356                 log(ast, MSG_IDENT, ast.getText());
357             }
358             // A literal (numeric or string) surrounded by parentheses.
359             else if (surrounded && TokenUtil.isOfType(type, LITERALS)) {
360                 parentToSkip = ast.getParent();
361                 if (type == TokenTypes.STRING_LITERAL) {
362                     log(ast, MSG_STRING,
363                         chopString(ast.getText()));
364                 }
365                 else if (type == TokenTypes.TEXT_BLOCK_LITERAL_BEGIN) {
366                     // Strip newline control characters to keep message as single-line, add
367                     // quotes to make string consistent with STRING_LITERAL
368                     final String logString = QUOTE
369                         + NEWLINE.matcher(
370                             ast.getFirstChild().getText()).replaceAll("\\\\n")
371                         + QUOTE;
372                     log(ast, MSG_STRING, chopString(logString));
373                 }
374                 else {
375                     log(ast, MSG_LITERAL, ast.getText());
376                 }
377             }
378             // The rhs of an assignment surrounded by parentheses.
379             else if (TokenUtil.isOfType(type, ASSIGNMENTS)) {
380                 assignDepth++;
381                 final DetailAST last = ast.getLastChild();
382                 if (last.getType() == TokenTypes.RPAREN) {
383                     log(ast, MSG_ASSIGN);
384                 }
385             }
386         }
387     }
388 
389     @Override
390     public void leaveToken(DetailAST ast) {
391         final int type = ast.getType();
392         final DetailAST parent = ast.getParent();
393 
394         // shouldn't process assign in annotation pairs
395         if (type != TokenTypes.ASSIGN
396             || parent.getType() != TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR) {
397             if (type == TokenTypes.EXPR) {
398                 checkExpression(ast);
399             }
400             else if (TokenUtil.isOfType(type, ASSIGNMENTS)) {
401                 assignDepth--;
402             }
403             else if (isSurrounded(ast) && unnecessaryParenAroundOperators(ast)) {
404                 log(ast.getPreviousSibling(), MSG_EXPR);
405             }
406         }
407     }
408 
409     /**
410      * Tests if the given {@code DetailAST} is surrounded by parentheses.
411      *
412      * @param ast the {@code DetailAST} to check if it is surrounded by
413      *        parentheses.
414      * @return {@code true} if {@code ast} is surrounded by
415      *         parentheses.
416      */
417     private static boolean isSurrounded(DetailAST ast) {
418         final DetailAST prev = ast.getPreviousSibling();
419         final DetailAST parent = ast.getParent();
420         final boolean isPreviousSiblingLeftParenthesis = prev != null
421                 && prev.getType() == TokenTypes.LPAREN;
422         final boolean isMethodCallWithUnnecessaryParenthesis =
423                 parent.getType() == TokenTypes.METHOD_CALL
424                 && parent.getPreviousSibling() != null
425                 && parent.getPreviousSibling().getType() == TokenTypes.LPAREN;
426         return isPreviousSiblingLeftParenthesis || isMethodCallWithUnnecessaryParenthesis;
427     }
428 
429     /**
430      * Tests if the given expression node is surrounded by parentheses.
431      *
432      * @param ast a {@code DetailAST} whose type is
433      *        {@code TokenTypes.EXPR}.
434      * @return {@code true} if the expression is surrounded by
435      *         parentheses.
436      */
437     private static boolean isExprSurrounded(DetailAST ast) {
438         return ast.getFirstChild().getType() == TokenTypes.LPAREN;
439     }
440 
441     /**
442      * Checks whether an expression is surrounded by parentheses.
443      *
444      * @param ast the {@code DetailAST} to check if it is surrounded by
445      *        parentheses.
446      */
447     private void checkExpression(DetailAST ast) {
448         // If 'parentToSkip' == 'ast', then we've already logged a
449         // warning about an immediate child node in visitToken, so we don't
450         // need to log another one here.
451         if (parentToSkip != ast && isExprSurrounded(ast)) {
452             if (ast.getParent().getType() == TokenTypes.LITERAL_RETURN) {
453                 log(ast, MSG_RETURN);
454             }
455             else if (assignDepth >= 1) {
456                 log(ast, MSG_ASSIGN);
457             }
458             else {
459                 log(ast, MSG_EXPR);
460             }
461         }
462     }
463 
464     /**
465      * Checks if conditional, relational, bitwise binary operator, unary and postfix operators
466      * in expressions are surrounded by unnecessary parentheses.
467      *
468      * @param ast the {@code DetailAST} to check if it is surrounded by
469      *        unnecessary parentheses.
470      * @return {@code true} if the expression is surrounded by
471      *         unnecessary parentheses.
472      */
473     private static boolean unnecessaryParenAroundOperators(DetailAST ast) {
474         final int type = ast.getType();
475         final boolean isConditionalOrRelational = TokenUtil.isOfType(type, CONDITIONAL_OPERATOR)
476                         || TokenUtil.isOfType(type, RELATIONAL_OPERATOR);
477         final boolean isBitwise = TokenUtil.isOfType(type, BITWISE_BINARY_OPERATORS);
478         final boolean hasUnnecessaryParentheses;
479         if (isConditionalOrRelational) {
480             hasUnnecessaryParentheses = checkConditionalOrRelationalOperator(ast);
481         }
482         else if (isBitwise) {
483             hasUnnecessaryParentheses = checkBitwiseBinaryOperator(ast);
484         }
485         else {
486             hasUnnecessaryParentheses = TokenUtil.isOfType(type, UNARY_AND_POSTFIX)
487                     && isBitWiseBinaryOrConditionalOrRelationalOperator(ast.getParent().getType());
488         }
489         return hasUnnecessaryParentheses;
490     }
491 
492     /**
493      * Check if conditional or relational operator has unnecessary parentheses.
494      *
495      * @param ast to check if surrounded by unnecessary parentheses
496      * @return true if unnecessary parenthesis
497      */
498     private static boolean checkConditionalOrRelationalOperator(DetailAST ast) {
499         final int type = ast.getType();
500         final int parentType = ast.getParent().getType();
501         final boolean isParentEqualityOperator =
502                 TokenUtil.isOfType(parentType, TokenTypes.EQUAL, TokenTypes.NOT_EQUAL);
503         final boolean result;
504         if (type == TokenTypes.LOR) {
505             result = !TokenUtil.isOfType(parentType, TokenTypes.LAND)
506                     && !TokenUtil.isOfType(parentType, BITWISE_BINARY_OPERATORS);
507         }
508         else if (type == TokenTypes.LAND) {
509             result = !TokenUtil.isOfType(parentType, BITWISE_BINARY_OPERATORS);
510         }
511         else {
512             result = true;
513         }
514         return result && !isParentEqualityOperator
515                 && isBitWiseBinaryOrConditionalOrRelationalOperator(parentType);
516     }
517 
518     /**
519      * Check if bitwise binary operator has unnecessary parentheses.
520      *
521      * @param ast to check if surrounded by unnecessary parentheses
522      * @return true if unnecessary parenthesis
523      */
524     private static boolean checkBitwiseBinaryOperator(DetailAST ast) {
525         final int type = ast.getType();
526         final int parentType = ast.getParent().getType();
527         final boolean result;
528         if (type == TokenTypes.BOR) {
529             result = !TokenUtil.isOfType(parentType, TokenTypes.BAND, TokenTypes.BXOR)
530                     && !TokenUtil.isOfType(parentType, RELATIONAL_OPERATOR);
531         }
532         else if (type == TokenTypes.BXOR) {
533             result = !TokenUtil.isOfType(parentType, TokenTypes.BAND)
534                     && !TokenUtil.isOfType(parentType, RELATIONAL_OPERATOR);
535         }
536         // we deal with bitwise AND here.
537         else {
538             result = !TokenUtil.isOfType(parentType, RELATIONAL_OPERATOR);
539         }
540         return result && isBitWiseBinaryOrConditionalOrRelationalOperator(parentType);
541     }
542 
543     /**
544      * Check if token type is bitwise binary or conditional or relational operator.
545      *
546      * @param type Token type to check
547      * @return true if it is bitwise binary or conditional operator
548      */
549     private static boolean isBitWiseBinaryOrConditionalOrRelationalOperator(int type) {
550         return TokenUtil.isOfType(type, CONDITIONAL_OPERATOR)
551                 || TokenUtil.isOfType(type, RELATIONAL_OPERATOR)
552                 || TokenUtil.isOfType(type, BITWISE_BINARY_OPERATORS);
553     }
554 
555     /**
556      * Tests if the given node has a single parameter, no defined type, and is surrounded
557      * by parentheses. This condition can only be true for lambdas.
558      *
559      * @param ast a {@code DetailAST} node
560      * @return {@code true} if the lambda has a single parameter, no defined type, and is
561      *         surrounded by parentheses.
562      */
563     private static boolean isLambdaSingleParameterSurrounded(DetailAST ast) {
564         final DetailAST firstChild = ast.getFirstChild();
565         boolean result = false;
566         if (TokenUtil.isOfType(firstChild, TokenTypes.LPAREN)) {
567             final DetailAST parameters = firstChild.getNextSibling();
568             if (parameters.getChildCount(TokenTypes.PARAMETER_DEF) == 1
569                     && !parameters.getFirstChild().findFirstToken(TokenTypes.TYPE).hasChildren()) {
570                 result = true;
571             }
572         }
573         return result;
574     }
575 
576     /**
577      *  Returns the direct LPAREN tokens children to a given QUESTION token which
578      *  contain an expression not a literal variable.
579      *
580      *  @param questionToken {@code DetailAST} question token to be checked
581      *  @return the direct children to the given question token which their types are LPAREN
582      *          tokens and not contain a literal inside the parentheses
583      */
584     private static List<DetailAST> getParenthesesChildrenAroundQuestion(DetailAST questionToken) {
585         final List<DetailAST> surroundedChildren = new ArrayList<>();
586         DetailAST directChild = questionToken.getFirstChild();
587         while (directChild != null) {
588             if (directChild.getType() == TokenTypes.LPAREN
589                     && !TokenUtil.isOfType(directChild.getNextSibling(), LITERALS)) {
590                 surroundedChildren.add(directChild);
591             }
592             directChild = directChild.getNextSibling();
593         }
594         return Collections.unmodifiableList(surroundedChildren);
595     }
596 
597     /**
598      * Returns the specified string chopped to {@code MAX_QUOTED_LENGTH}
599      * plus an ellipsis (...) if the length of the string exceeds {@code
600      * MAX_QUOTED_LENGTH}.
601      *
602      * @param value the string to potentially chop.
603      * @return the chopped string if {@code string} is longer than
604      *         {@code MAX_QUOTED_LENGTH}; otherwise {@code string}.
605      */
606     private static String chopString(String value) {
607         String result = value;
608         if (value.length() > MAX_QUOTED_LENGTH) {
609             result = value.substring(0, MAX_QUOTED_LENGTH) + "...\"";
610         }
611         return result;
612     }
613 
614 }