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.annotation;
021
022import java.util.ArrayList;
023import java.util.List;
024
025import com.puppycrawl.tools.checkstyle.StatelessCheck;
026import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
027import com.puppycrawl.tools.checkstyle.api.DetailAST;
028import com.puppycrawl.tools.checkstyle.api.TokenTypes;
029import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
030import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
031
032/**
033 * <div>
034 * Verifies that annotations are properly placed by
035 * <a href="https://cr.openjdk.org/~alundblad/styleguide/index-v6.html#toc-annotations">
036 * OpenJDK Style</a>.
037 * Declaration annotations must either reside entirely on a single line or
038 * have each annotation placed on its own separate line. Annotations may share
039 * a line with the target declaration only when all annotations and the complete
040 * target declaration are on that same line.
041 * </div>
042 *
043 * <p>
044 * Attention: Checkstyle ignores annotations placed among modifiers due to a technical limitation.
045 * The parser cannot distinguish whether an annotation applies to the method itself
046 * or to its return type.
047 * </p>
048 *
049 * @since 13.9.0
050 */
051@StatelessCheck
052public class OpenjdkAnnotationLocationCheck extends AbstractCheck {
053
054    /**
055     * A key is pointing to the warning message text in "messages.properties" file.
056     */
057    public static final String MSG_KEY_ANNOTATION_ALONE_OR_SAME = "annotation.alone.or.same";
058
059    /**
060     * A key is pointing to the warning message text in "messages.properties" file.
061     */
062    public static final String MSG_KEY_ANNOTATION_ON_TARGET_LINE = "annotation.on.target.line";
063
064    /**
065     * Creates a new {@code OpenjdkAnnotationLocationCheck} instance.
066     */
067    public OpenjdkAnnotationLocationCheck() {
068        // no code by default
069    }
070
071    @Override
072    public int[] getDefaultTokens() {
073        return getAcceptableTokens();
074    }
075
076    @Override
077    public int[] getAcceptableTokens() {
078        return new int[] {
079            TokenTypes.CLASS_DEF,
080            TokenTypes.INTERFACE_DEF,
081            TokenTypes.PACKAGE_DEF,
082            TokenTypes.ENUM_CONSTANT_DEF,
083            TokenTypes.ENUM_DEF,
084            TokenTypes.METHOD_DEF,
085            TokenTypes.CTOR_DEF,
086            TokenTypes.VARIABLE_DEF,
087            TokenTypes.ANNOTATION_DEF,
088            TokenTypes.ANNOTATION_FIELD_DEF,
089            TokenTypes.RECORD_DEF,
090            TokenTypes.COMPACT_CTOR_DEF,
091            TokenTypes.MODULE_DEF,
092        };
093    }
094
095    @Override
096    public int[] getRequiredTokens() {
097        return CommonUtil.EMPTY_INT_ARRAY;
098    }
099
100    @Override
101    public void visitToken(DetailAST ast) {
102        final DetailAST annotationParentNode = getAnnotationsNode(ast);
103        final DetailAST startOfTargetNode = getStartingAst(annotationParentNode);
104        final List<DetailAST> annotationList = getAnnotations(annotationParentNode);
105
106        final boolean areAnnotationsOnSameLine = areAllOnSameLine(annotationList);
107        if (!areAnnotationsOnSameLine && !areAllOnSeparateLines(annotationList)) {
108            log(startOfTargetNode, MSG_KEY_ANNOTATION_ALONE_OR_SAME, getTargetName(ast));
109        }
110        if (isAnyOnTargetLine(annotationList, startOfTargetNode)
111                && !(areAnnotationsOnSameLine
112                        && isSingleLineTarget(startOfTargetNode, ast))) {
113            log(startOfTargetNode, MSG_KEY_ANNOTATION_ON_TARGET_LINE, getTargetName(ast));
114        }
115    }
116
117    /**
118     * Finds the first node other than the annotation node in target ast.
119     *
120     * @param targetNode target node.
121     * @return the ast of the starting point
122     */
123    private static DetailAST getStartingAst(DetailAST targetNode) {
124        DetailAST annotation = targetNode.getFirstChild();
125        while (annotation != null && annotation.getType() == TokenTypes.ANNOTATION) {
126            annotation = annotation.getNextSibling();
127        }
128
129        final DetailAST startingAst;
130        if (annotation != null) {
131            startingAst = annotation;
132        }
133        else {
134            startingAst = targetNode.getNextSibling();
135        }
136        return startingAst;
137    }
138
139    /**
140     * Gets the parent node of annotations.
141     *
142     * @param ast token.
143     * @return the parent of annotations.
144     */
145    private static DetailAST getAnnotationsNode(DetailAST ast) {
146        DetailAST annotationParentNode = ast.findFirstToken(TokenTypes.MODIFIERS);
147        if (annotationParentNode == null) {
148            annotationParentNode = ast.findFirstToken(TokenTypes.ANNOTATIONS);
149        }
150        return annotationParentNode;
151    }
152
153    /**
154     * Gets all annotations of a target node.
155     *
156     * @param annotationParentNode parent node of annotations.
157     * @return the list of annotations.
158     */
159    private static List<DetailAST> getAnnotations(DetailAST annotationParentNode) {
160        final List<DetailAST> annotationList = new ArrayList<>();
161        DetailAST annotation = annotationParentNode.getFirstChild();
162        while (annotation != null && annotation.getType() == TokenTypes.ANNOTATION) {
163            annotationList.add(annotation);
164            annotation = annotation.getNextSibling();
165        }
166        return annotationList;
167    }
168
169    /**
170     * Checks whether all annotations are on the same line.
171     *
172     * @param annotationList list of annotations.
173     * @return true if all annotations are on the same line.
174     */
175    private static boolean areAllOnSameLine(List<DetailAST> annotationList) {
176        return annotationList.isEmpty()
177                || annotationList.getFirst().getLineNo() == annotationList.getLast().getLineNo();
178    }
179
180    /**
181     * Checks whether all annotations are on a separate line.
182     *
183     * @param annotationList list of annotations.
184     * @return true if all annotations are on separate lines.
185     */
186    private static boolean areAllOnSeparateLines(List<DetailAST> annotationList) {
187        boolean areOnSeparateLine = true;
188        for (int index = 0; index < annotationList.size() - 1; index++) {
189            if (annotationList.get(index).getLineNo()
190                    == annotationList.get(index + 1).getLineNo()) {
191                areOnSeparateLine = false;
192            }
193        }
194        return areOnSeparateLine;
195    }
196
197    /**
198     * Checks whether an annotation is on the target line.
199     *
200     * @param annotationList list of annotations.
201     * @param startOfTargetNode ast of starting point of target node.
202     * @return true if an annotation is on the target line.
203     */
204    private static boolean isAnyOnTargetLine(Iterable<DetailAST> annotationList,
205            DetailAST startOfTargetNode) {
206        boolean isOnTargetLine = false;
207        for (final DetailAST annotation : annotationList) {
208            if (TokenUtil.areOnSameLine(annotation, startOfTargetNode)) {
209                isOnTargetLine = true;
210            }
211        }
212        return isOnTargetLine;
213    }
214
215    /**
216     * Checks whether a target is single line or not.
217     *
218     * @param startOfTargetNode first node of the target after annotations.
219     * @param targetNode ast of target node.
220     * @return true if the target is single line.
221     */
222    private static boolean isSingleLineTarget(DetailAST startOfTargetNode,
223            DetailAST targetNode) {
224        DetailAST lastToken = targetNode;
225        while (lastToken.hasChildren()) {
226            lastToken = lastToken.getLastChild();
227        }
228        return TokenUtil.areOnSameLine(startOfTargetNode, lastToken);
229    }
230
231    /**
232     * Returns the name of the given target node.
233     *
234     * @param targetNode target node.
235     * @return target name.
236     */
237    private static String getTargetName(DetailAST targetNode) {
238        DetailAST identNode = targetNode.findFirstToken(TokenTypes.IDENT);
239        if (identNode == null) {
240            identNode = targetNode.findFirstToken(TokenTypes.DOT).findFirstToken(TokenTypes.IDENT);
241        }
242        return identNode.getText();
243    }
244
245}