001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2025 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.JavadocTokenTypes;
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    /**
068     * Specifies the column number of the leading asterisk
069     * without tabs expanded.
070     */
071    private int expectedColumnNumberWithoutExpandedTabs;
072
073    /** Specifies the lines of the file being processed. */
074    private String[] fileLines;
075
076    @Override
077    public int[] getDefaultJavadocTokens() {
078        return new int[] {
079            JavadocTokenTypes.LEADING_ASTERISK,
080        };
081    }
082
083    @Override
084    public int[] getRequiredJavadocTokens() {
085        return getAcceptableJavadocTokens();
086    }
087
088    @Override
089    public void beginJavadocTree(DetailNode rootAst) {
090        // this method processes and sets information of starting javadoc tag.
091        fileLines = getLines();
092        final String startLine = fileLines[rootAst.getLineNumber() - 1];
093        javadocStartLineNumber = rootAst.getLineNumber();
094        expectedColumnNumberTabsExpanded = CommonUtil.lengthExpandedTabs(
095            startLine, rootAst.getColumnNumber() - 1, getTabWidth());
096    }
097
098    @Override
099    public void visitJavadocToken(DetailNode ast) {
100        // this method checks the alignment of leading asterisks.
101        final boolean isJavadocStartingLine = ast.getLineNumber() == javadocStartLineNumber;
102
103        if (!isJavadocStartingLine) {
104            final Optional<Integer> leadingAsteriskColumnNumber =
105                                        getAsteriskColumnNumber(ast.getText());
106
107            leadingAsteriskColumnNumber
108                    .map(columnNumber -> expandedTabs(ast.getText(), columnNumber))
109                    .filter(columnNumber -> {
110                        return !hasValidAlignment(expectedColumnNumberTabsExpanded, columnNumber);
111                    })
112                    .ifPresent(columnNumber -> {
113                        logViolation(ast.getLineNumber(),
114                                columnNumber,
115                                expectedColumnNumberTabsExpanded);
116                    });
117        }
118    }
119
120    @Override
121    public void finishJavadocTree(DetailNode rootAst) {
122        // this method checks the alignment of closing javadoc tag.
123        final DetailAST javadocEndToken = getBlockCommentAst().getLastChild();
124        final String lastLine = fileLines[javadocEndToken.getLineNo() - 1];
125        final Optional<Integer> endingBlockColumnNumber = getAsteriskColumnNumber(lastLine);
126
127        endingBlockColumnNumber
128                .map(columnNumber -> expandedTabs(lastLine, columnNumber))
129                .filter(columnNumber -> {
130                    return !hasValidAlignment(expectedColumnNumberTabsExpanded, columnNumber);
131                })
132                .ifPresent(columnNumber -> {
133                    logViolation(javadocEndToken.getLineNo(),
134                            columnNumber,
135                            expectedColumnNumberTabsExpanded);
136                });
137    }
138
139    /**
140     * Processes and returns the column number of
141     * leading asterisk with tabs expanded.
142     * Also sets 'expectedColumnNumberWithoutExpandedTabs' if the leading asterisk is present.
143     *
144     * @param line javadoc comment line
145     * @param columnNumber column number of leading asterisk
146     * @return column number of leading asterisk with tabs expanded
147     */
148    private int expandedTabs(String line, int columnNumber) {
149        expectedColumnNumberWithoutExpandedTabs = columnNumber - 1;
150        return CommonUtil.lengthExpandedTabs(
151                    line, columnNumber, getTabWidth());
152    }
153
154    /**
155     * Processes and returns an OptionalInt containing
156     * the column number of leading asterisk without tabs expanded.
157     *
158     * @param line javadoc comment line
159     * @return asterisk's column number
160     */
161    private static Optional<Integer> getAsteriskColumnNumber(String line) {
162        final Pattern pattern = Pattern.compile("^(\\s*)\\*");
163        final Matcher matcher = pattern.matcher(line);
164
165        // We may not always have a leading asterisk because a javadoc line can start with
166        // a non-whitespace character or the javadoc line can be empty.
167        // In such cases, there is no leading asterisk and Optional will be empty.
168        return Optional.of(matcher)
169                .filter(Matcher::find)
170                .map(matcherInstance -> matcherInstance.group(1))
171                .map(groupLength -> groupLength.length() + 1);
172    }
173
174    /**
175     * Checks alignment of asterisks and logs violations.
176     *
177     * @param lineNumber line number of current comment line
178     * @param asteriskColNumber column number of leading asterisk
179     * @param expectedColNumber column number of javadoc starting token
180     */
181    private void logViolation(int lineNumber,
182                              int asteriskColNumber,
183                              int expectedColNumber) {
184
185        log(lineNumber,
186            expectedColumnNumberWithoutExpandedTabs,
187            MSG_KEY,
188            asteriskColNumber,
189            expectedColNumber);
190    }
191
192    /**
193     * Checks the column difference between
194     * expected column number and leading asterisk column number.
195     *
196     * @param expectedColNumber column number of javadoc starting token
197     * @param asteriskColNumber column number of leading asterisk
198     * @return true if the asterisk is aligned properly, false otherwise
199     */
200    private static boolean hasValidAlignment(int expectedColNumber,
201                                             int asteriskColNumber) {
202        return expectedColNumber - asteriskColNumber == 0;
203    }
204}