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 @@ -24,8 +24,12 @@
import static com.google.common.collect.Iterables.getOnlyElement;
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
import static com.google.errorprone.matchers.Matchers.SERIALIZATION_METHODS;
import static com.google.errorprone.matchers.Matchers.anyOf;
import static com.google.errorprone.matchers.method.MethodMatchers.instanceMethod;
import static com.google.errorprone.matchers.method.MethodMatchers.staticMethod;
import static com.google.errorprone.suppliers.Suppliers.typeFromString;
import static com.google.errorprone.util.ASTHelpers.canBeRemoved;
import static com.google.errorprone.util.ASTHelpers.constValue;
import static com.google.errorprone.util.ASTHelpers.findSuperMethods;
import static com.google.errorprone.util.ASTHelpers.getStartPosition;
import static com.google.errorprone.util.ASTHelpers.getSymbol;
Expand Down Expand Up @@ -66,6 +70,7 @@
import com.google.errorprone.fixes.SuggestedFixes;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.matchers.InjectMatchers;
import com.google.errorprone.matchers.Matcher;
import com.google.errorprone.suppliers.Supplier;
import com.google.errorprone.suppliers.Suppliers;
import com.google.errorprone.util.ASTHelpers;
Expand Down Expand Up @@ -226,7 +231,8 @@ public Description matchCompilationUnit(CompilationUnitTree tree, VisitorState s
// appropriate fixes for them.
ListMultimap<Symbol, TreePath> usageSites = variableFinder.usageSites;

FilterUsedVariables filterUsedVariables = new FilterUsedVariables(unusedElements, usageSites);
FilterUsedVariables filterUsedVariables =
new FilterUsedVariables(unusedElements, usageSites, state);
filterUsedVariables.scan(state.getPath(), null);

// Keeps track of whether a symbol was _ever_ used (between reassignments).
Expand Down Expand Up @@ -887,11 +893,39 @@ private static final class FilterUsedVariables extends TreePathScanner<Void, Voi

private final ImmutableMap<Symbol, TreePath> declarationSites;

private final VisitorState state;

/**
* Methods which look up a field by name (via a class literal and a string constant) and return
* a handle through which it is subsequently read and written. A field whose only accesses go
* through such a handle has no direct references in source, but is nonetheless used.
*/
private static final Matcher<ExpressionTree> REFLECTIVE_FIELD_LOOKUP =
anyOf(
instanceMethod()
.onDescendantOf("java.lang.invoke.MethodHandles.Lookup")
.namedAnyOf(
"findVarHandle",
"findStaticVarHandle",
"findGetter",
"findSetter",
"findStaticGetter",
"findStaticSetter"),
staticMethod()
.onClassAny(
"java.util.concurrent.atomic.AtomicIntegerFieldUpdater",
"java.util.concurrent.atomic.AtomicLongFieldUpdater",
"java.util.concurrent.atomic.AtomicReferenceFieldUpdater")
.named("newUpdater"));

private FilterUsedVariables(
Map<Symbol, TreePath> unusedElements, ListMultimap<Symbol, TreePath> usageSites) {
Map<Symbol, TreePath> unusedElements,
ListMultimap<Symbol, TreePath> usageSites,
VisitorState state) {
this.unusedElements = unusedElements;
this.usageSites = usageSites;
this.declarationSites = ImmutableMap.copyOf(unusedElements);
this.state = state;
}

private boolean isInExpressionStatementTree() {
Expand Down Expand Up @@ -1124,8 +1158,44 @@ public Void visitMethodInvocation(MethodInvocationTree tree, Void unused) {
inMethodCall++;
super.visitMethodInvocation(tree, null);
inMethodCall--;
if (REFLECTIVE_FIELD_LOOKUP.matches(tree, state)) {
markReflectivelyLookedUpFieldAsUsed(tree);
}
return null;
}

/**
* Given an invocation like {@code lookup.findVarHandle(Foo.class, "bar", int.class)} or {@code
* AtomicIntegerFieldUpdater.newUpdater(Foo.class, "bar")}, finds the field {@code Foo.bar} and
* treats it as used, since all subsequent reads and writes happen through the returned handle.
*/
private void markReflectivelyLookedUpFieldAsUsed(MethodInvocationTree tree) {
List<? extends ExpressionTree> arguments = tree.getArguments();
if (arguments.isEmpty()
|| !(arguments.get(0) instanceof MemberSelectTree memberSelect)
|| !memberSelect.getIdentifier().contentEquals("class")
|| !(getSymbol(memberSelect.getExpression()) instanceof ClassSymbol owner)) {
return;
}
String fieldName = null;
for (ExpressionTree argument : arguments.subList(1, arguments.size())) {
fieldName = constValue(argument, String.class);
if (fieldName != null) {
break;
}
}
if (fieldName == null) {
return;
}
String finalFieldName = fieldName;
unusedElements
.keySet()
.removeIf(
symbol ->
symbol.getKind() == ElementKind.FIELD
&& symbol.owner.equals(owner)
&& symbol.getSimpleName().contentEquals(finalFieldName));
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2372,4 +2372,156 @@ void regularMethod(String s) {
""")
.doTest();
}

@Test
public void fieldOnlyAccessedViaVarHandle_notFlagged() {
helper
.addSourceLines(
"Test.java",
"""
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;

class Test {
private final class Worker {
static final VarHandle RUN_NEXT;

static {
try {
RUN_NEXT =
MethodHandles.lookup().findVarHandle(Test.Worker.class, "runNext", Runnable.class);
} catch (ReflectiveOperationException e) {
throw new ExceptionInInitializerError(e);
}
}

volatile Runnable runNext;

Runnable take() {
return (Runnable) RUN_NEXT.getAndSet(this, null);
}
}

Runnable take(Worker w) {
return w.take();
}
}
""")
.doTest();
}

@Test
public void staticFieldOnlyAccessedViaStaticVarHandle_notFlagged() {
helper
.addSourceLines(
"Test.java",
"""
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;

class Test {
private static final VarHandle COUNT;

static {
try {
COUNT = MethodHandles.lookup().findStaticVarHandle(Test.class, "count", int.class);
} catch (ReflectiveOperationException e) {
throw new ExceptionInInitializerError(e);
}
}

private static volatile int count;

int increment() {
return (int) COUNT.getAndAdd(1);
}
}
""")
.doTest();
}

@Test
public void fieldOnlyAccessedViaAtomicFieldUpdater_notFlagged() {
helper
.addSourceLines(
"Test.java",
"""
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;

class Test {
private static final AtomicIntegerFieldUpdater<Test> COUNT =
AtomicIntegerFieldUpdater.newUpdater(Test.class, "count");
private static final AtomicReferenceFieldUpdater<Test, String> NAME =
AtomicReferenceFieldUpdater.newUpdater(Test.class, String.class, "name");

private volatile int count;
private volatile String name;

int increment() {
return COUNT.incrementAndGet(this);
}

String getName() {
return NAME.get(this);
}
}
""")
.doTest();
}

@Test
public void varHandleForDifferentField_unrelatedFieldStillFlagged() {
helper
.addSourceLines(
"Test.java",
"""
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;

class Test {
private static final VarHandle COUNT;

static {
try {
COUNT = MethodHandles.lookup().findVarHandle(Test.class, "count", int.class);
} catch (ReflectiveOperationException e) {
throw new ExceptionInInitializerError(e);
}
}

private volatile int count;

// BUG: Diagnostic contains: The field 'other' is never read
private volatile int other;

int increment() {
return (int) COUNT.getAndAdd(this, 1);
}
}
""")
.doTest();
}

@Test
public void nonLiteralOwnerWithLiteralFieldType_fieldTypeClassNotTreatedAsOwner() {
helper
.addSourceLines(
"Test.java",
"""
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;

class Test {
private static final class Foo {
// BUG: Diagnostic contains: The field 'value' is never read
private volatile String value;
}

static <T> AtomicReferenceFieldUpdater<T, Foo> updater(Class<T> ownerClass) {
return AtomicReferenceFieldUpdater.newUpdater(ownerClass, Foo.class, "value");
}
}
""")
.doTest();
}
}
Loading