Skip to content

Commit ef236f4

Browse files
vadym-shavaldaDagger Team
authored andcommitted
[PATCH] Add helpful message when using @assisted with @Inject.
Closes #4692. RELNOTES=N/A PiperOrigin-RevId: 749597348
1 parent 80be44d commit ef236f4

5 files changed

Lines changed: 186 additions & 91 deletions

File tree

dagger-compiler/main/java/dagger/internal/codegen/DelegateComponentProcessor.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
import dagger.internal.codegen.kotlin.KotlinMetadataFactory;
4646
import dagger.internal.codegen.processingstep.ProcessingStepsModule;
4747
import dagger.internal.codegen.validation.AnyBindingMethodValidator;
48+
import dagger.internal.codegen.validation.AssistedValidator;
4849
import dagger.internal.codegen.validation.BindingMethodValidatorsModule;
4950
import dagger.internal.codegen.validation.ComponentCreatorValidator;
5051
import dagger.internal.codegen.validation.ComponentValidator;
@@ -186,6 +187,10 @@ interface ProcessingRoundCacheModule {
186187
@IntoSet
187188
ClearableCache componentCreatorValidator(ComponentCreatorValidator cache);
188189

190+
@Binds
191+
@IntoSet
192+
ClearableCache assistedValidator(AssistedValidator cache);
193+
189194
@Binds
190195
@IntoSet
191196
ClearableCache kotlinMetadata(KotlinMetadataFactory cache);

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

Lines changed: 9 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,10 @@
1616

1717
package dagger.internal.codegen.processingstep;
1818

19-
import static androidx.room.compiler.processing.XElementKt.isConstructor;
20-
import static androidx.room.compiler.processing.XElementKt.isMethod;
21-
import static dagger.internal.codegen.binding.AssistedInjectionAnnotations.assistedFactoryMethod;
22-
import static dagger.internal.codegen.binding.AssistedInjectionAnnotations.isAssistedFactoryType;
23-
import static dagger.internal.codegen.xprocessing.XElements.asMethod;
24-
import static dagger.internal.codegen.xprocessing.XElements.closestEnclosingTypeElement;
25-
import static dagger.internal.codegen.xprocessing.XElements.getSimpleName;
26-
2719
import androidx.room.compiler.codegen.XClassName;
28-
import androidx.room.compiler.processing.XExecutableElement;
2920
import androidx.room.compiler.processing.XExecutableParameterElement;
30-
import androidx.room.compiler.processing.XTypeElement;
3121
import com.google.common.collect.ImmutableSet;
32-
import dagger.internal.codegen.binding.InjectionAnnotations;
33-
import dagger.internal.codegen.validation.ValidationReport;
22+
import dagger.internal.codegen.validation.AssistedValidator;
3423
import dagger.internal.codegen.xprocessing.XTypeNames;
3524
import javax.inject.Inject;
3625

@@ -40,11 +29,11 @@
4029
* <p>This processing step should run after {@link AssistedFactoryProcessingStep}.
4130
*/
4231
final class AssistedProcessingStep extends TypeCheckingProcessingStep<XExecutableParameterElement> {
43-
private final InjectionAnnotations injectionAnnotations;
32+
private final AssistedValidator assistedValidator;
4433

4534
@Inject
46-
AssistedProcessingStep(InjectionAnnotations injectionAnnotations) {
47-
this.injectionAnnotations = injectionAnnotations;
35+
AssistedProcessingStep(AssistedValidator assistedValidator) {
36+
this.assistedValidator = assistedValidator;
4837
}
4938

5039
@Override
@@ -55,59 +44,11 @@ public ImmutableSet<XClassName> annotationClassNames() {
5544
@Override
5645
protected void process(
5746
XExecutableParameterElement assisted, ImmutableSet<XClassName> annotations) {
58-
new AssistedValidator().validate(assisted).printMessagesTo(messager);
59-
}
60-
61-
private final class AssistedValidator {
62-
ValidationReport validate(XExecutableParameterElement assisted) {
63-
ValidationReport.Builder report = ValidationReport.about(assisted);
64-
65-
XExecutableElement enclosingElement = assisted.getEnclosingElement();
66-
if (!isAssistedInjectConstructor(enclosingElement)
67-
&& !isAssistedFactoryCreateMethod(enclosingElement)
68-
// The generated java stubs for kotlin data classes contain a "copy" method that has
69-
// the same parameters (and annotations) as the constructor, so just ignore it.
70-
&& !isKotlinDataClassCopyMethod(enclosingElement)) {
71-
report.addError(
72-
"@Assisted parameters can only be used within an @AssistedInject-annotated "
73-
+ "constructor.",
74-
assisted);
75-
}
76-
77-
injectionAnnotations
78-
.getQualifiers(assisted)
79-
.forEach(
80-
qualifier ->
81-
report.addError(
82-
"Qualifiers cannot be used with @Assisted parameters.", assisted, qualifier));
83-
84-
return report.build();
47+
// If the AssistedValidator already validated this element as part of InjectValidator, then we
48+
// don't need to report the errors again.
49+
if (assistedValidator.containsCache(assisted)) {
50+
return;
8551
}
86-
}
87-
88-
private boolean isAssistedInjectConstructor(XExecutableElement executableElement) {
89-
return isConstructor(executableElement)
90-
&& executableElement.hasAnnotation(XTypeNames.ASSISTED_INJECT);
91-
}
92-
93-
private boolean isAssistedFactoryCreateMethod(XExecutableElement executableElement) {
94-
if (isMethod(executableElement)) {
95-
XTypeElement enclosingElement = closestEnclosingTypeElement(executableElement);
96-
return isAssistedFactoryType(enclosingElement)
97-
// This assumes we've already validated AssistedFactory and that a valid method exists.
98-
&& assistedFactoryMethod(enclosingElement).equals(executableElement);
99-
}
100-
return false;
101-
}
102-
103-
private boolean isKotlinDataClassCopyMethod(XExecutableElement executableElement) {
104-
// Note: This is a best effort. Technically, we could check the return type and parameters of
105-
// the copy method to verify it's the one associated with the constructor, but I'd rather keep
106-
// this simple to avoid encoding too many details of kapt's stubs. At worst, we'll be allowing
107-
// an @Assisted annotation that has no affect, which is already true for many of Dagger's other
108-
// annotations.
109-
return isMethod(executableElement)
110-
&& getSimpleName(asMethod(executableElement)).contentEquals("copy")
111-
&& closestEnclosingTypeElement(executableElement.getEnclosingElement()).isDataClass();
52+
assistedValidator.validate(assisted).printMessagesTo(messager);
11253
}
11354
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/*
2+
* Copyright (C) 2025 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.internal.codegen.validation;
18+
19+
import static androidx.room.compiler.processing.XElementKt.isConstructor;
20+
import static androidx.room.compiler.processing.XElementKt.isMethod;
21+
import static com.google.common.base.Preconditions.checkArgument;
22+
import static dagger.internal.codegen.binding.AssistedInjectionAnnotations.assistedFactoryMethod;
23+
import static dagger.internal.codegen.binding.AssistedInjectionAnnotations.isAssistedFactoryType;
24+
import static dagger.internal.codegen.xprocessing.XElements.asMethod;
25+
import static dagger.internal.codegen.xprocessing.XElements.closestEnclosingTypeElement;
26+
import static dagger.internal.codegen.xprocessing.XElements.getSimpleName;
27+
28+
import androidx.room.compiler.processing.XExecutableElement;
29+
import androidx.room.compiler.processing.XExecutableParameterElement;
30+
import androidx.room.compiler.processing.XTypeElement;
31+
import dagger.internal.codegen.base.ClearableCache;
32+
import dagger.internal.codegen.binding.InjectionAnnotations;
33+
import dagger.internal.codegen.xprocessing.XTypeNames;
34+
import java.util.HashMap;
35+
import java.util.Map;
36+
import javax.inject.Inject;
37+
import javax.inject.Singleton;
38+
39+
/** Validates an {@link dagger.assisted.Assisted}-annotated parameter. */
40+
@Singleton
41+
public final class AssistedValidator implements ClearableCache {
42+
private final InjectionAnnotations injectionAnnotations;
43+
private final Map<XExecutableParameterElement, ValidationReport> cache = new HashMap<>();
44+
45+
@Inject
46+
AssistedValidator(InjectionAnnotations injectionAnnotations) {
47+
this.injectionAnnotations = injectionAnnotations;
48+
}
49+
50+
@Override
51+
public void clearCache() {
52+
cache.clear();
53+
}
54+
55+
public boolean containsCache(XExecutableParameterElement assisted) {
56+
return cache.containsKey(assisted);
57+
}
58+
59+
public ValidationReport validate(XExecutableParameterElement assisted) {
60+
checkArgument(assisted.hasAnnotation(XTypeNames.ASSISTED));
61+
return cache.computeIfAbsent(assisted, this::validateUncached);
62+
}
63+
64+
65+
private ValidationReport validateUncached(XExecutableParameterElement assisted) {
66+
ValidationReport.Builder report = ValidationReport.about(assisted);
67+
68+
XExecutableElement enclosingElement = assisted.getEnclosingElement();
69+
if (!isAssistedInjectConstructor(enclosingElement)
70+
&& !isAssistedFactoryCreateMethod(enclosingElement)
71+
// The generated java stubs for kotlin data classes contain a "copy" method that has
72+
// the same parameters (and annotations) as the constructor, so just ignore it.
73+
&& !isKotlinDataClassCopyMethod(enclosingElement)) {
74+
report.addError(
75+
"@Assisted parameters can only be used within an @AssistedInject-annotated "
76+
+ "constructor.",
77+
assisted);
78+
}
79+
80+
injectionAnnotations
81+
.getQualifiers(assisted)
82+
.forEach(
83+
qualifier ->
84+
report.addError(
85+
"Qualifiers cannot be used with @Assisted parameters.", assisted, qualifier));
86+
87+
return report.build();
88+
}
89+
90+
private boolean isAssistedInjectConstructor(XExecutableElement executableElement) {
91+
return isConstructor(executableElement)
92+
&& executableElement.hasAnnotation(XTypeNames.ASSISTED_INJECT);
93+
}
94+
95+
private boolean isAssistedFactoryCreateMethod(XExecutableElement executableElement) {
96+
if (isMethod(executableElement)) {
97+
XTypeElement enclosingElement = closestEnclosingTypeElement(executableElement);
98+
return isAssistedFactoryType(enclosingElement)
99+
// This assumes we've already validated AssistedFactory and that a valid method exists.
100+
&& assistedFactoryMethod(enclosingElement).equals(executableElement);
101+
}
102+
return false;
103+
}
104+
105+
private boolean isKotlinDataClassCopyMethod(XExecutableElement executableElement) {
106+
// Note: This is a best effort. Technically, we could check the return type and parameters of
107+
// the copy method to verify it's the one associated with the constructor, but I'd rather keep
108+
// this simple to avoid encoding too many details of kapt's stubs. At worst, we'll be allowing
109+
// an @Assisted annotation that has no affect, which is already true for many of Dagger's other
110+
// annotations.
111+
return isMethod(executableElement)
112+
&& getSimpleName(asMethod(executableElement)).contentEquals("copy")
113+
&& closestEnclosingTypeElement(executableElement.getEnclosingElement()).isDataClass();
114+
}
115+
}

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

Lines changed: 31 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,15 @@
1919
import static com.google.common.collect.Iterables.getOnlyElement;
2020
import static dagger.internal.codegen.base.Util.reentrantComputeIfAbsent;
2121
import static dagger.internal.codegen.binding.AssistedInjectionAnnotations.assistedInjectedConstructors;
22+
import static dagger.internal.codegen.binding.AssistedInjectionAnnotations.isAssistedParameter;
2223
import static dagger.internal.codegen.binding.InjectionAnnotations.injectedConstructors;
2324
import static dagger.internal.codegen.binding.SourceFiles.factoryNameForElement;
2425
import static dagger.internal.codegen.binding.SourceFiles.membersInjectorNameForType;
2526
import static dagger.internal.codegen.extension.DaggerStreams.toImmutableList;
2627
import static dagger.internal.codegen.xprocessing.XElements.closestEnclosingTypeElement;
27-
import static dagger.internal.codegen.xprocessing.XElements.getAnyAnnotation;
2828
import static dagger.internal.codegen.xprocessing.XMethodElements.hasTypeParameters;
29-
import static dagger.internal.codegen.xprocessing.XTypeNames.injectTypeNames;
3029
import static dagger.internal.codegen.xprocessing.XTypes.isSubtype;
3130

32-
import androidx.room.compiler.codegen.XClassName;
3331
import androidx.room.compiler.codegen.XTypeName;
3432
import androidx.room.compiler.processing.XAnnotation;
3533
import androidx.room.compiler.processing.XConstructorElement;
@@ -49,7 +47,6 @@
4947
import dagger.internal.codegen.compileroption.CompilerOptions;
5048
import dagger.internal.codegen.langmodel.Accessibility;
5149
import dagger.internal.codegen.model.Scope;
52-
import dagger.internal.codegen.xprocessing.XAnnotations;
5350
import dagger.internal.codegen.xprocessing.XTypeNames;
5451
import java.util.HashMap;
5552
import java.util.Map;
@@ -72,6 +69,7 @@ public final class InjectValidator implements ClearableCache {
7269
private final MethodSignatureFormatter methodSignatureFormatter;
7370
private final InternalValidator validator;
7471
private final InternalValidator validatorWhenGeneratingCode;
72+
private final AssistedValidator assistedValidator;
7573

7674
@Inject
7775
InjectValidator(
@@ -80,12 +78,14 @@ public final class InjectValidator implements ClearableCache {
8078
CompilerOptions compilerOptions,
8179
InjectionAnnotations injectionAnnotations,
8280
DaggerSuperficialValidation superficialValidation,
83-
MethodSignatureFormatter methodSignatureFormatter) {
81+
MethodSignatureFormatter methodSignatureFormatter,
82+
AssistedValidator assistedValidator) {
8483
this.processingEnv = processingEnv;
8584
this.dependencyRequestValidator = dependencyRequestValidator;
8685
this.injectionAnnotations = injectionAnnotations;
8786
this.superficialValidation = superficialValidation;
8887
this.methodSignatureFormatter = methodSignatureFormatter;
88+
this.assistedValidator = assistedValidator;
8989

9090
// When validating types that require a generated factory class we need to error on private and
9191
// static inject members even if the compiler options are set to not error.
@@ -191,21 +191,24 @@ private ValidationReport validateConstructor(XConstructorElement constructorElem
191191
ValidationReport.Builder builder =
192192
ValidationReport.about(constructorElement.getEnclosingElement());
193193

194-
if (InjectionAnnotations.hasInjectAnnotation(constructorElement)
195-
&& constructorElement.hasAnnotation(XTypeNames.ASSISTED_INJECT)) {
194+
boolean isInjectConstructor = InjectionAnnotations.hasInjectAnnotation(constructorElement);
195+
boolean isAssistedInjectConstructor =
196+
InjectionAnnotations.hasAssistedInjectAnnotation(constructorElement);
197+
final String injectAnnotationName;
198+
if (isInjectConstructor && isAssistedInjectConstructor) {
196199
builder.addError("Constructors cannot be annotated with both @Inject and @AssistedInject");
200+
// The rest of the validation assumes that only one of the annotations is present so return
201+
// early if there are both.
202+
return builder.build();
203+
} else if (isInjectConstructor) {
204+
injectAnnotationName = "Inject";
205+
} else if (isAssistedInjectConstructor) {
206+
injectAnnotationName = "AssistedInject";
207+
} else {
208+
throw new AssertionError(
209+
"No @Inject or @AssistedInject annotation found: " + constructorElement);
197210
}
198211

199-
XClassName injectAnnotation =
200-
getAnyAnnotation(
201-
constructorElement,
202-
ImmutableSet.<XClassName>builder()
203-
.addAll(injectTypeNames())
204-
.add(XTypeNames.ASSISTED_INJECT)
205-
.build())
206-
.map(XAnnotations::asClassName)
207-
.get();
208-
209212
if (constructorElement.isPrivate()) {
210213
builder.addError(
211214
"Dagger does not support injection into private constructors", constructorElement);
@@ -221,15 +224,15 @@ private ValidationReport validateConstructor(XConstructorElement constructorElem
221224
builder.addError(
222225
String.format(
223226
"@Qualifier annotations are not allowed on @%s constructors",
224-
injectAnnotation.getSimpleName()),
227+
injectAnnotationName),
225228
constructorElement,
226229
qualifier);
227230
}
228231

229232
String scopeErrorMsg =
230233
String.format(
231234
"@Scope annotations are not allowed on @%s constructors",
232-
injectAnnotation.getSimpleName());
235+
injectAnnotationName);
233236

234237
if (InjectionAnnotations.hasInjectAnnotation(constructorElement)) {
235238
scopeErrorMsg += "; annotate the class instead";
@@ -243,14 +246,19 @@ private ValidationReport validateConstructor(XConstructorElement constructorElem
243246

244247
for (XExecutableParameterElement parameter : constructorElement.getParameters()) {
245248
superficialValidation.validateTypeOf(parameter);
246-
validateDependencyRequest(builder, parameter);
249+
if (isAssistedParameter(parameter)) {
250+
builder.addSubreport(assistedValidator.validate(parameter));
251+
} else {
252+
// Only validate dependency requests for non-assisted parameters.
253+
validateDependencyRequest(builder, parameter);
254+
}
247255
}
248256

249257
if (throwsCheckedExceptions(constructorElement)) {
250258
builder.addItem(
251259
String.format(
252260
"Dagger does not support checked exceptions on @%s constructors",
253-
injectAnnotation.getSimpleName()),
261+
injectAnnotationName),
254262
privateMemberDiagnosticKind,
255263
constructorElement);
256264
}
@@ -262,7 +270,7 @@ private ValidationReport validateConstructor(XConstructorElement constructorElem
262270
builder.addError(
263271
String.format(
264272
"@%s is nonsense on the constructor of an abstract class",
265-
injectAnnotation.getSimpleName()),
273+
injectAnnotationName),
266274
constructorElement);
267275
}
268276

@@ -271,7 +279,7 @@ private ValidationReport validateConstructor(XConstructorElement constructorElem
271279
String.format(
272280
"@%s constructors are invalid on inner classes. "
273281
+ "Did you mean to make the class static?",
274-
injectAnnotation.getSimpleName()),
282+
injectAnnotationName),
275283
constructorElement);
276284
}
277285

0 commit comments

Comments
 (0)