diff --git a/check_api/src/main/java/com/google/errorprone/scanner/ErrorProneInjector.java b/check_api/src/main/java/com/google/errorprone/scanner/ErrorProneInjector.java new file mode 100644 index 00000000000..16215048dc0 --- /dev/null +++ b/check_api/src/main/java/com/google/errorprone/scanner/ErrorProneInjector.java @@ -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. + * + *

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 instances = MutableClassToInstanceMap.create(); + + public static ErrorProneInjector create() { + return new ErrorProneInjector(); + } + + @CanIgnoreReturnValue + public ErrorProneInjector addBinding(Class clazz, T instance) { + instances.putInstance(clazz, instance); + return this; + } + + public synchronized T getInstance(Class clazz) { + return getInstance(clazz, new ArrayList<>()); + } + + private synchronized T getInstance(Class clazz, List> path) { + var instance = instances.getInstance(clazz); + if (instance != null) { + return instance; + } + path.add(clazz); + Constructor 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 Optional> findConstructor(Class 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 Optional> findConstructorMatching( + Class clazz, Predicate> predicate) { + return stream(clazz.getDeclaredConstructors()) + .filter(predicate) + .map(c -> (Constructor) c) + .findFirst(); + } + + private static String printPath(List> path) { + return reverse(path).stream().map(Class::getSimpleName).collect(joining(" <- ")); + } +} diff --git a/check_api/src/main/java/com/google/errorprone/scanner/ScannerSupplierImpl.java b/check_api/src/main/java/com/google/errorprone/scanner/ScannerSupplierImpl.java index 276f478a9fc..3bc1237c2d3 100644 --- a/check_api/src/main/java/com/google/errorprone/scanner/ScannerSupplierImpl.java +++ b/check_api/src/main/java/com/google/errorprone/scanner/ScannerSupplierImpl.java @@ -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 @@ -46,7 +40,7 @@ class ScannerSupplierImpl extends ScannerSupplier implements Serializable { private final ImmutableSet disabled; private final ErrorProneFlags flags; // Lazily initialized to make serialization easy. - private transient Injector injector; + private transient ErrorProneInjector injector; ScannerSupplierImpl( ImmutableBiMap checks, @@ -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[], though the return type is - * Constructor[]. See getConstructors() javadoc for more info. */ - Optional> flagsConstructor = - Arrays.stream((Constructor[]) 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 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 diff --git a/check_api/src/test/java/com/google/errorprone/scanner/ErrorProneInjectorTest.java b/check_api/src/test/java/com/google/errorprone/scanner/ErrorProneInjectorTest.java new file mode 100644 index 00000000000..023b9ea30ac --- /dev/null +++ b/check_api/src/test/java/com/google/errorprone/scanner/ErrorProneInjectorTest.java @@ -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; + } + } +}