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 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 /** Specifies the lines of the file being processed. */ 068 private String[] fileLines; 069 070 /** 071 * Creates a new {@code JavadocLeadingAsteriskAlignCheck} instance. 072 */ 073 public JavadocLeadingAsteriskAlignCheck() { 074 // no code by default 075 } 076 077 @Override 078 public int[] getDefaultJavadocTokens() { 079 return new int[] { 080 JavadocCommentsTokenTypes.LEADING_ASTERISK, 081 JavadocCommentsTokenTypes.LEADING_ASTERISKS, 082 }; 083 } 084 085 @Override 086 public int[] getRequiredJavadocTokens() { 087 return getAcceptableJavadocTokens(); 088 } 089 090 @Override 091 public void beginJavadocTree(DetailNode rootAst) { 092 // this method processes and sets information of starting javadoc tag. 093 fileLines = getLines(); 094 final String startLine = fileLines[rootAst.getLineNumber() - 1]; 095 javadocStartLineNumber = rootAst.getLineNumber(); 096 expectedColumnNumberTabsExpanded = CommonUtil.lengthExpandedTabs( 097 startLine, rootAst.getColumnNumber() - 1, getTabWidth()); 098 } 099 100 @Override 101 public void visitJavadocToken(DetailNode ast) { 102 // this method checks the alignment of leading asterisks. 103 final boolean isJavadocOpeningLine = ast.getLineNumber() == javadocStartLineNumber; 104 105 if (isJavadocOpeningLine) { 106 if (ast.getType() == JavadocCommentsTokenTypes.LEADING_ASTERISK) { 107 final int previousColumn = ast.getColumnNumber() - 1; 108 if (Character.isWhitespace( 109 fileLines[ast.getLineNumber() - 1].charAt(previousColumn))) { 110 expectedColumnNumberTabsExpanded = getColumnNumberTabsExpanded(ast); 111 } 112 } 113 } 114 else { 115 final int columnNumberTabsExpanded = getColumnNumberTabsExpanded(ast); 116 117 if (!hasValidAlignment(expectedColumnNumberTabsExpanded, columnNumberTabsExpanded)) { 118 log(ast, MSG_KEY, columnNumberTabsExpanded, expectedColumnNumberTabsExpanded); 119 } 120 } 121 } 122 123 @Override 124 public void finishJavadocTree(DetailNode rootAst) { 125 // this method checks the alignment of closing javadoc tag. 126 final DetailAST javadocEndToken = getBlockCommentAst().getLastChild(); 127 final String lastLine = fileLines[javadocEndToken.getLineNo() - 1]; 128 final Optional<Integer> endingBlockColumnNumber = getAsteriskColumnNumber(lastLine); 129 130 endingBlockColumnNumber 131 .filter(columnNumber -> columnNumber - 1 == javadocEndToken.getColumnNo()) 132 .ifPresent(columnNumber -> { 133 final int columnNumberTabsExpanded = CommonUtil.lengthExpandedTabs( 134 lastLine, columnNumber, getTabWidth()); 135 136 if (!hasValidAlignment( 137 expectedColumnNumberTabsExpanded, columnNumberTabsExpanded)) { 138 log(javadocEndToken, MSG_KEY, 139 columnNumberTabsExpanded, expectedColumnNumberTabsExpanded); 140 } 141 }); 142 } 143 144 /** 145 * Processes and returns an OptionalInt containing 146 * the column number of leading asterisk without tabs expanded. 147 * 148 * @param line javadoc comment line 149 * @return asterisk's column number 150 */ 151 private static Optional<Integer> getAsteriskColumnNumber(String line) { 152 final Pattern pattern = Pattern.compile("^(\\s*)\\*"); 153 final Matcher matcher = pattern.matcher(line); 154 155 // We may not always have a leading asterisk because a javadoc line can start with 156 // a non-whitespace character or the javadoc line can be empty. 157 // In such cases, there is no leading asterisk and Optional will be empty. 158 return Optional.of(matcher) 159 .filter(Matcher::find) 160 .map(matcherInstance -> matcherInstance.group(1)) 161 .map(groupLength -> groupLength.length() + 1); 162 } 163 164 /** 165 * Returns the tab-expanded, one-based column number of the leading asterisk node. 166 * 167 * @param ast leading asterisk node 168 * @return tab-expanded column number 169 */ 170 private int getColumnNumberTabsExpanded(DetailNode ast) { 171 return 1 + CommonUtil.lengthExpandedTabs( 172 fileLines[ast.getLineNumber() - 1], 173 ast.getColumnNumber(), 174 getTabWidth()); 175 } 176 177 /** 178 * Checks the column difference between 179 * expected column number and leading asterisk column number. 180 * 181 * @param expectedColNumber column number of javadoc starting token 182 * @param asteriskColNumber column number of leading asterisk 183 * @return true if the asterisk is aligned properly, false otherwise 184 */ 185 private static boolean hasValidAlignment(int expectedColNumber, 186 int asteriskColNumber) { 187 return expectedColNumber - asteriskColNumber == 0; 188 } 189 190}