1 ////////////////////////////////////////////////////////////////////////////////
2 // checkstyle: Checks Java source code for adherence to a set of rules.
3 // Copyright (C) 2001-2015 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 package com.puppycrawl.tools.checkstyle.checks.indentation;
20
21 import com.puppycrawl.tools.checkstyle.api.DetailAST;
22 import com.puppycrawl.tools.checkstyle.api.TokenTypes;
23 import com.puppycrawl.tools.checkstyle.api.Utils;
24 import java.util.Arrays;
25
26 /**
27 * Abstract base class for all handlers.
28 *
29 * @author jrichard
30 */
31 public abstract class ExpressionHandler
32 {
33
34 /**
35 * A key is pointing to the warning message text in "messages.properties"
36 * file.
37 */
38 public static final String MSG_ERROR = "indentation.error";
39
40 /**
41 * A key is pointing to the warning message text in "messages.properties"
42 * file.
43 */
44 public static final String MSG_ERROR_MULTI = "indentation.error.multi";
45
46 /**
47 * A key is pointing to the warning message text in "messages.properties"
48 * file.
49 */
50 public static final String MSG_CHILD_ERROR = "indentation.child.error";
51
52 /**
53 * A key is pointing to the warning message text in "messages.properties"
54 * file.
55 */
56 public static final String MSG_CHILD_ERROR_MULTI = "indentation.child.error.multi";
57
58 /**
59 * The instance of <code>IndentationCheck</code> using this handler.
60 */
61 private final IndentationCheck indentCheck;
62
63 /** the AST which is handled by this handler */
64 private final DetailAST mainAst;
65
66 /** name used during output to user */
67 private final String typeName;
68
69 /** containing AST handler */
70 private final ExpressionHandler parent;
71
72 /** indentation amount for this handler */
73 private IndentLevel level;
74
75 /**
76 * Construct an instance of this handler with the given indentation check,
77 * name, abstract syntax tree, and parent handler.
78 *
79 * @param indentCheck the indentation check
80 * @param typeName the name of the handler
81 * @param expr the abstract syntax tree
82 * @param parent the parent handler
83 */
84 public ExpressionHandler(IndentationCheck indentCheck,
85 String typeName, DetailAST expr, ExpressionHandler parent)
86 {
87 this.indentCheck = indentCheck;
88 this.typeName = typeName;
89 mainAst = expr;
90 this.parent = parent;
91 }
92
93 /**
94 * Get the indentation amount for this handler. For performance reasons,
95 * this value is cached. The first time this method is called, the
96 * indentation amount is computed and stored. On further calls, the stored
97 * value is returned.
98 *
99 * @return the expected indentation amount
100 */
101 public final IndentLevel getLevel()
102 {
103 if (level == null) {
104 level = getLevelImpl();
105 }
106 return level;
107 }
108
109 /**
110 * Compute the indentation amount for this handler.
111 *
112 * @return the expected indentation amount
113 */
114 protected IndentLevel getLevelImpl()
115 {
116 return parent.suggestedChildLevel(this);
117 }
118
119 /**
120 * Indentation level suggested for a child element. Children don't have
121 * to respect this, but most do.
122 *
123 * @param child child AST (so suggestion level can differ based on child
124 * type)
125 *
126 * @return suggested indentation for child
127 */
128 public IndentLevel suggestedChildLevel(ExpressionHandler child)
129 {
130 return new IndentLevel(getLevel(), getBasicOffset());
131 }
132
133 /**
134 * Log an indentation error.
135 *
136 * @param ast the expression that caused the error
137 * @param subtypeName the type of the expression
138 * @param actualLevel the actual indent level of the expression
139 */
140 protected final void logError(DetailAST ast, String subtypeName,
141 int actualLevel)
142 {
143 logError(ast, subtypeName, actualLevel, getLevel());
144 }
145
146 /**
147 * Log an indentation error.
148 *
149 * @param ast the expression that caused the error
150 * @param subtypeName the type of the expression
151 * @param actualLevel the actual indent level of the expression
152 * @param expectedLevel the expected indent level of the expression
153 */
154 protected final void logError(DetailAST ast, String subtypeName,
155 int actualLevel, IndentLevel expectedLevel)
156 {
157 final String typeStr =
158 "".equals(subtypeName) ? "" : " " + subtypeName;
159 String messageKey = MSG_ERROR;
160 if (expectedLevel.isMultiLevel()) {
161 messageKey = MSG_ERROR_MULTI;
162 }
163 indentCheck.indentationLog(ast.getLineNo(), messageKey,
164 typeName + typeStr, actualLevel, expectedLevel);
165 }
166
167 /**
168 * Log child indentation error.
169 *
170 * @param line the expression that caused the error
171 * @param actualLevel the actual indent level of the expression
172 * @param expectedLevel the expected indent level of the expression
173 */
174 private void logChildError(int line,
175 int actualLevel,
176 IndentLevel expectedLevel)
177 {
178 String messageKey = MSG_CHILD_ERROR;
179 if (expectedLevel.isMultiLevel()) {
180 messageKey = MSG_CHILD_ERROR_MULTI;
181 }
182 indentCheck.indentationLog(line, messageKey,
183 typeName, actualLevel, expectedLevel);
184 }
185
186 /**
187 * Determines if the given expression is at the start of a line.
188 *
189 * @param ast the expression to check
190 *
191 * @return true if it is, false otherwise
192 */
193 protected final boolean startsLine(DetailAST ast)
194 {
195 return getLineStart(ast) == expandedTabsColumnNo(ast);
196 }
197
198 /**
199 * Determines if two expressions are on the same line.
200 *
201 * @param ast1 the first expression
202 * @param ast2 the second expression
203 *
204 * @return true if they are, false otherwise
205 */
206 static boolean areOnSameLine(DetailAST ast1, DetailAST ast2)
207 {
208 return ast1 != null && ast2 != null
209 && ast1.getLineNo() == ast2.getLineNo();
210 }
211
212 /**
213 * Searchs in given sub-tree (including given node) for the token
214 * which represents first symbol for this sub-tree in file.
215 * @param ast a root of sub-tree in which the search shoul be performed.
216 * @return a token which occurs first in the file.
217 */
218 static DetailAST getFirstToken(DetailAST ast)
219 {
220 DetailAST first = ast;
221 DetailAST child = ast.getFirstChild();
222
223 while (child != null) {
224 final DetailAST toTest = getFirstToken(child);
225 if (toTest.getLineNo() < first.getLineNo()
226 || toTest.getLineNo() == first.getLineNo()
227 && toTest.getColumnNo() < first.getColumnNo())
228 {
229 first = toTest;
230 }
231 child = child.getNextSibling();
232 }
233
234 return first;
235 }
236
237 /**
238 * Get the start of the line for the given expression.
239 *
240 * @param ast the expression to find the start of the line for
241 *
242 * @return the start of the line for the given expression
243 */
244 protected final int getLineStart(DetailAST ast)
245 {
246 final String line = indentCheck.getLine(ast.getLineNo() - 1);
247 return getLineStart(line);
248 }
249
250 /**
251 * Check the indentation of consecutive lines for the expression we are
252 * handling.
253 *
254 * @param startLine the first line to check
255 * @param endLine the last line to check
256 * @param indentLevel the required indent level
257 */
258 protected final void checkLinesIndent(int startLine, int endLine,
259 IndentLevel indentLevel)
260 {
261 // check first line
262 checkSingleLine(startLine, indentLevel);
263
264 // check following lines
265 final IndentLevel offsetLevel =
266 new IndentLevel(indentLevel, getBasicOffset());
267 for (int i = startLine + 1; i <= endLine; i++) {
268 checkSingleLine(i, offsetLevel);
269 }
270 }
271
272 /**
273 * @return true if indentation should be increased after
274 * fisrt line in checkLinesIndent()
275 * false otherwise
276 */
277 protected boolean shouldIncreaseIndent()
278 {
279 return true;
280 }
281
282 /**
283 * Check the indentation for a set of lines.
284 *
285 * @param lines the set of lines to check
286 * @param indentLevel the indentation level
287 * @param firstLineMatches whether or not the first line has to match
288 * @param firstLine firstline of whole expression
289 */
290 private void checkLinesIndent(LineSet lines,
291 IndentLevel indentLevel,
292 boolean firstLineMatches,
293 int firstLine)
294 {
295 if (lines.isEmpty()) {
296 return;
297 }
298
299 // check first line
300 final int startLine = lines.firstLine();
301 final int endLine = lines.lastLine();
302 final int startCol = lines.firstLineCol();
303
304 final int realStartCol =
305 getLineStart(indentCheck.getLine(startLine - 1));
306
307 if (realStartCol == startCol) {
308 checkSingleLine(startLine, startCol, indentLevel,
309 firstLineMatches);
310 }
311
312 // if first line starts the line, following lines are indented
313 // one level; but if the first line of this expression is
314 // nested with the previous expression (which is assumed if it
315 // doesn't start the line) then don't indent more, the first
316 // indentation is absorbed by the nesting
317
318 IndentLevel theLevel = indentLevel;
319 if (firstLineMatches
320 || firstLine > mainAst.getLineNo() && shouldIncreaseIndent())
321 {
322 theLevel = new IndentLevel(indentLevel, getBasicOffset());
323 }
324
325 // check following lines
326 for (int i = startLine + 1; i <= endLine; i++) {
327 final Integer col = lines.getStartColumn(i);
328 // startCol could be null if this line didn't have an
329 // expression that was required to be checked (it could be
330 // checked by a child expression)
331
332 if (col != null) {
333 checkSingleLine(i, col.intValue(), theLevel, false);
334 }
335 }
336 }
337
338 /**
339 * Check the indent level for a single line.
340 *
341 * @param lineNum the line number to check
342 * @param indentLevel the required indent level
343 */
344 private void checkSingleLine(int lineNum, IndentLevel indentLevel)
345 {
346 final String line = indentCheck.getLine(lineNum - 1);
347 final int start = getLineStart(line);
348 if (indentLevel.gt(start)) {
349 logChildError(lineNum, start, indentLevel);
350 }
351 }
352
353 /**
354 * Check the indentation for a single line.
355 *
356 * @param lineNum the number of the line to check
357 * @param colNum the column number we are starting at
358 * @param indentLevel the indentation level
359 * @param mustMatch whether or not the indentation level must match
360 */
361
362 private void checkSingleLine(int lineNum, int colNum,
363 IndentLevel indentLevel, boolean mustMatch)
364 {
365 final String line = indentCheck.getLine(lineNum - 1);
366 final int start = getLineStart(line);
367 // if must match is set, it is an error if the line start is not
368 // at the correct indention level; otherwise, it is an only an
369 // error if this statement starts the line and it is less than
370 // the correct indentation level
371 if (mustMatch ? !indentLevel.accept(start)
372 : colNum == start && indentLevel.gt(start))
373 {
374 logChildError(lineNum, start, indentLevel);
375 }
376 }
377
378 /**
379 * Get the start of the specified line.
380 *
381 * @param line the specified line number
382 *
383 * @return the start of the specified line
384 */
385 protected final int getLineStart(String line)
386 {
387 for (int start = 0; start < line.length(); start++) {
388 final char c = line.charAt(start);
389
390 if (!Character.isWhitespace(c)) {
391 return Utils.lengthExpandedTabs(
392 line, start, indentCheck.getIndentationTabWidth());
393 }
394 }
395 return 0;
396 }
397
398 /**
399 * Check the indent level of the children of the specified parent
400 * expression.
401 *
402 * @param parent the parent whose children we are checking
403 * @param tokenTypes the token types to check
404 * @param startLevel the starting indent level
405 * @param firstLineMatches whether or not the first line needs to match
406 * @param allowNesting whether or not nested children are allowed
407 */
408 protected final void checkChildren(DetailAST parent,
409 int[] tokenTypes,
410 IndentLevel startLevel,
411 boolean firstLineMatches,
412 boolean allowNesting)
413 {
414 Arrays.sort(tokenTypes);
415 for (DetailAST child = parent.getFirstChild();
416 child != null;
417 child = child.getNextSibling())
418 {
419 if (Arrays.binarySearch(tokenTypes, child.getType()) >= 0) {
420 checkExpressionSubtree(child, startLevel,
421 firstLineMatches, allowNesting);
422 }
423 }
424 }
425
426 /**
427 * Check the indentation level for an expression subtree.
428 *
429 * @param tree the expression subtree to check
430 * @param level the indentation level
431 * @param firstLineMatches whether or not the first line has to match
432 * @param allowNesting whether or not subtree nesting is allowed
433 */
434 protected final void checkExpressionSubtree(
435 DetailAST tree,
436 IndentLevel level,
437 boolean firstLineMatches,
438 boolean allowNesting
439 )
440 {
441 final LineSet subtreeLines = new LineSet();
442 final int firstLine = getFirstLine(Integer.MAX_VALUE, tree);
443 if (firstLineMatches && !allowNesting) {
444 subtreeLines.addLineAndCol(firstLine,
445 getLineStart(indentCheck.getLine(firstLine - 1)));
446 }
447 findSubtreeLines(subtreeLines, tree, allowNesting);
448
449 checkLinesIndent(subtreeLines, level, firstLineMatches, firstLine);
450 }
451
452 /**
453 * Get the first line for a given expression.
454 *
455 * @param startLine the line we are starting from
456 * @param tree the expression to find the first line for
457 *
458 * @return the first line of the expression
459 */
460 protected final int getFirstLine(int startLine, DetailAST tree)
461 {
462 int realStart = startLine;
463 final int currLine = tree.getLineNo();
464 if (currLine < realStart) {
465 realStart = currLine;
466 }
467
468 // check children
469 for (DetailAST node = tree.getFirstChild();
470 node != null;
471 node = node.getNextSibling())
472 {
473 realStart = getFirstLine(realStart, node);
474 }
475
476 return realStart;
477 }
478
479 /**
480 * Get the column number for the start of a given expression, expanding
481 * tabs out into spaces in the process.
482 *
483 * @param ast the expression to find the start of
484 *
485 * @return the column number for the start of the expression
486 */
487 protected final int expandedTabsColumnNo(DetailAST ast)
488 {
489 final String line =
490 indentCheck.getLine(ast.getLineNo() - 1);
491
492 return Utils.lengthExpandedTabs(line, ast.getColumnNo(),
493 indentCheck.getIndentationTabWidth());
494 }
495
496 /**
497 * Find the set of lines for a given subtree.
498 *
499 * @param lines the set of lines to add to
500 * @param tree the subtree to examine
501 * @param allowNesting whether or not to allow nested subtrees
502 */
503 protected final void findSubtreeLines(LineSet lines, DetailAST tree,
504 boolean allowNesting)
505 {
506 if (getIndentCheck().getHandlerFactory().isHandledType(tree.getType())
507 || tree.getLineNo() < 0)
508 {
509 return;
510 }
511
512 final int lineNum = tree.getLineNo();
513 final Integer colNum = lines.getStartColumn(lineNum);
514
515 final int thisLineColumn = expandedTabsColumnNo(tree);
516 if (colNum == null || thisLineColumn < colNum.intValue()) {
517 lines.addLineAndCol(lineNum, thisLineColumn);
518 }
519
520 // check children
521 for (DetailAST node = tree.getFirstChild();
522 node != null;
523 node = node.getNextSibling())
524 {
525 findSubtreeLines(lines, node, allowNesting);
526 }
527 }
528
529 /**
530 * Check the indentation level of modifiers.
531 */
532 protected void checkModifiers()
533 {
534 final DetailAST modifiers =
535 mainAst.findFirstToken(TokenTypes.MODIFIERS);
536 for (DetailAST modifier = modifiers.getFirstChild();
537 modifier != null;
538 modifier = modifier.getNextSibling())
539 {
540 if (startsLine(modifier)
541 && !getLevel().accept(expandedTabsColumnNo(modifier)))
542 {
543 logError(modifier, "modifier",
544 expandedTabsColumnNo(modifier));
545 }
546 }
547 }
548
549 /**
550 * Check the indentation of the expression we are handling.
551 */
552 public abstract void checkIndentation();
553
554 /**
555 * Accessor for the IndentCheck attribute.
556 *
557 * @return the IndentCheck attribute
558 */
559 protected final IndentationCheck getIndentCheck()
560 {
561 return indentCheck;
562 }
563
564 /**
565 * Accessor for the MainAst attribute.
566 *
567 * @return the MainAst attribute
568 */
569 protected final DetailAST getMainAst()
570 {
571 return mainAst;
572 }
573
574 /**
575 * Accessor for the Parent attribute.
576 *
577 * @return the Parent attribute
578 */
579 protected final ExpressionHandler getParent()
580 {
581 return parent;
582 }
583
584 /**
585 * A shortcut for <code>IndentationCheck</code> property.
586 * @return value of basicOffset property of <code>IndentationCheck</code>
587 */
588 protected final int getBasicOffset()
589 {
590 return getIndentCheck().getBasicOffset();
591 }
592
593 /**
594 * A shortcut for <code>IndentationCheck</code> property.
595 * @return value of braceAdjustment property
596 * of <code>IndentationCheck</code>
597 */
598 protected final int getBraceAdjustement()
599 {
600 return getIndentCheck().getBraceAdjustement();
601 }
602
603 /**
604 * Check the indentation of the right parenthesis.
605 * @param rparen parenthesis to check
606 * @param lparen left parenthesis associated with aRparen
607 */
608 protected final void checkRParen(DetailAST lparen, DetailAST rparen)
609 {
610 // no paren - no check :)
611 if (rparen == null) {
612 return;
613 }
614
615 // the rcurly can either be at the correct indentation,
616 // or not first on the line ...
617 final int rparenLevel = expandedTabsColumnNo(rparen);
618 if (getLevel().accept(rparenLevel) || !startsLine(rparen)) {
619 return;
620 }
621
622 // or has <lparen level> + 1 indentation
623 final int lparenLevel = expandedTabsColumnNo(lparen);
624 if (rparenLevel == lparenLevel + 1) {
625 return;
626 }
627
628 logError(rparen, "rparen", rparenLevel);
629 }
630
631 /**
632 * Check the indentation of the left parenthesis.
633 * @param lparen parenthesis to check
634 */
635 protected final void checkLParen(final DetailAST lparen)
636 {
637 // the rcurly can either be at the correct indentation, or on the
638 // same line as the lcurly
639 if (lparen == null
640 || getLevel().accept(expandedTabsColumnNo(lparen))
641 || !startsLine(lparen))
642 {
643 return;
644 }
645 logError(lparen, "lparen", expandedTabsColumnNo(lparen));
646 }
647 }