diff --git a/compiler/pom.xml b/compiler/pom.xml index 88100674be8..942afe318e3 100644 --- a/compiler/pom.xml +++ b/compiler/pom.xml @@ -51,6 +51,11 @@ junit test + + org.mockito + mockito-core + test + org.easytesting fest-assert diff --git a/compiler/src/main/java/dagger/internal/codegen/GraphAnalysisLoader.java b/compiler/src/main/java/dagger/internal/codegen/GraphAnalysisLoader.java index c3cd022d978..fe81458de39 100644 --- a/compiler/src/main/java/dagger/internal/codegen/GraphAnalysisLoader.java +++ b/compiler/src/main/java/dagger/internal/codegen/GraphAnalysisLoader.java @@ -15,6 +15,7 @@ */ package dagger.internal.codegen; +import com.google.common.annotations.VisibleForTesting; import dagger.internal.Binding; import dagger.internal.Loader; import dagger.internal.ModuleAdapter; @@ -22,6 +23,7 @@ import javax.annotation.processing.ProcessingEnvironment; import javax.lang.model.element.ElementKind; import javax.lang.model.element.TypeElement; +import javax.lang.model.util.Elements; /** * A {@code Binding.Resolver} suitable for tool use at build time. The bindings created by @@ -39,8 +41,7 @@ public GraphAnalysisLoader(ProcessingEnvironment processingEnv) { @Override public Binding getAtInjectBinding( String key, String className, ClassLoader classLoader, boolean mustHaveInjections) { - String sourceClassName = className.replace('$', '.'); - TypeElement type = processingEnv.getElementUtils().getTypeElement(sourceClassName); + TypeElement type = resolveType(processingEnv.getElementUtils(), className); if (type == null) { // We've encountered a type that the compiler can't introspect. If this // causes problems in practice (due to incremental compiles, etc.) we @@ -54,6 +55,85 @@ public GraphAnalysisLoader(ProcessingEnvironment processingEnv) { return GraphAnalysisInjectBinding.create(type, mustHaveInjections); } + /** + * Resolves the given class name into a {@link TypeElement}. The class name is a binary name, but + * {@link Elements#getTypeElement(CharSequence)} wants a canonical name. So this method searches + * the space of possible canonical names, starting with the most likely (since '$' is rarely used + * in canonical class names). + */ + @VisibleForTesting static TypeElement resolveType(Elements elements, String className) { + int index = nextDollar(className, className, 0); + if (index == -1) { + return getTypeElement(elements, className); + } + // have to test various possibilities of replacing '$' with '.' since '.' in a canonical name + // of a nested type is replaced with '$' in the binary name. + StringBuilder sb = new StringBuilder(className); + return resolveType(elements, className, sb, index); + } + + /** + * Recursively explores the space of possible canonical names for a given binary class name. + * + * @param elements used to resolve a name into a {@link TypeElement} + * @param className binary class name + * @param sb the current permutation of canonical name to attempt to resolve + * @param index the index of a {@code '$'} which may be changed to {@code '.'} in a canonical name + */ + private static TypeElement resolveType(Elements elements, String className, StringBuilder sb, + final int index) { + + // We assume '$' should be converted to '.'. So we search for classes with dots first. + sb.setCharAt(index, '.'); + int nextIndex = nextDollar(className, sb, index + 1); + TypeElement type = nextIndex == -1 + ? getTypeElement(elements, sb) + : resolveType(elements, className, sb, nextIndex); + if (type != null) { + return type; + } + + // if not found, change back to dollar and search. + sb.setCharAt(index, '$'); + nextIndex = nextDollar(className, sb, index + 1); + return nextIndex == -1 + ? getTypeElement(elements, sb) + : resolveType(elements, className, sb, nextIndex); + } + + /** + * Finds the next {@code '$'} in a class name which can be changed to a {@code '.'} when computing + * a canonical class name. + */ + private static int nextDollar(String className, CharSequence current, int searchStart) { + while (true) { + int index = className.indexOf('$', searchStart); + if (index == -1) { + return -1; + } + // We'll never have two dots nor will a type name end or begin with dot. So no need to + // consider dollars at the beginning, end, or adjacent to dots. + if (index == 0 || index == className.length() - 1 + || current.charAt(index - 1) == '.' || current.charAt(index + 1) == '.') { + searchStart = index + 1; + continue; + } + return index; + } + } + + private static TypeElement getTypeElement(Elements elements, CharSequence className) { + try { + return elements.getTypeElement(className); + } catch (ClassCastException e) { + // work-around issue in javac in Java 7 where querying for non-existent type can + // throw a ClassCastException + // TODO(jh): refer to Oracle Bug ID if/when one is assigned to bug report + // (Review ID: JI-9027367) + return null; + } + } + @Override public ModuleAdapter getModuleAdapter(Class moduleClass) { throw new UnsupportedOperationException(); } diff --git a/compiler/src/test/java/dagger/internal/codegen/GraphAnalysisLoaderTest.java b/compiler/src/test/java/dagger/internal/codegen/GraphAnalysisLoaderTest.java new file mode 100644 index 00000000000..4a258bd5336 --- /dev/null +++ b/compiler/src/test/java/dagger/internal/codegen/GraphAnalysisLoaderTest.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2015 Square Inc. + * + * 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 dagger.internal.codegen; + +import com.google.common.collect.ImmutableList; +import java.util.ArrayList; +import java.util.List; +import javax.lang.model.element.TypeElement; +import javax.lang.model.util.Elements; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(JUnit4.class) +public class GraphAnalysisLoaderTest { + @Test public void resolveType() { + final List resolveAttempts = new ArrayList(); + Elements elements = mock(Elements.class); + when(elements.getTypeElement(any(CharSequence.class))).then(new Answer() { + @Override public TypeElement answer(InvocationOnMock invocationOnMock) throws Throwable { + resolveAttempts.add(invocationOnMock.getArguments()[0].toString()); + return null; + } + }); + + assertNull(GraphAnalysisLoader.resolveType(elements, "blah.blah.Foo$Bar$Baz")); + List expectedAttempts = ImmutableList.builder() + .add("blah.blah.Foo.Bar.Baz") + .add("blah.blah.Foo.Bar$Baz") + .add("blah.blah.Foo$Bar.Baz") + .add("blah.blah.Foo$Bar$Baz") + .build(); + assertEquals(expectedAttempts, resolveAttempts); + + resolveAttempts.clear(); + assertNull(GraphAnalysisLoader.resolveType(elements, "$$Foo$$Bar$$Baz$$")); + expectedAttempts = ImmutableList.builder() + .add("$.Foo.$Bar.$Baz.$") + .add("$.Foo.$Bar.$Baz$$") + .add("$.Foo.$Bar$.Baz.$") + .add("$.Foo.$Bar$.Baz$$") + .add("$.Foo.$Bar$$Baz.$") + .add("$.Foo.$Bar$$Baz$$") + .add("$.Foo$.Bar.$Baz.$") + .add("$.Foo$.Bar.$Baz$$") + .add("$.Foo$.Bar$.Baz.$") + .add("$.Foo$.Bar$.Baz$$") + .add("$.Foo$.Bar$$Baz.$") + .add("$.Foo$.Bar$$Baz$$") + .add("$.Foo$$Bar.$Baz.$") + .add("$.Foo$$Bar.$Baz$$") + .add("$.Foo$$Bar$.Baz.$") + .add("$.Foo$$Bar$.Baz$$") + .add("$.Foo$$Bar$$Baz.$") + .add("$.Foo$$Bar$$Baz$$") + .add("$$Foo.$Bar.$Baz.$") + .add("$$Foo.$Bar.$Baz$$") + .add("$$Foo.$Bar$.Baz.$") + .add("$$Foo.$Bar$.Baz$$") + .add("$$Foo.$Bar$$Baz.$") + .add("$$Foo.$Bar$$Baz$$") + .add("$$Foo$.Bar.$Baz.$") + .add("$$Foo$.Bar.$Baz$$") + .add("$$Foo$.Bar$.Baz.$") + .add("$$Foo$.Bar$.Baz$$") + .add("$$Foo$.Bar$$Baz.$") + .add("$$Foo$.Bar$$Baz$$") + .add("$$Foo$$Bar.$Baz.$") + .add("$$Foo$$Bar.$Baz$$") + .add("$$Foo$$Bar$.Baz.$") + .add("$$Foo$$Bar$.Baz$$") + .add("$$Foo$$Bar$$Baz.$") + .add("$$Foo$$Bar$$Baz$$") + .build(); + assertEquals(expectedAttempts, resolveAttempts); + + Mockito.validateMockitoUsage(); + } + + +} diff --git a/compiler/src/test/java/dagger/tests/integration/codegen/InjectAdapterGenerationTest.java b/compiler/src/test/java/dagger/tests/integration/codegen/InjectAdapterGenerationTest.java index fb2e566b949..621b7ca0e2b 100644 --- a/compiler/src/test/java/dagger/tests/integration/codegen/InjectAdapterGenerationTest.java +++ b/compiler/src/test/java/dagger/tests/integration/codegen/InjectAdapterGenerationTest.java @@ -35,7 +35,11 @@ public final class InjectAdapterGenerationTest { "import javax.inject.Inject;", "class Basic {", " static class A { @Inject A() { } }", - " @Module(injects = A.class)", + " static class Foo$Bar {", + " @Inject Foo$Bar() { }", + " static class Baz { @Inject Baz() { } }", + " }", + " @Module(injects = { A.class, Foo$Bar.class, Foo$Bar.Baz.class })", " static class AModule { }", "}")); @@ -47,7 +51,8 @@ public final class InjectAdapterGenerationTest { "import java.lang.String;", "public final class Basic$AModule$$ModuleAdapter", " extends ModuleAdapter {", - " private static final String[] INJECTS = {\"members/Basic$A\"};", + " private static final String[] INJECTS = {", + " \"members/Basic$A\", \"members/Basic$Foo$Bar\", \"members/Basic$Foo$Bar$Baz\"};", " private static final Class[] STATIC_INJECTIONS = {};", " private static final Class[] INCLUDES = {};", " public Basic$AModule$$ModuleAdapter() {", @@ -59,7 +64,7 @@ public final class InjectAdapterGenerationTest { " }", "}")); - JavaFileObject expectedInjectAdapter = + JavaFileObject expectedInjectAdapterA = JavaFileObjects.forSourceString("Basic$A$$InjectAdapter", Joiner.on("\n").join( "import dagger.internal.Binding;", "import java.lang.Override;", @@ -75,9 +80,44 @@ public final class InjectAdapterGenerationTest { " }", "}")); + JavaFileObject expectedInjectAdapterFooBar = + JavaFileObjects.forSourceString("Basic$Foo$Bar$$InjectAdapter", Joiner.on("\n").join( + "import dagger.internal.Binding;", + "import java.lang.Override;", + "import javax.inject.Provider;", + "public final class Basic$Foo$Bar$$InjectAdapter", + " extends Binding implements Provider {", + " public Basic$Foo$Bar$$InjectAdapter() {", + " super(\"Basic$Foo$Bar\", \"members/Basic$Foo$Bar\",", + " NOT_SINGLETON, Basic.Foo$Bar.class);", + " }", + " @Override public Basic.Foo$Bar get() {", + " Basic.Foo$Bar result = new Basic.Foo$Bar();", + " return result;", + " }", + "}")); + + JavaFileObject expectedInjectAdapterFooBarBaz = + JavaFileObjects.forSourceString("Basic$Foo$Bar$Baz$$InjectAdapter", Joiner.on("\n").join( + "import dagger.internal.Binding;", + "import java.lang.Override;", + "import javax.inject.Provider;", + "public final class Basic$Foo$Bar$Baz$$InjectAdapter", + " extends Binding implements Provider {", + " public Basic$Foo$Bar$Baz$$InjectAdapter() {", + " super(\"Basic$Foo$Bar$Baz\", \"members/Basic$Foo$Bar$Baz\",", + " NOT_SINGLETON, Basic.Foo$Bar.Baz.class);", + " }", + " @Override public Basic.Foo$Bar.Baz get() {", + " Basic.Foo$Bar.Baz result = new Basic.Foo$Bar.Baz();", + " return result;", + " }", + "}")); + ASSERT.about(javaSource()).that(sourceFile).processedWith(daggerProcessors()) .compilesWithoutError().and() - .generatesSources(expectedModuleAdapter, expectedInjectAdapter); + .generatesSources(expectedModuleAdapter, expectedInjectAdapterA, + expectedInjectAdapterFooBar, expectedInjectAdapterFooBarBaz); } } diff --git a/core/src/main/java/dagger/internal/Memoizer.java b/core/src/main/java/dagger/internal/Memoizer.java index 04cdc104c70..a168aabfc1a 100644 --- a/core/src/main/java/dagger/internal/Memoizer.java +++ b/core/src/main/java/dagger/internal/Memoizer.java @@ -22,7 +22,9 @@ import java.util.concurrent.locks.ReentrantReadWriteLock; /** - * Represents an operation to be + * Represents an operation whose results are memoized. Results returned by invocations of + * {@link #create(Object)} are memoized so that the same object is returned for multiple invocations + * of {@link #get(Object)} for the same key. */ abstract class Memoizer { private final Map map; diff --git a/core/src/test/java/dagger/internal/FailoverLoaderTest.java b/core/src/test/java/dagger/internal/FailoverLoaderTest.java index cb17ef68528..5cde6a66092 100644 --- a/core/src/test/java/dagger/internal/FailoverLoaderTest.java +++ b/core/src/test/java/dagger/internal/FailoverLoaderTest.java @@ -33,7 +33,7 @@ @RunWith(JUnit4.class) public final class FailoverLoaderTest { - @Module(injects = EntryPoint.class) + @Module(injects = Entry$Point.class) static class TestModule { @Provides String aString() { return "a"; } } @@ -45,12 +45,12 @@ static final class TestModule$$ModuleAdapter extends TestingModuleAdapter