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        };
082    }
083
084    @Override
085    public int[] getRequiredJavadocTokens() {
086        return getAcceptableJavadocTokens();
087    }
088
089    @Override
090    public void beginJavadocTree(DetailNode rootAst) {
091        // this method processes and sets information of starting javadoc tag.
092        fileLines = getLines();
093        final String startLine = fileLines[rootAst.getLineNumber() - 1];
094        javadocStartLineNumber = rootAst.getLineNumber();
095        expectedColumnNumberTabsExpanded = CommonUtil.lengthExpandedTabs(
096            startLine, rootAst.getColumnNumber() - 1, getTabWidth());
097    }
098
099    @Override
100    public void visitJavadocToken(DetailNode ast) {
101        // this method checks the alignment of leading asterisks.
102        final boolean isJavadocStartingLine = ast.getLineNumber() == javadocStartLineNumber;
103
104        if (!isJavadocStartingLine) {
105            final Optional<Integer> leadingAsteriskColumnNumber =
106                                        getAsteriskColumnNumber(ast.getText());
107
108            leadingAsteriskColumnNumber.ifPresent(columnNumber -> {
109                final int columnNumberTabsExpanded = CommonUtil.lengthExpandedTabs(
110                        ast.getText(), columnNumber, getTabWidth());
111
112                if (!hasValidAlignment(
113                        expectedColumnNumberTabsExpanded, columnNumberTabsExpanded)) {
114                    log(ast.getLineNumber(), columnNumber - 1, MSG_KEY,
115                            columnNumberTabsExpanded, expectedColumnNumberTabsExpanded);
116                }
117            });
118        }
119    }
120
121    @Override
122    public void finishJavadocTree(DetailNode rootAst) {
123        // this method checks the alignment of closing javadoc tag.
124        final DetailAST javadocEndToken = getBlockCommentAst().getLastChild();
125        final String lastLine = fileLines[javadocEndToken.getLineNo() - 1];
126        final Optional<Integer> endingBlockColumnNumber = getAsteriskColumnNumber(lastLine);
127
128        endingBlockColumnNumber
129                .filter(columnNumber -> columnNumber - 1 == javadocEndToken.getColumnNo())
130                .ifPresent(columnNumber -> {
131                    final int columnNumberTabsExpanded = CommonUtil.lengthExpandedTabs(
132                            lastLine, columnNumber, getTabWidth());
133
134                    if (!hasValidAlignment(
135                            expectedColumnNumberTabsExpanded, columnNumberTabsExpanded)) {
136                        log(javadocEndToken, MSG_KEY,
137                                columnNumberTabsExpanded, expectedColumnNumberTabsExpanded);
138                    }
139                });
140    }
141
142    /**
143     * Processes and returns an OptionalInt containing
144     * the column number of leading asterisk without tabs expanded.
145     *
146     * @param line javadoc comment line
147     * @return asterisk's column number
148     */
149    private static Optional<Integer> getAsteriskColumnNumber(String line) {
150        final Pattern pattern = Pattern.compile("^(\\s*)\\*");
151        final Matcher matcher = pattern.matcher(line);
152
153        // We may not always have a leading asterisk because a javadoc line can start with
154        // a non-whitespace character or the javadoc line can be empty.
155        // In such cases, there is no leading asterisk and Optional will be empty.
156        return Optional.of(matcher)
157                .filter(Matcher::find)
158                .map(matcherInstance -> matcherInstance.group(1))
159                .map(groupLength -> groupLength.length() + 1);
160    }
161
162    /**
163     * Checks the column difference between
164     * expected column number and leading asterisk column number.
165     *
166     * @param expectedColNumber column number of javadoc starting token
167     * @param asteriskColNumber column number of leading asterisk
168     * @return true if the asterisk is aligned properly, false otherwise
169     */
170    private static boolean hasValidAlignment(int expectedColNumber,
171                                             int asteriskColNumber) {
172        return expectedColNumber - asteriskColNumber == 0;
173    }
174
175}