Skip to content

Commit 4c81e94

Browse files
trexemDagger Team
authored andcommitted
Introduce parameterless @BINDS methods to explicitly bind @Inject constructors.
This change allows users to use a zero-parameter @BINDS method to explicitly bind the @Inject constructor of the return type. Annotations on the @BINDS method (like pinmultibindings) are applied to the resulting injection binding. One-parameter @BINDS methods continue to function as delegate bindings. Validation is updated to ensure parameterless @BINDS methods return a type with exactly one @Inject constructor. Scopes and qualifiers would not be available in either the implicit @Inject nor the explicit parameterless @BINDS to avoid confusions. RELNOTES=Introduced parameterless @BINDS methods to explicitly bind @Inject constructors PiperOrigin-RevId: 902919023
1 parent 0fca48f commit 4c81e94

24 files changed

Lines changed: 1301 additions & 24 deletions

dagger-compiler/main/java/dagger/internal/codegen/binding/BindingFactory.java

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,24 +53,29 @@
5353
import dagger.internal.codegen.xprocessing.XTypeNames;
5454
import java.util.Optional;
5555
import javax.inject.Inject;
56+
import javax.inject.Provider;
5657

5758
/** A factory for {@link Binding} objects. */
5859
public final class BindingFactory {
5960
private final KeyFactory keyFactory;
6061
private final DependencyRequestFactory dependencyRequestFactory;
6162
private final InjectionSiteFactory injectionSiteFactory;
6263
private final InjectionAnnotations injectionAnnotations;
64+
// We need a provider to avoid circular dependencies.
65+
private final Provider<InjectBindingRegistry> injectBindingRegistryProvider;
6366

6467
@Inject
6568
BindingFactory(
6669
KeyFactory keyFactory,
6770
DependencyRequestFactory dependencyRequestFactory,
6871
InjectionSiteFactory injectionSiteFactory,
69-
InjectionAnnotations injectionAnnotations) {
72+
InjectionAnnotations injectionAnnotations,
73+
Provider<InjectBindingRegistry> injectBindingRegistryProvider) {
7074
this.keyFactory = keyFactory;
7175
this.dependencyRequestFactory = dependencyRequestFactory;
7276
this.injectionSiteFactory = injectionSiteFactory;
7377
this.injectionAnnotations = injectionAnnotations;
78+
this.injectBindingRegistryProvider = injectBindingRegistryProvider;
7479
}
7580

7681
/**
@@ -162,6 +167,47 @@ public AssistedInjectionBinding assistedInjectionBinding(
162167
.build();
163168
}
164169

170+
/**
171+
* Returns an {@link BindingKind#INJECTION} binding returned by a parameterless {@code @Binds}
172+
* method.
173+
*
174+
* <p>Although these are {@code @Binds} methods, they are represented as {@link InjectionBinding}s
175+
* rather than {@link DelegateBinding}s. This is because a parameterless {@code @Binds} method
176+
* binds the return type to its {@code @Inject} constructor. If this were a {@link
177+
* DelegateBinding}, both the delegate and the underlying {@code @Inject} binding would have the
178+
* same key, leading to duplicate binding errors and cyclical dependency issues in both binding
179+
* graph resolution and code generation. Representing it as an {@link InjectionBinding} allows us
180+
* to augment the implicit injection binding with metadata from the {@code @Binds} method (e.g.,
181+
* {@code contributingModule}) without creating duplicate keys.
182+
*
183+
* @param bindsMethod the parameterless {@code @Binds}-annotated method
184+
* @param module the installed module that declares or inherits the method
185+
*/
186+
public Optional<InjectionBinding> explicitInjectionBinding(
187+
XMethodElement bindsMethod, XTypeElement module) {
188+
checkArgument(bindsMethod.hasAnnotation(XTypeNames.BINDS));
189+
checkArgument(bindsMethod.getParameters().isEmpty());
190+
// Normally, we would use the input method as the binding element, but as in this case it is an
191+
// Binds method that breaks assumptions for an InjectionBinding. Instead, we use the @Inject
192+
// constructor as the binding element and expose the @Binds method via
193+
// InjectionBinding#declaringElement().
194+
// We call InjectBindingRegistry#getOrFindInjectionBinding() rather than calling
195+
// BindingFactory#injectionBinding() directly because the former ensures that the binding is
196+
// properly validated before returning the binding.
197+
Key key = keyFactory.forDelegateDeclaration(bindsMethod, module); // Key from @Binds
198+
return injectBindingRegistryProvider
199+
.get()
200+
.getOrFindInjectionBinding(key)
201+
.map(
202+
binding ->
203+
((InjectionBinding) binding)
204+
.toBuilder()
205+
.key(key)
206+
.contributingModule(module) // Mark as coming from module
207+
.declaringElement(bindsMethod)
208+
.build());
209+
}
210+
165211
public AssistedFactoryBinding assistedFactoryBinding(
166212
XTypeElement factory, Optional<XType> resolvedFactoryType) {
167213

dagger-compiler/main/java/dagger/internal/codegen/binding/DeclarationFormatter.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,15 @@ public String format(Declaration declaration) {
6767
return formatSubcomponentDeclaration((SubcomponentDeclaration) declaration);
6868
}
6969

70+
if (declaration instanceof InjectionBinding) {
71+
InjectionBinding injectionBinding = (InjectionBinding) declaration;
72+
if (injectionBinding.declaringElement().isPresent()) {
73+
return methodSignatureFormatter.format(
74+
injectionBinding.declaringElement().get(),
75+
injectionBinding.contributingModule().map(XTypeElement::getType));
76+
}
77+
}
78+
7079
if (declaration.bindingElement().isPresent()) {
7180
XElement bindingElement = declaration.bindingElement().get();
7281
if (isMethodParameter(bindingElement)) {

dagger-compiler/main/java/dagger/internal/codegen/binding/DelegateDeclaration.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ public static final class Factory {
7070

7171
public DelegateDeclaration create(XMethodElement method, XTypeElement contributingModule) {
7272
checkArgument(DelegateBinding.hasDelegateAnnotation(method));
73+
checkArgument(method.getParameters().size() == 1);
7374
XMethodType resolvedMethod = method.asMemberOf(contributingModule.getType());
7475
DependencyRequest delegateRequest =
7576
dependencyRequestFactory.forRequiredResolvedVariable(

dagger-compiler/main/java/dagger/internal/codegen/binding/InjectionBinding.java

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import static dagger.internal.codegen.extension.DaggerStreams.toImmutableSet;
2020

21+
import androidx.room3.compiler.processing.XMethodElement;
2122
import com.google.auto.value.AutoValue;
2223
import com.google.auto.value.extension.memoized.Memoized;
2324
import com.google.common.collect.ImmutableSet;
@@ -30,7 +31,15 @@
3031
import dagger.internal.codegen.xprocessing.Nullability;
3132
import java.util.Optional;
3233

33-
/** A binding for a {@link BindingKind#INJECTION}. */
34+
/**
35+
* A binding for a {@link BindingKind#INJECTION}.
36+
*
37+
* <p>This also represents parameterless {@code @Binds} methods, which are used to explicitly bind
38+
* an {@link javax.inject.Inject}-annotated constructor. By treating these as {@link
39+
* InjectionBinding}s rather than {@code DelegateBinding}s, we avoid issues with duplicate bindings
40+
* and cyclical dependencies, as both the {@code @Binds} method and the {@code @Inject} constructor
41+
* would otherwise have the same key.
42+
*/
3443
@CheckReturnValue
3544
@AutoValue
3645
public abstract class InjectionBinding extends ContributionBinding {
@@ -72,9 +81,17 @@ public ImmutableSet<DependencyRequest> dependencies() {
7281
.build();
7382
}
7483

84+
@Override
85+
public boolean requiresModuleInstance() {
86+
return false;
87+
}
88+
7589
@Override
7690
public abstract Builder toBuilder();
7791

92+
/** The element that declares this binding, used for parameterless {@code @Binds} methods. */
93+
public abstract Optional<XMethodElement> declaringElement();
94+
7895
@Memoized
7996
@Override
8097
public abstract int hashCode();
@@ -93,5 +110,7 @@ abstract static class Builder extends ContributionBinding.Builder<InjectionBindi
93110
abstract Builder constructorDependencies(Iterable<DependencyRequest> constructorDependencies);
94111

95112
abstract Builder injectionSites(ImmutableSortedSet<InjectionSite> injectionSites);
113+
114+
abstract Builder declaringElement(XMethodElement declaringElement);
96115
}
97116
}

dagger-compiler/main/java/dagger/internal/codegen/binding/MethodSignatureFormatter.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ public String format(XExecutableElement method, Optional<XType> container) {
9393

9494
private String format(
9595
XExecutableElement method, Optional<XType> container, boolean includeReturnType) {
96-
return container.isPresent()
96+
return container.isPresent() && !getSimpleName(method).contentEquals("<init>")
9797
? format(
9898
method,
9999
method.asMemberOf(container.get()),

dagger-compiler/main/java/dagger/internal/codegen/binding/ModuleDescriptor.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,8 +153,19 @@ public ModuleDescriptor createUncached(XTypeElement moduleElement) {
153153
bindings.add(bindingFactory.producesMethodBinding(moduleMethod, moduleElement));
154154
}
155155
if (DelegateBinding.hasDelegateAnnotation(moduleMethod)) {
156-
delegates.add(
157-
bindingDelegateDeclarationFactory.create(moduleMethod, moduleElement));
156+
if (moduleMethod.hasAnnotation(XTypeNames.BINDS)
157+
&& moduleMethod.getParameters().isEmpty()) {
158+
// Parameterless @Binds methods are treated as explicit InjectionBindings
159+
// to avoid duplicate binding errors and cyclical dependencies that arise
160+
// if they were modeled as DelegateBindings on the same key as the @Inject
161+
// constructor.
162+
bindingFactory
163+
.explicitInjectionBinding(moduleMethod, moduleElement)
164+
.ifPresent(bindings::add);
165+
} else {
166+
delegates.add(
167+
bindingDelegateDeclarationFactory.create(moduleMethod, moduleElement));
168+
}
158169
}
159170
if (moduleMethod.hasAnnotation(XTypeNames.MULTIBINDS)) {
160171
multibindingDeclarations.add(

dagger-compiler/main/java/dagger/internal/codegen/model/BindingKind.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,9 @@ public enum BindingKind {
9898
OPTIONAL,
9999

100100
/**
101-
* A binding for {@link dagger.Binds}-annotated method that that delegates from requests for one
102-
* key to another.
101+
* A binding for a {@link dagger.Binds} or {@code dagger.Alias}-annotated method that delegates
102+
* from requests for one key to another. Note that parameterless {@code @Binds} methods are
103+
* represented as {@link #INJECTION} bindings to avoid issues with duplicate bindings.
103104
*/
104105
// TODO(dpb,ronshapiro): This name is confusing and could use work. Not all usages of @Binds
105106
// bindings are simple delegations and we should have a name that better reflects that

dagger-compiler/main/java/dagger/internal/codegen/processingstep/ModuleProcessingStep.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ private void generateForMethodsIn(XTypeElement module) {
120120
generate(factoryGenerator, bindingFactory.providesMethodBinding(method, module));
121121
} else if (method.hasAnnotation(XTypeNames.PRODUCES)) {
122122
generate(producerFactoryGenerator, bindingFactory.producesMethodBinding(method, module));
123-
} else if (method.hasAnnotation(XTypeNames.BINDS)) {
123+
} else if (method.hasAnnotation(XTypeNames.BINDS) && !method.getParameters().isEmpty()) {
124124
inaccessibleMapKeyProxyGenerator.generate(bindsMethodBinding(module, method), messager);
125125
}
126126
}

dagger-compiler/main/java/dagger/internal/codegen/validation/BindsMethodValidator.java

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,13 @@
2020
import static dagger.internal.codegen.validation.BindingElementValidator.AllowsScoping.ALLOWS_SCOPING;
2121
import static dagger.internal.codegen.validation.BindingMethodValidator.Abstractness.MUST_BE_ABSTRACT;
2222
import static dagger.internal.codegen.validation.BindingMethodValidator.ExceptionSuperclass.NO_EXCEPTIONS;
23+
import static dagger.internal.codegen.xprocessing.XTypes.isDeclared;
2324
import static dagger.internal.codegen.xprocessing.XTypes.isPrimitive;
2425

2526
import androidx.room3.compiler.processing.XMethodElement;
2627
import androidx.room3.compiler.processing.XProcessingEnv;
2728
import androidx.room3.compiler.processing.XType;
29+
import androidx.room3.compiler.processing.XTypeElement;
2830
import androidx.room3.compiler.processing.XVariableElement;
2931
import com.google.common.collect.ImmutableSet;
3032
import dagger.internal.codegen.base.ContributionType;
@@ -40,6 +42,7 @@
4042
final class BindsMethodValidator extends BindingMethodValidator {
4143
private final BindsTypeChecker bindsTypeChecker;
4244
private final DaggerSuperficialValidation superficialValidation;
45+
private final InjectionAnnotations injectionAnnotations;
4346

4447
@Inject
4548
BindsMethodValidator(
@@ -60,6 +63,7 @@ final class BindsMethodValidator extends BindingMethodValidator {
6063
injectionAnnotations);
6164
this.bindsTypeChecker = bindsTypeChecker;
6265
this.superficialValidation = superficialValidation;
66+
this.injectionAnnotations = injectionAnnotations;
6367
}
6468

6569
@Override
@@ -77,12 +81,16 @@ private class Validator extends MethodValidator {
7781

7882
@Override
7983
protected void checkParameters() {
80-
if (method.getParameters().size() != 1) {
84+
if (method.getParameters().size() > 1) {
8185
report.addError(
8286
bindingMethods(
83-
"must have exactly one parameter, whose type is assignable to the return type"));
84-
} else {
87+
"may have at most one parameter."
88+
+ " To bind an @Inject constructor, use a zero-parameter @Binds method."
89+
+ " To bind an instance, use a one-parameter @Binds method."));
90+
} else if (method.getParameters().size() == 1) {
8591
super.checkParameters();
92+
} else { // Parameters size == 0
93+
checkZeroParameterBinds();
8694
}
8795
}
8896

@@ -118,6 +126,67 @@ protected void checkParameter(XVariableElement parameter) {
118126
}
119127
}
120128

129+
private void checkZeroParameterBinds() {
130+
ContributionType contributionType = ContributionType.fromBindingElement(method);
131+
if (!contributionType.equals(ContributionType.UNIQUE)) {
132+
report.addError(
133+
"Parameterless @Binds methods cannot be used with multibinding annotations"
134+
+ " (@IntoSet, @ElementsIntoSet, @IntoMap).",
135+
method);
136+
}
137+
138+
if (!method.getAnnotationsAnnotatedWith(XTypeNames.MAP_KEY).isEmpty()) {
139+
report.addError("Parameterless @Binds methods cannot have a @MapKey.", method);
140+
}
141+
142+
XType returnType = method.getReturnType();
143+
if (!isDeclared(returnType)) {
144+
report.addError("Parameterless @Binds methods must return a declared type.", method);
145+
return;
146+
}
147+
// Disallow parameterized types for parameterless @Binds.
148+
// This feature is intended for making the @Inject binding of a concrete, non-generic
149+
// class explicit to apply method-level annotations.
150+
// Binding specific instantiations of generic types should be done with @Provides,
151+
// which can properly handle dependencies related to the type parameters.
152+
// Allowing generics here would lead to ambiguity in annotation scoping and
153+
// dependency resolution.
154+
if (!returnType.getTypeArguments().isEmpty()) {
155+
report.addError("Parameterless @Binds methods cannot return a parameterized type.", method);
156+
return;
157+
}
158+
XTypeElement returnTypeElement = returnType.getTypeElement();
159+
if (returnTypeElement == null) {
160+
report.addError("Parameterless @Binds methods must return a valid type.", method);
161+
return;
162+
}
163+
if (!returnTypeElement.getTypeParameters().isEmpty()) {
164+
report.addError(
165+
"Parameterless @Binds methods cannot bind generic types with @Inject constructors.",
166+
method);
167+
}
168+
if (injectionAnnotations.getQualifier(method).isPresent()) {
169+
report.addError("Parameterless @Binds methods may not have qualifiers.", method);
170+
}
171+
if (injectionAnnotations.getScope(method).isPresent()) {
172+
report.addError("Parameterless @Binds methods may not be scoped.", method);
173+
}
174+
if (injectionAnnotations.getScope(returnTypeElement).isPresent()) {
175+
report.addError(
176+
"Parameterless @Binds methods cannot bind types with scoped @Inject constructors.",
177+
method);
178+
}
179+
if (returnTypeElement.getConstructors().stream()
180+
.filter(InjectionAnnotations::hasInjectAnnotation)
181+
.count()
182+
!= 1) {
183+
report.addError(
184+
"Parameterless @Binds methods must return a type with exactly one @Inject"
185+
+ " constructor.",
186+
method);
187+
}
188+
}
189+
121190
private XType boxIfNecessary(XType maybePrimitive) {
122191
return isPrimitive(maybePrimitive) ? maybePrimitive.boxed() : maybePrimitive;
123192
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Copyright (C) 2026 The Dagger Authors.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
load(
16+
"//:build_defs.bzl",
17+
"DOCLINT_HTML_AND_SYNTAX",
18+
"DOCLINT_REFERENCES",
19+
)
20+
load("//:test_defs.bzl", "GenJavaTests")
21+
22+
package(default_visibility = ["//:src"])
23+
24+
GenJavaTests(
25+
name = "BindsExplicitForInjectTest",
26+
srcs = ["BindsExplicitForInjectTest.java"],
27+
javacopts = DOCLINT_HTML_AND_SYNTAX + DOCLINT_REFERENCES,
28+
deps = [
29+
"//third_party/java/dagger",
30+
"//third_party/java/jsr330_inject",
31+
"//third_party/java/junit",
32+
"//third_party/java/truth",
33+
],
34+
)

0 commit comments

Comments
 (0)