Skip to content

Commit fae46c7

Browse files
Chang-EricDagger Team
authored andcommitted
Fix an issue where a scoped @BINDS used in a cycle would cause an NPE on component creation.
RELNOTES=Fix an issue where a scoped @BINDS used in a cycle would cause an NPE on component creation. PiperOrigin-RevId: 507636194
1 parent 85a2cb2 commit fae46c7

23 files changed

Lines changed: 1466 additions & 31 deletions

File tree

java/dagger/internal/codegen/writing/ComponentImplementation.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -480,7 +480,7 @@ public final class ShardImplementation implements GeneratedImplementation {
480480

481481
private ShardImplementation(ClassName name) {
482482
this.name = name;
483-
this.switchingProviders = new SwitchingProviders(this);
483+
this.switchingProviders = new SwitchingProviders(this, processingEnv);
484484
this.experimentalSwitchingProviders =
485485
new ExperimentalSwitchingProviders(this, componentRequestRepresentationsProvider);
486486

java/dagger/internal/codegen/writing/FrameworkFieldInitializer.java

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@
3434
import dagger.internal.codegen.binding.FrameworkField;
3535
import dagger.internal.codegen.javapoet.AnnotationSpecs;
3636
import dagger.internal.codegen.javapoet.TypeNames;
37-
import dagger.internal.codegen.writing.ComponentImplementation.CompilerMode;
3837
import dagger.internal.codegen.writing.ComponentImplementation.ShardImplementation;
3938
import dagger.spi.model.BindingKind;
4039
import java.util.Optional;
@@ -65,7 +64,6 @@ default Optional<ClassName> alternativeFrameworkClass() {
6564
private final ShardImplementation shardImplementation;
6665
private final ContributionBinding binding;
6766
private final FrameworkInstanceCreationExpression frameworkInstanceCreationExpression;
68-
private final CompilerMode compilerMode;
6967
private FieldSpec fieldSpec;
7068
private InitializationState fieldInitializationState = InitializationState.UNINITIALIZED;
7169

@@ -75,7 +73,6 @@ default Optional<ClassName> alternativeFrameworkClass() {
7573
FrameworkInstanceCreationExpression frameworkInstanceCreationExpression) {
7674
this.binding = checkNotNull(binding);
7775
this.shardImplementation = checkNotNull(componentImplementation).shardImplementation(binding);
78-
this.compilerMode = componentImplementation.compilerMode();
7976
this.frameworkInstanceCreationExpression = checkNotNull(frameworkInstanceCreationExpression);
8077
}
8178

@@ -113,12 +110,13 @@ private void initializeField() {
113110
case INITIALIZING:
114111
fieldSpec = getOrCreateField();
115112
// We were recursively invoked, so create a delegate factory instead to break the loop.
116-
// However, because SwitchingProvider takes no dependencies, even if they are recursively
117-
// invoked, we don't need to delegate it since there is no dependency cycle.
118-
if (FrameworkInstanceKind.from(binding, compilerMode)
119-
.equals(FrameworkInstanceKind.SWITCHING_PROVIDER)) {
120-
break;
121-
}
113+
114+
// TODO(erichang): For the most part SwitchingProvider takes no dependencies so even if they
115+
// are recursively invoked, we don't need to delegate it since there is no dependency cycle.
116+
// However, there is a case with a scoped @Binds where we reference the impl binding when
117+
// passing it into DoubleCheck. For this case, we do need to delegate it. There might be
118+
// a way to only do delegates in this situation, but we'd need to keep track of what other
119+
// bindings use this.
122120

123121
fieldInitializationState = InitializationState.DELEGATED;
124122
shardImplementation.addInitialization(

java/dagger/internal/codegen/writing/SwitchingProviders.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import static javax.lang.model.element.Modifier.PUBLIC;
3131
import static javax.lang.model.element.Modifier.STATIC;
3232

33+
import androidx.room.compiler.processing.XProcessingEnv;
3334
import com.google.common.collect.ImmutableList;
3435
import com.google.common.collect.Lists;
3536
import com.squareup.javapoet.ClassName;
@@ -42,6 +43,7 @@
4243
import dagger.internal.codegen.javapoet.CodeBlocks;
4344
import dagger.internal.codegen.writing.ComponentImplementation.ShardImplementation;
4445
import dagger.internal.codegen.writing.FrameworkFieldInitializer.FrameworkInstanceCreationExpression;
46+
import dagger.internal.codegen.xprocessing.XProcessingEnvs;
4547
import dagger.spi.model.BindingKind;
4648
import dagger.spi.model.Key;
4749
import java.util.HashMap;
@@ -77,9 +79,11 @@ final class SwitchingProviders {
7779
new LinkedHashMap<>();
7880

7981
private final ShardImplementation shardImplementation;
82+
private final XProcessingEnv processingEnv;
8083

81-
SwitchingProviders(ShardImplementation shardImplementation) {
84+
SwitchingProviders(ShardImplementation shardImplementation, XProcessingEnv processingEnv) {
8285
this.shardImplementation = checkNotNull(shardImplementation);
86+
this.processingEnv = checkNotNull(processingEnv);
8387
}
8488

8589
/** Returns the framework instance creation expression for an inner switching provider class. */
@@ -133,7 +137,9 @@ private CodeBlock getNewInstanceCodeBlock(
133137
// Add the type parameter explicitly when the binding is scoped because Java can't resolve
134138
// the type when wrapped. For example, the following will error:
135139
// fooProvider = DoubleCheck.provider(new SwitchingProvider<>(1));
136-
(binding.scope().isPresent() || binding.kind().equals(BindingKind.ASSISTED_FACTORY))
140+
(binding.scope().isPresent()
141+
|| binding.kind().equals(BindingKind.ASSISTED_FACTORY)
142+
|| XProcessingEnvs.isPreJava8SourceVersion(processingEnv))
137143
? CodeBlock.of(
138144
"$T", shardImplementation.accessibleTypeName(binding.contributedType()))
139145
: "",

javatests/dagger/functional/binds/BUILD

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,20 +25,59 @@ load("//:test_defs.bzl", "GenJavaTests")
2525
package(default_visibility = ["//:src"])
2626

2727
GenJavaTests(
28-
name = "binds",
29-
srcs = glob(["*.java"]),
28+
name = "BindsTest",
29+
srcs = [
30+
"AccessesExposedComponent.java",
31+
"BindsTest.java",
32+
"Foo.java",
33+
"FooOfObjects.java",
34+
"FooOfStrings.java",
35+
"InterfaceModule.java",
36+
"NeedsFactory.java",
37+
"SimpleBindingModule.java",
38+
"SomeQualifier.java",
39+
"TestComponent.java",
40+
],
3041
gen_library_deps = [
3142
"//javatests/dagger/functional/binds/subpackage",
3243
],
3344
javacopts = DOCLINT_HTML_AND_SYNTAX + DOCLINT_REFERENCES,
3445
deps = [
3546
"//:dagger_with_compiler",
3647
"//third_party/java/auto:factory",
37-
"//third_party/java/guava/base",
38-
"//third_party/java/guava/collect",
39-
"//third_party/java/guava/util/concurrent",
40-
"//third_party/java/jsr305_annotations",
41-
"//third_party/java/jsr330_inject",
48+
"//third_party/java/junit",
49+
"//third_party/java/truth",
50+
],
51+
)
52+
53+
GenJavaTests(
54+
name = "BindsCollectionsWithoutMultibindingsTest",
55+
srcs = ["BindsCollectionsWithoutMultibindingsTest.java"],
56+
javacopts = DOCLINT_HTML_AND_SYNTAX + DOCLINT_REFERENCES,
57+
deps = [
58+
"//:dagger_with_compiler",
59+
"//third_party/java/junit",
60+
"//third_party/java/truth",
61+
],
62+
)
63+
64+
GenJavaTests(
65+
name = "ScopedBindsTest",
66+
srcs = ["ScopedBindsTest.java"],
67+
javacopts = DOCLINT_HTML_AND_SYNTAX + DOCLINT_REFERENCES,
68+
deps = [
69+
"//:dagger_with_compiler",
70+
"//third_party/java/junit",
71+
"//third_party/java/truth",
72+
],
73+
)
74+
75+
GenJavaTests(
76+
name = "RecursiveBindsTest",
77+
srcs = ["RecursiveBindsTest.java"],
78+
javacopts = DOCLINT_HTML_AND_SYNTAX + DOCLINT_REFERENCES,
79+
deps = [
80+
"//:dagger_with_compiler",
4281
"//third_party/java/junit",
4382
"//third_party/java/truth",
4483
],
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
* Copyright (C) 2023 The Dagger Authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package dagger.functional.binds;
18+
19+
import static com.google.common.truth.Truth.assertThat;
20+
21+
import dagger.Binds;
22+
import dagger.Component;
23+
import dagger.Module;
24+
import javax.inject.Inject;
25+
import javax.inject.Provider;
26+
import javax.inject.Singleton;
27+
import org.junit.Test;
28+
import org.junit.runner.RunWith;
29+
import org.junit.runners.JUnit4;
30+
31+
// This is a regression test for b/267223822 where a scoped @Binds used in a cycle caused problems
32+
// in fastInit mode.
33+
@RunWith(JUnit4.class)
34+
public class RecursiveBindsTest {
35+
36+
public interface Foo {}
37+
38+
public static final class FooImpl implements Foo {
39+
@Inject FooImpl(@SuppressWarnings("unused") Provider<Foo> provider) {}
40+
}
41+
42+
@Module
43+
public interface FooModule {
44+
// This binding must be scoped to create the cycle. Otherwise without a scope, the generated
45+
// code just doesn't have a field for this @Binds because we can directly use FooImpl's
46+
// provider as they are equivalent.
47+
@Binds
48+
@Singleton
49+
Foo bindFoo(FooImpl impl);
50+
}
51+
52+
@Component(modules = FooModule.class)
53+
@Singleton
54+
public interface TestSingletonComponent {
55+
// We get the impl here to create a cycle where the impl factory needs to be delegated.
56+
// That way the scoped binds (which does something like DoubleCheck.provider(implFactory)) is
57+
// the one that would fail if it wasn't delegated properly.
58+
Provider<FooImpl> getFooImplProvider();
59+
}
60+
61+
@Test
62+
public void test() {
63+
// Technically the NPE would happen when just initializing the component.
64+
assertThat(DaggerRecursiveBindsTest_TestSingletonComponent.create().getFooImplProvider().get())
65+
.isNotNull();
66+
}
67+
}

javatests/dagger/functional/kotlinsrc/binds/BUILD

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,20 +25,48 @@ load("//:test_defs.bzl", "GenKtTests")
2525
package(default_visibility = ["//:src"])
2626

2727
GenKtTests(
28-
name = "binds",
29-
srcs = glob(["*.kt"]),
28+
name = "BindsTest",
29+
srcs = [
30+
"AccessesExposedComponent.kt",
31+
"BindsTest.kt",
32+
"Foo.kt",
33+
"FooOfObjects.kt",
34+
"FooOfStrings.kt",
35+
"InterfaceModule.kt",
36+
"NeedsFactory.kt",
37+
"SimpleBindingModule.kt",
38+
"SomeQualifier.kt",
39+
"TestComponent.kt",
40+
],
3041
gen_library_deps = [
3142
"//javatests/dagger/functional/kotlinsrc/binds/subpackage",
3243
],
3344
javacopts = DOCLINT_HTML_AND_SYNTAX + DOCLINT_REFERENCES,
3445
deps = [
3546
"//:dagger_with_compiler",
3647
"//third_party/java/auto:factory",
37-
"//third_party/java/guava/base",
38-
"//third_party/java/guava/collect",
39-
"//third_party/java/guava/util/concurrent",
40-
"//third_party/java/jsr305_annotations",
41-
"//third_party/java/jsr330_inject",
48+
"//third_party/java/junit",
49+
"//third_party/java/truth",
50+
],
51+
)
52+
53+
GenKtTests(
54+
name = "BindsCollectionsWithoutMultibindingsTest",
55+
srcs = ["BindsCollectionsWithoutMultibindingsTest.kt"],
56+
javacopts = DOCLINT_HTML_AND_SYNTAX + DOCLINT_REFERENCES,
57+
deps = [
58+
"//:dagger_with_compiler",
59+
"//third_party/java/junit",
60+
"//third_party/java/truth",
61+
],
62+
)
63+
64+
GenKtTests(
65+
name = "ScopedBindsTest",
66+
srcs = ["ScopedBindsTest.kt"],
67+
javacopts = DOCLINT_HTML_AND_SYNTAX + DOCLINT_REFERENCES,
68+
deps = [
69+
"//:dagger_with_compiler",
4270
"//third_party/java/junit",
4371
"//third_party/java/truth",
4472
],
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
* Copyright (C) 2023 The Dagger Authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package dagger.functional.kotlinsrc.binds
18+
19+
import com.google.common.truth.Truth.assertThat
20+
import dagger.Binds
21+
import dagger.Component
22+
import dagger.Module
23+
import javax.inject.Inject
24+
import javax.inject.Provider
25+
import javax.inject.Singleton
26+
import org.junit.Test
27+
import org.junit.runner.RunWith
28+
import org.junit.runners.JUnit4
29+
30+
// This is a regression test for b/267223822 where a scoped @Binds used in a cycle caused problems
31+
// in fastInit mode.
32+
@RunWith(JUnit4::class)
33+
class RecursiveBindsTest {
34+
35+
interface Foo {}
36+
37+
class FooImpl internal @Inject constructor(@SuppressWarnings("unused") provider: Provider<Foo>) :
38+
Foo {}
39+
40+
@Module
41+
interface FooModule {
42+
// This binding must be scoped to create the cycle. Otherwise without a scope, the generated
43+
// code just doesn't have a field for this @Binds because we can directly use FooImpl's
44+
// provider as they are equivalent.
45+
@Binds @Singleton fun bindFoo(impl: FooImpl): Foo
46+
}
47+
48+
@Component(modules = FooModule::class)
49+
@Singleton
50+
interface TestSingletonComponent {
51+
// We get the impl here to create a cycle where the impl factory needs to be delegated.
52+
// That way the scoped binds (which does something like DoubleCheck.provider(implFactory)) is
53+
// the one that would fail if it wasn't delegated properly.
54+
fun getFooImplProvider(): Provider<FooImpl>
55+
}
56+
57+
@Test
58+
fun test() {
59+
// Technically the NPE would happen when just initializing the component.
60+
assertThat(DaggerRecursiveBindsTest_TestSingletonComponent.create().getFooImplProvider().get())
61+
.isNotNull()
62+
}
63+
}

0 commit comments

Comments
 (0)