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.whitespace; 021 022import java.util.ArrayList; 023import java.util.Collection; 024import java.util.List; 025import java.util.SequencedCollection; 026import java.util.stream.IntStream; 027 028import com.puppycrawl.tools.checkstyle.StatelessCheck; 029import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 030import com.puppycrawl.tools.checkstyle.api.DetailAST; 031import com.puppycrawl.tools.checkstyle.api.TokenTypes; 032import com.puppycrawl.tools.checkstyle.utils.CheckUtil; 033import com.puppycrawl.tools.checkstyle.utils.CommonUtil; 034import com.puppycrawl.tools.checkstyle.utils.TokenUtil; 035 036/** 037 * <div> 038 * Checks that the parameters of a wrapped method declaration follow one of the two forms 039 * allowed by the OpenJDK style guidelines. 040 * </div> 041 * 042 * <p> 043 * The 044 * <a href="https://checkstyle.org/styleguides/openjdk-java-style-v6/openjdk-styleguide.html#wrapping-method-declarations"> 045 * OpenJDK Java Style Guidelines</a> allow a wrapped method declaration to be formatted 046 * either by listing the parameters vertically, one per line, or by breaking the line and 047 * indenting the continuation by eight extra spaces. This check validates a wrapped 048 * declaration against those two forms: 049 * </p> 050 * <ul> 051 * <li> 052 * when every parameter that starts a new line is indented eight spaces past the declaration, 053 * the declaration uses the eight extra spaces form and any number of parameters may share a line; 054 * </li> 055 * <li> 056 * otherwise the parameters are expected to be listed vertically, that is, every parameter that 057 * starts a new line is aligned with the first parameter and each line holds exactly one parameter. 058 * </li> 059 * </ul> 060 * 061 * <p> 062 * A declaration whose parameter list fits on a single line is ignored. A wrapped declaration 063 * in which no parameter starts a new line, because it is wrapped inside a parameter type 064 * rather than between two parameters, is expected to be listed vertically, so a line that 065 * holds more than one parameter is reported. 066 * </p> 067 * 068 * @since 14.4.0 069 */ 070@StatelessCheck 071public class OpenjdkMethodParameterAlignmentCheck extends AbstractCheck { 072 073 /** 074 * A key is pointing to the warning message text in "messages.properties" file. 075 */ 076 public static final String MSG_KEY = "method.parameter.alignment"; 077 078 /** 079 * A key is pointing to the warning message text in "messages.properties" file. 080 */ 081 public static final String MSG_WRAP = "method.parameter.wrap"; 082 083 /** The number of extra spaces used to indent a wrapped continuation line. */ 084 private static final int EIGHT_SPACES = 8; 085 086 /** 087 * Creates a new {@code OpenjdkMethodParameterAlignmentCheck} instance. 088 */ 089 public OpenjdkMethodParameterAlignmentCheck() { 090 // no code by default 091 } 092 093 @Override 094 public int[] getDefaultTokens() { 095 return getRequiredTokens(); 096 } 097 098 @Override 099 public int[] getAcceptableTokens() { 100 return getRequiredTokens(); 101 } 102 103 @Override 104 public int[] getRequiredTokens() { 105 return new int[] { 106 TokenTypes.METHOD_DEF, 107 TokenTypes.CTOR_DEF, 108 }; 109 } 110 111 @Override 112 public void visitToken(DetailAST ast) { 113 final List<DetailAST> parameters = 114 getParameters(ast.findFirstToken(TokenTypes.PARAMETERS)); 115 116 if (!parameters.isEmpty() && spansMoreThanOneLine(parameters)) { 117 final List<Integer> lineStartColumns = getLineStartColumns(parameters); 118 final boolean wrappedByEightSpaces = !lineStartColumns.isEmpty() 119 && isAlignedOn(lineStartColumns, expandedColumnNo(ast) + EIGHT_SPACES); 120 121 if (!wrappedByEightSpaces) { 122 final boolean listedVertically = isAlignedOn(lineStartColumns, 123 expandedColumnNo(CheckUtil.getFirstNode(parameters.getFirst()))); 124 final DetailAST name = ast.findFirstToken(TokenTypes.IDENT); 125 126 if (listedVertically && hasMultipleParametersOnLine(parameters)) { 127 log(name, MSG_KEY); 128 } 129 else if (!listedVertically) { 130 log(name, MSG_WRAP); 131 } 132 } 133 } 134 } 135 136 /** 137 * Collects the parameters of the given parameter list. 138 * 139 * @param parameters the {@code PARAMETERS} node of a method declaration 140 * @return every parameter, in declaration order 141 */ 142 private static List<DetailAST> getParameters(DetailAST parameters) { 143 final List<DetailAST> parameterList = new ArrayList<>(); 144 TokenUtil.forEachChild(parameters, TokenTypes.PARAMETER_DEF, parameterList::add); 145 return parameterList; 146 } 147 148 /** 149 * Checks whether the parameters of a declaration are spread over more than one line. A 150 * declaration can be wrapped without any parameter starting a new line, when it is wrapped 151 * inside a parameter type rather than between two parameters. Parameters are listed in 152 * source order, so a parameter that starts a new line always leaves the last parameter on a 153 * later line than the first one. 154 * 155 * @param parameters every parameter of a method declaration 156 * @return {@code true} if the parameters do not all start on the same line 157 */ 158 private static boolean spansMoreThanOneLine(SequencedCollection<DetailAST> parameters) { 159 return parameters.getFirst().getLineNo() != parameters.getLast().getLineNo(); 160 } 161 162 /** 163 * Collects the column the line of every parameter that starts a new line begins in. The 164 * first parameter of a declaration is the column the other parameters are compared 165 * against, so it is never collected. 166 * 167 * @param parameters every parameter of a method declaration 168 * @return the column every parameter that starts a new line is expected to be aligned in 169 */ 170 private List<Integer> getLineStartColumns(List<DetailAST> parameters) { 171 final List<Integer> columns = new ArrayList<>(); 172 for (int index = 1; index < parameters.size(); index++) { 173 if (!sharesLineWithPreviousParameter(parameters, index)) { 174 columns.add(expandedColumnNo(getLineStart(parameters.get(index)))); 175 } 176 } 177 return columns; 178 } 179 180 /** 181 * Checks whether any parameter is declared on the line the parameter before it ends on. The 182 * comma that separates two parameters is not taken into account, as a parameter list can be 183 * wrapped before that comma instead of after it. 184 * 185 * @param parameters every parameter of a method declaration 186 * @return {@code true} if a line holds more than one parameter 187 */ 188 private static boolean hasMultipleParametersOnLine(List<DetailAST> parameters) { 189 return IntStream.range(1, parameters.size()) 190 .anyMatch(index -> sharesLineWithPreviousParameter(parameters, index)); 191 } 192 193 /** 194 * Checks whether the parameter at the given position begins on the line the parameter 195 * before it ends on. A parameter can end on a later line than it begins on, when it is 196 * wrapped inside its own type or annotations. 197 * 198 * @param parameters every parameter of a method declaration 199 * @param index the position of the parameter to locate, never the first one 200 * @return {@code true} if the parameter shares a line with the parameter before it 201 */ 202 private static boolean sharesLineWithPreviousParameter(List<DetailAST> parameters, 203 int index) { 204 final DetailAST previousParameter = parameters.get(index - 1); 205 final DetailAST parameterStart = CheckUtil.getFirstNode(parameters.get(index)); 206 return getLastLineNo(previousParameter) == parameterStart.getLineNo(); 207 } 208 209 /** 210 * Returns the number of the last line the given parameter is declared on. 211 * 212 * @param ast the parameter to measure 213 * @return the greatest line number of the tokens of the parameter 214 */ 215 private static int getLastLineNo(DetailAST ast) { 216 int lastLineNo = ast.getLineNo(); 217 for (DetailAST child = ast.getFirstChild(); child != null; 218 child = child.getNextSibling()) { 219 lastLineNo = Math.max(lastLineNo, getLastLineNo(child)); 220 } 221 return lastLineNo; 222 } 223 224 /** 225 * Returns the token the line of a parameter that starts a new line begins with. A parameter 226 * list can be wrapped before the comma that separates two parameters instead of after it, 227 * which leaves that comma as the first token of the line, so such a line begins at the 228 * comma rather than at the parameter. The comma that separates a parameter from the 229 * parameter before it is the sibling preceding it, so a parameter that does not start a new 230 * line is never passed here. 231 * 232 * @param parameter the parameter that starts a new line 233 * @return the comma preceding the parameter when both are on one line, the first token of 234 * the parameter otherwise 235 */ 236 private static DetailAST getLineStart(DetailAST parameter) { 237 final DetailAST firstNode = CheckUtil.getFirstNode(parameter); 238 final DetailAST comma = parameter.getPreviousSibling(); 239 DetailAST result = firstNode; 240 241 if (comma.getLineNo() == firstNode.getLineNo()) { 242 result = comma; 243 } 244 return result; 245 } 246 247 /** 248 * Checks whether every parameter that starts a new line begins in the given column. A 249 * declaration whose parameters all start on one line has no parameter that starts a new 250 * line, so it is aligned on every column and is never reported. 251 * 252 * @param lineStartColumns the column of every parameter that starts a new line 253 * @param column the column the parameters are expected to begin in 254 * @return {@code true} if every parameter that starts a new line begins in that column 255 */ 256 private static boolean isAlignedOn(Collection<Integer> lineStartColumns, int column) { 257 return lineStartColumns.stream() 258 .allMatch(lineStartColumn -> lineStartColumn == column); 259 } 260 261 /** 262 * Returns the column the given token starts in, counting the tabs that precede it on its 263 * line as the configured tab width. Alignment is what the reader sees, so a declaration 264 * indented with tabs has to be measured the same way an editor renders it. 265 * 266 * @param ast the token to locate 267 * @return the column of the token, with preceding tabs expanded 268 */ 269 private int expandedColumnNo(DetailAST ast) { 270 return CommonUtil.lengthExpandedTabs( 271 getLine(ast.getLineNo() - 1), ast.getColumnNo(), getTabWidth()); 272 } 273 274}