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 should
039 * not share a line with the target declaration, except for single-line methods and fields.
040 * </div>
041 *
042 * <p>
043 * Attention: Checkstyle ignores annotations placed among modifiers due to a technical limitation.
044 * The parser cannot distinguish whether an annotation applies to the method itself
045 * or to its return type.
046 * </p>
047 *
048 * @since 13.9.0
049 */
050@StatelessCheck
051public class OpenjdkAnnotationLocationCheck extends AbstractCheck {
052
053    /**
054     * A key is pointing to the warning message text in "messages.properties"
055     * 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"
061     * file.
062     */
063    public static final String MSG_KEY_ANNOTATION_ON_TARGET_LINE = "annotation.on.target.line";
064
065    /**
066     * Creates a new {@code OpenjdkAnnotationLocationCheck} instance.
067     */
068    public OpenjdkAnnotationLocationCheck() {
069        // no code by default
070    }
071
072    @Override
073    public int[] getDefaultTokens() {
074        return getAcceptableTokens();
075    }
076
077    @Override
078    public int[] getAcceptableTokens() {
079        return new int[] {
080            TokenTypes.CLASS_DEF,
081            TokenTypes.INTERFACE_DEF,
082            TokenTypes.PACKAGE_DEF,
083            TokenTypes.ENUM_CONSTANT_DEF,
084            TokenTypes.ENUM_DEF,
085            TokenTypes.METHOD_DEF,
086            TokenTypes.CTOR_DEF,
087            TokenTypes.VARIABLE_DEF,
088            TokenTypes.ANNOTATION_DEF,
089            TokenTypes.ANNOTATION_FIELD_DEF,
090            TokenTypes.RECORD_DEF,
091            TokenTypes.COMPACT_CTOR_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        if (!isLocalVariable(ast)) {
103            final DetailAST annotationParentNode = getAnnotationsNode(ast);
104            final DetailAST startOfTargetNode = getStartingAst(annotationParentNode);
105            final List<DetailAST> annotationList = getAnnotations(annotationParentNode);
106
107            if (!areAllOnSameLine(annotationList) && !areAllOnSeparateLines(annotationList)) {
108                log(startOfTargetNode, MSG_KEY_ANNOTATION_ALONE_OR_SAME, getTargetName(ast));
109            }
110            if (isAnyOnTargetLine(annotationList, startOfTargetNode)
111                    && !isSingleLineMethodOrField(ast)) {
112                log(startOfTargetNode, MSG_KEY_ANNOTATION_ON_TARGET_LINE, getTargetName(ast));
113            }
114        }
115    }
116
117    /**
118     * Checks whether the variable is local or not.
119     *
120     * @param ast variable.
121     * @return true if local variable.
122     */
123    private static boolean isLocalVariable(DetailAST ast) {
124        return ast.getType() == TokenTypes.VARIABLE_DEF
125                && ast.getParent().getType() != TokenTypes.OBJBLOCK
126                && ast.getParent().getType() != TokenTypes.COMPACT_COMPILATION_UNIT;
127    }
128
129    /**
130     * Finds the first node other than the annotation node in target ast.
131     *
132     * @param targetNode target node.
133     * @return the ast of the starting point
134     */
135    private static DetailAST getStartingAst(DetailAST targetNode) {
136        DetailAST annotation = targetNode.getFirstChild();
137        while (annotation != null && annotation.getType() == TokenTypes.ANNOTATION) {
138            annotation = annotation.getNextSibling();
139        }
140
141        final DetailAST startingAst;
142        if (annotation != null) {
143            startingAst = annotation;
144        }
145        else {
146            startingAst = targetNode.getNextSibling();
147        }
148        return startingAst;
149    }
150
151    /**
152     * Gets the parent node of annotations.
153     *
154     * @param ast token.
155     * @return the parent of annotations.
156     */
157    private static DetailAST getAnnotationsNode(DetailAST ast) {
158        DetailAST annotationParentNode = ast.findFirstToken(TokenTypes.MODIFIERS);
159        if (annotationParentNode == null) {
160            annotationParentNode = ast.findFirstToken(TokenTypes.ANNOTATIONS);
161        }
162        return annotationParentNode;
163    }
164
165    /**
166     * Gets all annotations of a target node.
167     *
168     * @param annotationParentNode parent node of annotations.
169     * @return the list of annotations.
170     */
171    private static List<DetailAST> getAnnotations(DetailAST annotationParentNode) {
172        final List<DetailAST> annotationList = new ArrayList<>();
173        DetailAST annotation = annotationParentNode.getFirstChild();
174        while (annotation != null && annotation.getType() == TokenTypes.ANNOTATION) {
175            annotationList.add(annotation);
176            annotation = annotation.getNextSibling();
177        }
178        return annotationList;
179    }
180
181    /**
182     * Checks whether all annotations are on the same line.
183     *
184     * @param annotationList list of annotations.
185     * @return true if all annotations are on the same line.
186     */
187    private static boolean areAllOnSameLine(List<DetailAST> annotationList) {
188        return annotationList.isEmpty()
189                || annotationList.getFirst().getLineNo() == annotationList.getLast().getLineNo();
190    }
191
192    /**
193     * Checks whether all annotations are on a separate line.
194     *
195     * @param annotationList list of annotations.
196     * @return true if all annotations are on separate lines.
197     */
198    private static boolean areAllOnSeparateLines(List<DetailAST> annotationList) {
199        boolean areOnSeparateLine = true;
200        for (int index = 0; index < annotationList.size() - 1; index++) {
201            if (annotationList.get(index).getLineNo()
202                    == annotationList.get(index + 1).getLineNo()) {
203                areOnSeparateLine = false;
204            }
205        }
206        return areOnSeparateLine;
207    }
208
209    /**
210     * Checks whether an annotation is on the target line.
211     *
212     * @param annotationList list of annotations.
213     * @param startOfTargetNode ast of starting point of target node.
214     * @return true if an annotation is on the target line.
215     */
216    private static boolean isAnyOnTargetLine(Iterable<DetailAST> annotationList,
217            DetailAST startOfTargetNode) {
218        boolean isOnTargetLine = false;
219        for (final DetailAST annotation : annotationList) {
220            if (TokenUtil.areOnSameLine(annotation, startOfTargetNode)) {
221                isOnTargetLine = true;
222            }
223        }
224        return isOnTargetLine;
225    }
226
227    /**
228     * Checks whether a method or field is single line or not.
229     *
230     * @param targetNode ast of target node
231     * @return true if the method or field is single line.
232     */
233    private static boolean isSingleLineMethodOrField(DetailAST targetNode) {
234        boolean result = false;
235        if (targetNode.getType() == TokenTypes.METHOD_DEF
236                || targetNode.getType() == TokenTypes.ANNOTATION_FIELD_DEF
237                || targetNode.getType() == TokenTypes.VARIABLE_DEF) {
238            final DetailAST lastToken = targetNode.getLastChild();
239            if (lastToken.getType() == TokenTypes.SLIST) {
240                final DetailAST rightCurly = lastToken.getLastChild();
241                result = TokenUtil.areOnSameLine(targetNode, rightCurly);
242            }
243            else {
244                result = TokenUtil.areOnSameLine(targetNode, lastToken);
245            }
246        }
247
248        return result;
249    }
250
251    /**
252     * Returns the name of the given target node.
253     *
254     * @param targetNode target node.
255     * @return target name.
256     */
257    private static String getTargetName(DetailAST targetNode) {
258        DetailAST identNode = targetNode.findFirstToken(TokenTypes.IDENT);
259        if (identNode == null) {
260            identNode = targetNode.findFirstToken(TokenTypes.DOT).findFirstToken(TokenTypes.IDENT);
261        }
262        return identNode.getText();
263    }
264
265}