Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,6 @@

package com.google.errorprone.bugpatterns;

import static com.google.common.base.Ascii.isUpperCase;
import static com.google.common.base.Ascii.toLowerCase;
import static com.google.common.base.Ascii.toUpperCase;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
Expand All @@ -34,7 +31,6 @@
import static com.google.errorprone.util.ASTHelpers.getSymbol;
import static com.google.errorprone.util.ASTHelpers.hasAnnotation;
import static com.google.errorprone.util.ASTHelpers.isStatic;
import static java.lang.Character.isDigit;
import static java.util.stream.Collectors.joining;
import static javax.lang.model.element.ElementKind.BINDING_VARIABLE;
import static javax.lang.model.element.ElementKind.EXCEPTION_PARAMETER;
Expand All @@ -45,7 +41,6 @@
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableSet;
import com.google.errorprone.BugPattern;
import com.google.errorprone.ErrorProneFlags;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker.ClassTreeMatcher;
import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher;
Expand Down Expand Up @@ -104,25 +99,24 @@ public final class IdentifierName extends BugChecker
", with acronyms treated as words"
+ " (https://google.github.io/styleguide/javaguide.html#s5.3-camel-case)";

private final boolean allowInitialismsInTypeName;
private final IdentifierNames identifierNames;

@Inject
IdentifierName(ErrorProneFlags flags) {
this.allowInitialismsInTypeName =
flags.getBoolean("IdentifierName:AllowInitialismsInTypeName").orElse(false);
IdentifierName(IdentifierNames identifierNames) {
this.identifierNames = identifierNames;
}

@Override
public Description matchClass(ClassTree tree, VisitorState state) {
ClassSymbol symbol = getSymbol(tree);
String name = tree.getSimpleName().toString();
if (name.isEmpty() || isConformantTypeName(name)) {
if (name.isEmpty() || identifierNames.isConformantTypeName(name)) {
// The name can be empty for enum member declarations, which are desugared early to class
// declarations.
return NO_MATCH;
}
String renamed = suggestedClassRename(name);
String suggested = allowInitialismsInTypeName ? renamed : fixInitialisms(renamed);
String suggested = identifierNames.fixInitialismsIfNeeded(renamed);
boolean fixable = !suggested.equals(name) && canBeRemoved(symbol);
String diagnostic =
"Classes should be named in UpperCamelCase"
Expand Down Expand Up @@ -171,7 +165,7 @@ public Description matchMethod(MethodTree tree, VisitorState state) {
return NO_MATCH;
}
String renamed = suggestedRename(symbol, name);
String suggested = fixInitialisms(renamed);
String suggested = IdentifierNames.fixInitialisms(renamed);
boolean fixable = !suggested.equals(name) && canBeRemoved(symbol, state);
String diagnostic =
"Methods and non-static variables should be named in lowerCamelCase"
Expand Down Expand Up @@ -222,7 +216,7 @@ public Description matchVariable(VariableTree tree, VisitorState state) {
return NO_MATCH;
}
String renamed = suggestedRename(symbol, name);
String suggested = fixInitialisms(renamed);
String suggested = IdentifierNames.fixInitialisms(renamed);
boolean fixable = !suggested.equals(name) && canBeRenamed(symbol);
String diagnostic =
(isStaticVariable(symbol) ? STATIC_VARIABLE_FINDING : message())
Expand Down Expand Up @@ -284,56 +278,18 @@ private static boolean isConformant(Symbol symbol, String name) {
if (name.isEmpty()) {
return true;
}
return isConformantLowerCamelName(name);
return IdentifierNames.isConformantLowerCamelName(name);
}

private static boolean isConformantStaticVariableName(String name) {
return UPPER_UNDERSCORE_PATTERN.matcher(name).matches();
}

private static boolean isConformantLowerCamelName(String name) {
return underscoresAreFlankedByDigits(name)
&& !isUpperCase(name.charAt(0))
&& !PROBABLE_INITIALISM.matcher(name).find();
}

private boolean isConformantTypeName(String name) {
return underscoresAreFlankedByDigits(name)
&& isUpperCase(name.charAt(0))
&& (allowInitialismsInTypeName || !PROBABLE_INITIALISM.matcher(name).find());
}

private static boolean underscoresAreFlankedByDigits(String name) {
if (name.startsWith("_") || name.endsWith("_")) {
return false;
}
for (int i = 1; i < name.length() - 1; i++) {
if (name.charAt(i) == '_') {
boolean flankedByDigits = isDigit(name.charAt(i - 1)) && isDigit(name.charAt(i + 1));
if (!flankedByDigits) {
return false;
}
}
}
return true;
}

private static boolean isStaticVariable(Symbol symbol) {
return symbol instanceof VarSymbol && isStatic(symbol);
}

private static String fixInitialisms(String input) {
return PROBABLE_INITIALISM.matcher(input).replaceAll(r -> titleCase(r.group(1)) + r.group(2));
}

private static String titleCase(String input) {
String lower = toLowerCase(input);
return toUpperCase(lower.charAt(0)) + lower.substring(1);
}

private static final Pattern LOWER_UNDERSCORE_PATTERN = Pattern.compile("[a-z0-9_]+");
private static final Pattern UPPER_UNDERSCORE_PATTERN = Pattern.compile("[A-Z0-9_]+");
private static final java.util.regex.Pattern PROBABLE_INITIALISM =
java.util.regex.Pattern.compile("([A-Z]{2,})([A-Z][^A-Z]|$)");
private static final Splitter UNDERSCORE_SPLITTER = Splitter.on('_');
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
* Copyright 2025 The Error Prone Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.errorprone.bugpatterns;

import static com.google.common.base.Ascii.isUpperCase;
import static com.google.common.base.Ascii.toLowerCase;
import static com.google.common.base.Ascii.toUpperCase;
import static java.lang.Character.isDigit;

import com.google.errorprone.ErrorProneFlags;
import java.util.regex.Pattern;
import javax.inject.Inject;

/** Utilities for dealing with identifier names. */
final class IdentifierNames {

private final boolean allowInitialismsInTypeName;

@Inject
IdentifierNames(ErrorProneFlags flags) {
this.allowInitialismsInTypeName =
flags.getBoolean("IdentifierName:AllowInitialismsInTypeName").orElse(false);
}

static boolean isConformantLowerCamelName(String name) {
return underscoresAreFlankedByDigits(name)
&& !isUpperCase(name.charAt(0))
&& !PROBABLE_INITIALISM.matcher(name).find();
}

boolean isConformantTypeName(String name) {
return underscoresAreFlankedByDigits(name)
&& isUpperCase(name.charAt(0))
&& (allowInitialismsInTypeName || !PROBABLE_INITIALISM.matcher(name).find());
}

private static boolean underscoresAreFlankedByDigits(String name) {
if (name.startsWith("_") || name.endsWith("_")) {
return false;
}
for (int i = 1; i < name.length() - 1; i++) {
if (name.charAt(i) == '_') {
boolean flankedByDigits = isDigit(name.charAt(i - 1)) && isDigit(name.charAt(i + 1));
if (!flankedByDigits) {
return false;
}
}
}
return true;
}

String fixInitialismsIfNeeded(String input) {
return allowInitialismsInTypeName ? input : fixInitialisms(input);
}

static String fixInitialisms(String input) {
return PROBABLE_INITIALISM.matcher(input).replaceAll(r -> titleCase(r.group(1)) + r.group(2));
}

private static String titleCase(String input) {
String lower = toLowerCase(input);
return toUpperCase(lower.charAt(0)) + lower.substring(1);
}

private static final Pattern PROBABLE_INITIALISM = Pattern.compile("([A-Z]{2,})([A-Z][^A-Z]|$)");
}
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ public Description matchInstanceOf(InstanceOfTree instanceOfTree, VisitorState s

private static String generateVariableName(Type targetType, VisitorState state) {
Type unboxed = state.getTypes().unboxedType(targetType);
String simpleName = targetType.tsym.getSimpleName().toString();
String simpleName = IdentifierNames.fixInitialisms(targetType.tsym.getSimpleName().toString());
String lowerFirstLetter = toLowerCase(String.valueOf(simpleName.charAt(0)));
String camelCased = lowerFirstLetter + simpleName.substring(1);
if (SourceVersion.isKeyword(camelCased)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -969,4 +969,42 @@ void rawTypeNecessary(ArrayList<Integer> l) {}
""")
.doTest(BugCheckerRefactoringTestHelper.TestMode.TEXT_MATCH);
}

@Test
public void unstylishTypeName_stylishIdentifierName() {
helper
.addInputLines(
"Class.java",
"""
import java.io.InterruptedIOException;
import java.sql.SQLException;

class Class {
void test(Object e) {
if (e instanceof InterruptedIOException) {
test((InterruptedIOException) e);
} else if (e instanceof SQLException) {
test((SQLException) e);
}
}
}
""")
.addOutputLines(
"Class.java",
"""
import java.io.InterruptedIOException;
import java.sql.SQLException;

class Class {
void test(Object e) {
if (e instanceof InterruptedIOException interruptedIoException) {
test(interruptedIoException);
} else if (e instanceof SQLException sqlException) {
test(sqlException);
}
}
}
""")
.doTest();
}
}
Loading