1 ///////////////////////////////////////////////////////////////////////////////////////////////
2 // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3 // Copyright (C) 2001-2026 the original author or authors.
4 //
5 // This library is free software; you can redistribute it and/or
6 // modify it under the terms of the GNU Lesser General Public
7 // License as published by the Free Software Foundation; either
8 // version 2.1 of the License, or (at your option) any later version.
9 //
10 // This library is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 // Lesser General Public License for more details.
14 //
15 // You should have received a copy of the GNU Lesser General Public
16 // License along with this library; if not, write to the Free Software
17 // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18 ///////////////////////////////////////////////////////////////////////////////////////////////
19
20 package com.puppycrawl.tools.checkstyle.checks.javadoc;
21
22 import java.util.Optional;
23 import java.util.regex.Matcher;
24 import java.util.regex.Pattern;
25
26 import com.puppycrawl.tools.checkstyle.GlobalStatefulCheck;
27 import com.puppycrawl.tools.checkstyle.api.DetailAST;
28 import com.puppycrawl.tools.checkstyle.api.DetailNode;
29 import com.puppycrawl.tools.checkstyle.api.JavadocCommentsTokenTypes;
30 import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
31
32 /**
33 * <div>
34 * Checks the alignment of
35 * <a href="https://docs.oracle.com/en/java/javase/14/docs/specs/javadoc/doc-comment-spec.html#leading-asterisks">
36 * leading asterisks</a> in a Javadoc comment. The Check ensures that leading asterisks
37 * are aligned vertically under the first asterisk ( * )
38 * of opening Javadoc tag. The alignment of closing Javadoc tag ( */ ) is also checked.
39 * If a closing Javadoc tag contains non-whitespace character before it
40 * then it's alignment will be ignored.
41 * If the ending javadoc line contains a leading asterisk, then that leading asterisk's alignment
42 * will be considered, the closing Javadoc tag will be ignored.
43 * </div>
44 *
45 * <p>
46 * If you're using tabs then specify the the tab width in the
47 * <a href="https://checkstyle.org/config.html#tabWidth">tabWidth</a> property.
48 * </p>
49 *
50 * @since 10.18.0
51 */
52 @GlobalStatefulCheck
53 public class JavadocLeadingAsteriskAlignCheck extends AbstractJavadocCheck {
54
55 /**
56 * A key is pointing to the warning message text in "messages.properties"
57 * file.
58 */
59 public static final String MSG_KEY = "javadoc.asterisk.indentation";
60
61 /** Specifies the line number of starting block of the javadoc comment. */
62 private int javadocStartLineNumber;
63
64 /** Specifies the column number of starting block of the javadoc comment with tabs expanded. */
65 private int expectedColumnNumberTabsExpanded;
66
67 /** Specifies the lines of the file being processed. */
68 private String[] fileLines;
69
70 /**
71 * Creates a new {@code JavadocLeadingAsteriskAlignCheck} instance.
72 */
73 public JavadocLeadingAsteriskAlignCheck() {
74 // no code by default
75 }
76
77 @Override
78 public int[] getDefaultJavadocTokens() {
79 return new int[] {
80 JavadocCommentsTokenTypes.LEADING_ASTERISK,
81 JavadocCommentsTokenTypes.LEADING_ASTERISKS,
82 };
83 }
84
85 @Override
86 public int[] getRequiredJavadocTokens() {
87 return getAcceptableJavadocTokens();
88 }
89
90 @Override
91 public void beginJavadocTree(DetailNode rootAst) {
92 // this method processes and sets information of starting javadoc tag.
93 fileLines = getLines();
94 final String startLine = fileLines[rootAst.getLineNumber() - 1];
95 javadocStartLineNumber = rootAst.getLineNumber();
96 expectedColumnNumberTabsExpanded = CommonUtil.lengthExpandedTabs(
97 startLine, rootAst.getColumnNumber() - 1, getTabWidth());
98 }
99
100 @Override
101 public void visitJavadocToken(DetailNode ast) {
102 // this method checks the alignment of leading asterisks.
103 final boolean isJavadocOpeningLine = ast.getLineNumber() == javadocStartLineNumber;
104
105 if (isJavadocOpeningLine) {
106 if (ast.getType() == JavadocCommentsTokenTypes.LEADING_ASTERISK) {
107 final int previousColumn = ast.getColumnNumber() - 1;
108 if (Character.isWhitespace(
109 fileLines[ast.getLineNumber() - 1].charAt(previousColumn))) {
110 expectedColumnNumberTabsExpanded = getColumnNumberTabsExpanded(ast);
111 }
112 }
113 }
114 else {
115 final int columnNumberTabsExpanded = getColumnNumberTabsExpanded(ast);
116
117 if (!hasValidAlignment(expectedColumnNumberTabsExpanded, columnNumberTabsExpanded)) {
118 log(ast, MSG_KEY, columnNumberTabsExpanded, expectedColumnNumberTabsExpanded);
119 }
120 }
121 }
122
123 @Override
124 public void finishJavadocTree(DetailNode rootAst) {
125 // this method checks the alignment of closing javadoc tag.
126 final DetailAST javadocEndToken = getBlockCommentAst().getLastChild();
127 final String lastLine = fileLines[javadocEndToken.getLineNo() - 1];
128 final Optional<Integer> endingBlockColumnNumber = getAsteriskColumnNumber(lastLine);
129
130 endingBlockColumnNumber
131 .filter(columnNumber -> columnNumber - 1 == javadocEndToken.getColumnNo())
132 .ifPresent(columnNumber -> {
133 final int columnNumberTabsExpanded = CommonUtil.lengthExpandedTabs(
134 lastLine, columnNumber, getTabWidth());
135
136 if (!hasValidAlignment(
137 expectedColumnNumberTabsExpanded, columnNumberTabsExpanded)) {
138 log(javadocEndToken, MSG_KEY,
139 columnNumberTabsExpanded, expectedColumnNumberTabsExpanded);
140 }
141 });
142 }
143
144 /**
145 * Processes and returns an OptionalInt containing
146 * the column number of leading asterisk without tabs expanded.
147 *
148 * @param line javadoc comment line
149 * @return asterisk's column number
150 */
151 private static Optional<Integer> getAsteriskColumnNumber(String line) {
152 final Pattern pattern = Pattern.compile("^(\\s*)\\*");
153 final Matcher matcher = pattern.matcher(line);
154
155 // We may not always have a leading asterisk because a javadoc line can start with
156 // a non-whitespace character or the javadoc line can be empty.
157 // In such cases, there is no leading asterisk and Optional will be empty.
158 return Optional.of(matcher)
159 .filter(Matcher::find)
160 .map(matcherInstance -> matcherInstance.group(1))
161 .map(groupLength -> groupLength.length() + 1);
162 }
163
164 /**
165 * Returns the tab-expanded, one-based column number of the leading asterisk node.
166 *
167 * @param ast leading asterisk node
168 * @return tab-expanded column number
169 */
170 private int getColumnNumberTabsExpanded(DetailNode ast) {
171 return 1 + CommonUtil.lengthExpandedTabs(
172 fileLines[ast.getLineNumber() - 1],
173 ast.getColumnNumber(),
174 getTabWidth());
175 }
176
177 /**
178 * Checks the column difference between
179 * expected column number and leading asterisk column number.
180 *
181 * @param expectedColNumber column number of javadoc starting token
182 * @param asteriskColNumber column number of leading asterisk
183 * @return true if the asterisk is aligned properly, false otherwise
184 */
185 private static boolean hasValidAlignment(int expectedColNumber,
186 int asteriskColNumber) {
187 return expectedColNumber - asteriskColNumber == 0;
188 }
189
190 }