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.naming; 021 022import java.util.Set; 023import java.util.regex.Pattern; 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.AnnotationUtil; 030import com.puppycrawl.tools.checkstyle.utils.NullUtil; 031 032/** 033 * <div> 034 * Checks that method names conform to the 035 * <a href= 036 * "https://google.github.io/styleguide/javaguide.html#s5.2.3-method-names"> 037 * Google Java Style Guide</a> for method naming. 038 * </div> 039 * 040 * <p> 041 * This check enforces Google's specific method naming requirements: 042 * </p> 043 * <ul> 044 * <li>Method names must start with a lowercase letter and use uppercase letters 045 * for word boundaries.</li> 046 * <li>Underscores may appear in JUnit test method names to separate logical 047 * components.</li> 048 * <li>Underscores may be used to separate adjacent numbers (e.g., version 049 * numbers like 050 * {@code guava33_4_5}), but NOT between letters and digits.</li> 051 * </ul> 052 * 053 * @since 13.3.0 054 */ 055@StatelessCheck 056public class GoogleMethodNameCheck extends AbstractCheck { 057 058 /** 059 * A key is pointing to the violation message text in "messages.properties" file. 060 */ 061 public static final String MSG_KEY_FORMAT_REGULAR = "google.method.name.format.regular"; 062 063 /** 064 * A key is pointing to the violation message text in "messages.properties" file. 065 */ 066 public static final String MSG_KEY_FORMAT_TEST = "google.method.name.format.test"; 067 068 /** 069 * A key is pointing to the violation message text in "messages.properties" file. 070 */ 071 public static final String MSG_KEY_UNDERSCORE_REGULAR = "google.method.name.underscore.regular"; 072 073 /** 074 * A key is pointing to the violation message text in "messages.properties" file. 075 */ 076 public static final String MSG_KEY_UNDERSCORE_TEST = "google.method.name.underscore.test"; 077 078 /** 079 * Pattern for valid regular method names in Google style. 080 * Format: start with lowercase, have at least 2 chars, optionally followed by numbering suffix. 081 * 082 * <p> 083 * Explanation: 084 * <ul> 085 * <li>{@code ^(?![a-z]$)} - Negative lookahead: cannot be single lowercase char</li> 086 * <li>{@code (?![a-z][A-Z])} - Negative lookahead: cannot be like "fO"</li> 087 * <li>{@code [a-z]} - Must start with lowercase</li> 088 * <li>{@code [a-z0-9]*+} - Followed by lowercase or digits</li> 089 * <li>{@code (?:[A-Z][a-z0-9]*+)*+} - CamelCase humps (uppercase followed by 090 * lowercase/digits)</li> 091 * <li>{@code $} - End of string (numbering suffix validated separately)</li> 092 * </ul> 093 */ 094 private static final Pattern REGULAR_METHOD_NAME_PATTERN = Pattern 095 .compile("^(?![a-z]$)(?![a-z][A-Z])[a-z][a-z0-9]*+(?:[A-Z][a-z0-9]*+)*+$"); 096 097 /** 098 * Pattern for valid test method names in Google style. 099 * <ul> 100 * <li>Single segment (no underscore): follows regular method naming rules</li> 101 * <li>Multi-segment (with underscore): each segment must be lowerCamelCase 102 * (contain at least one uppercase letter)</li> 103 * <li>Each segment must start with lowercase</li> 104 * <li>Each segment must be at least 2 characters long</li> 105 * <li>Not start with single lowercase followed by uppercase (e.g., "fO")</li> 106 * </ul> 107 */ 108 private static final Pattern TEST_METHOD_NAME_PATTERN = Pattern.compile( 109 "^(?)(?![a-z][A-Z])(?:[a-z][a-z0-9]*+(?:[A-Z][a-z0-9]*+)*+$" 110 + "|[a-z][a-z0-9]*+(?:[A-Z][a-z0-9]*+)++" 111 + "(?:_(?)(?![a-z][A-Z])[a-z][a-z0-9]*+(?:[A-Z][a-z0-9]*+)++)+)$"); 112 113 /** 114 * Pattern to strip trailing numbering suffix (underscore followed by digits). 115 */ 116 private static final Pattern NUMBERING_SUFFIX_PATTERN = Pattern.compile("(?:_[0-9]++)+$"); 117 118 /** 119 * Matches invalid underscore usage for regular methods: leading, trailing, double, 120 * or between any characters (letter-letter, letter-digit, digit-letter). 121 */ 122 private static final Pattern INVALID_UNDERSCORE_PATTERN_REGULAR = 123 Pattern.compile("^_|_$|__|[a-zA-Z]_[a-zA-Z]|[a-zA-Z]_\\d|\\d_[a-zA-Z]"); 124 125 /** 126 * Matches invalid underscore usage for test methods: leading, trailing, double, or between 127 * letter-digit/digit-letter. 128 */ 129 private static final Pattern INVALID_UNDERSCORE_PATTERN_TEST = 130 Pattern.compile("^_|_$|__|[a-zA-Z]_\\d|\\d_[a-zA-Z]"); 131 132 /** 133 * Set of JUnit 5 test annotation names that indicate a test method. 134 */ 135 private static final Set<String> TEST_ANNOTATIONS = Set.of( 136 "Test", 137 "org.junit.jupiter.api.Test", 138 "org.junit.Test", 139 "ParameterizedTest", 140 "org.junit.jupiter.params.ParameterizedTest", 141 "RepeatedTest", 142 "org.junit.jupiter.api.RepeatedTest"); 143 144 @Override 145 public int[] getDefaultTokens() { 146 return getRequiredTokens(); 147 } 148 149 @Override 150 public int[] getAcceptableTokens() { 151 return getRequiredTokens(); 152 } 153 154 @Override 155 public int[] getRequiredTokens() { 156 return new int[] {TokenTypes.METHOD_DEF}; 157 } 158 159 @Override 160 public void visitToken(DetailAST ast) { 161 if (!AnnotationUtil.hasOverrideAnnotation(ast)) { 162 final DetailAST nameAst = NullUtil.notNull(ast.findFirstToken(TokenTypes.IDENT)); 163 final String methodName = nameAst.getText(); 164 165 if (hasTestAnnotation(ast)) { 166 validateTestMethodName(nameAst, methodName); 167 } 168 else { 169 validateRegularMethodName(nameAst, methodName); 170 } 171 } 172 } 173 174 /** 175 * Checks if the method has any test annotation. 176 * 177 * @param methodDef the METHOD_DEF AST node 178 * @return true if the method has @Test, @ParameterizedTest, or @RepeatedTest annotation. 179 */ 180 private static boolean hasTestAnnotation(DetailAST methodDef) { 181 return AnnotationUtil.containsAnnotation(methodDef, TEST_ANNOTATIONS); 182 } 183 184 /** 185 * Validates a regular (non-test) method name according to Google style. 186 * 187 * @param nameAst the IDENT AST node containing the method name 188 * @param methodName the method name string 189 */ 190 private void validateRegularMethodName(DetailAST nameAst, String methodName) { 191 if (INVALID_UNDERSCORE_PATTERN_REGULAR.matcher(methodName).find()) { 192 log(nameAst, MSG_KEY_UNDERSCORE_REGULAR, methodName); 193 } 194 else { 195 final String nameWithoutNumberingSuffix = NUMBERING_SUFFIX_PATTERN 196 .matcher(methodName).replaceAll(""); 197 if (!REGULAR_METHOD_NAME_PATTERN.matcher(nameWithoutNumberingSuffix).matches()) { 198 log(nameAst, MSG_KEY_FORMAT_REGULAR, methodName); 199 } 200 } 201 } 202 203 /** 204 * Validates a test method name according to Google style. 205 * 206 * @param nameAst the IDENT AST node containing the method name 207 * @param methodName the method name string 208 */ 209 private void validateTestMethodName(DetailAST nameAst, String methodName) { 210 211 if (INVALID_UNDERSCORE_PATTERN_TEST.matcher(methodName).find()) { 212 log(nameAst, MSG_KEY_UNDERSCORE_TEST, methodName); 213 } 214 else { 215 216 final String nameWithoutSuffix = NUMBERING_SUFFIX_PATTERN 217 .matcher(methodName).replaceAll(""); 218 if (!TEST_METHOD_NAME_PATTERN.matcher(nameWithoutSuffix).matches()) { 219 log(nameAst, MSG_KEY_FORMAT_TEST, methodName); 220 } 221 } 222 } 223}