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-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-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}