1 ///////////////////////////////////////////////////////////////////////////////////////////////
2 // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3 // Copyright (C) 2001-2025 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 /**
68 * Specifies the column number of the leading asterisk
69 * without tabs expanded.
70 */
71 private int expectedColumnNumberWithoutExpandedTabs;
72
73 /** Specifies the lines of the file being processed. */
74 private String[] fileLines;
75
76 @Override
77 public int[] getDefaultJavadocTokens() {
78 return new int[] {
79 JavadocCommentsTokenTypes.LEADING_ASTERISK,
80 };
81 }
82
83 @Override
84 public int[] getRequiredJavadocTokens() {
85 return getAcceptableJavadocTokens();
86 }
87
88 @Override
89 public void beginJavadocTree(DetailNode rootAst) {
90 // this method processes and sets information of starting javadoc tag.
91 fileLines = getLines();
92 final String startLine = fileLines[rootAst.getLineNumber() - 1];
93 javadocStartLineNumber = rootAst.getLineNumber();
94 expectedColumnNumberTabsExpanded = CommonUtil.lengthExpandedTabs(
95 startLine, rootAst.getColumnNumber() - 1, getTabWidth());
96 }
97
98 @Override
99 public void visitJavadocToken(DetailNode ast) {
100 // this method checks the alignment of leading asterisks.
101 final boolean isJavadocStartingLine = ast.getLineNumber() == javadocStartLineNumber;
102
103 if (!isJavadocStartingLine) {
104 final Optional<Integer> leadingAsteriskColumnNumber =
105 getAsteriskColumnNumber(ast.getText());
106
107 leadingAsteriskColumnNumber
108 .map(columnNumber -> expandedTabs(ast.getText(), columnNumber))
109 .filter(columnNumber -> {
110 return !hasValidAlignment(expectedColumnNumberTabsExpanded, columnNumber);
111 })
112 .ifPresent(columnNumber -> {
113 logViolation(ast.getLineNumber(),
114 columnNumber,
115 expectedColumnNumberTabsExpanded);
116 });
117 }
118 }
119
120 @Override
121 public void finishJavadocTree(DetailNode rootAst) {
122 // this method checks the alignment of closing javadoc tag.
123 final DetailAST javadocEndToken = getBlockCommentAst().getLastChild();
124 final String lastLine = fileLines[javadocEndToken.getLineNo() - 1];
125 final Optional<Integer> endingBlockColumnNumber = getAsteriskColumnNumber(lastLine);
126
127 endingBlockColumnNumber
128 .map(columnNumber -> expandedTabs(lastLine, columnNumber))
129 .filter(columnNumber -> {
130 return !hasValidAlignment(expectedColumnNumberTabsExpanded, columnNumber);
131 })
132 .ifPresent(columnNumber -> {
133 logViolation(javadocEndToken.getLineNo(),
134 columnNumber,
135 expectedColumnNumberTabsExpanded);
136 });
137 }
138
139 /**
140 * Processes and returns the column number of
141 * leading asterisk with tabs expanded.
142 * Also sets 'expectedColumnNumberWithoutExpandedTabs' if the leading asterisk is present.
143 *
144 * @param line javadoc comment line
145 * @param columnNumber column number of leading asterisk
146 * @return column number of leading asterisk with tabs expanded
147 */
148 private int expandedTabs(String line, int columnNumber) {
149 expectedColumnNumberWithoutExpandedTabs = columnNumber - 1;
150 return CommonUtil.lengthExpandedTabs(
151 line, columnNumber, getTabWidth());
152 }
153
154 /**
155 * Processes and returns an OptionalInt containing
156 * the column number of leading asterisk without tabs expanded.
157 *
158 * @param line javadoc comment line
159 * @return asterisk's column number
160 */
161 private static Optional<Integer> getAsteriskColumnNumber(String line) {
162 final Pattern pattern = Pattern.compile("^(\\s*)\\*");
163 final Matcher matcher = pattern.matcher(line);
164
165 // We may not always have a leading asterisk because a javadoc line can start with
166 // a non-whitespace character or the javadoc line can be empty.
167 // In such cases, there is no leading asterisk and Optional will be empty.
168 return Optional.of(matcher)
169 .filter(Matcher::find)
170 .map(matcherInstance -> matcherInstance.group(1))
171 .map(groupLength -> groupLength.length() + 1);
172 }
173
174 /**
175 * Checks alignment of asterisks and logs violations.
176 *
177 * @param lineNumber line number of current comment line
178 * @param asteriskColNumber column number of leading asterisk
179 * @param expectedColNumber column number of javadoc starting token
180 */
181 private void logViolation(int lineNumber,
182 int asteriskColNumber,
183 int expectedColNumber) {
184
185 log(lineNumber,
186 expectedColumnNumberWithoutExpandedTabs,
187 MSG_KEY,
188 asteriskColNumber,
189 expectedColNumber);
190 }
191
192 /**
193 * Checks the column difference between
194 * expected column number and leading asterisk column number.
195 *
196 * @param expectedColNumber column number of javadoc starting token
197 * @param asteriskColNumber column number of leading asterisk
198 * @return true if the asterisk is aligned properly, false otherwise
199 */
200 private static boolean hasValidAlignment(int expectedColNumber,
201 int asteriskColNumber) {
202 return expectedColNumber - asteriskColNumber == 0;
203 }
204 }