001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2025 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 ( * ) 038 * of opening Javadoc tag. The alignment of closing Javadoc tag ( */ ) 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 * 050 * @since 10.18.0 051 */ 052@GlobalStatefulCheck 053public class JavadocLeadingAsteriskAlignCheck extends AbstractJavadocCheck { 054 055 /** 056 * A key is pointing to the warning message text in "messages.properties" 057 * file. 058 */ 059 public static final String MSG_KEY = "javadoc.asterisk.indentation"; 060 061 /** Specifies the line number of starting block of the javadoc comment. */ 062 private int javadocStartLineNumber; 063 064 /** Specifies the column number of starting block of the javadoc comment with tabs expanded. */ 065 private int expectedColumnNumberTabsExpanded; 066 067 /** 068 * Specifies the column number of the leading asterisk 069 * without tabs expanded. 070 */ 071 private int expectedColumnNumberWithoutExpandedTabs; 072 073 /** Specifies the lines of the file being processed. */ 074 private String[] fileLines; 075 076 @Override 077 public int[] getDefaultJavadocTokens() { 078 return new int[] { 079 JavadocTokenTypes.LEADING_ASTERISK, 080 }; 081 } 082 083 @Override 084 public int[] getRequiredJavadocTokens() { 085 return getAcceptableJavadocTokens(); 086 } 087 088 @Override 089 public void beginJavadocTree(DetailNode rootAst) { 090 // this method processes and sets information of starting javadoc tag. 091 fileLines = getLines(); 092 final String startLine = fileLines[rootAst.getLineNumber() - 1]; 093 javadocStartLineNumber = rootAst.getLineNumber(); 094 expectedColumnNumberTabsExpanded = CommonUtil.lengthExpandedTabs( 095 startLine, rootAst.getColumnNumber() - 1, getTabWidth()); 096 } 097 098 @Override 099 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}