001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2024 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 * <ul>
050 * <li>
051 * Property {@code violateExecutionOnNonTightHtml} - Control when to print violations if the
052 * Javadoc being examined by this check violates the tight html rules defined at
053 * <a href="https://checkstyle.org/writingjavadocchecks.html#Tight-HTML_rules">Tight-HTML Rules</a>.
054 * Type is {@code boolean}.
055 * Default value is {@code false}.
056 * </li>
057 * </ul>
058 *
059 * <p>
060 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
061 * </p>
062 *
063 * <p>
064 * Violation Message Keys:
065 * </p>
066 * <ul>
067 * <li>
068 * {@code javadoc.asterisk.indentation}
069 * </li>
070 * <li>
071 * {@code javadoc.missed.html.close}
072 * </li>
073 * <li>
074 * {@code javadoc.parse.rule.error}
075 * </li>
076 * <li>
077 * {@code javadoc.unclosedHtml}
078 * </li>
079 * <li>
080 * {@code javadoc.wrong.singleton.html.tag}
081 * </li>
082 * </ul>
083 *
084 * @since 10.18.0
085 */
086@GlobalStatefulCheck
087public class JavadocLeadingAsteriskAlignCheck extends AbstractJavadocCheck {
088
089    /**
090     * A key is pointing to the warning message text in "messages.properties"
091     * file.
092     */
093    public static final String MSG_KEY = "javadoc.asterisk.indentation";
094
095    /** Specifies the line number of starting block of the javadoc comment. */
096    private int javadocStartLineNumber;
097
098    /** Specifies the column number of starting block of the javadoc comment with tabs expanded. */
099    private int expectedColumnNumberTabsExpanded;
100
101    /**
102     * Specifies the column number of the leading asterisk
103     * without tabs expanded.
104     */
105    private int expectedColumnNumberWithoutExpandedTabs;
106
107    /** Specifies the lines of the file being processed. */
108    private String[] fileLines;
109
110    @Override
111    public int[] getDefaultJavadocTokens() {
112        return new int[] {
113            JavadocTokenTypes.LEADING_ASTERISK,
114        };
115    }
116
117    @Override
118    public int[] getRequiredJavadocTokens() {
119        return getAcceptableJavadocTokens();
120    }
121
122    @Override
123    public void beginJavadocTree(DetailNode rootAst) {
124        // this method processes and sets information of starting javadoc tag.
125        fileLines = getLines();
126        final String startLine = fileLines[rootAst.getLineNumber() - 1];
127        javadocStartLineNumber = rootAst.getLineNumber();
128        expectedColumnNumberTabsExpanded = CommonUtil.lengthExpandedTabs(
129            startLine, rootAst.getColumnNumber() - 1, getTabWidth());
130    }
131
132    @Override
133    public void visitJavadocToken(DetailNode ast) {
134        // this method checks the alignment of leading asterisks.
135        final boolean isJavadocStartingLine = ast.getLineNumber() == javadocStartLineNumber;
136
137        if (!isJavadocStartingLine) {
138            final Optional<Integer> leadingAsteriskColumnNumber =
139                                        getAsteriskColumnNumber(ast.getText());
140
141            leadingAsteriskColumnNumber
142                    .map(columnNumber -> expandedTabs(ast.getText(), columnNumber))
143                    .filter(columnNumber -> {
144                        return !hasValidAlignment(expectedColumnNumberTabsExpanded, columnNumber);
145                    })
146                    .ifPresent(columnNumber -> {
147                        logViolation(ast.getLineNumber(),
148                                columnNumber,
149                                expectedColumnNumberTabsExpanded);
150                    });
151        }
152    }
153
154    @Override
155    public void finishJavadocTree(DetailNode rootAst) {
156        // this method checks the alignment of closing javadoc tag.
157        final DetailAST javadocEndToken = getBlockCommentAst().getLastChild();
158        final String lastLine = fileLines[javadocEndToken.getLineNo() - 1];
159        final Optional<Integer> endingBlockColumnNumber = getAsteriskColumnNumber(lastLine);
160
161        endingBlockColumnNumber
162                .map(columnNumber -> expandedTabs(lastLine, columnNumber))
163                .filter(columnNumber -> {
164                    return !hasValidAlignment(expectedColumnNumberTabsExpanded, columnNumber);
165                })
166                .ifPresent(columnNumber -> {
167                    logViolation(javadocEndToken.getLineNo(),
168                            columnNumber,
169                            expectedColumnNumberTabsExpanded);
170                });
171    }
172
173    /**
174     * Processes and returns the column number of
175     * leading asterisk with tabs expanded.
176     * Also sets 'expectedColumnNumberWithoutExpandedTabs' if the leading asterisk is present.
177     *
178     * @param line javadoc comment line
179     * @param columnNumber column number of leading asterisk
180     * @return column number of leading asterisk with tabs expanded
181     */
182    private int expandedTabs(String line, int columnNumber) {
183        expectedColumnNumberWithoutExpandedTabs = columnNumber - 1;
184        return CommonUtil.lengthExpandedTabs(
185                    line, columnNumber, getTabWidth());
186    }
187
188    /**
189     * Processes and returns an OptionalInt containing
190     * the column number of leading asterisk without tabs expanded.
191     *
192     * @param line javadoc comment line
193     * @return asterisk's column number
194     */
195    private static Optional<Integer> getAsteriskColumnNumber(String line) {
196        final Pattern pattern = Pattern.compile("^(\\s*)\\*");
197        final Matcher matcher = pattern.matcher(line);
198
199        // We may not always have a leading asterisk because a javadoc line can start with
200        // a non-whitespace character or the javadoc line can be empty.
201        // In such cases, there is no leading asterisk and Optional will be empty.
202        return Optional.of(matcher)
203                .filter(Matcher::find)
204                .map(matcherInstance -> matcherInstance.group(1))
205                .map(groupLength -> groupLength.length() + 1);
206    }
207
208    /**
209     * Checks alignment of asterisks and logs violations.
210     *
211     * @param lineNumber line number of current comment line
212     * @param asteriskColNumber column number of leading asterisk
213     * @param expectedColNumber column number of javadoc starting token
214     */
215    private void logViolation(int lineNumber,
216                              int asteriskColNumber,
217                              int expectedColNumber) {
218
219        log(lineNumber,
220            expectedColumnNumberWithoutExpandedTabs,
221            MSG_KEY,
222            asteriskColNumber,
223            expectedColNumber);
224    }
225
226    /**
227     * Checks the column difference between
228     * expected column number and leading asterisk column number.
229     *
230     * @param expectedColNumber column number of javadoc starting token
231     * @param asteriskColNumber column number of leading asterisk
232     * @return true if the asterisk is aligned properly, false otherwise
233     */
234    private static boolean hasValidAlignment(int expectedColNumber,
235                                             int asteriskColNumber) {
236        return expectedColNumber - asteriskColNumber == 0;
237    }
238}