Skip to content

Commit 74ec25c

Browse files
java-team-github-botGuice Team
authored andcommitted
Add Guice hints when dealing with Kotlin declaration site variance.
PiperOrigin-RevId: 698431304
1 parent e032518 commit 74ec25c

15 files changed

+429
-47
lines changed

core/src/com/google/inject/internal/KotlinSupport.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,5 +74,10 @@ public boolean isLocalClass(Class<?> clazz) {
7474
public boolean isValueClass(Class<?> clazz) {
7575
return false;
7676
}
77+
78+
@Override
79+
public boolean isKotlinClass(Class<?> clazz) {
80+
return false;
81+
}
7782
}
7883
}

core/src/com/google/inject/internal/KotlinSupportInterface.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,7 @@ public interface KotlinSupportInterface {
3333

3434
/** Returns whether the {@code clazz} is a Kotlin value class. */
3535
boolean isValueClass(Class<?> clazz);
36+
37+
/** Returns whether the {@code clazz} is a Kotlin class. */
38+
boolean isKotlinClass(Class<?> clazz);
3639
}

core/src/com/google/inject/internal/MissingImplementationError.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ public MissingImplementationError(Key<T> key, Injector injector, List<Object> so
2424
key,
2525
// Defer building suggestions until messages are requested, to avoid the work associated
2626
// with iterating bindings in scenarios where the exceptions are discarded.
27-
Suppliers.memoize(() -> MissingImplementationErrorHints.getSuggestions(key, injector)),
27+
Suppliers.memoize(
28+
() -> MissingImplementationErrorHints.getSuggestions(key, injector, sources)),
2829
sources);
2930
}
3031

core/src/com/google/inject/internal/MissingImplementationErrorHints.java

Lines changed: 161 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,19 @@
1111
import com.google.inject.Key;
1212
import com.google.inject.TypeLiteral;
1313
import com.google.inject.spi.BindingSourceRestriction;
14+
import com.google.inject.spi.Dependency;
15+
import com.google.inject.spi.ElementSource;
1416
import com.google.inject.spi.UntargettedBinding;
1517
import java.lang.reflect.GenericArrayType;
18+
import java.lang.reflect.Member;
1619
import java.lang.reflect.ParameterizedType;
1720
import java.lang.reflect.Type;
1821
import java.lang.reflect.WildcardType;
1922
import java.util.ArrayList;
2023
import java.util.Formatter;
2124
import java.util.List;
2225
import java.util.Map;
26+
import java.util.function.BiFunction;
2327

2428
// TODO(b/165344346): Migrate to use suggest hints API.
2529
/** Helper class to find hints for {@link MissingImplementationError}. */
@@ -33,6 +37,15 @@ private MissingImplementationErrorHints() {}
3337
/** When a binding is not found, show at most this many bindings that have some similarities */
3438
private static final int MAX_RELATED_TYPES_REPORTED = 3;
3539

40+
private static final String WILDCARD_EXTENDS = "? extends ";
41+
private static final String WILDCARD_SUPER = "? super ";
42+
43+
private static final String WILDCARDS_WARNING =
44+
"\nYou might be running into a @JvmSuppressWildcards or @JvmWildcards issue.";
45+
46+
private static final String WILDCARDS_POSSIBLE_FIXES =
47+
"\nConsider these options instead (these are guesses but use your best judgment):";
48+
3649
/**
3750
* If the key is unknown and it is one of these types, it generally means there is a missing
3851
* annotation.
@@ -52,13 +65,23 @@ private MissingImplementationErrorHints() {}
5265
* generic arguments (e.g. Optional&lt;String&gt; won't be similar to Optional&lt;Integer&gt;).
5366
*/
5467
static boolean areSimilarLookingTypes(Type a, Type b) {
68+
return areSimilarTypes(
69+
a, b, (aClass, bClass) -> aClass.getSimpleName().equals(bClass.getSimpleName()));
70+
}
71+
72+
private static boolean areOnlyDifferencesInVariance(Type a, Type b) {
73+
return areSimilarTypes(a, b, Object::equals);
74+
}
75+
76+
private static boolean areSimilarTypes(
77+
Type a, Type b, BiFunction<Class<?>, Class<?>, Boolean> classSimilarityChecker) {
5578
if (a instanceof Class && b instanceof Class) {
56-
return ((Class) a).getSimpleName().equals(((Class) b).getSimpleName());
79+
return classSimilarityChecker.apply((Class<?>) a, (Class<?>) b);
5780
}
5881
if (a instanceof ParameterizedType && b instanceof ParameterizedType) {
5982
ParameterizedType aType = (ParameterizedType) a;
6083
ParameterizedType bType = (ParameterizedType) b;
61-
if (!areSimilarLookingTypes(aType.getRawType(), bType.getRawType())) {
84+
if (!areSimilarTypes(aType.getRawType(), bType.getRawType(), classSimilarityChecker)) {
6285
return false;
6386
}
6487
Type[] aArgs = aType.getActualTypeArguments();
@@ -67,7 +90,7 @@ static boolean areSimilarLookingTypes(Type a, Type b) {
6790
return false;
6891
}
6992
for (int i = 0; i < aArgs.length; i++) {
70-
if (!areSimilarLookingTypes(aArgs[i], bArgs[i])) {
93+
if (!areSimilarTypes(aArgs[i], bArgs[i], classSimilarityChecker)) {
7194
return false;
7295
}
7396
}
@@ -76,8 +99,8 @@ static boolean areSimilarLookingTypes(Type a, Type b) {
7699
if (a instanceof GenericArrayType && b instanceof GenericArrayType) {
77100
GenericArrayType aType = (GenericArrayType) a;
78101
GenericArrayType bType = (GenericArrayType) b;
79-
return areSimilarLookingTypes(
80-
aType.getGenericComponentType(), bType.getGenericComponentType());
102+
return areSimilarTypes(
103+
aType.getGenericComponentType(), bType.getGenericComponentType(), classSimilarityChecker);
81104
}
82105
if (a instanceof WildcardType && b instanceof WildcardType) {
83106
WildcardType aType = (WildcardType) a;
@@ -88,7 +111,7 @@ static boolean areSimilarLookingTypes(Type a, Type b) {
88111
return false;
89112
}
90113
for (int i = 0; i < aLowerBounds.length; i++) {
91-
if (!areSimilarLookingTypes(aLowerBounds[i], bLowerBounds[i])) {
114+
if (!areSimilarTypes(aLowerBounds[i], bLowerBounds[i], classSimilarityChecker)) {
92115
return false;
93116
}
94117
}
@@ -98,7 +121,7 @@ static boolean areSimilarLookingTypes(Type a, Type b) {
98121
return false;
99122
}
100123
for (int i = 0; i < aUpperBounds.length; i++) {
101-
if (!areSimilarLookingTypes(aUpperBounds[i], bUpperBounds[i])) {
124+
if (!areSimilarTypes(aUpperBounds[i], bUpperBounds[i], classSimilarityChecker)) {
102125
return false;
103126
}
104127
}
@@ -113,53 +136,165 @@ static boolean areSimilarLookingTypes(Type a, Type b) {
113136
Type[] lowerBounds = wildcardType.getLowerBounds();
114137
if (upperBounds.length == 1 && lowerBounds.length == 0) {
115138
// This is the '? extends Foo' case
116-
return areSimilarLookingTypes(upperBounds[0], otherType);
139+
return areSimilarTypes(upperBounds[0], otherType, classSimilarityChecker);
117140
}
118141
if (lowerBounds.length == 1
119142
&& upperBounds.length == 1
120143
&& upperBounds[0].equals(Object.class)) {
121144
// this is the '? super Foo' case
122-
return areSimilarLookingTypes(lowerBounds[0], otherType);
145+
return areSimilarTypes(lowerBounds[0], otherType, classSimilarityChecker);
123146
}
124147
}
125148
return false;
126149
}
127150

128-
static <T> ImmutableList<String> getSuggestions(Key<T> key, Injector injector) {
151+
/**
152+
* Conceptually, this method converts aType to be like bType by adding
153+
* appropriate @JvmSuppressWildcards or @JvmWildcards annotations. This assumes that the two types
154+
* only differ in variance (e.g. `Foo` vs `? extends Foo`) and that aType is implemented in
155+
* Kotlin. For example, if this method is called with the (List&lt;? super String&gt;,
156+
* List&lt;String&gt;), it will return "List&lt;@JvmSuppressWildcards String&gt;".
157+
*/
158+
static String convertToLatterViaJvmAnnotations(TypeLiteral<?> aType, TypeLiteral<?> bType) {
159+
String a = aType.toString();
160+
String b = bType.toString();
161+
int j = 0;
162+
StringBuilder conversion = new StringBuilder();
163+
for (int i = 0; i < a.length() && j < b.length(); i++, j++) {
164+
if (a.charAt(i) != b.charAt(j)) {
165+
if (a.startsWith(WILDCARD_EXTENDS, i)) {
166+
conversion.append("@JvmSuppressWildcards ");
167+
i += WILDCARD_EXTENDS.length(); // Skip over the "? extends " part
168+
} else if (a.startsWith(WILDCARD_SUPER, i)) {
169+
conversion.append("@JvmSuppressWildcards ");
170+
i += WILDCARD_SUPER.length(); // Skip over the "? super " part
171+
} else if (b.startsWith(WILDCARD_EXTENDS, j)) {
172+
conversion.append("@JvmWildcards ");
173+
j += WILDCARD_EXTENDS.length(); // Skip over the "? extends " part
174+
} else if (b.startsWith(WILDCARD_SUPER, j)) {
175+
conversion.append("@JvmWildcards ");
176+
j += WILDCARD_SUPER.length(); // Skip over the "? super " part
177+
}
178+
}
179+
conversion.append(a.charAt(i));
180+
}
181+
182+
// convert any remaining wildcard types to the Kotlin-equivalent.
183+
return conversion.toString().replaceAll("\\? extends ", "out ").replaceAll("\\? super ", "in ");
184+
}
185+
186+
private static boolean wasBoundInKotlin(Binding<?> binding) {
187+
if (binding.getSource() instanceof ElementSource) {
188+
ElementSource elementSource = (ElementSource) binding.getSource();
189+
Object declaringSource = elementSource.getDeclaringSource();
190+
if (declaringSource instanceof Member) {
191+
Member member = (Member) declaringSource;
192+
if (KotlinSupport.getInstance().isKotlinClass(member.getDeclaringClass())) {
193+
return true;
194+
}
195+
}
196+
if (declaringSource instanceof StackTraceElement) {
197+
StackTraceElement stackTraceElement = (StackTraceElement) declaringSource;
198+
if (stackTraceElement.getFileName() != null
199+
&& stackTraceElement.getFileName().endsWith(".kt")) {
200+
return true;
201+
}
202+
}
203+
}
204+
return false;
205+
}
206+
207+
private static boolean isInjectionFromKotlin(List<Object> sources) {
208+
for (Object source : sources) {
209+
if (!(source instanceof Dependency)) {
210+
continue;
211+
}
212+
Dependency<?> dependency = (Dependency<?>) source;
213+
if (KotlinSupport.getInstance()
214+
.isKotlinClass(dependency.getInjectionPoint().getMember().getDeclaringClass())) {
215+
return true;
216+
}
217+
}
218+
return false;
219+
}
220+
221+
static <T> ImmutableList<String> getSuggestions(
222+
Key<T> key, Injector injector, List<Object> sources) {
129223
ImmutableList.Builder<String> suggestions = ImmutableList.builder();
130224
TypeLiteral<T> type = key.getTypeLiteral();
131225

132226
BindingSourceRestriction.getMissingImplementationSuggestion(GuiceInternal.GUICE_INTERNAL, key)
133227
.ifPresent(suggestions::add);
134228

135-
// Keys which have similar strings as the desired key
136-
List<String> possibleMatches = new ArrayList<>();
137-
ImmutableList<Binding<?>> similarTypes =
229+
boolean injectionFromKotlin = isInjectionFromKotlin(sources);
230+
231+
// Keys which have a similar appearance as the desired key
232+
ImmutableList<Binding<?>> similarBindings =
138233
injector.getAllBindings().values().stream()
139234
.filter(b -> !(b instanceof UntargettedBinding)) // These aren't valid matches
140235
.filter(
141236
b -> areSimilarLookingTypes(b.getKey().getTypeLiteral().getType(), type.getType()))
142237
.collect(toImmutableList());
143-
if (!similarTypes.isEmpty()) {
238+
239+
// Bindings which differ only in variance.
240+
ImmutableList<Binding<?>> wildcardSuggestionBindings =
241+
similarBindings.stream()
242+
.filter(
243+
b ->
244+
(injectionFromKotlin || wasBoundInKotlin(b))
245+
&& areOnlyDifferencesInVariance(
246+
b.getKey().getTypeLiteral().getType(), type.getType()))
247+
.collect(toImmutableList());
248+
249+
if (!wildcardSuggestionBindings.isEmpty()) {
250+
suggestions.add(WILDCARDS_WARNING);
251+
suggestions.add(WILDCARDS_POSSIBLE_FIXES);
252+
for (Binding<?> wildcardSuggestionBinding : wildcardSuggestionBindings) {
253+
TypeLiteral<?> similarType = wildcardSuggestionBinding.getKey().getTypeLiteral();
254+
255+
if (injectionFromKotlin) {
256+
suggestions.add(
257+
"\n * Inject this: " + convertToLatterViaJvmAnnotations(type, similarType));
258+
}
259+
if (wasBoundInKotlin(wildcardSuggestionBinding)) {
260+
suggestions
261+
.add("\n * ")
262+
.add(injectionFromKotlin ? "Or bind this: " : "Bind this: ")
263+
.add(
264+
Messages.format(
265+
"%s %s",
266+
convertToLatterViaJvmAnnotations(similarType, type),
267+
formatVarianceSuggestion(wildcardSuggestionBinding)));
268+
}
269+
}
270+
}
271+
272+
similarBindings =
273+
similarBindings.stream()
274+
.filter(b -> !wildcardSuggestionBindings.contains(b))
275+
.collect(toImmutableList());
276+
277+
List<String> possibleMatches = new ArrayList<>();
278+
if (!similarBindings.isEmpty()) {
144279
suggestions.add("\nDid you mean?");
145-
int howMany = min(similarTypes.size(), MAX_MATCHING_TYPES_REPORTED);
280+
int howMany = min(similarBindings.size(), MAX_MATCHING_TYPES_REPORTED);
146281
for (int i = 0; i < howMany; ++i) {
147-
Key<?> bindingKey = similarTypes.get(i).getKey();
282+
Key<?> bindingKey = similarBindings.get(i).getKey();
148283
// TODO: Look into a better way to prioritize suggestions. For example, possibly
149284
// use levenshtein distance of the given annotation vs actual annotation.
150285
suggestions.add(
151286
Messages.format(
152287
"\n * %s",
153288
formatSuggestion(bindingKey, injector.getExistingBinding(bindingKey))));
154289
}
155-
int remaining = similarTypes.size() - MAX_MATCHING_TYPES_REPORTED;
290+
int remaining = similarBindings.size() - MAX_MATCHING_TYPES_REPORTED;
156291
if (remaining > 0) {
157292
String plural = (remaining == 1) ? "" : "s";
158293
suggestions.add(
159294
Messages.format(
160295
"\n * %d more binding%s with other annotations.", remaining, plural));
161296
}
162-
} else {
297+
} else if (wildcardSuggestionBindings.isEmpty()) {
163298
// For now, do a simple substring search for possibilities. This can help spot
164299
// issues when there are generics being used (such as a wrapper class) and the
165300
// user has forgotten they need to bind based on the wrapper, not the underlying
@@ -196,8 +331,9 @@ static <T> ImmutableList<String> getSuggestions(Key<T> key, Injector injector) {
196331

197332
// If where are no possibilities to suggest, then handle the case of missing
198333
// annotations on simple types. This is usually a bad idea.
199-
if (similarTypes.isEmpty()
334+
if (similarBindings.isEmpty()
200335
&& possibleMatches.isEmpty()
336+
&& wildcardSuggestionBindings.isEmpty()
201337
&& key.getAnnotationType() == null
202338
&& COMMON_AMBIGUOUS_TYPES.contains(key.getTypeLiteral().getRawType())) {
203339
// We don't recommend using such simple types without annotations.
@@ -213,4 +349,10 @@ private static String formatSuggestion(Key<?> key, Binding<?> binding) {
213349
new SourceFormatter(binding.getSource(), fmt, /* omitPreposition= */ false).format();
214350
return fmt.toString();
215351
}
352+
353+
private static String formatVarianceSuggestion(Binding<?> binding) {
354+
Formatter fmt = new Formatter();
355+
new SourceFormatter(binding.getSource(), fmt, /* omitPreposition= */ false).format();
356+
return fmt.toString();
357+
}
216358
}

core/test/com/google/inject/errors/ErrorMessageTestUtils.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ private static String getExpectedError(String fileName) throws IOException {
4747
ErrorMessageTestUtils.class.getResource(
4848
"/core/test/com/google/inject/errors/testdata/" + fileName);
4949
}
50-
return Resources.toString(resource, UTF_8);
50+
String expectedError = Resources.toString(resource, UTF_8);
51+
return expectedError;
5152
}
5253
}

0 commit comments

Comments
 (0)