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