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
161 changes: 161 additions & 0 deletions check_api/src/main/java/com/google/errorprone/ClassName.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
/*
* Copyright 2026 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;

import static com.google.common.base.Preconditions.checkArgument;
import static java.util.Objects.requireNonNull;

import com.google.errorprone.VisitorState.SharedState;
import com.google.errorprone.annotations.Immutable;
import com.google.errorprone.suppliers.Supplier;
import com.sun.tools.javac.code.Symbol.ClassSymbol;
import com.sun.tools.javac.code.Type;
import com.sun.tools.javac.util.Name;
import java.lang.ref.SoftReference;
import org.jspecify.annotations.Nullable;

/**
* Encapsulates a qualified class name and caches its resolved javac {@link ClassSymbol}, {@link
* Type}, and {@link Name} across a compilation.
*/
@Immutable
public final class ClassName {

/**
* Constructs a {@link ClassName} from a qualified canonical or binary class name (e.g. {@code
* "java.util.List"}, {@code "java.util.Map.Entry"}, or {@code "java.util.Map$Entry"}).
*
* @throws IllegalArgumentException if {@code name} is empty, ends with {@code '.'}, is
* primitive/void, is an array type ({@code "[...]"}), or is parameterized ({@code "<...>"})
*/
public static ClassName of(String name) {
requireNonNull(name);
validateClassName(name);
return new ClassName(VisitorState.inferBinaryName(name));
}

private record CacheEntry(
JavacInvocationInstance invocation,
Name binaryName,
@Nullable ClassSymbol symbol,
@Nullable SharedState lastAbsentSharedState) {}

private final String binaryNameString;

// Memoized per-Context javac state via SoftReference
@SuppressWarnings("Immutable")
private volatile SoftReference<CacheEntry> cache = new SoftReference<>(null);

// Stateless method reference delegating to type(VisitorState)
@SuppressWarnings("Immutable")
private final Supplier<@Nullable Type> typeSupplier = this::type;

private ClassName(String binaryNameString) {
this.binaryNameString = binaryNameString;
}

/** Returns the cached javac {@link Name} for the binary name. */
public Name binaryName(VisitorState state) {
JavacInvocationInstance invocation = state.javacInvocationInstance();
CacheEntry entry = cache.get();
if (entry != null && entry.invocation() == invocation) {
return entry.binaryName();
}
Name binaryName = state.getName(binaryNameString);
cache =
new SoftReference<>(
new CacheEntry(
invocation, binaryName, /* symbol= */ null, /* lastAbsentSharedState= */ null));
return binaryName;
}

/**
* Resolves and caches the {@link ClassSymbol} for this class in the current compilation, or
* returns {@code null} if the symbol is not available.
*/
public @Nullable ClassSymbol symbol(VisitorState state) {
JavacInvocationInstance invocation = state.javacInvocationInstance();
SharedState sharedState = state.sharedState();
CacheEntry entry = cache.get();
Name binaryName;
if (entry != null && entry.invocation() == invocation) {
if (entry.symbol() != null) {
return entry.symbol();
}
if (entry.lastAbsentSharedState() == sharedState) {
return null;
}
binaryName = entry.binaryName();
} else {
binaryName = state.getName(binaryNameString);
}
ClassSymbol sym = state.getSymbolFromName(binaryName);
cache =
new SoftReference<>(
new CacheEntry(invocation, binaryName, sym, sym == null ? sharedState : null));
return sym;
}

/**
* Resolves and caches the {@link Type} for this class in the current compilation, or returns
* {@code null} if the symbol is not available.
*/
public @Nullable Type type(VisitorState state) {
ClassSymbol sym = symbol(state);
return sym == null ? null : sym.asType();
}

/** Returns a {@link Supplier} view of {@link #type(VisitorState)}. */
public Supplier<@Nullable Type> asTypeSupplier() {
return typeSupplier;
}

@Override
public boolean equals(@Nullable Object obj) {
return obj instanceof ClassName that && this.binaryNameString.equals(that.binaryNameString);
}

@Override
public int hashCode() {
return binaryNameString.hashCode();
}

@Override
public String toString() {
return binaryNameString;
}

private static void validateClassName(String name) {
checkArgument(!name.isEmpty(), "class name must be non-empty");
checkArgument(name.charAt(name.length() - 1) != '.', "invalid class name: %s", name);
checkArgument(
!name.contains("[") && !name.contains("]"),
"Cannot create ClassName for array types (%s), build them using Suppliers.arrayOf() or"
+ " VisitorState.arrayTypeForType()",
name);
checkArgument(
!name.contains("<") && !name.contains(">"),
"Cannot create ClassName for generic types (%s), build them using VisitorState.getType()",
name);
switch (name) {
case "byte", "short", "int", "long", "float", "double", "boolean", "char", "void" ->
throw new IllegalArgumentException(
String.format("Cannot create ClassName for primitive or void type (%s)", name));
default -> {}
}
}
}
55 changes: 50 additions & 5 deletions check_api/src/main/java/com/google/errorprone/VisitorState.java
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,14 @@ public Map<String, SeverityLevel> severityMap() {
return sharedState.severityMap;
}

SharedState sharedState() {
return sharedState;
}

JavacInvocationInstance javacInvocationInstance() {
return sharedState.javacInvocationInstance;
}

public void reportMatch(Description description) {
checkNotNull(description, "Use Description.NO_MATCH to denote an absent finding.");
if (description == Description.NO_MATCH) {
Expand Down Expand Up @@ -291,10 +299,10 @@ public Name getName(String nameStr) {
/**
* Given the binary name of a class, returns the {@link Type}.
*
* <p>Prefer not to use this method for constant strings, or strings otherwise known at compile
* time. Instead, save the result of {@link
* com.google.errorprone.suppliers.Suppliers#typeFromString} as a class constant, and use its
* {@link Supplier#get} method to look up the Type when needed. This lookup will be faster,
* <p>Prefer not to use this method for constant class or interface name strings, or strings
* otherwise known at compile time. Instead, save the result of {@link ClassName#of} as a class
* constant, and use its {@link ClassName#type} method to look up the Type when needed (or use
* {@link #getSymtab()} for primitive and {@code void} types). This lookup will be faster,
* improving Error Prone's analysis time.
*
* <p>If this method returns null, the compiler doesn't have access to this type, which means that
Expand Down Expand Up @@ -329,6 +337,10 @@ public Name getName(String nameStr) {
}

/**
* Looks up the {@link Symbol} for a class name.
*
* <p>Prefer {@link ClassName#of} and {@link ClassName#symbol} for constant class names.
*
* @param symStr the string representation of a symbol
* @return the Symbol object, or null if it cannot be found
*/
Expand All @@ -341,6 +353,8 @@ public Name getName(String nameStr) {
* Returns the Name object corresponding to the named class, converting it to binary form along
* the way if necessary (i.e., replacing Foo.Bar with Foo$Bar). To get the Name corresponding to
* some string that is not a class name, see the more general {@link #getName(String)}.
*
* <p>Prefer {@link ClassName#of} and {@link ClassName#binaryName} for constant class names.
*/
public Name binaryNameFromClassname(String className) {
return getName(inferBinaryName(className));
Expand All @@ -352,6 +366,36 @@ public Name binaryNameFromClassname(String className) {
* @param name the name to look up, which must be in binary form (i.e. with $ for nested classes).
*/
public @Nullable ClassSymbol getSymbolFromName(Name name) {
Optional<ClassSymbol> cached = sharedState.symbolCache.get(name);
if (cached != null) {
return cached.orElse(null);
}
ClassSymbol sym = getSymbolFromNameInternal(name);
sharedState.symbolCache.put(name, Optional.ofNullable(sym));
return sym;
}

private @Nullable ClassSymbol getSymbolFromNameInternal(Name name) {
ClassSymbol result = lookupSymbolFromName(name);
if (result != null) {
return result;
}
String nameStr = name.toString();
int firstDollar = nameStr.indexOf('$');
if (firstDollar == -1) {
return null;
}
boolean completedEnclosing = false;
int startIdx = firstDollar == 0 ? nameStr.indexOf('$', 1) : firstDollar;
for (int idx = startIdx; idx != -1; idx = nameStr.indexOf('$', idx + 1)) {
if (getSymbolFromName(getName(nameStr.substring(0, idx))) != null) {
completedEnclosing = true;
}
}
return completedEnclosing ? lookupSymbolFromName(name) : null;
}

private @Nullable ClassSymbol lookupSymbolFromName(Name name) {
boolean modular = sharedState.modules.getDefaultModule() != getSymtab().noModule;
if (!modular) {
return getSymbolFromString(getSymtab().noModule, name);
Expand Down Expand Up @@ -689,7 +733,7 @@ public static <T> Supplier<T> memoize(Supplier<T> f) {
* <p>For the types that are typically stored in {@link Context}, caching the references over
* {@code SomeClass.instance(context)} has sizable performance improvements in aggregate.
*/
private static final class SharedState {
static final class SharedState {
private final Modules modules;
private final Names names;
private final Symtab symtab;
Expand All @@ -708,6 +752,7 @@ private static final class SharedState {
// smallest build and see how many types are loaded and use that. Or perhaps a heuristic
// based on number of files?
private final Map<String, Optional<Type>> typeCache = new HashMap<>();
private final Map<Name, Optional<ClassSymbol>> symbolCache = new HashMap<>();

SharedState(
Context context,
Expand Down
Loading
Loading