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