Skip to content
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

IlyaGulya
Copy link
Contributor

This fixes #750

@IlyaGulya IlyaGulya force-pushed the bugfix/binding-supertype-narrower-than-return-type-wrongly-allowed branch from 34dce0c to 70e9bbb Compare January 9, 2024 07:41
@IlyaGulya
Copy link
Contributor Author

@ZacSweers
I've fixed ktlint complaints and made TypeReference.isAssignableFrom extension to be class function which is now exposed as public API.
Also I've excluded generated sources from ktlint tasks.

Comment on lines -98 to -107
private fun MemberFunctionReference.Psi.parameterMatchesReturnType(): Boolean {
return parameterSuperTypes()
?.contains(returnType().asClassReference())
?: false
Copy link
Member

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.

@IlyaGulya IlyaGulya force-pushed the bugfix/binding-supertype-narrower-than-return-type-wrongly-allowed branch from 659c117 to 823bf4d Compare January 11, 2024 14:40
@IlyaGulya IlyaGulya changed the title [bugfix] Binding supertype which is narrower than return type is wrongly allowed [WIP][bugfix] Binding supertype which is narrower than return type is wrongly allowed Jan 11, 2024
@IlyaGulya IlyaGulya force-pushed the bugfix/binding-supertype-narrower-than-return-type-wrongly-allowed branch 3 times, most recently from e2a167d to 2757667 Compare January 12, 2024 12:48
@IlyaGulya IlyaGulya changed the title [WIP][bugfix] Binding supertype which is narrower than return type is wrongly allowed [bugfix] Binding supertype which is narrower than return type is wrongly allowed Jan 12, 2024
@IlyaGulya IlyaGulya force-pushed the bugfix/binding-supertype-narrower-than-return-type-wrongly-allowed branch 2 times, most recently from 2c1d874 to ff9405a Compare January 12, 2024 13:52
@IlyaGulya IlyaGulya force-pushed the bugfix/binding-supertype-narrower-than-return-type-wrongly-allowed branch 2 times, most recently from 2436aba to 82a4023 Compare January 12, 2024 14:03
@IlyaGulya
Copy link
Contributor Author

@JoelWilcox I've made all requested fixes. Please take a look.

@IlyaGulya IlyaGulya force-pushed the bugfix/binding-supertype-narrower-than-return-type-wrongly-allowed branch from 82a4023 to bac0ac6 Compare January 18, 2024 09:12
Comment on lines 141 to 171
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()
Copy link
Member

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

Copy link
Contributor Author

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)
}
}
}
Copy link
Member

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

Copy link
Contributor Author

@IlyaGulya IlyaGulya Jan 27, 2024

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
Copy link
Member

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.

Copy link
Contributor Author

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(
Copy link
Member

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.

Copy link
Contributor Author

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(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here

Copy link
Contributor Author

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(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't necessary.

Comment on lines 136 to 138
internal fun TypeName.toStringOfSimpleNamesOnly(): String {
return removePackageNames(toString())
}
Copy link
Member

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.

Copy link
Contributor Author

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.

Comment on lines 87 to 91
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())
Copy link
Member

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(...)
}

Copy link
Contributor Author

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() }
Copy link
Member

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.

Copy link
Contributor Author

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

Comment on lines 138 to 147
private fun TypeReference?.superTypes(): Sequence<TypeName> {
this ?: return emptySequence()
return sequence {
yield(this@superTypes)
yieldAll(
this@superTypes
.asClassReference()
.directSuperTypeReferences(),
)
}.map { it.asTypeName() }
Copy link
Member

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.

Copy link
Contributor Author

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

Copy link
Contributor Author

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 🙂

@IlyaGulya
Copy link
Contributor Author

Wow, ok I'll try to get some time to fix this

@IlyaGulya IlyaGulya force-pushed the bugfix/binding-supertype-narrower-than-return-type-wrongly-allowed branch from bac0ac6 to ab9fe67 Compare January 27, 2024 04:17
@IlyaGulya IlyaGulya force-pushed the bugfix/binding-supertype-narrower-than-return-type-wrongly-allowed branch from ab9fe67 to 91f19c4 Compare January 27, 2024 06:27
@IlyaGulya
Copy link
Contributor Author

@RBusarow I've made all requested fixes. Please take a look.
I've also introduced new extension TypeReference.allSuperTypeReferences(includeSelf: Boolean = false): Sequence<TypeReference> which works the same as ClassReference.allSuperTypeClassReferences but returns TypeReferences instead.
This is required to keep correct generic type arguments in the TypeName and provide better error message.
If I would use allSuperTypeClassReferences instead, then TypeName will not contain specific type argument which were used in the binding (i.e. ItemMapper<ItemDetail.DetailTypeA>), but instead will contain just generic type variable (i.e. ItemMapper<T : ItemDetail>)

@RBusarow RBusarow merged commit 1a412a8 into square:main Feb 16, 2024
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Incorrect type validation for @Binds
3 participants