001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2026 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.checks.indentation;
021
022import java.util.Arrays;
023
024import com.puppycrawl.tools.checkstyle.api.DetailAST;
025import com.puppycrawl.tools.checkstyle.api.TokenTypes;
026import com.puppycrawl.tools.checkstyle.utils.AnnotationUtil;
027import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
028import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
029
030/**
031 * Abstract base class for all handlers.
032 *
033 */
034public abstract class AbstractExpressionHandler {
035
036    /**
037     * The indentation context providing config values, source lines, and
038     * violation reporting without a direct reference to the check.
039     */
040    private final IndentationContext context;
041
042    /** The AST which is handled by this handler. */
043    private final DetailAST mainAst;
044
045    /** Name used during output to user. */
046    private final String typeName;
047
048    /** Containing AST handler. */
049    private final AbstractExpressionHandler parent;
050
051    /** Indentation amount for this handler. */
052    private IndentLevel indent;
053
054    /**
055     * Construct an instance of this handler with the given indentation context,
056     * name, abstract syntax tree, and parent handler.
057     *
058     * @param context   the indentation context
059     * @param typeName  the name of the handler
060     * @param expr      the abstract syntax tree
061     * @param parent    the parent handler
062     */
063    protected AbstractExpressionHandler(IndentationContext context, String typeName,
064            DetailAST expr, AbstractExpressionHandler parent) {
065        this.context = context;
066        this.typeName = typeName;
067        mainAst = expr;
068        this.parent = parent;
069    }
070
071    /**
072     * Check the indentation of the expression we are handling.
073     */
074    public abstract void checkIndentation();
075
076    /**
077     * Get the indentation amount for this handler. For performance reasons,
078     * this value is cached. The first time this method is called, the
079     * indentation amount is computed and stored. On further calls, the stored
080     * value is returned.
081     *
082     * @return the expected indentation amount
083     * @noinspection WeakerAccess
084     * @noinspectionreason WeakerAccess - we avoid 'protected' when possible
085     */
086    public final IndentLevel getIndent() {
087        if (indent == null) {
088            indent = getIndentImpl();
089        }
090        return indent;
091    }
092
093    /**
094     * Compute the indentation amount for this handler.
095     *
096     * @return the expected indentation amount
097     */
098    protected IndentLevel getIndentImpl() {
099        return parent.getSuggestedChildIndent(this);
100    }
101
102    /**
103     * Indentation level suggested for a child element. Children don't have
104     * to respect this, but most do.
105     *
106     * @param child  child AST (so suggestion level can differ based on child
107     *                  type)
108     *
109     * @return suggested indentation for child
110     * @noinspection WeakerAccess
111     * @noinspectionreason WeakerAccess - we avoid 'protected' when possible
112     */
113    public IndentLevel getSuggestedChildIndent(AbstractExpressionHandler child) {
114        return new IndentLevel(getIndent(), getBasicOffset());
115    }
116
117    /**
118     * Log an indentation error.
119     *
120     * @param ast           the expression that caused the error
121     * @param subtypeName   the type of the expression
122     * @param actualIndent  the actual indent level of the expression
123     */
124    protected final void logError(DetailAST ast, String subtypeName,
125                                  int actualIndent) {
126        logError(ast, subtypeName, actualIndent, getIndent());
127    }
128
129    /**
130     * Log an indentation error.
131     *
132     * @param ast            the expression that caused the error
133     * @param subtypeName    the type of the expression
134     * @param actualIndent   the actual indent level of the expression
135     * @param expectedIndent the expected indent level of the expression
136     */
137    protected final void logError(DetailAST ast, String subtypeName,
138                                  int actualIndent, IndentLevel expectedIndent) {
139        final String typeStr;
140
141        if (subtypeName.isEmpty()) {
142            typeStr = "";
143        }
144        else {
145            typeStr = " " + subtypeName;
146        }
147        String messageKey = IndentationContext.MSG_ERROR;
148        if (expectedIndent.isMultiLevel()) {
149            messageKey = IndentationContext.MSG_ERROR_MULTI;
150        }
151        context.indentationLog(ast, messageKey,
152            typeName + typeStr, actualIndent, expectedIndent);
153    }
154
155    /**
156     * Log child indentation error.
157     *
158     * @param ast            the abstract syntax tree that causes the error
159     * @param actualIndent   the actual indent level of the expression
160     * @param expectedIndent the expected indent level of the expression
161     */
162    protected final void logChildError(DetailAST ast,
163                                       int actualIndent,
164                                       IndentLevel expectedIndent) {
165        String messageKey = IndentationContext.MSG_CHILD_ERROR;
166        if (expectedIndent.isMultiLevel()) {
167            messageKey = IndentationContext.MSG_CHILD_ERROR_MULTI;
168        }
169        context.indentationLog(ast, messageKey,
170            typeName, actualIndent, expectedIndent);
171    }
172
173    /**
174     * Determines if the given expression is at the start of a line.
175     *
176     * @param ast   the expression to check
177     *
178     * @return true if it is, false otherwise
179     */
180    protected final boolean isOnStartOfLine(DetailAST ast) {
181        return getLineStart(ast) == expandedTabsColumnNo(ast);
182    }
183
184    /**
185     * Searches in given subtree (including given node) for the token
186     * which represents first symbol for this subtree in file.
187     *
188     * @param ast a root of subtree in which the search should be performed.
189     * @return a token which occurs first in the file.
190     * @noinspection WeakerAccess
191     * @noinspectionreason WeakerAccess - we avoid 'protected' when possible
192     */
193    public static DetailAST getFirstToken(DetailAST ast) {
194        DetailAST first = ast;
195        DetailAST child = ast.getFirstChild();
196
197        while (child != null) {
198            final DetailAST toTest = getFirstToken(child);
199            if (toTest.getColumnNo() < first.getColumnNo()) {
200                first = toTest;
201            }
202            child = child.getNextSibling();
203        }
204
205        return first;
206    }
207
208    /**
209     * Get the start of the line for the given expression.
210     *
211     * @param ast   the expression to find the start of the line for
212     *
213     * @return the start of the line for the given expression
214     */
215    protected final int getLineStart(DetailAST ast) {
216        return getLineStart(ast.getLineNo());
217    }
218
219    /**
220     * Get the start of the line for the given line number.
221     *
222     * @param lineNo   the line number to find the start for
223     *
224     * @return the start of the line for the given expression
225     */
226    protected final int getLineStart(int lineNo) {
227        return getLineStart(context.getLine(lineNo - 1));
228    }
229
230    /**
231     * Get the start of the specified line.
232     *
233     * @param line   the specified line number
234     *
235     * @return the start of the specified line
236     */
237    private int getLineStart(String line) {
238        int index = 0;
239        while (Character.isWhitespace(line.charAt(index))) {
240            index++;
241        }
242        return CommonUtil.lengthExpandedTabs(
243            line, index, context.getIndentationTabWidth());
244    }
245
246    /**
247     * Checks that indentation should be increased after first line in checkLinesIndent().
248     *
249     * @return true if indentation should be increased after
250     *              first line in checkLinesIndent()
251     *         false otherwise
252     */
253    protected boolean shouldIncreaseIndent() {
254        boolean result = true;
255        if (TokenUtil.isOfType(mainAst, TokenTypes.LITERAL_CATCH)) {
256            final DetailAST parameterAst = mainAst.findFirstToken(TokenTypes.PARAMETER_DEF);
257            result = !AnnotationUtil.containsAnnotation(parameterAst);
258        }
259        return result;
260    }
261
262    /**
263     * Check the indentation for a set of lines.
264     *
265     * @param astSet             the set of abstract syntax tree to check
266     * @param indentLevel        the indentation level
267     * @param firstLineMatches   whether or not the first line has to match
268     * @param firstLine          first line of whole expression
269     * @param allowNesting       whether or not subtree nesting is allowed
270     */
271    private void checkLinesIndent(DetailAstSet astSet,
272                                  IndentLevel indentLevel,
273                                  boolean firstLineMatches,
274                                  int firstLine,
275                                  boolean allowNesting) {
276        if (!astSet.isEmpty()) {
277            // check first line
278            final DetailAST startLineAst = astSet.firstLine();
279            int startCol = expandedTabsColumnNo(startLineAst);
280
281            final int realStartCol =
282                getLineStart(context.getLine(startLineAst.getLineNo() - 1));
283
284            if (firstLineMatches && !allowNesting) {
285                startCol = realStartCol;
286            }
287
288            if (realStartCol == startCol) {
289                checkLineIndent(startLineAst, indentLevel,
290                    firstLineMatches);
291            }
292
293            checkRemainingLines(firstLineMatches, indentLevel, firstLine, astSet);
294
295        }
296    }
297
298    /**
299     * Check the indentation of remaining lines present in the astSet.
300     *
301     * @param firstLineMatches   whether or not the first line has to match
302     * @param indentLevel        the indentation level
303     * @param firstLine          first line of whole expression
304     * @param astSet             the set of abstract syntax tree to check
305     */
306    private void checkRemainingLines(boolean firstLineMatches,
307                                     IndentLevel indentLevel,
308                                     int firstLine,
309                                     DetailAstSet astSet) {
310        // if first line starts the line, following lines are indented
311        // one level; but if the first line of this expression is
312        // nested with the previous expression (which is assumed if it
313        // doesn't start the line) then don't indent more, the first
314        // indentation is absorbed by the nesting
315        final DetailAST startLineAst = astSet.firstLine();
316        final int endLine = astSet.lastLine();
317        IndentLevel level = indentLevel;
318
319        if (shouldIncreaseIndent()
320                && startLineAst.getType() != TokenTypes.ANNOTATION
321                && (firstLineMatches || firstLine > mainAst.getLineNo())) {
322            level = new IndentLevel(indentLevel,
323                    context.getLineWrappingIndentation());
324        }
325
326        // check following lines
327        for (int index = startLineAst.getLineNo() + 1; index <= endLine; index++) {
328            final Integer col = astSet.getStartColumn(index);
329            // startCol could be null if this line didn't have an
330            // expression that was required to be checked (it could be
331            // checked by a child expression)
332
333            if (col != null) {
334                final DetailAST ast = astSet.getAst(index);
335                final boolean textBlockEndAligned =
336                    ast.getType() == TokenTypes.TEXT_BLOCK_LITERAL_END
337                        && isOnStartOfLine(ast.getParent())
338                        && indentLevel.isAcceptable(expandedTabsColumnNo(ast));
339                if (!textBlockEndAligned) {
340                    checkLineIndent(ast, level, false);
341                }
342            }
343        }
344    }
345
346    /**
347     * Check the indentation for a single-line.
348     *
349     * @param ast           the abstract syntax tree to check
350     * @param indentLevel   the indentation level
351     * @param mustMatch     whether or not the indentation level must match
352     */
353    private void checkLineIndent(DetailAST ast,
354        IndentLevel indentLevel, boolean mustMatch) {
355        final String line = context.getLine(ast.getLineNo() - 1);
356        final int start = getLineStart(line);
357        final int columnNumber = expandedTabsColumnNo(ast);
358        // if must match is set, it is a violation if the line start is not
359        // at the correct indention level; otherwise, it is an only a
360        // violation if this statement starts the line and it is less than
361        // the correct indentation level
362        if (mustMatch && !indentLevel.isAcceptable(start)
363                || !mustMatch && columnNumber == start && indentLevel.isGreaterThan(start)) {
364            logChildError(ast, start, indentLevel);
365        }
366    }
367
368    /**
369     * Checks indentation on wrapped lines between and including
370     * {@code firstNode} and {@code lastNode}.
371     *
372     * @param firstNode First node to start examining.
373     * @param lastNode Last node to examine inclusively.
374     */
375    protected void checkWrappingIndentation(DetailAST firstNode, DetailAST lastNode) {
376        context.getLineWrappingHandler().checkIndentation(firstNode, lastNode);
377    }
378
379    /**
380     * Checks indentation on wrapped lines between and including
381     * {@code firstNode} and {@code lastNode}.
382     *
383     * @param firstNode First node to start examining.
384     * @param lastNode Last node to examine inclusively.
385     * @param wrappedIndentLevel Indentation all wrapped lines should use.
386     * @param startIndent Indentation first line before wrapped lines used.
387     * @param ignoreFirstLine Test if first line's indentation should be checked or not.
388     */
389    protected void checkWrappingIndentation(DetailAST firstNode, DetailAST lastNode,
390            int wrappedIndentLevel, int startIndent,
391            LineWrappingHandler.LineWrappingOptions ignoreFirstLine) {
392        context.getLineWrappingHandler().checkIndentation(firstNode, lastNode,
393                wrappedIndentLevel, startIndent, ignoreFirstLine);
394    }
395
396    /**
397     * Check the indent level of the children of the specified parent
398     * expression.
399     *
400     * @param parentNode         the parent whose children we are checking
401     * @param tokenTypes         the token types to check
402     * @param startIndent        the starting indent level
403     * @param firstLineMatches   whether or not the first line needs to match
404     * @param allowNesting       whether or not nested children are allowed
405     */
406    protected final void checkChildren(DetailAST parentNode,
407                                       int[] tokenTypes,
408                                       IndentLevel startIndent,
409                                       boolean firstLineMatches,
410                                       boolean allowNesting) {
411        Arrays.sort(tokenTypes);
412        for (DetailAST child = parentNode.getFirstChild();
413                child != null;
414                child = child.getNextSibling()) {
415            if (Arrays.binarySearch(tokenTypes, child.getType()) >= 0
416                    && shouldCheckIndentationForChild(child)) {
417                checkExpressionSubtree(child, startIndent,
418                    firstLineMatches, allowNesting);
419            }
420        }
421    }
422
423    /**
424     * Decide whether to check indentation for a specific child.
425     *
426     * @param child child AST node
427     * @return true if indentation should be checked
428     */
429    protected boolean shouldCheckIndentationForChild(DetailAST child) {
430        return true;
431    }
432
433    /**
434     * Check the indentation level for an expression subtree.
435     *
436     * @param tree               the expression subtree to check
437     * @param indentLevel        the indentation level
438     * @param firstLineMatches   whether or not the first line has to match
439     * @param allowNesting       whether or not subtree nesting is allowed
440     */
441    protected final void checkExpressionSubtree(
442        DetailAST tree,
443        IndentLevel indentLevel,
444        boolean firstLineMatches,
445        boolean allowNesting
446    ) {
447        final DetailAstSet subtreeAst = new DetailAstSet(context);
448        final int firstLine = getFirstLine(tree);
449        if (firstLineMatches && !allowNesting) {
450            final DetailAST firstAst = getFirstAstNode(tree);
451            subtreeAst.addAst(firstAst);
452        }
453        findSubtreeAst(subtreeAst, tree, allowNesting);
454
455        checkLinesIndent(subtreeAst, indentLevel, firstLineMatches, firstLine, allowNesting);
456    }
457
458    /**
459     * Get the first line number for given expression.
460     *
461     * @param tree      the expression to find the first line for
462     * @return          the first line of expression
463     */
464    protected static int getFirstLine(DetailAST tree) {
465        return getFirstAstNode(tree).getLineNo();
466    }
467
468    /**
469     * Get the first ast for given expression.
470     *
471     * @param ast         the abstract syntax tree for which the starting ast is to be found
472     *
473     * @return            the first ast of the expression
474     */
475    protected static DetailAST getFirstAstNode(DetailAST ast) {
476
477        DetailAST curNode = ast;
478        DetailAST realStart = ast;
479        while (curNode != null) {
480            if (curNode.getLineNo() < realStart.getLineNo()
481                    || curNode.getLineNo() == realStart.getLineNo()
482                    && Math.min(curNode.getColumnNo(), realStart.getColumnNo())
483                    == curNode.getColumnNo()) {
484                realStart = curNode;
485            }
486            DetailAST toVisit = curNode.getFirstChild();
487            while (curNode != ast && toVisit == null) {
488                toVisit = curNode.getNextSibling();
489                curNode = curNode.getParent();
490            }
491            curNode = toVisit;
492        }
493        return realStart;
494    }
495
496    /**
497     * Get the column number for the start of a given expression, expanding
498     * tabs out into spaces in the process.
499     *
500     * @param ast   the expression to find the start of
501     *
502     * @return the column number for the start of the expression
503     */
504    protected final int expandedTabsColumnNo(DetailAST ast) {
505        final String line =
506            context.getLine(ast.getLineNo() - 1);
507
508        return CommonUtil.lengthExpandedTabs(line, ast.getColumnNo(),
509            context.getIndentationTabWidth());
510    }
511
512    /**
513     * Find the set of abstract syntax tree for a given subtree.
514     *
515     * @param astSet         the set of ast to add
516     * @param tree           the subtree to examine
517     * @param allowNesting   whether or not to allow nested subtrees
518     */
519    protected final void findSubtreeAst(DetailAstSet astSet, DetailAST tree,
520        boolean allowNesting) {
521        if (!context.getHandlerFactory().isHandledType(tree.getType())) {
522            final int lineNum = tree.getLineNo();
523            final Integer colNum = astSet.getStartColumn(lineNum);
524
525            final int thisLineColumn = expandedTabsColumnNo(tree);
526            if (colNum == null || thisLineColumn < colNum) {
527                astSet.addAst(tree);
528            }
529
530            // check children
531            for (DetailAST node = tree.getFirstChild();
532                node != null;
533                node = node.getNextSibling()) {
534                findSubtreeAst(astSet, node, allowNesting);
535            }
536        }
537    }
538
539    /**
540     * Accessor for the indentation context.
541     *
542     * @return the indentation context
543     */
544    protected final IndentationContext getContext() {
545        return context;
546    }
547
548    /**
549     * Accessor for the MainAst attribute.
550     *
551     * @return the MainAst attribute
552     */
553    protected final DetailAST getMainAst() {
554        return mainAst;
555    }
556
557    /**
558     * Accessor for the Parent attribute.
559     *
560     * @return the Parent attribute
561     */
562    protected final AbstractExpressionHandler getParent() {
563        return parent;
564    }
565
566    /**
567     * A shortcut for {@code basicOffset} property.
568     *
569     * @return value of basicOffset property
570     */
571    protected final int getBasicOffset() {
572        return context.getBasicOffset();
573    }
574
575    /**
576     * A shortcut for {@code braceAdjustment} property.
577     *
578     * @return value of braceAdjustment property
579     */
580    protected final int getBraceAdjustment() {
581        return context.getBraceAdjustment();
582    }
583
584    /**
585     * Check the indentation of the right parenthesis.
586     *
587     * @param lparen left parenthesis associated with aRparen
588     * @param rparen parenthesis to check
589     */
590    protected final void checkRightParen(DetailAST lparen, DetailAST rparen) {
591        if (rparen != null) {
592            // the rcurly can either be at the correct indentation,
593            // or not first on the line
594            final int rparenLevel = expandedTabsColumnNo(rparen);
595            // or has <lparen level> + 1 indentation
596            final int lparenLevel = expandedTabsColumnNo(lparen);
597
598            if (rparenLevel != lparenLevel + 1
599                    && !getIndent().isAcceptable(rparenLevel)
600                    && isOnStartOfLine(rparen)) {
601                logError(rparen, "rparen", rparenLevel);
602            }
603        }
604    }
605
606    /**
607     * Check the indentation of the left parenthesis.
608     *
609     * @param lparen parenthesis to check
610     */
611    protected final void checkLeftParen(final DetailAST lparen) {
612        // the rcurly can either be at the correct indentation, or on the
613        // same line as the lcurly
614        if (lparen != null
615                && !getIndent().isAcceptable(expandedTabsColumnNo(lparen))
616                && isOnStartOfLine(lparen)) {
617            logError(lparen, "lparen", expandedTabsColumnNo(lparen));
618        }
619    }
620
621}