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