View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2026 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ///////////////////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle.checks.naming;
21  
22  import java.util.Set;
23  import java.util.regex.Pattern;
24  
25  import com.puppycrawl.tools.checkstyle.StatelessCheck;
26  import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
27  import com.puppycrawl.tools.checkstyle.api.DetailAST;
28  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
29  import com.puppycrawl.tools.checkstyle.utils.AnnotationUtil;
30  import com.puppycrawl.tools.checkstyle.utils.NullUtil;
31  
32  /**
33   * <div>
34   * Checks that method names conform to the
35   * <a href=
36   * "https://google.github.io/styleguide/javaguide.html#s5.2.3-method-names">
37   * Google Java Style Guide</a> for method naming.
38   * </div>
39   *
40   * <p>Regular method names must:</p>
41   * <ul>
42   * <li>Be at least 2 characters long.</li>
43   * <li>Start with a lowercase letter.</li>
44   * <li>Not have a single lowercase letter followed by uppercase.</li>
45   * <li>Not have a consecutive uppercase.</li>
46   * <li>Contain only letters and digits and underscores.</li>
47   * </ul>
48   *
49   * <p>Test method names must:</p>
50   * <ul>
51   * <li>Be at least 2 characters long per segment.</li>
52   * <li>Start each segment with a lowercase letter.</li>
53   * <li>Not have a single lowercase letter followed by uppercase in any segment.</li>
54   * <li>Not have a consecutive uppercase.</li>
55   * <li>Allow underscores to separate segment and digits.</li>
56   * <li>Not have leading, trailing, or consecutive underscores.</li>
57   * </ul>
58   *
59   * <p>
60   * Notes:
61   * Methods annotated with {@code @Override} are ignored, as they must
62   * match the name defined in the parent class or interface.
63   * </p>
64   *
65   * @since 13.10.0
66   */
67  @StatelessCheck
68  public class GoogleMethodNameCheck extends AbstractCheck {
69  
70      /**
71       * A key is pointing to the violation message text in "messages.properties" file.
72       */
73      public static final String MSG_KEY_FORMAT_REGULAR = "google.method.name.format.regular";
74  
75      /**
76       * A key is pointing to the violation message text in "messages.properties" file.
77       */
78      public static final String MSG_KEY_FORMAT_TEST = "google.method.name.format.test";
79  
80      /**
81       * A key is pointing to the violation message text in "messages.properties" file.
82       */
83      public static final String MSG_KEY_UNDERSCORE_REGULAR = "google.method.name.underscore.regular";
84  
85      /**
86       * A key is pointing to the violation message text in "messages.properties" file.
87       */
88      public static final String MSG_KEY_UNDERSCORE_TEST = "google.method.name.underscore.test";
89  
90      /**
91       * Pattern for valid regular method names in Google style.
92       *
93       * <p>
94       * Explanation:
95       * <ul>
96       * <li>{@code ^} - Start of string</li>
97       * <li>{@code [a-z]} - Must start with a single lowercase letter</li>
98       * <li>{@code [a-z0-9]++} - One or more lowercase letters or digits possessively
99       * (ensures the initial lowercase block is at least 2 characters long)</li>
100      * <li>{@code (?:[A-Z][a-z0-9]++)*+} - Zero or more camelCase humps possessively,
101      * where each uppercase letter must be followed by one or more lowercase letters or digits</li>
102      * <li>{@code [A-Z]?} - An optional single uppercase letter allowed at the very end</li>
103      * <li>{@code $} - End of string</li>
104      * </ul>
105      */
106     private static final Pattern REGULAR_METHOD_NAME_PATTERN = Pattern
107             .compile("^[a-z][a-z0-9]++(?:[A-Z][a-z0-9]++)*+[A-Z]?$");
108     /**
109      * Pattern for valid test method names in Google style.
110      * <ul>
111      * <li>Single segment (no underscore): follows regular method naming rules</li>
112      * <li>Multi-segment (with underscore): each segment must be a valid lowerCamelCase name</li>
113      * <li>Each segment must start with lowercase</li>
114      * <li>Each segment must be at least 2 characters long</li>
115      * <li>No segment may start with a single lowercase followed by uppercase (e.g., "fO")</li>
116      * </ul>
117      */
118     private static final Pattern TEST_METHOD_NAME_PATTERN = Pattern.compile(
119             "^(?:[a-z][a-z0-9]++(?:[A-Z][a-z0-9]++)*+[A-Z]?$"
120                     + "|[a-z][a-z0-9]++(?:[A-Z][a-z0-9]++)*+[A-Z]?"
121                     + "(?:_[a-z][a-z0-9]++(?:[A-Z][a-z0-9]++)*+[A-Z]?)+)$");
122 
123     /**
124      * Pattern to strip trailing numbering suffix (underscore followed by digits).
125      */
126     private static final Pattern NUMBERING_SUFFIX_PATTERN = Pattern.compile("(?:_[0-9]++)+$");
127 
128     /**
129      * Matches invalid underscore usage for regular methods: leading, trailing, double,
130      * or between any characters (letter-letter, letter-digit, digit-letter).
131      */
132     private static final Pattern INVALID_UNDERSCORE_PATTERN_REGULAR =
133         Pattern.compile("^_|_$|__|[a-zA-Z]_[a-zA-Z]|[a-zA-Z]_\\d|\\d_[a-zA-Z]");
134 
135     /**
136      * Matches invalid underscore usage for test methods: leading, trailing, or double.
137      */
138     private static final Pattern INVALID_UNDERSCORE_PATTERN_TEST =
139         Pattern.compile("^_|_$|__|[a-zA-Z]_\\d|\\d_[a-zA-Z]");
140 
141     /**
142      * Set of JUnit test annotation names that indicate a test method.
143      */
144     private static final Set<String> TEST_ANNOTATIONS = Set.of(
145             "Test",
146             "org.junit.jupiter.api.Test",
147             "org.junit.Test",
148             "ParameterizedTest",
149             "org.junit.jupiter.params.ParameterizedTest",
150             "RepeatedTest",
151             "org.junit.jupiter.api.RepeatedTest",
152             "TestFactory",
153             "org.junit.jupiter.api.TestFactory"
154     );
155 
156     /** Creates a new {@code GoogleMethodNameCheck} instance. */
157     public GoogleMethodNameCheck() {
158         // no code by default
159     }
160 
161     @Override
162     public int[] getDefaultTokens() {
163         return getRequiredTokens();
164     }
165 
166     @Override
167     public int[] getAcceptableTokens() {
168         return getRequiredTokens();
169     }
170 
171     @Override
172     public int[] getRequiredTokens() {
173         return new int[] {TokenTypes.METHOD_DEF};
174     }
175 
176     @Override
177     public void visitToken(DetailAST ast) {
178         if (!AnnotationUtil.hasOverrideAnnotation(ast)) {
179             final DetailAST nameAst = getIdent(ast);
180             final String methodName = nameAst.getText();
181             if (hasTestAnnotation(ast)) {
182                 validateTestMethodName(nameAst, methodName);
183             }
184             else {
185                 validateRegularMethodName(nameAst, methodName);
186             }
187         }
188     }
189 
190     /**
191      * Returns the IDENT node of the given AST.
192      *
193      * @param ast the AST node
194      * @return the IDENT child node
195      */
196     private static DetailAST getIdent(DetailAST ast) {
197         return NullUtil.notNull(ast.findFirstToken(TokenTypes.IDENT));
198     }
199 
200     /**
201      * Checks if the method has any test annotation.
202      *
203      * @param methodDef the METHOD_DEF AST node
204      * @return true if the method has @Test, @ParameterizedTest, or @RepeatedTest annotation.
205      */
206     private static boolean hasTestAnnotation(DetailAST methodDef) {
207         return AnnotationUtil.containsAnnotation(methodDef, TEST_ANNOTATIONS);
208     }
209 
210     /**
211      * Validates a regular (non-test) method name according to Google style.
212      *
213      * @param nameAst    the IDENT AST node containing the method name
214      * @param methodName the method name string
215      */
216     private void validateRegularMethodName(DetailAST nameAst, String methodName) {
217         if (INVALID_UNDERSCORE_PATTERN_REGULAR.matcher(methodName).find()) {
218             log(nameAst, MSG_KEY_UNDERSCORE_REGULAR, methodName);
219         }
220         else {
221             final String nameWithoutNumberingSuffix = NUMBERING_SUFFIX_PATTERN
222                     .matcher(methodName).replaceAll("");
223             if (!REGULAR_METHOD_NAME_PATTERN.matcher(nameWithoutNumberingSuffix).matches()) {
224                 log(nameAst, MSG_KEY_FORMAT_REGULAR, methodName);
225             }
226         }
227     }
228 
229     /**
230      * Validates a test method name according to Google style.
231      *
232      * @param nameAst    the IDENT AST node containing the method name
233      * @param methodName the method name string
234      */
235     private void validateTestMethodName(DetailAST nameAst, String methodName) {
236 
237         if (INVALID_UNDERSCORE_PATTERN_TEST.matcher(methodName).find()) {
238             log(nameAst, MSG_KEY_UNDERSCORE_TEST, methodName);
239         }
240         else {
241 
242             final String nameWithoutSuffix = NUMBERING_SUFFIX_PATTERN
243                     .matcher(methodName).replaceAll("");
244             if (!TEST_METHOD_NAME_PATTERN.matcher(nameWithoutSuffix).matches()) {
245                 log(nameAst, MSG_KEY_FORMAT_TEST, methodName);
246             }
247         }
248     }
249 
250 }