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