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.regex.Pattern;
023
024import com.puppycrawl.tools.checkstyle.StatelessCheck;
025import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
026import com.puppycrawl.tools.checkstyle.api.DetailAST;
027import com.puppycrawl.tools.checkstyle.api.TokenTypes;
028import com.puppycrawl.tools.checkstyle.utils.NullUtil;
029import com.puppycrawl.tools.checkstyle.utils.ScopeUtil;
030
031/**
032 * <div>
033 * Checks that member names conform to the
034 * <a href=
035 * "https://google.github.io/styleguide/javaguide.html#s5.2.5-non-constant-field-names">
036 * Google Java Style Guide</a> for non-constant field naming.
037 * </div>
038 *
039 * <p>
040 * This check enforces Google's specific member naming requirements:
041 * </p>
042 * <ul>
043 * <li>Member names must start with a lowercase letter and use uppercase letters
044 * for word boundaries.</li>
045 * <li>Underscores may be used to separate adjacent numbers (e.g., version
046 * numbers like {@code guava33_4_5}), but NOT between letters and digits.</li>
047 * </ul>
048 *
049 * @since 13.1.0
050 */
051@StatelessCheck
052public class GoogleMemberNameCheck extends AbstractCheck {
053
054    /**
055     * A key is pointing to the violation message text in "messages.properties" file.
056     */
057    public static final String MSG_KEY_INVALID_FORMAT = "google.member.name.format";
058
059    /**
060     * A key pointing to the violation message for invalid underscore usage.
061     */
062    public static final String MSG_KEY_INVALID_UNDERSCORE = "google.member.name.underscore";
063
064    /**
065     * Pattern for valid member names in Google style.
066     * Format: lowerCamelCase, optionally followed by numbering suffix.
067     *
068     * <p>
069     * Explanation:
070     * <ul>
071     * <li>{@code ^(?![a-z]$)} - Negative lookahead: cannot be single lowercase char</li>
072     * <li>{@code (?![a-z][A-Z])} - Negative lookahead: cannot be like "fO"</li>
073     * <li>{@code [a-z]} - Must start with lowercase</li>
074     * <li>{@code [a-z0-9]*} - Followed by lowercase or digits</li>
075     * <li>{@code (?:[A-Z][a-z0-9]*)*} - CamelCase humps (uppercase followed by lowercase)</li>
076     * <li>{@code $} - End of string (numbering suffix validated separately)</li>
077     * </ul>
078     */
079    private static final Pattern MEMBER_NAME_PATTERN = Pattern
080            .compile("^(?![a-z]$)(?![a-z][A-Z])[a-z][a-z0-9]*(?:[A-Z][a-z0-9]*)*$");
081
082    /**
083     * Pattern to strip trailing numbering suffix (underscore followed by digits).
084     */
085    private static final Pattern NUMBERING_SUFFIX_PATTERN = Pattern.compile("(?:_[0-9]+)+$");
086
087    /**
088     * Pattern to detect invalid underscore usage: leading, trailing, consecutive,
089     * or between letter-letter, letter-digit, or digit-letter combinations.
090     */
091    private static final Pattern INVALID_UNDERSCORE_PATTERN =
092            Pattern.compile("^_|_$|__|[a-zA-Z]_[a-zA-Z]|[a-zA-Z]_\\d|\\d_[a-zA-Z]");
093
094    @Override
095    public int[] getDefaultTokens() {
096        return getRequiredTokens();
097    }
098
099    @Override
100    public int[] getAcceptableTokens() {
101        return getRequiredTokens();
102    }
103
104    @Override
105    public int[] getRequiredTokens() {
106        return new int[] {TokenTypes.VARIABLE_DEF};
107    }
108
109    @Override
110    public void visitToken(DetailAST ast) {
111        if (mustCheckName(ast)) {
112            final DetailAST nameAst = NullUtil.notNull(ast.findFirstToken(TokenTypes.IDENT));
113            final String memberName = nameAst.getText();
114
115            validateMemberName(nameAst, memberName);
116        }
117    }
118
119    /**
120     * Checks if this field should be validated. Returns true for instance fields
121     * and static non-final fields. Constants (static final), local variables,
122     * and interface/annotation fields are excluded.
123     *
124     * @param ast the VARIABLE_DEF AST node
125     * @return true if this variable should be checked
126     */
127    private static boolean mustCheckName(DetailAST ast) {
128        final DetailAST modifiersAST = NullUtil.notNull(ast.findFirstToken(TokenTypes.MODIFIERS));
129        final boolean isStatic =
130            modifiersAST.findFirstToken(TokenTypes.LITERAL_STATIC) != null;
131        final boolean isFinal =
132            modifiersAST.findFirstToken(TokenTypes.FINAL) != null;
133
134        final boolean isConstant = isStatic && isFinal;
135
136        return !isConstant
137            && !ScopeUtil.isInInterfaceOrAnnotationBlock(ast)
138            && !ScopeUtil.isLocalVariableDef(ast);
139    }
140
141    /**
142     * Validates a member name according to Google style.
143     *
144     * @param nameAst    the IDENT AST node containing the member name
145     * @param memberName the member name string
146     */
147    private void validateMemberName(DetailAST nameAst, String memberName) {
148        if (INVALID_UNDERSCORE_PATTERN.matcher(memberName).find()) {
149            log(nameAst, MSG_KEY_INVALID_UNDERSCORE, memberName);
150        }
151        else {
152            final String nameWithoutNumberingSuffix = NUMBERING_SUFFIX_PATTERN
153                    .matcher(memberName).replaceAll("");
154            if (!MEMBER_NAME_PATTERN.matcher(nameWithoutNumberingSuffix).matches()) {
155                log(nameAst, MSG_KEY_INVALID_FORMAT, memberName);
156            }
157        }
158    }
159}