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"
056     * file.
057     */
058    public static final String MSG_KEY_ANNOTATION_ALONE_OR_SAME = "annotation.alone.or.same";
059
060    /**
061     * A key is pointing to the warning message text in "messages.properties"
062     * file.
063     */
064    public static final String MSG_KEY_ANNOTATION_ON_TARGET_LINE = "annotation.on.target.line";
065
066    /**
067     * Creates a new {@code OpenjdkAnnotationLocationCheck} instance.
068     */
069    public OpenjdkAnnotationLocationCheck() {
070        // no code by default
071    }
072
073    @Override
074    public int[] getDefaultTokens() {
075        return getAcceptableTokens();
076    }
077
078    @Override
079    public int[] getAcceptableTokens() {
080        return new int[] {
081            TokenTypes.CLASS_DEF,
082            TokenTypes.INTERFACE_DEF,
083            TokenTypes.PACKAGE_DEF,
084            TokenTypes.ENUM_CONSTANT_DEF,
085            TokenTypes.ENUM_DEF,
086            TokenTypes.METHOD_DEF,
087            TokenTypes.CTOR_DEF,
088            TokenTypes.VARIABLE_DEF,
089            TokenTypes.ANNOTATION_DEF,
090            TokenTypes.ANNOTATION_FIELD_DEF,
091            TokenTypes.RECORD_DEF,
092            TokenTypes.COMPACT_CTOR_DEF,
093            TokenTypes.MODULE_DEF,
094        };
095    }
096
097    @Override
098    public int[] getRequiredTokens() {
099        return CommonUtil.EMPTY_INT_ARRAY;
100    }
101
102    @Override
103    public void visitToken(DetailAST ast) {
104        final DetailAST annotationParentNode = getAnnotationsNode(ast);
105        final DetailAST startOfTargetNode = getStartingAst(annotationParentNode);
106        final List<DetailAST> annotationList = getAnnotations(annotationParentNode);
107
108        final boolean areAnnotationsOnSameLine = areAllOnSameLine(annotationList);
109        if (!areAnnotationsOnSameLine && !areAllOnSeparateLines(annotationList)) {
110            log(startOfTargetNode, MSG_KEY_ANNOTATION_ALONE_OR_SAME, getTargetName(ast));
111        }
112        if (isAnyOnTargetLine(annotationList, startOfTargetNode)
113                && !(areAnnotationsOnSameLine
114                        && isSingleLineTarget(startOfTargetNode, ast))) {
115            log(startOfTargetNode, MSG_KEY_ANNOTATION_ON_TARGET_LINE, getTargetName(ast));
116        }
117    }
118
119    /**
120     * Finds the first node other than the annotation node in target ast.
121     *
122     * @param targetNode target node.
123     * @return the ast of the starting point
124     */
125    private static DetailAST getStartingAst(DetailAST targetNode) {
126        DetailAST annotation = targetNode.getFirstChild();
127        while (annotation != null && annotation.getType() == TokenTypes.ANNOTATION) {
128            annotation = annotation.getNextSibling();
129        }
130
131        final DetailAST startingAst;
132        if (annotation != null) {
133            startingAst = annotation;
134        }
135        else {
136            startingAst = targetNode.getNextSibling();
137        }
138        return startingAst;
139    }
140
141    /**
142     * Gets the parent node of annotations.
143     *
144     * @param ast token.
145     * @return the parent of annotations.
146     */
147    private static DetailAST getAnnotationsNode(DetailAST ast) {
148        DetailAST annotationParentNode = ast.findFirstToken(TokenTypes.MODIFIERS);
149        if (annotationParentNode == null) {
150            annotationParentNode = ast.findFirstToken(TokenTypes.ANNOTATIONS);
151        }
152        return annotationParentNode;
153    }
154
155    /**
156     * Gets all annotations of a target node.
157     *
158     * @param annotationParentNode parent node of annotations.
159     * @return the list of annotations.
160     */
161    private static List<DetailAST> getAnnotations(DetailAST annotationParentNode) {
162        final List<DetailAST> annotationList = new ArrayList<>();
163        DetailAST annotation = annotationParentNode.getFirstChild();
164        while (annotation != null && annotation.getType() == TokenTypes.ANNOTATION) {
165            annotationList.add(annotation);
166            annotation = annotation.getNextSibling();
167        }
168        return annotationList;
169    }
170
171    /**
172     * Checks whether all annotations are on the same line.
173     *
174     * @param annotationList list of annotations.
175     * @return true if all annotations are on the same line.
176     */
177    private static boolean areAllOnSameLine(List<DetailAST> annotationList) {
178        return annotationList.isEmpty()
179                || annotationList.getFirst().getLineNo() == annotationList.getLast().getLineNo();
180    }
181
182    /**
183     * Checks whether all annotations are on a separate line.
184     *
185     * @param annotationList list of annotations.
186     * @return true if all annotations are on separate lines.
187     */
188    private static boolean areAllOnSeparateLines(List<DetailAST> annotationList) {
189        boolean areOnSeparateLine = true;
190        for (int index = 0; index < annotationList.size() - 1; index++) {
191            if (annotationList.get(index).getLineNo()
192                    == annotationList.get(index + 1).getLineNo()) {
193                areOnSeparateLine = false;
194            }
195        }
196        return areOnSeparateLine;
197    }
198
199    /**
200     * Checks whether an annotation is on the target line.
201     *
202     * @param annotationList list of annotations.
203     * @param startOfTargetNode ast of starting point of target node.
204     * @return true if an annotation is on the target line.
205     */
206    private static boolean isAnyOnTargetLine(Iterable<DetailAST> annotationList,
207            DetailAST startOfTargetNode) {
208        boolean isOnTargetLine = false;
209        for (final DetailAST annotation : annotationList) {
210            if (TokenUtil.areOnSameLine(annotation, startOfTargetNode)) {
211                isOnTargetLine = true;
212            }
213        }
214        return isOnTargetLine;
215    }
216
217    /**
218     * Checks whether a target is single line or not.
219     *
220     * @param startOfTargetNode first node of the target after annotations.
221     * @param targetNode ast of target node.
222     * @return true if the target is single line.
223     */
224    private static boolean isSingleLineTarget(DetailAST startOfTargetNode,
225            DetailAST targetNode) {
226        DetailAST lastToken = targetNode;
227        while (lastToken.hasChildren()) {
228            lastToken = lastToken.getLastChild();
229        }
230        return TokenUtil.areOnSameLine(startOfTargetNode, lastToken);
231    }
232
233    /**
234     * Returns the name of the given target node.
235     *
236     * @param targetNode target node.
237     * @return target name.
238     */
239    private static String getTargetName(DetailAST targetNode) {
240        DetailAST identNode = targetNode.findFirstToken(TokenTypes.IDENT);
241        if (identNode == null) {
242            identNode = targetNode.findFirstToken(TokenTypes.DOT).findFirstToken(TokenTypes.IDENT);
243        }
244        return identNode.getText();
245    }
246
247}