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