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 non-constant field 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 non-constant field naming requirements:
041 * </p>
042 * <ul>
043 * <li>Non-constant field 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 * <p>
050 * Static fields are skipped because Checkstyle cannot determine type immutability
051 * to distinguish constants from non-constants.
052 * </p>
053 *
054 * @since 13.2.0
055 */
056@StatelessCheck
057public class GoogleNonConstantFieldNameCheck extends AbstractCheck {
058
059    /**
060     * A key is pointing to the violation message text in "messages.properties" file.
061     */
062    public static final String MSG_KEY_INVALID_FORMAT = "google.non.constant.field.name.format";
063
064    /**
065     * A key pointing to the violation message for invalid underscore usage.
066     */
067    public static final String MSG_KEY_INVALID_UNDERSCORE =
068            "google.non.constant.field.name.underscore";
069
070    /**
071     * Pattern for valid non-constant field name in Google style.
072     * Format: start with lowercase, have at least 2 chars, optionally followed by numbering suffix.
073     *
074     * <p>
075     * Explanation:
076     * <ul>
077     * <li>{@code ^(?![a-z]$)} - Negative lookahead: cannot be single lowercase char</li>
078     * <li>{@code (?![a-z][A-Z])} - Negative lookahead: cannot be like "fO"</li>
079     * <li>{@code [a-z]} - Must start with lowercase</li>
080     * <li>{@code [a-z0-9]*+} - Followed by lowercase or digits</li>
081     * <li>{@code (?:[A-Z][a-z0-9]*+)*+} - CamelCase humps (uppercase followed by lowercase)</li>
082     * <li>{@code $} - End of string (numbering suffix validated separately)</li>
083     * </ul>
084     */
085    private static final Pattern NON_CONSTANT_FIELD_NAME_PATTERN = Pattern
086            .compile("^(?![a-z]$)(?![a-z][A-Z])[a-z][a-z0-9]*+(?:[A-Z][a-z0-9]*+)*+$");
087
088    /**
089     * Pattern to strip trailing numbering suffix (underscore followed by digits).
090     */
091    private static final Pattern NUMBERING_SUFFIX_PATTERN = Pattern.compile("(?:_[0-9]++)+$");
092
093    /**
094     * Pattern to detect invalid underscore usage: leading, trailing, consecutive,
095     * or between letter-letter, letter-digit, or digit-letter combinations.
096     */
097    private static final Pattern INVALID_UNDERSCORE_PATTERN =
098            Pattern.compile("^_|_$|__|[a-zA-Z]_[a-zA-Z]|[a-zA-Z]_\\d|\\d_[a-zA-Z]");
099
100    @Override
101    public int[] getDefaultTokens() {
102        return getRequiredTokens();
103    }
104
105    @Override
106    public int[] getAcceptableTokens() {
107        return getRequiredTokens();
108    }
109
110    @Override
111    public int[] getRequiredTokens() {
112        return new int[] {TokenTypes.VARIABLE_DEF};
113    }
114
115    @Override
116    public void visitToken(DetailAST ast) {
117        if (mustCheckName(ast)) {
118            final DetailAST nameAst = NullUtil.notNull(ast.findFirstToken(TokenTypes.IDENT));
119            final String fieldName = nameAst.getText();
120
121            validateNonConstantFieldName(nameAst, fieldName);
122        }
123    }
124
125    /**
126     * Checks if this field should be validated. Returns true for instance fields only.
127     * Static fields are excluded because Checkstyle cannot determine type immutability.
128     * Local variables and interface/annotation fields are also excluded.
129     *
130     * @param ast the VARIABLE_DEF AST node
131     * @return true if this variable should be checked
132     */
133    private static boolean mustCheckName(DetailAST ast) {
134        final DetailAST modifiersAST = NullUtil.notNull(ast.findFirstToken(TokenTypes.MODIFIERS));
135        final boolean isStatic =
136            modifiersAST.findFirstToken(TokenTypes.LITERAL_STATIC) != null;
137
138        return !isStatic
139            && !ScopeUtil.isInInterfaceOrAnnotationBlock(ast)
140            && !ScopeUtil.isLocalVariableDef(ast);
141    }
142
143    /**
144     * Validates a non-constant field according to Google style.
145     *
146     * @param nameAst    the IDENT AST node containing the field name
147     * @param fieldName the field name string
148     */
149    private void validateNonConstantFieldName(DetailAST nameAst, String fieldName) {
150        if (INVALID_UNDERSCORE_PATTERN.matcher(fieldName).find()) {
151            log(nameAst, MSG_KEY_INVALID_UNDERSCORE, fieldName);
152        }
153        else {
154            final String nameWithoutNumberingSuffix = NUMBERING_SUFFIX_PATTERN
155                    .matcher(fieldName).replaceAll("");
156            if (!NON_CONSTANT_FIELD_NAME_PATTERN.matcher(nameWithoutNumberingSuffix).matches()) {
157                log(nameAst, MSG_KEY_INVALID_FORMAT, fieldName);
158            }
159        }
160    }
161}