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>Regular method names must:</p>
041 * <ul>
042 * <li>Be at least 2 characters long.</li>
043 * <li>Start with a lowercase letter.</li>
044 * <li>Not have a single lowercase letter followed by uppercase.</li>
045 * <li>Not have a consecutive uppercase.</li>
046 * <li>Contain only letters and digits and underscores.</li>
047 * </ul>
048 *
049 * <p>Test method names must:</p>
050 * <ul>
051 * <li>Be at least 2 characters long per segment.</li>
052 * <li>Start each segment with a lowercase letter.</li>
053 * <li>Not have a single lowercase letter followed by uppercase in any segment.</li>
054 * <li>Not have a consecutive uppercase.</li>
055 * <li>Allow underscores to separate segment and digits.</li>
056 * <li>Not have leading, trailing, or consecutive underscores.</li>
057 * </ul>
058 *
059 * <p>
060 * Notes:
061 * Methods annotated with {@code @Override} are ignored, as they must
062 * match the name defined in the parent class or interface.
063 * </p>
064 *
065 * @since 13.10.0
066 */
067@StatelessCheck
068public class GoogleMethodNameCheck extends AbstractCheck {
069
070    /**
071     * A key is pointing to the violation message text in "messages.properties" file.
072     */
073    public static final String MSG_KEY_FORMAT_REGULAR = "google.method.name.format.regular";
074
075    /**
076     * A key is pointing to the violation message text in "messages.properties" file.
077     */
078    public static final String MSG_KEY_FORMAT_TEST = "google.method.name.format.test";
079
080    /**
081     * A key is pointing to the violation message text in "messages.properties" file.
082     */
083    public static final String MSG_KEY_UNDERSCORE_REGULAR = "google.method.name.underscore.regular";
084
085    /**
086     * A key is pointing to the violation message text in "messages.properties" file.
087     */
088    public static final String MSG_KEY_UNDERSCORE_TEST = "google.method.name.underscore.test";
089
090    /**
091     * Pattern for valid regular method names in Google style.
092     *
093     * <p>
094     * Explanation:
095     * <ul>
096     * <li>{@code ^} - Start of string</li>
097     * <li>{@code [a-z]} - Must start with a single lowercase letter</li>
098     * <li>{@code [a-z0-9]++} - One or more lowercase letters or digits possessively
099     * (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}