Skip to content

Commit

Permalink
Provide a minimal DI implementation for EP.
Browse files Browse the repository at this point in the history
PiperOrigin-RevId: 507013426
  • Loading branch information
graememorgan authored and Error Prone Team committed Feb 3, 2023
1 parent a010209 commit 1c3c09f
Show file tree
Hide file tree
Showing 3 changed files with 210 additions and 50 deletions.
@@ -0,0 +1,115 @@
/*
* Copyright 2023 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.scanner;

import static com.google.common.collect.Lists.reverse;
import static java.util.Arrays.stream;
import static java.util.stream.Collectors.joining;

import com.google.common.collect.ClassToInstanceMap;
import com.google.common.collect.MutableClassToInstanceMap;
import com.google.errorprone.ErrorProneFlags;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.inject.ProvisionException;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.function.Predicate;

/**
* An injector for ErrorProne.
*
* <p>This implements a very simplified subset of the functionality that Guice does. Specifically,
* it allows injecting only non-generic classes, and treats everything as a singleton within a given
* compilation.
*/
final class ErrorProneInjector {
private final ClassToInstanceMap<Object> instances = MutableClassToInstanceMap.create();

public static ErrorProneInjector create() {
return new ErrorProneInjector();
}

@CanIgnoreReturnValue
public <T> ErrorProneInjector addBinding(Class<T> clazz, T instance) {
instances.putInstance(clazz, instance);
return this;
}

public synchronized <T> T getInstance(Class<T> clazz) {
return getInstance(clazz, new ArrayList<>());
}

private synchronized <T> T getInstance(Class<T> clazz, List<Class<?>> path) {
var instance = instances.getInstance(clazz);
if (instance != null) {
return instance;
}
path.add(clazz);
Constructor<T> constructor =
findConstructor(clazz)
.orElseThrow(
() ->
new ProvisionException(
"Failed to find an injectable constructor for "
+ clazz.getCanonicalName()
+ " requested by "
+ printPath(path)));

constructor.setAccessible(true);

Object[] args =
stream(constructor.getParameterTypes()).map(c -> getInstance(c, path)).toArray();
T newInstance;
try {
newInstance = constructor.newInstance(args);
} catch (ReflectiveOperationException e) {
throw new ProvisionException("Failed to initialize " + clazz.getCanonicalName(), e);
}
instances.putInstance(clazz, newInstance);
return newInstance;
}

public static <T> Optional<Constructor<T>> findConstructor(Class<T> clazz) {
return findConstructorMatching(
clazz,
c ->
stream(c.getAnnotations())
.anyMatch(a -> a.annotationType().getSimpleName().equals("Inject")))
.or(
() ->
findConstructorMatching(
clazz,
c ->
stream(c.getParameters())
.allMatch(p -> p.getType().equals(ErrorProneFlags.class))));
}

@SuppressWarnings("unchecked")
private static <T> Optional<Constructor<T>> findConstructorMatching(
Class<T> clazz, Predicate<Constructor<?>> predicate) {
return stream(clazz.getDeclaredConstructors())
.filter(predicate)
.map(c -> (Constructor<T>) c)
.findFirst();
}

private static String printPath(List<Class<?>> path) {
return reverse(path).stream().map(Class::getSimpleName).collect(joining(" <- "));
}
}
Expand Up @@ -27,13 +27,7 @@
import com.google.errorprone.BugPattern.SeverityLevel;
import com.google.errorprone.ErrorProneFlags;
import com.google.errorprone.bugpatterns.BugChecker;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.ProvisionException;
import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.util.Arrays;
import java.util.Optional;

/**
* An implementation of a {@link ScannerSupplier}, abstracted as a set of all known {@link
Expand All @@ -46,7 +40,7 @@ class ScannerSupplierImpl extends ScannerSupplier implements Serializable {
private final ImmutableSet<String> disabled;
private final ErrorProneFlags flags;
// Lazily initialized to make serialization easy.
private transient Injector injector;
private transient ErrorProneInjector injector;

ScannerSupplierImpl(
ImmutableBiMap<String, BugCheckerInfo> checks,
Expand All @@ -67,50 +61,9 @@ class ScannerSupplierImpl extends ScannerSupplier implements Serializable {

private BugChecker instantiateChecker(BugCheckerInfo checker) {
if (injector == null) {
injector =
Guice.createInjector(binder -> binder.bind(ErrorProneFlags.class).toInstance(flags));
}
try {
return injector.getInstance(checker.checkerClass());
} catch (ProvisionException | com.google.inject.ConfigurationException e) {
// Fall back to the old path for external checks.
// TODO(b/263227221): Consider stripping this internally after careful testing.
return instantiateCheckerOldPath(checker);
}
}

private BugChecker instantiateCheckerOldPath(BugCheckerInfo checker) {
// Invoke BugChecker(ErrorProneFlags) constructor, if it exists.
@SuppressWarnings("unchecked")
/* getConstructors() actually returns Constructor<BugChecker>[], though the return type is
* Constructor<?>[]. See getConstructors() javadoc for more info. */
Optional<Constructor<BugChecker>> flagsConstructor =
Arrays.stream((Constructor<BugChecker>[]) checker.checkerClass().getConstructors())
.filter(
c -> Arrays.equals(c.getParameterTypes(), new Class<?>[] {ErrorProneFlags.class}))
.findFirst();
if (flagsConstructor.isPresent()) {
try {
return flagsConstructor.get().newInstance(getFlags());
} catch (ReflectiveOperationException e) {
throw new LinkageError("Could not instantiate BugChecker.", e);
}
}

// If no flags constructor, invoke default constructor.
Class<? extends BugChecker> checkerClass = checker.checkerClass();
try {
return checkerClass.getConstructor().newInstance();
} catch (NoSuchMethodException | IllegalAccessException e) {
throw new LinkageError(
String.format(
"Could not instantiate BugChecker %s: Are both the class and the zero-arg"
+ " constructor public?",
checkerClass),
e);
} catch (ReflectiveOperationException e) {
throw new LinkageError("Could not instantiate BugChecker.", e);
injector = ErrorProneInjector.create().addBinding(ErrorProneFlags.class, flags);
}
return injector.getInstance(checker.checkerClass());
}

@Override
Expand Down
@@ -0,0 +1,92 @@
/*
* Copyright 2023 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.scanner;

import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;

import com.google.inject.ProvisionException;
import javax.inject.Inject;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

@RunWith(JUnit4.class)
public final class ErrorProneInjectorTest {
@Test
public void retrievesPredefinedInstance() {
var injector = ErrorProneInjector.create().addBinding(Integer.class, 2);

assertThat(injector.getInstance(Integer.class))
.isSameInstanceAs(injector.getInstance(Integer.class));
}

@Test
public void noConstructor_injectable() {
var injector = ErrorProneInjector.create();

var unused = injector.getInstance(NoConstructor.class);
}

@Test
public void injectConstructor_injectable() {
var injector = ErrorProneInjector.create();

var unused = injector.getInstance(InjectConstructor.class);
}

@Test
public void bothConstructors_injectable() {
var injector = ErrorProneInjector.create().addBinding(Integer.class, 2);

var obj = injector.getInstance(InjectConstructorAndZeroArgConstructor.class);

assertThat(obj.x).isEqualTo(2);
}

@Test
public void pathInError() {
var injector = ErrorProneInjector.create();

var e =
assertThrows(
ProvisionException.class,
() -> injector.getInstance(InjectConstructorAndZeroArgConstructor.class));

assertThat(e).hasMessageThat().contains("Integer <- InjectConstructorAndZeroArgConstructor");
}

public static final class NoConstructor {}

public static final class InjectConstructor {
@Inject
InjectConstructor() {}
}

public static final class InjectConstructorAndZeroArgConstructor {
final int x;

@Inject
InjectConstructorAndZeroArgConstructor(Integer x) {
this.x = x;
}

InjectConstructorAndZeroArgConstructor() {
this.x = 0;
}
}
}

0 comments on commit 1c3c09f

Please sign in to comment.