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}