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.javadoc;
021
022import java.util.Optional;
023import java.util.regex.Matcher;
024import java.util.regex.Pattern;
025
026import com.puppycrawl.tools.checkstyle.GlobalStatefulCheck;
027import com.puppycrawl.tools.checkstyle.api.DetailAST;
028import com.puppycrawl.tools.checkstyle.api.DetailNode;
029import com.puppycrawl.tools.checkstyle.api.JavadocCommentsTokenTypes;
030import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
031
032/**
033 * <div>
034 * Checks the alignment of
035 * <a href="https://docs.oracle.com/en/java/javase/14/docs/specs/javadoc/doc-comment-spec.html#leading-asterisks">
036 * leading asterisks</a> in a Javadoc comment. The Check ensures that leading asterisks
037 * are aligned vertically under the first asterisk ( &#42; )
038 * of opening Javadoc tag. The alignment of closing Javadoc tag ( &#42;/ ) is also checked.
039 * If a closing Javadoc tag contains non-whitespace character before it
040 * then it's alignment will be ignored.
041 * If the ending javadoc line contains a leading asterisk, then that leading asterisk's alignment
042 * will be considered, the closing Javadoc tag will be ignored.
043 * </div>
044 *
045 * <p>
046 * If you're using tabs then specify the the tab width in the
047 * <a href="https://checkstyle.org/config.html#tabWidth">tabWidth</a> property.
048 * </p>
049 *
050 * @since 10.18.0
051 */
052@GlobalStatefulCheck
053public class JavadocLeadingAsteriskAlignCheck extends AbstractJavadocCheck {
054
055    /**
056     * A key is pointing to the warning message text in "messages.properties"
057     * file.
058     */
059    public static final String MSG_KEY = "javadoc.asterisk.indentation";
060
061    /** Specifies the line number of starting block of the javadoc comment. */
062    private int javadocStartLineNumber;
063
064    /** Specifies the column number of starting block of the javadoc comment with tabs expanded. */
065    private int expectedColumnNumberTabsExpanded;
066
067    /** Specifies the lines of the file being processed. */
068    private String[] fileLines;
069
070    /**
071     * Creates a new {@code JavadocLeadingAsteriskAlignCheck} instance.
072     */
073    public JavadocLeadingAsteriskAlignCheck() {
074        // no code by default
075    }
076
077    @Override
078    public int[] getDefaultJavadocTokens() {
079        return new int[] {
080            JavadocCommentsTokenTypes.LEADING_ASTERISK,
081            JavadocCommentsTokenTypes.LEADING_ASTERISKS,
082        };
083    }
084
085    @Override
086    public int[] getRequiredJavadocTokens() {
087        return getAcceptableJavadocTokens();
088    }
089
090    @Override
091    public void beginJavadocTree(DetailNode rootAst) {
092        // this method processes and sets information of starting javadoc tag.
093        fileLines = getLines();
094        final String startLine = fileLines[rootAst.getLineNumber() - 1];
095        javadocStartLineNumber = rootAst.getLineNumber();
096        expectedColumnNumberTabsExpanded = CommonUtil.lengthExpandedTabs(
097            startLine, rootAst.getColumnNumber() - 1, getTabWidth());
098    }
099
100    @Override
101    public void visitJavadocToken(DetailNode ast) {
102        // this method checks the alignment of leading asterisks.
103        final boolean isJavadocOpeningLine = ast.getLineNumber() == javadocStartLineNumber;
104
105        if (isJavadocOpeningLine) {
106            if (ast.getType() == JavadocCommentsTokenTypes.LEADING_ASTERISK) {
107                final int previousColumn = ast.getColumnNumber() - 1;
108                if (Character.isWhitespace(
109                        fileLines[ast.getLineNumber() - 1].charAt(previousColumn))) {
110                    expectedColumnNumberTabsExpanded = getColumnNumberTabsExpanded(ast);
111                }
112            }
113        }
114        else {
115            final int columnNumberTabsExpanded = getColumnNumberTabsExpanded(ast);
116
117            if (!hasValidAlignment(expectedColumnNumberTabsExpanded, columnNumberTabsExpanded)) {
118                log(ast, MSG_KEY, columnNumberTabsExpanded, expectedColumnNumberTabsExpanded);
119            }
120        }
121    }
122
123    @Override
124    public void finishJavadocTree(DetailNode rootAst) {
125        // this method checks the alignment of closing javadoc tag.
126        final DetailAST javadocEndToken = getBlockCommentAst().getLastChild();
127        final String lastLine = fileLines[javadocEndToken.getLineNo() - 1];
128        final Optional<Integer> endingBlockColumnNumber = getAsteriskColumnNumber(lastLine);
129
130        endingBlockColumnNumber
131                .filter(columnNumber -> columnNumber - 1 == javadocEndToken.getColumnNo())
132                .ifPresent(columnNumber -> {
133                    final int columnNumberTabsExpanded = CommonUtil.lengthExpandedTabs(
134                            lastLine, columnNumber, getTabWidth());
135
136                    if (!hasValidAlignment(
137                            expectedColumnNumberTabsExpanded, columnNumberTabsExpanded)) {
138                        log(javadocEndToken, MSG_KEY,
139                                columnNumberTabsExpanded, expectedColumnNumberTabsExpanded);
140                    }
141                });
142    }
143
144    /**
145     * Processes and returns an OptionalInt containing
146     * the column number of leading asterisk without tabs expanded.
147     *
148     * @param line javadoc comment line
149     * @return asterisk's column number
150     */
151    private static Optional<Integer> getAsteriskColumnNumber(String line) {
152        final Pattern pattern = Pattern.compile("^(\\s*)\\*");
153        final Matcher matcher = pattern.matcher(line);
154
155        // We may not always have a leading asterisk because a javadoc line can start with
156        // a non-whitespace character or the javadoc line can be empty.
157        // In such cases, there is no leading asterisk and Optional will be empty.
158        return Optional.of(matcher)
159                .filter(Matcher::find)
160                .map(matcherInstance -> matcherInstance.group(1))
161                .map(groupLength -> groupLength.length() + 1);
162    }
163
164    /**
165     * Returns the tab-expanded, one-based column number of the leading asterisk node.
166     *
167     * @param ast leading asterisk node
168     * @return tab-expanded column number
169     */
170    private int getColumnNumberTabsExpanded(DetailNode ast) {
171        return 1 + CommonUtil.lengthExpandedTabs(
172                fileLines[ast.getLineNumber() - 1],
173                ast.getColumnNumber(),
174                getTabWidth());
175    }
176
177    /**
178     * Checks the column difference between
179     * expected column number and leading asterisk column number.
180     *
181     * @param expectedColNumber column number of javadoc starting token
182     * @param asteriskColNumber column number of leading asterisk
183     * @return true if the asterisk is aligned properly, false otherwise
184     */
185    private static boolean hasValidAlignment(int expectedColNumber,
186                                             int asteriskColNumber) {
187        return expectedColNumber - asteriskColNumber == 0;
188    }
189
190}