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}