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

ObjCName annotation #4815

Merged
merged 1 commit into from
Jul 23, 2022

Conversation

rickclephas
Copy link
Contributor

@rickclephas rickclephas commented May 8, 2022

Relates to KT-42297, KT-50767, KT-48076, KT-48072, KT-43087 and KT-40796.

Sample

@ObjCName(swiftName = "MySwiftArray")
class MyKotlinArray {
    @ObjCName(swiftName = "index")
    fun indexOf(@ObjCName(swiftName = "of") element: String): Int = TODO()
}
// Current usage without the ObjCName annotations
let array = MyKotlinArray()
let index = array.indexOf(element: "element")

// Usage with the ObjCName annotations
let array = MySwiftArray()
let index = array.index(of: "element")

Logic

The ObjCName accepts an ObjC name and a Swift name, both are optional.
The name of the Swift/ObjC declaration is determined as follows:

  • The Swift name from the ObjCName annotation (Swift only)
  • The ObjC name from the ObjCName annotation
  • The Kotlin name from the source file

@rickclephas rickclephas marked this pull request as ready for review May 14, 2022 16:22
@SvyatoslavScherbina
Copy link
Contributor

KT-48948

It is not related: the issue is about producing C API for Kotlin code, the implemented annotation doesn't affect it, IIUC.

@rickclephas
Copy link
Contributor Author

It is not related: the issue is about producing C API for Kotlin code, the implemented annotation doesn't affect it, IIUC.

Yeah you are correct this doesn't affect the C API just Objective-C.

@@ -902,6 +922,45 @@ private fun ObjCExportMapper.canHaveSameName(first: PropertyDescriptor, second:
return bridgePropertyType(first) == bridgePropertyType(second)
}

private fun DeclarationDescriptor.getObjCName(forSwift: Boolean): String {
Copy link
Contributor

Choose a reason for hiding this comment

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

If the compiler has to mangle an @ObjCName-annotated declaration to prevent a name clash, should it report something in this case?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ideally yes. But IMO it should report a warning for all mangled names (even if the annotation isn't present).

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, I agree. So, in case of a clash, it probably makes sense to emit

  • an error, if the mangling is applied to something with @ObjCName
  • a warning, otherwise

What do you think?

Generally, current implementation of naming in ObjCExport makes this not simple, so here I'm mostly trying to bring us at the same page regarding the concept, not insisting on implementing this as a part of this PR.

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 I like that! Specifying conflicting @ObjCName annotations is definitely an error.
But that would indeed be a big addition for this PR, so probably best to add that separately.

Copy link
Contributor

@madsager madsager left a comment

Choose a reason for hiding this comment

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

Sorry for jumping in late in this review. At Google we are interested in controlling Objective-C and Swift names as well. We were going to look into this until I noticed that you are already doing this Rick. Thank you! A couple of questions came to mind while reading the change, so added them as review comments. :)

@rickclephas rickclephas force-pushed the feature/objc-name branch 2 times, most recently from 4f11a7e to 5a3eb1a Compare June 4, 2022 17:33
@SvyatoslavScherbina
Copy link
Contributor

Btw, does the annotation work for enum entries?
Relevant motivational example: https://youtrack.jetbrains.com/issue/KT-40796/Enums-in-multiplatform-code-with-PascalCase-named-entries-are-fully-lower-cased-in-swift

@rickclephas
Copy link
Contributor Author

Btw, does the annotation work for enum entries?

Turns out it can be applied to enum entries, but at the moment it doesn't have any effect.
I guess we should support that as well. Although KT-40796 should probably have a fix itself, right?
Besides that it doesn't make sense to have an exact enum entry name (which is currently allowed as well).

@SvyatoslavScherbina
Copy link
Contributor

Although KT-40796 should probably have a fix itself, right?

It should, but this would be a breaking change, which makes fixing it properly harder. Having @ObjCName for that sounds like a nice trade off to me.

}

private fun CallableDescriptor.getObjCName(): ObjCName =
overriddenDescriptors.firstOrNull()?.getObjCName() ?: (this as DeclarationDescriptor).getObjCName()
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we do this without casts? (also below).
Motivation: Although these are cast to supertypes, the compiler doesn't check that. Now, imagine we are going to rewrite this code to a different representation for declarations. If we forget to change a target type in a particular cast, and we don't have tests covering this case, the compiler won't help us to detect this either, and this bug will remain unnoticed.

Let me propose alternatives:

  1. Implement and use a special generic upcast function that doesn't do as, but simply returns the argument as its supertype.
  2. Use different names for "conflicting" getObjCName functions.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I actually like that the functions have the same name (and naming is hard), so I went with the upcast function.
Not sure if this is a common use case, but I guess upcast could even be part of stdlib.

}

fun ValueArgument.getBooleanValue(): Boolean =
(getArgumentExpression() as? KtConstantExpression)?.node?.text?.toBoolean() ?: false
Copy link
Contributor

Choose a reason for hiding this comment

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

This code is used in IDE.
toBoolean will probably throw an exception if the string is not true or false.
This is not fine:

  1. What if it is not a boolean literal, but a reference to a const val? Supporting this case would be complicated, but at least IDE shouldn't throw an exception in this case.
  2. A user might have simply written incorrect code. For example, @ObjCName("abc", exact = "def"). IDE should highlight this as error, but not throw an exception.

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 I wasn't sure if this was actually the correct way to get the boolean.
We could use toBooleanOrNull to solve the second issue, but the first one seems harder to fix.

The following compiles fine, but ignores name and exact:

private const val name = "ObjCNameObjCObject"
private const val exact = true

@ObjCName(name, "ObjCNameSwiftObject", exact)
object ObjCNameKotlinObject

Do you have any pointers for this?

Copy link
Contributor

Choose a reason for hiding this comment

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

As far as I can tell, now that the @ObjCName annotation can change the name, we can no longer compute the name based on the PSI structure, but need to perform resolution to get the name.

I hacked together the following: google@d0232bf

Which just removes the eager computation of the name in the lazy objective-c export code and replaces it with lazy computation based on descriptors which means that resolution has to happen to get the name. I didn't have the time to clean it up, but wanted to share in case it is useful.

@SvyatoslavScherbina does that seem like the right way to go? I don't really see how the lazy objective-c exporter can get the right name unless we go through resolution for it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I hacked together the following: google@d0232bf

Interesting. Where are the tests for the lazy exporter located?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

IIUC testObjCExport is using the lazy exporter and the output (expectedLazy.h) contains the correct names.

Copy link
Contributor

Choose a reason for hiding this comment

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

but the first one seems harder to fix.

Exactly, that's why I proposed not to support it in IDE for now :)

I hacked together the following: google@d0232bf
@SvyatoslavScherbina does that seem like the right way to go?

Hard to say, I'm not actually an expert in IDE. IIUC, this would mean that indexing would trigger resolution for every class, right? This might be a huge performance hit.

I don't really see how the lazy objective-c exporter can get the right name unless we go through resolution for it?

I see a couple of options. For example, we could prohibit using anything but boolean literals for exact values, or make it a separate annotation (e.g. @ExactObjCName or @ObjCName.Exact).

Interesting. Where are the tests for the lazy exporter located?

expectedLazy.h is the only test I'm aware of.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Alright I think I get the issues now (correct me if I am wrong).

  1. using const val references as values for the annotation isn't currently working and would require resolution.
    I don't think there is a real use case for this (since the names and exact value are tightly coupled to a declaration). Meaning we could prohibit it, only allowing literal string and boolean values, correct?
  2. exceptions are thrown in case the arguments aren't in the expected format.
    In this case we should make sure to silently ignore such values.
    The compiler will fail with an appropriate error message anyway.

expectedLazy.h is the only test I'm aware of.

That one is passing, so we shouldn't have any issues if we prohibit the usage of reference values.

Copy link
Contributor

Choose a reason for hiding this comment

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

Alright I think I get the issues now (correct me if I am wrong).

  1. using const val references as values for the annotation isn't currently working and would require resolution.
    I don't think there is a real use case for this (since the names and exact value are tightly coupled to a declaration). Meaning we could prohibit it, only allowing literal string and boolean values, correct?

Yes. I agree that it is probably fine to limit to string constants and boolean constants for now. If we would like to expand on it in the future, that should be relatively easy as well. In what I hacked up we do resolution always to get the name. However, we can see the @ObjCName in the PSI, so we could do resolution when getting the name only when that annotation is present. Then at least the cost would only be incurred for the declarations where it is necessary. I would be happy to do that if we end up seeing a need, but I would support just limiting to constants for now.

  1. exceptions are thrown in case the arguments aren't in the expected format.
    In this case we should make sure to silently ignore such values.
    The compiler will fail with an appropriate error message anyway.

That is my understanding as well. If the code cannot be compiled, the editor does a best effort. :)

expectedLazy.h is the only test I'm aware of.

That one is passing, so we shouldn't have any issues if we prohibit the usage of reference values.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

toBoolean wouldn't actually throw an exception, but instead return false. Updated to toBooleanStrictOrNull to make the intended behaviour more clear.

The frontend checker will now prohibit non literal string and boolean values, making sure we don't need resolution in the lazy exporter.

I have also added a test case to verify that there aren't any exceptions in case the user writes invalid code.

Copy link
Contributor

Choose a reason for hiding this comment

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

Alright I think I get the issues now (correct me if I am wrong).

Totally correct, thank you!

@rickclephas
Copy link
Contributor Author

Enum entries are now correctly supported and the frontend checker will prohibit exact on enum entries.

@SvyatoslavScherbina
Copy link
Contributor

Status: IIUC, there are no more unresolved comments. We are waiting for the review of the annotation declaration from the Kotlin Libraries team.
I might look through the entire PR again later, just in case I've missed a tricky problem before.

Copy link
Contributor

@SvyatoslavScherbina SvyatoslavScherbina left a comment

Choose a reason for hiding this comment

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

Status: everything seems fine, the CI is green, we are good to go!
GitHub reports some merge conflicts (?), @rickclephas could you please rebase the branch to master?

Also, please squash the commits and update the message(s). Otherwise, I will squash everything to a single commit and write some commit message myself.

@@ -465,12 +491,12 @@ internal class ObjCExportNamerImpl(
parameters.forEachIndexed { index, (bridge, it) ->
val name = when (bridge) {
is MethodBridgeValueParameter.Mapped -> when {
it is ReceiverParameterDescriptor -> ""
it is ReceiverParameterDescriptor -> it.getObjCName().asIdentifier(false) { "" }
method is PropertySetterDescriptor -> when (parameters.size) {
Copy link
Contributor

Choose a reason for hiding this comment

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

(Possible future improvement:)
In certain cases Kotlin property accessors are translated to methods. In this case, @ObjCName can be applied to the setter parameter but likely has no effect.

@rickclephas
Copy link
Contributor Author

@SvyatoslavScherbina rebased and squashed 👍🏻

@SvyatoslavScherbina SvyatoslavScherbina merged commit 5a5e6ad into JetBrains:master Jul 23, 2022
@SvyatoslavScherbina
Copy link
Contributor

Merged.
@rickclephas great job! Thank you for the contribution 👍

@rickclephas rickclephas deleted the feature/objc-name branch July 24, 2022 15:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants