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}