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.indentation; 021 022import javax.annotation.Nullable; 023 024import com.puppycrawl.tools.checkstyle.StatelessCheck; 025import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 026import com.puppycrawl.tools.checkstyle.api.DetailAST; 027import com.puppycrawl.tools.checkstyle.api.TokenTypes; 028 029/** 030 * <div> 031 * Checks that a wrapped class, interface, enum, or record header is wrapped according to the 032 * <a href="https://cr.openjdk.org/~alundblad/styleguide/index-v6.html#toc-wrapping-class-declarations"> 033 * OpenJDK Java Style Guide</a>. 034 * </div> 035 * 036 * <p> 037 * This check only applies when the type declaration header, from the declaration 038 * keyword through the end of its {@code extends}/{@code implements} clauses (or its 039 * name/type parameters/record components when neither clause is present), spans more 040 * than one line. A header whose content is entirely on one line is not in scope, even 041 * if its opening curly brace is pushed onto its own following line; that is a brace 042 * placement choice for other checks (such as {@code LeftCurly}) to enforce, not a 043 * wrapping concern this check is about. 044 * </p> 045 * 046 * <p> 047 * The two rules enforced are: 048 * </p> 049 * <ol> 050 * <li>Each of the {@code extends} and {@code implements} clauses, when present, must 051 * <em>stand out</em> on its own new line once the header has started wrapping, or once 052 * that clause's own list of supertypes wraps across lines. Wrapping that happens only 053 * inside a single supertype's type arguments (the same way method arguments may wrap) 054 * does not require the clause keyword itself to move to a new line.</li> 055 * <li>Only checked when rule 1 above found nothing to report: the header should not have 056 * been wrapped in the first place if, joined back onto one line, it would still fit 057 * within 80 columns.</li> 058 * </ol> 059 * 060 * @since 14.4.0 061 */ 062@StatelessCheck 063public class ClassHeaderWrapOpenjdkCheck extends AbstractCheck { 064 065 /** 066 * A key is pointing to the warning message text in "messages.properties" file. 067 */ 068 public static final String MSG_KEY_NOT_ON_NEW_LINE = "openjdk.classheader.new.line"; 069 070 /** 071 * A key is pointing to the warning message text in "messages.properties" file. 072 */ 073 public static final String MSG_KEY_UNNECESSARY_WRAP = "openjdk.classheader.unnecessary.wrap"; 074 075 /** 076 * The maximum number of columns a class header may occupy before wrapping it 077 * is considered necessary. 078 */ 079 private static final int MAX_COLUMN_LIMIT = 80; 080 081 /** 082 * Creates a new {@code ClassHeaderWrapOpenjdkCheck} instance. 083 */ 084 public ClassHeaderWrapOpenjdkCheck() { 085 // no code by default 086 } 087 088 @Override 089 public int[] getDefaultTokens() { 090 return getAcceptableTokens(); 091 } 092 093 @Override 094 public int[] getAcceptableTokens() { 095 return new int[] { 096 TokenTypes.CLASS_DEF, 097 TokenTypes.INTERFACE_DEF, 098 TokenTypes.ENUM_DEF, 099 TokenTypes.RECORD_DEF, 100 }; 101 } 102 103 @Override 104 public int[] getRequiredTokens() { 105 return getAcceptableTokens(); 106 } 107 108 @Override 109 public void visitToken(DetailAST ast) { 110 final DetailAST headerStart = findHeaderStart(ast); 111 final DetailAST objBlock = ast.findFirstToken(TokenTypes.OBJBLOCK); 112 final DetailAST contentEnd = objBlock.getPreviousSibling(); 113 114 if (headerStart.getLineNo() != getEndLineNo(contentEnd)) { 115 final DetailAST extendsClause = ast.findFirstToken(TokenTypes.EXTENDS_CLAUSE); 116 final DetailAST implementsClause = ast.findFirstToken(TokenTypes.IMPLEMENTS_CLAUSE); 117 final boolean extendsMisplaced = isClauseMisplaced(extendsClause); 118 final boolean implementsMisplaced = isClauseMisplaced(implementsClause); 119 120 if (!extendsMisplaced && !implementsMisplaced) { 121 final DetailAST lcurly = objBlock.findFirstToken(TokenTypes.LCURLY); 122 checkNecessaryWrap(headerStart, lcurly); 123 } 124 } 125 } 126 127 /** 128 * Checks the given clause's placement if it is present. 129 * 130 * @param clause the {@code EXTENDS_CLAUSE} or {@code IMPLEMENTS_CLAUSE} to check, or 131 * {@code null} if the type declaration has none 132 * @return {@code true} if a violation was logged for this clause 133 */ 134 private boolean isClauseMisplaced(@Nullable DetailAST clause) { 135 return clause != null && checkClausePlacement(clause); 136 } 137 138 /** 139 * Finds the token that marks the true start of the header on the declaration's own 140 * line: the first modifier that already shares a line with the declaration keyword 141 * ({@code class}/{@code interface}/{@code enum}/{@code record}), skipping past any 142 * annotations placed on their own preceding line, or the declaration keyword itself 143 * if there is no such modifier. 144 * 145 * @param ast the class, interface, enum, or record declaration 146 * @return the token that starts the header 147 */ 148 private static DetailAST findHeaderStart(DetailAST ast) { 149 final DetailAST modifiers = ast.getFirstChild(); 150 final DetailAST declarationKeyword = modifiers.getNextSibling(); 151 DetailAST headerStart = declarationKeyword; 152 DetailAST modifier = modifiers.getFirstChild(); 153 154 while (modifier != null) { 155 if (modifier.getLineNo() == declarationKeyword.getLineNo()) { 156 headerStart = modifier; 157 break; 158 } 159 modifier = modifier.getNextSibling(); 160 } 161 return headerStart; 162 } 163 164 /** 165 * Checks that the given {@code extends}/{@code implements} clause stands out on its 166 * own new line, if required. It is required once a preceding {@code extends} clause 167 * has already achieved its own new line (so {@code implements} must match it), or 168 * once this clause's own list of supertypes wraps across multiple lines. Wrapping in 169 * whatever precedes the clause for some other reason, such as a wrapped type 170 * parameter or record component list, does not by itself require the clause to move. 171 * 172 * @param clause the {@code EXTENDS_CLAUSE} or {@code IMPLEMENTS_CLAUSE} to check 173 * @return {@code true} if a violation was logged for this clause 174 */ 175 private boolean checkClausePlacement(DetailAST clause) { 176 final DetailAST previous = clause.getPreviousSibling(); 177 final boolean sharesLineWithPrevious = clause.getLineNo() == getEndLineNo(previous); 178 final boolean misplaced = sharesLineWithPrevious 179 && (isPrecededByWrappedClause(previous) || isTopLevelWrapped(clause)); 180 181 if (misplaced) { 182 log(clause, MSG_KEY_NOT_ON_NEW_LINE, clause.getText()); 183 } 184 return misplaced; 185 } 186 187 /** 188 * Checks whether the given node is an {@code EXTENDS_CLAUSE} that already stands out 189 * on its own new line, meaning a following {@code implements} clause must match it. 190 * 191 * @param previous the node immediately preceding the clause under check 192 * @return {@code true} if {@code previous} is an {@code EXTENDS_CLAUSE} that does not 193 * share a line with whatever precedes it 194 */ 195 private static boolean isPrecededByWrappedClause(DetailAST previous) { 196 return previous.getType() == TokenTypes.EXTENDS_CLAUSE 197 && previous.getLineNo() != getEndLineNo(previous.getPreviousSibling()); 198 } 199 200 /** 201 * Checks whether the header, reconstructed as a single line, would still have fit 202 * within the maximum column limit, in which case wrapping it was unnecessary. 203 * 204 * @param headerStart the declaration keyword ({@code class}/{@code interface}/ 205 * {@code enum}/{@code record}) that starts the header 206 * @param lcurly the opening brace of the type's body 207 */ 208 private void checkNecessaryWrap(DetailAST headerStart, DetailAST lcurly) { 209 final StringBuilder unwrapped = new StringBuilder(MAX_COLUMN_LIMIT); 210 final String[] lines = getLines(); 211 212 for (int lineNo = headerStart.getLineNo(); lineNo <= lcurly.getLineNo(); lineNo++) { 213 final String line = lines[lineNo - 1]; 214 final int fromIndex = findFromIndex(headerStart, lineNo); 215 final int toIndex = findToIndex(lcurly, lineNo, line.length()); 216 final String segment = line.substring(fromIndex, toIndex).trim(); 217 218 if (!segment.isEmpty()) { 219 if (!unwrapped.isEmpty()) { 220 unwrapped.append(' '); 221 } 222 unwrapped.append(segment); 223 } 224 } 225 226 if (headerStart.getColumnNo() + unwrapped.length() <= MAX_COLUMN_LIMIT) { 227 log(headerStart, MSG_KEY_UNNECESSARY_WRAP); 228 } 229 } 230 231 /** 232 * Finds the index at which the given line's relevant content starts: the header's own 233 * starting column on the header's first line, or the start of the line otherwise. 234 * 235 * @param headerStart the token that starts the header 236 * @param lineNo the 1-indexed line number currently being processed 237 * @return the index at which to start extracting content from this line 238 */ 239 private static int findFromIndex(DetailAST headerStart, int lineNo) { 240 int fromIndex = 0; 241 if (lineNo == headerStart.getLineNo()) { 242 fromIndex = headerStart.getColumnNo(); 243 } 244 return fromIndex; 245 } 246 247 /** 248 * Finds the index at which the given line's relevant content ends: just past the 249 * opening curly brace on the line it occupies, or the end of the line otherwise. 250 * 251 * @param lcurly the opening brace of the type's body 252 * @param lineNo the 1-indexed line number currently being processed 253 * @param lineLength the length of the line currently being processed 254 * @return the index at which to stop extracting content from this line 255 */ 256 private static int findToIndex(DetailAST lcurly, int lineNo, int lineLength) { 257 int toIndex = lineLength; 258 if (lineNo == lcurly.getLineNo()) { 259 toIndex = lcurly.getColumnNo() + 1; 260 } 261 return toIndex; 262 } 263 264 /** 265 * Checks whether the given node's direct children span more than one source line, 266 * ignoring further wrapping nested inside any of those children (e.g. inside a 267 * supertype's own type arguments). 268 * 269 * @param clause the {@code EXTENDS_CLAUSE} or {@code IMPLEMENTS_CLAUSE} to inspect 270 * @return {@code true} if two direct children of {@code clause} start on different lines 271 */ 272 private static boolean isTopLevelWrapped(DetailAST clause) { 273 final int firstLineNo = clause.getFirstChild().getLineNo(); 274 boolean wrapped = false; 275 DetailAST child = clause.getFirstChild().getNextSibling(); 276 277 while (child != null) { 278 if (child.getLineNo() != firstLineNo) { 279 wrapped = true; 280 break; 281 } 282 child = child.getNextSibling(); 283 } 284 return wrapped; 285 } 286 287 /** 288 * Returns the line number of the last token in the given subtree. 289 * 290 * @param ast the root of the subtree to inspect 291 * @return the 1-indexed line number of the deepest last child of {@code ast} 292 */ 293 private static int getEndLineNo(DetailAST ast) { 294 DetailAST last = ast; 295 while (last.getLastChild() != null) { 296 last = last.getLastChild(); 297 } 298 return last.getLineNo(); 299 } 300 301}