Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -53,24 +53,29 @@
import dagger.internal.codegen.xprocessing.XTypeNames;
import java.util.Optional;
import javax.inject.Inject;
import javax.inject.Provider;

/** A factory for {@link Binding} objects. */
public final class BindingFactory {
private final KeyFactory keyFactory;
private final DependencyRequestFactory dependencyRequestFactory;
private final InjectionSiteFactory injectionSiteFactory;
private final InjectionAnnotations injectionAnnotations;
// We need a provider to avoid circular dependencies.
private final Provider<InjectBindingRegistry> injectBindingRegistryProvider;

@Inject
BindingFactory(
KeyFactory keyFactory,
DependencyRequestFactory dependencyRequestFactory,
InjectionSiteFactory injectionSiteFactory,
InjectionAnnotations injectionAnnotations) {
InjectionAnnotations injectionAnnotations,
Provider<InjectBindingRegistry> injectBindingRegistryProvider) {
this.keyFactory = keyFactory;
this.dependencyRequestFactory = dependencyRequestFactory;
this.injectionSiteFactory = injectionSiteFactory;
this.injectionAnnotations = injectionAnnotations;
this.injectBindingRegistryProvider = injectBindingRegistryProvider;
}

/**
Expand Down Expand Up @@ -162,6 +167,47 @@ public AssistedInjectionBinding assistedInjectionBinding(
.build();
}

/**
* Returns an {@link BindingKind#INJECTION} binding returned by a parameterless {@code @Binds}
* method.
*
* <p>Although these are {@code @Binds} methods, they are represented as {@link InjectionBinding}s
* rather than {@link DelegateBinding}s. This is because a parameterless {@code @Binds} method
* binds the return type to its {@code @Inject} constructor. If this were a {@link
* DelegateBinding}, both the delegate and the underlying {@code @Inject} binding would have the
* same key, leading to duplicate binding errors and cyclical dependency issues in both binding
* graph resolution and code generation. Representing it as an {@link InjectionBinding} allows us
* to augment the implicit injection binding with metadata from the {@code @Binds} method (e.g.,
* {@code contributingModule}) without creating duplicate keys.
*
* @param bindsMethod the parameterless {@code @Binds}-annotated method
* @param module the installed module that declares or inherits the method
*/
public Optional<InjectionBinding> explicitInjectionBinding(
XMethodElement bindsMethod, XTypeElement module) {
checkArgument(bindsMethod.hasAnnotation(XTypeNames.BINDS));
checkArgument(bindsMethod.getParameters().isEmpty());
// Normally, we would use the input method as the binding element, but as in this case it is an
// Binds method that breaks assumptions for an InjectionBinding. Instead, we use the @Inject
// constructor as the binding element and expose the @Binds method via
// InjectionBinding#declaringElement().
// We call InjectBindingRegistry#getOrFindInjectionBinding() rather than calling
// BindingFactory#injectionBinding() directly because the former ensures that the binding is
// properly validated before returning the binding.
Key key = keyFactory.forDelegateDeclaration(bindsMethod, module); // Key from @Binds
return injectBindingRegistryProvider
.get()
.getOrFindInjectionBinding(key)
.map(
binding ->
((InjectionBinding) binding)
.toBuilder()
.key(key)
.contributingModule(module) // Mark as coming from module
.declaringElement(bindsMethod)
.build());
}

public AssistedFactoryBinding assistedFactoryBinding(
XTypeElement factory, Optional<XType> resolvedFactoryType) {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,15 @@ public String format(Declaration declaration) {
return formatSubcomponentDeclaration((SubcomponentDeclaration) declaration);
}

if (declaration instanceof InjectionBinding) {
InjectionBinding injectionBinding = (InjectionBinding) declaration;
if (injectionBinding.declaringElement().isPresent()) {
return methodSignatureFormatter.format(
injectionBinding.declaringElement().get(),
injectionBinding.contributingModule().map(XTypeElement::getType));
}
}

if (declaration.bindingElement().isPresent()) {
XElement bindingElement = declaration.bindingElement().get();
if (isMethodParameter(bindingElement)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ public static final class Factory {

public DelegateDeclaration create(XMethodElement method, XTypeElement contributingModule) {
checkArgument(DelegateBinding.hasDelegateAnnotation(method));
checkArgument(method.getParameters().size() == 1);
XMethodType resolvedMethod = method.asMemberOf(contributingModule.getType());
DependencyRequest delegateRequest =
dependencyRequestFactory.forRequiredResolvedVariable(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

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

import androidx.room3.compiler.processing.XMethodElement;
import com.google.auto.value.AutoValue;
import com.google.auto.value.extension.memoized.Memoized;
import com.google.common.collect.ImmutableSet;
Expand All @@ -30,7 +31,15 @@
import dagger.internal.codegen.xprocessing.Nullability;
import java.util.Optional;

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

@Override
public boolean requiresModuleInstance() {
return false;
}

@Override
public abstract Builder toBuilder();

/** The element that declares this binding, used for parameterless {@code @Binds} methods. */
public abstract Optional<XMethodElement> declaringElement();

@Memoized
@Override
public abstract int hashCode();
Expand All @@ -93,5 +110,7 @@ abstract static class Builder extends ContributionBinding.Builder<InjectionBindi
abstract Builder constructorDependencies(Iterable<DependencyRequest> constructorDependencies);

abstract Builder injectionSites(ImmutableSortedSet<InjectionSite> injectionSites);

abstract Builder declaringElement(XMethodElement declaringElement);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ public String format(XExecutableElement method, Optional<XType> container) {

private String format(
XExecutableElement method, Optional<XType> container, boolean includeReturnType) {
return container.isPresent()
return container.isPresent() && !getSimpleName(method).contentEquals("<init>")
? format(
method,
method.asMemberOf(container.get()),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,8 +153,19 @@ public ModuleDescriptor createUncached(XTypeElement moduleElement) {
bindings.add(bindingFactory.producesMethodBinding(moduleMethod, moduleElement));
}
if (DelegateBinding.hasDelegateAnnotation(moduleMethod)) {
delegates.add(
bindingDelegateDeclarationFactory.create(moduleMethod, moduleElement));
if (moduleMethod.hasAnnotation(XTypeNames.BINDS)
&& moduleMethod.getParameters().isEmpty()) {
// Parameterless @Binds methods are treated as explicit InjectionBindings
// to avoid duplicate binding errors and cyclical dependencies that arise
// if they were modeled as DelegateBindings on the same key as the @Inject
// constructor.
bindingFactory
.explicitInjectionBinding(moduleMethod, moduleElement)
.ifPresent(bindings::add);
} else {
delegates.add(
bindingDelegateDeclarationFactory.create(moduleMethod, moduleElement));
}
}
if (moduleMethod.hasAnnotation(XTypeNames.MULTIBINDS)) {
multibindingDeclarations.add(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,9 @@ public enum BindingKind {
OPTIONAL,

/**
* A binding for {@link dagger.Binds}-annotated method that that delegates from requests for one
* key to another.
* A binding for a {@link dagger.Binds} or {@code dagger.Alias}-annotated method that delegates
* from requests for one key to another. Note that parameterless {@code @Binds} methods are
* represented as {@link #INJECTION} bindings to avoid issues with duplicate bindings.
*/
// TODO(dpb,ronshapiro): This name is confusing and could use work. Not all usages of @Binds
// bindings are simple delegations and we should have a name that better reflects that
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ private void generateForMethodsIn(XTypeElement module) {
generate(factoryGenerator, bindingFactory.providesMethodBinding(method, module));
} else if (method.hasAnnotation(XTypeNames.PRODUCES)) {
generate(producerFactoryGenerator, bindingFactory.producesMethodBinding(method, module));
} else if (method.hasAnnotation(XTypeNames.BINDS)) {
} else if (method.hasAnnotation(XTypeNames.BINDS) && !method.getParameters().isEmpty()) {
inaccessibleMapKeyProxyGenerator.generate(bindsMethodBinding(module, method), messager);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@
import static dagger.internal.codegen.validation.BindingElementValidator.AllowsScoping.ALLOWS_SCOPING;
import static dagger.internal.codegen.validation.BindingMethodValidator.Abstractness.MUST_BE_ABSTRACT;
import static dagger.internal.codegen.validation.BindingMethodValidator.ExceptionSuperclass.NO_EXCEPTIONS;
import static dagger.internal.codegen.xprocessing.XTypes.isDeclared;
import static dagger.internal.codegen.xprocessing.XTypes.isPrimitive;

import androidx.room3.compiler.processing.XMethodElement;
import androidx.room3.compiler.processing.XProcessingEnv;
import androidx.room3.compiler.processing.XType;
import androidx.room3.compiler.processing.XTypeElement;
import androidx.room3.compiler.processing.XVariableElement;
import com.google.common.collect.ImmutableSet;
import dagger.internal.codegen.base.ContributionType;
Expand All @@ -40,6 +42,7 @@
final class BindsMethodValidator extends BindingMethodValidator {
private final BindsTypeChecker bindsTypeChecker;
private final DaggerSuperficialValidation superficialValidation;
private final InjectionAnnotations injectionAnnotations;

@Inject
BindsMethodValidator(
Expand All @@ -60,6 +63,7 @@ final class BindsMethodValidator extends BindingMethodValidator {
injectionAnnotations);
this.bindsTypeChecker = bindsTypeChecker;
this.superficialValidation = superficialValidation;
this.injectionAnnotations = injectionAnnotations;
}

@Override
Expand All @@ -77,12 +81,16 @@ private class Validator extends MethodValidator {

@Override
protected void checkParameters() {
if (method.getParameters().size() != 1) {
if (method.getParameters().size() > 1) {
report.addError(
bindingMethods(
"must have exactly one parameter, whose type is assignable to the return type"));
} else {
"may have at most one parameter."
+ " To bind an @Inject constructor, use a zero-parameter @Binds method."
+ " To bind an instance, use a one-parameter @Binds method."));
} else if (method.getParameters().size() == 1) {
super.checkParameters();
} else { // Parameters size == 0
checkZeroParameterBinds();
}
}

Expand Down Expand Up @@ -118,6 +126,67 @@ protected void checkParameter(XVariableElement parameter) {
}
}

private void checkZeroParameterBinds() {
ContributionType contributionType = ContributionType.fromBindingElement(method);
if (!contributionType.equals(ContributionType.UNIQUE)) {
report.addError(
"Parameterless @Binds methods cannot be used with multibinding annotations"
+ " (@IntoSet, @ElementsIntoSet, @IntoMap).",
method);
}

if (!method.getAnnotationsAnnotatedWith(XTypeNames.MAP_KEY).isEmpty()) {
report.addError("Parameterless @Binds methods cannot have a @MapKey.", method);
}

XType returnType = method.getReturnType();
if (!isDeclared(returnType)) {
report.addError("Parameterless @Binds methods must return a declared type.", method);
return;
}
// Disallow parameterized types for parameterless @Binds.
// This feature is intended for making the @Inject binding of a concrete, non-generic
// class explicit to apply method-level annotations.
// Binding specific instantiations of generic types should be done with @Provides,
// which can properly handle dependencies related to the type parameters.
// Allowing generics here would lead to ambiguity in annotation scoping and
// dependency resolution.
if (!returnType.getTypeArguments().isEmpty()) {
report.addError("Parameterless @Binds methods cannot return a parameterized type.", method);
return;
}
XTypeElement returnTypeElement = returnType.getTypeElement();
if (returnTypeElement == null) {
report.addError("Parameterless @Binds methods must return a valid type.", method);
return;
}
if (!returnTypeElement.getTypeParameters().isEmpty()) {
report.addError(
"Parameterless @Binds methods cannot bind generic types with @Inject constructors.",
method);
}
if (injectionAnnotations.getQualifier(method).isPresent()) {
report.addError("Parameterless @Binds methods may not have qualifiers.", method);
}
if (injectionAnnotations.getScope(method).isPresent()) {
report.addError("Parameterless @Binds methods may not be scoped.", method);
}
if (injectionAnnotations.getScope(returnTypeElement).isPresent()) {
report.addError(
"Parameterless @Binds methods cannot bind types with scoped @Inject constructors.",
method);
}
if (returnTypeElement.getConstructors().stream()
.filter(InjectionAnnotations::hasInjectAnnotation)
.count()
!= 1) {
report.addError(
"Parameterless @Binds methods must return a type with exactly one @Inject"
+ " constructor.",
method);
}
}

private XType boxIfNecessary(XType maybePrimitive) {
return isPrimitive(maybePrimitive) ? maybePrimitive.boxed() : maybePrimitive;
}
Expand Down
34 changes: 34 additions & 0 deletions javatests/dagger/functional/explicitbinds/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Copyright (C) 2026 The Dagger 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.

load(
"//:build_defs.bzl",
"DOCLINT_HTML_AND_SYNTAX",
"DOCLINT_REFERENCES",
)
load("//:test_defs.bzl", "GenJavaTests")

package(default_visibility = ["//:src"])

GenJavaTests(
name = "BindsExplicitForInjectTest",
srcs = ["BindsExplicitForInjectTest.java"],
javacopts = DOCLINT_HTML_AND_SYNTAX + DOCLINT_REFERENCES,
deps = [
"//third_party/java/dagger",
"//third_party/java/jsr330_inject",
"//third_party/java/junit",
"//third_party/java/truth",
],
)
Loading
Loading