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 ( * ) 040 * of opening Javadoc tag. The alignment of closing Javadoc tag ( */ ) 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}