-
Notifications
You must be signed in to change notification settings - Fork 91
[bugfix] Binding supertype which is narrower than return type is wrongly allowed #833
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[bugfix] Binding supertype which is narrower than return type is wrongly allowed #833
Conversation
compiler-utils/src/main/java/com/squareup/anvil/compiler/internal/reference/ClassReference.kt
Outdated
Show resolved
Hide resolved
34dce0c to
70e9bbb
Compare
|
@ZacSweers |
compiler-utils/src/main/java/com/squareup/anvil/compiler/internal/reference/ClassReference.kt
Outdated
Show resolved
Hide resolved
| private fun MemberFunctionReference.Psi.parameterMatchesReturnType(): Boolean { | ||
| return parameterSuperTypes() | ||
| ?.contains(returnType().asClassReference()) | ||
| ?: false |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would propose keeping the existing structure in the validator and modifying this method to utilize TypeName instead of ClassReference since it contains more of the information that we need in this case. You should be able to use
?.map { it.asTypeName() }
?.contains(returnType().asClassReference().asTypeName())
check for the full type info. Then parameterSuperTypes() would also be updated accordingly to provide TypeReferences.
compiler/src/test/java/com/squareup/anvil/compiler/dagger/BindsMethodValidatorTest.kt
Show resolved
Hide resolved
659c117 to
823bf4d
Compare
e2a167d to
2757667
Compare
2c1d874 to
ff9405a
Compare
…red fails to compile.
2436aba to
82a4023
Compare
|
@JoelWilcox I've made all requested fixes. Please take a look. |
82a4023 to
bac0ac6
Compare
| if (type.isEmpty()) return "" | ||
|
|
||
| val builder = StringBuilder() | ||
| var current = "" | ||
| for (i in type.indices) { | ||
| if (i == type.lastIndex) { | ||
| builder.append(current + type[i]) | ||
| break | ||
| } | ||
| when (val char = type[i]) { | ||
| '.' -> { | ||
| current = "" | ||
| } | ||
|
|
||
| ',', '<', '>' -> { | ||
| builder.append(current + char) | ||
| current = "" | ||
| } | ||
|
|
||
| ' ' -> { | ||
| builder.append(char) | ||
| current = "" | ||
| } | ||
|
|
||
| else -> { | ||
| current += char | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return builder.toString() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you could simplify this using a regex, something like type.replace("([a-z]+\\.)+".toRegex(), ""). Probably needs a little more tinkering to handle nested class cases like com.squareup.Foo.Bar
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed this completely
| it.file.path.contains(excludePath) | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this necessary in the context of the fix? Unclear what the related error/issue is currently
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not necessary in the context of fix, but without this ktlint checks are failing on generated sources during build:
> Task :gradle-plugin:ktlintGradleTestSourceSetCheck FAILED
/Users/ilyagulya/Projects/Community/anvil/gradle-plugin/build/root-build/generated/sources/buildConfig/gradleTest/com/squareup/anvil/plugin/buildProperties/BuildProperties.kt:8:1 Unexpected indentation (4) (should be 2) (standard:indent)
| when (returnTypeName) { | ||
| in parameterSuperTypes -> return | ||
| in receiverSuperTypes -> return | ||
| else -> Unit |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: since we don't have an else case here it would feel a bit nicer to keep it as an if (condition || condition) { // fail } structure, which is also beneficial over the early returns since that means this check isn't required to stay at the bottom of the validations list as we add more or shift things around.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
| assertThat(messages).contains( | ||
| "Expected binding of type Bar but impl parameter of type Foo only has the following " + | ||
| "supertypes: [Ipsum, Lorem]", | ||
| Errors.bindsParameterMustBeAssignable( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd prefer we keep the hardcoded expected value in this case, since it's possible a bug could be introduced in bindsParameterMustBeAssignable() later on which automatically changes the test to expect an incorrect value.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
| ) | ||
| if (!useDagger) { | ||
| assertThat(messages).contains( | ||
| Errors.bindsParameterMustBeAssignable( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
|
|
||
| target.extensions.configure(KtlintExtension::class.java) { ktlint -> | ||
| ktlint.version.set(target.libs.versions.ktlint) | ||
| val pathsToExclude = listOf( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This isn't necessary.
| internal fun TypeName.toStringOfSimpleNamesOnly(): String { | ||
| return removePackageNames(toString()) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This extension (and the subsequent String parsing function) ignore all of the work that TypeName has already achieved.
If the type name has simple names, then it's a ClassName or a ParameterizedTypeName. You could do something like:
internal fun TypeName.toStringOfSimpleNamesOnly(): String {
return when (this) {
is ParameterizedTypeName -> rawType.toStringOfSimpleNamesOnly()
is ClassName -> simpleNames.joinToString(separator = ".")
is LambdaTypeName,
is TypeVariableName,
is WildcardTypeName,
Dynamic,
-> error("???")
}
}From the context in which this is being called, the type name should be one of the first two. If I was the one creating this extension, it would be inside the BindsMethodValidator class.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, I see! But this part is no longer needed since I've removed simple names parsing.
| val parameters = function.parameters | ||
| val psiFunction = function.function | ||
| val hasSingleBindingParameter = | ||
| (function.parameters.size == 1 && !function.function.isExtensionDeclaration()) || | ||
| (function.parameters.isEmpty() && function.function.isExtensionDeclaration()) | ||
| (parameters.size == 1 && !psiFunction.isExtensionDeclaration()) || | ||
| (parameters.isEmpty() && psiFunction.isExtensionDeclaration()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This logic and the new logic below start off by doing the same thing -- trying to figure out what the bound parameter is. You can simplify your logic to something like:
val bindingParameter = listOfNotNull(
function.singleParameterTypeOrNull(),
function.receiverTypeOrNull()
)
.singleOrNull()
?: throw AnvilCompilationExceptionFunctionReference(...)
val returnTypeName = function.returnTypeOrNull()?.asClassReference()?.asTypeName()
?: throw AnvilCompilationExceptionFunctionReference(...)
val paramSuperTypes = bindingParameter.superTypes()
if (returnTypeName !in paramSuperTypes) {
throw AnvilCompilationExceptionFunctionReference(...)
}There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, that's much cleaner approach, thanks!
| val paramSuperTypes = | ||
| parameterSuperTypes | ||
| .ifEmpty { receiverSuperTypes } | ||
| .map { it.toStringOfSimpleNamesOnly() } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wouldn't bother trying to parse the simple names at all, since they're only used in the exception message. Just print out the fully qualified name.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I've just thought it would be nice to have, but sure, I will remove this part
| private fun TypeReference?.superTypes(): Sequence<TypeName> { | ||
| this ?: return emptySequence() | ||
| return sequence { | ||
| yield(this@superTypes) | ||
| yieldAll( | ||
| this@superTypes | ||
| .asClassReference() | ||
| .directSuperTypeReferences(), | ||
| ) | ||
| }.map { it.asTypeName() } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I might be missing something, but it seems like we should continue to use allSuperTypeClassReferences(includeSelf = true). This version only returns the type and its immediate supertypes, whereas allSuperTypeClassReferences() is recursive.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately, allSuperTypeClassReferences lose generic type information (I assume that's because ClassReference can't contain (or should not contain?) information about which types are used as type arguments in specific binding declaration. That's why I'm using TypeReferences here instead of ClassReferences
I will try to make a similar to allSuperTypeClassReferences function which returns TypeReferences instead of ClassReferences
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, I'm not quite sure that allSuperTypeClassReferences is recursive. Or, at least, I don't see it 🙂
|
Wow, ok I'll try to get some time to fix this |
bac0ac6 to
ab9fe67
Compare
…h supertype is narrower than return type.
ab9fe67 to
91f19c4
Compare
|
@RBusarow I've made all requested fixes. Please take a look. |
…n-type-wrongly-allowed
This fixes #750