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.1.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     * Each segment between underscores must:
100     * <ul>
101     * <li>Start with lowercase</li>
102     * <li>Be at least 2 characters long</li>
103     * <li>Not start with single lowercase followed by uppercase (e.g., "fO")</li>
104     * </ul>
105     */
106    private static final Pattern TEST_METHOD_NAME_PATTERN = Pattern.compile(
107            "^(?![a-z](?:_|$))(?![a-z][A-Z])[a-z][a-z0-9]*+(?:[A-Z][a-z0-9]*+)*+"
108            + "(?:_(?![a-z](?:_|$))(?![a-z][A-Z])[a-z][a-z0-9]*+(?:[A-Z][a-z0-9]*+)*+)*+$");
109
110    /**
111     * Pattern to strip trailing numbering suffix (underscore followed by digits).
112     */
113    private static final Pattern NUMBERING_SUFFIX_PATTERN = Pattern.compile("(?:_[0-9]++)+$");
114
115    /**
116     * Matches invalid underscore usage for regular methods: leading, trailing, double,
117     * or between any characters (letter-letter, letter-digit, digit-letter).
118     */
119    private static final Pattern INVALID_UNDERSCORE_PATTERN_REGULAR =
120        Pattern.compile("^_|_$|__|[a-zA-Z]_[a-zA-Z]|[a-zA-Z]_\\d|\\d_[a-zA-Z]");
121
122    /**
123     * Matches invalid underscore usage for test methods: leading, trailing, double, or between
124     * letter-digit/digit-letter.
125     */
126    private static final Pattern INVALID_UNDERSCORE_PATTERN_TEST =
127        Pattern.compile("^_|_$|__|[a-zA-Z]_\\d|\\d_[a-zA-Z]");
128
129    /**
130     * Set of JUnit 5 test annotation names that indicate a test method.
131     */
132    private static final Set<String> TEST_ANNOTATIONS = Set.of(
133            "Test",
134            "org.junit.jupiter.api.Test",
135            "org.junit.Test",
136            "ParameterizedTest",
137            "org.junit.jupiter.params.ParameterizedTest",
138            "RepeatedTest",
139            "org.junit.jupiter.api.RepeatedTest");
140
141    @Override
142    public int[] getDefaultTokens() {
143        return getRequiredTokens();
144    }
145
146    @Override
147    public int[] getAcceptableTokens() {
148        return getRequiredTokens();
149    }
150
151    @Override
152    public int[] getRequiredTokens() {
153        return new int[] {TokenTypes.METHOD_DEF};
154    }
155
156    @Override
157    public void visitToken(DetailAST ast) {
158        if (!AnnotationUtil.hasOverrideAnnotation(ast)) {
159            final DetailAST nameAst = NullUtil.notNull(ast.findFirstToken(TokenTypes.IDENT));
160            final String methodName = nameAst.getText();
161
162            if (hasTestAnnotation(ast)) {
163                validateTestMethodName(nameAst, methodName);
164            }
165            else {
166                validateRegularMethodName(nameAst, methodName);
167            }
168        }
169    }
170
171    /**
172     * Checks if the method has any test annotation.
173     *
174     * @param methodDef the METHOD_DEF AST node
175     * @return true if the method has @Test, @ParameterizedTest, or @RepeatedTest annotation.
176     */
177    private static boolean hasTestAnnotation(DetailAST methodDef) {
178        return AnnotationUtil.containsAnnotation(methodDef, TEST_ANNOTATIONS);
179    }
180
181    /**
182     * Validates a regular (non-test) method name according to Google style.
183     *
184     * @param nameAst    the IDENT AST node containing the method name
185     * @param methodName the method name string
186     */
187    private void validateRegularMethodName(DetailAST nameAst, String methodName) {
188        if (INVALID_UNDERSCORE_PATTERN_REGULAR.matcher(methodName).find()) {
189            log(nameAst, MSG_KEY_UNDERSCORE_REGULAR, methodName);
190        }
191        else {
192            final String nameWithoutNumberingSuffix = NUMBERING_SUFFIX_PATTERN
193                    .matcher(methodName).replaceAll("");
194            if (!REGULAR_METHOD_NAME_PATTERN.matcher(nameWithoutNumberingSuffix).matches()) {
195                log(nameAst, MSG_KEY_FORMAT_REGULAR, methodName);
196            }
197        }
198    }
199
200    /**
201     * Validates a test method name according to Google style.
202     *
203     * @param nameAst    the IDENT AST node containing the method name
204     * @param methodName the method name string
205     */
206    private void validateTestMethodName(DetailAST nameAst, String methodName) {
207
208        if (INVALID_UNDERSCORE_PATTERN_TEST.matcher(methodName).find()) {
209            log(nameAst, MSG_KEY_UNDERSCORE_TEST, methodName);
210        }
211        else {
212
213            final String nameWithoutSuffix = NUMBERING_SUFFIX_PATTERN
214                    .matcher(methodName).replaceAll("");
215            if (!TEST_METHOD_NAME_PATTERN.matcher(nameWithoutSuffix).matches()) {
216                log(nameAst, MSG_KEY_FORMAT_TEST, methodName);
217            }
218        }
219    }
220}