-
Notifications
You must be signed in to change notification settings - Fork 87
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
[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 TypeReference
s.
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
@@ -54,8 +56,11 @@ class BindsMethodValidatorTest( | |||
) | |||
if (!useDagger) { | |||
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
@@ -13,6 +13,18 @@ open class KtlintConventionPlugin : Plugin<Project> { | |||
|
|||
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 TypeReference
s here instead of ClassReference
s
I will try to make a similar to allSuperTypeClassReferences
function which returns TypeReference
s instead of ClassReference
s
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