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}