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 at the number of columns to the right of the slash ( / ) of the
038 * opening Javadoc tag specified by the {@code indentation} property. By default they are
039 * aligned under the first asterisk ( &#42; )
040 * of opening Javadoc tag. The alignment of closing Javadoc tag ( &#42;/ ) is also checked.
041 * If a closing Javadoc tag contains non-whitespace character before it
042 * then it's alignment will be ignored.
043 * If the ending javadoc line contains a leading asterisk, then that leading asterisk's alignment
044 * will be considered, the closing Javadoc tag will be ignored.
045 * If the opening Javadoc tag line contains leading asterisks, then that line defines
046 * the expected alignment and the {@code indentation} property is not applied.
047 * </div>
048 *
049 * <p>
050 * If you're using tabs then specify the the tab width in the
051 * <a href="https://checkstyle.org/config.html#tabWidth">tabWidth</a> property.
052 * </p>
053 *
054 * @since 10.18.0
055 */
056@GlobalStatefulCheck
057public class JavadocLeadingAsteriskAlignCheck extends AbstractJavadocCheck {
058
059    /**
060     * A key is pointing to the warning message text in "messages.properties" file.
061     */
062    public static final String MSG_KEY = "javadoc.asterisk.indentation";
063
064    /** Default indentation of leading asterisks. */
065    private static final int DEFAULT_INDENTATION = 1;
066
067    /** Specifies the line number of starting block of the javadoc comment. */
068    private int javadocStartLineNumber;
069
070    /** Specifies the column number of the slash of the opening javadoc tag with tabs expanded. */
071    private int javadocStartColumnNumberTabsExpanded;
072
073    /** Specifies the expected column number of leading asterisks with tabs expanded. */
074    private int expectedColumnNumberTabsExpanded;
075
076    /** Specifies the lines of the file being processed. */
077    private String[] fileLines;
078
079    /**
080     * Specify the number of columns to the right of the slash ( / ) of the opening
081     * javadoc tag at which the leading asterisks are expected. A negative value
082     * expects the leading asterisks to the left of the slash ( / ).
083     */
084    private int indentation = DEFAULT_INDENTATION;
085
086    /**
087     * Creates a new {@code JavadocLeadingAsteriskAlignCheck} instance.
088     */
089    public JavadocLeadingAsteriskAlignCheck() {
090        // no code by default
091    }
092
093    /**
094     * Setter to specify the number of columns to the right of the slash ( / ) of the
095     * opening javadoc tag at which the leading asterisks are expected. A negative value
096     * expects the leading asterisks to the left of the slash ( / ).
097     *
098     * @param indentation number of columns
099     * @since 14.4.0
100     */
101    public void setIndentation(int indentation) {
102        this.indentation = indentation;
103    }
104
105    @Override
106    public int[] getDefaultJavadocTokens() {
107        return new int[] {
108            JavadocCommentsTokenTypes.LEADING_ASTERISK,
109            JavadocCommentsTokenTypes.LEADING_ASTERISKS,
110        };
111    }
112
113    @Override
114    public int[] getRequiredJavadocTokens() {
115        return getAcceptableJavadocTokens();
116    }
117
118    @Override
119    public void beginJavadocTree(DetailNode rootAst) {
120        // this method processes and sets information of starting javadoc tag.
121        fileLines = getLines();
122        final String startLine = fileLines[rootAst.getLineNumber() - 1];
123        javadocStartLineNumber = rootAst.getLineNumber();
124        javadocStartColumnNumberTabsExpanded = 1 + CommonUtil.lengthExpandedTabs(
125            startLine, getBlockCommentAst().getColumnNo(), getTabWidth());
126        final int columnNumberBeforeJavadocContent = CommonUtil.lengthExpandedTabs(
127            startLine, rootAst.getColumnNumber() - 1, getTabWidth());
128
129        // Javadoc content starts right after the opening tag, so the column before it is
130        // the first asterisk of that tag. Any other column means that the opening line
131        // carries leading asterisks, which define the alignment on their own.
132        final boolean isAlignmentDefinedOnOpeningLine = columnNumberBeforeJavadocContent
133                != javadocStartColumnNumberTabsExpanded + 1;
134
135        if (isAlignmentDefinedOnOpeningLine) {
136            expectedColumnNumberTabsExpanded = columnNumberBeforeJavadocContent;
137        }
138        else {
139            expectedColumnNumberTabsExpanded =
140                    javadocStartColumnNumberTabsExpanded + indentation;
141        }
142    }
143
144    @Override
145    public void visitJavadocToken(DetailNode ast) {
146        // this method checks the alignment of leading asterisks.
147        final boolean isJavadocOpeningLine = ast.getLineNumber() == javadocStartLineNumber;
148
149        if (isJavadocOpeningLine) {
150            if (ast.getType() == JavadocCommentsTokenTypes.LEADING_ASTERISK) {
151                final int previousColumn = ast.getColumnNumber() - 1;
152                if (Character.isWhitespace(
153                        fileLines[ast.getLineNumber() - 1].charAt(previousColumn))) {
154                    expectedColumnNumberTabsExpanded = getColumnNumberTabsExpanded(ast);
155                }
156            }
157        }
158        else {
159            final int columnNumberTabsExpanded = getColumnNumberTabsExpanded(ast);
160
161            if (!hasValidAlignment(expectedColumnNumberTabsExpanded, columnNumberTabsExpanded)) {
162                log(ast, MSG_KEY, getIndentationLevel(columnNumberTabsExpanded),
163                        getIndentationLevel(expectedColumnNumberTabsExpanded));
164            }
165        }
166    }
167
168    @Override
169    public void finishJavadocTree(DetailNode rootAst) {
170        // this method checks the alignment of closing javadoc tag.
171        final DetailAST javadocEndToken = getBlockCommentAst().getLastChild();
172        final String lastLine = fileLines[javadocEndToken.getLineNo() - 1];
173        final Optional<Integer> endingBlockColumnNumber = getAsteriskColumnNumber(lastLine);
174
175        endingBlockColumnNumber
176                .filter(columnNumber -> columnNumber - 1 == javadocEndToken.getColumnNo())
177                .ifPresent(columnNumber -> {
178                    final int columnNumberTabsExpanded = CommonUtil.lengthExpandedTabs(
179                            lastLine, columnNumber, getTabWidth());
180                    if (!hasValidAlignment(
181                            expectedColumnNumberTabsExpanded, columnNumberTabsExpanded)) {
182                        log(javadocEndToken, MSG_KEY,
183                                getIndentationLevel(columnNumberTabsExpanded),
184                                getIndentationLevel(expectedColumnNumberTabsExpanded));
185                    }
186                });
187    }
188
189    /**
190     * Processes and returns an OptionalInt containing
191     * the column number of leading asterisk without tabs expanded.
192     *
193     * @param line javadoc comment line
194     * @return asterisk's column number
195     */
196    private static Optional<Integer> getAsteriskColumnNumber(String line) {
197        final Pattern pattern = Pattern.compile("^(\\s*)\\*");
198        final Matcher matcher = pattern.matcher(line);
199
200        // We may not always have a leading asterisk because a javadoc line can start with
201        // a non-whitespace character or the javadoc line can be empty.
202        // In such cases, there is no leading asterisk and Optional will be empty.
203        return Optional.of(matcher)
204                .filter(Matcher::find)
205                .map(matcherInstance -> matcherInstance.group(1))
206                .map(groupLength -> groupLength.length() + 1);
207    }
208
209    /**
210     * Returns the tab-expanded, one-based column number of the leading asterisk node.
211     *
212     * @param ast leading asterisk node
213     * @return tab-expanded column number
214     */
215    private int getColumnNumberTabsExpanded(DetailNode ast) {
216        return 1 + CommonUtil.lengthExpandedTabs(
217                fileLines[ast.getLineNumber() - 1],
218                ast.getColumnNumber(),
219                getTabWidth());
220    }
221
222    /**
223     * Converts a tab-expanded column number into an indentation level, which is
224     * the number of columns the given column is to the right of the slash ( / )
225     * of the opening javadoc tag.
226     *
227     * @param columnNumberTabsExpanded tab-expanded column number
228     * @return indentation level
229     */
230    private int getIndentationLevel(int columnNumberTabsExpanded) {
231        return columnNumberTabsExpanded - javadocStartColumnNumberTabsExpanded;
232    }
233
234    /**
235     * Checks the column difference between
236     * expected column number and leading asterisk column number.
237     *
238     * @param expectedColNumber column number of javadoc starting token
239     * @param asteriskColNumber column number of leading asterisk
240     * @return true if the asterisk is aligned properly, false otherwise
241     */
242    private static boolean hasValidAlignment(int expectedColNumber,
243                                             int asteriskColNumber) {
244        return expectedColNumber - asteriskColNumber == 0;
245    }
246
247}