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

Match extension functions #4459

Merged
merged 2 commits into from Mar 24, 2022
Merged

Match extension functions #4459

merged 2 commits into from Mar 24, 2022

Conversation

BraisGabin
Copy link
Member

@BraisGabin BraisGabin commented Jan 6, 2022

More context here: #4448 (comment) I'm implementing the no breaking option. It isn't nice but it works.

Related with #4448 but this PR doesn't fix the issue yet.

@codecov
Copy link

codecov bot commented Jan 6, 2022

Codecov Report

Merging #4459 (6a9f0ca) into main (1ca3ab5) will increase coverage by 0.00%.
The diff coverage is 66.66%.

Impacted file tree graph

@@            Coverage Diff            @@
##               main    #4459   +/-   ##
=========================================
  Coverage     84.13%   84.13%           
  Complexity     3313     3313           
=========================================
  Files           477      477           
  Lines         10922    10924    +2     
  Branches       2030     2030           
=========================================
+ Hits           9189     9191    +2     
  Misses          700      700           
  Partials       1033     1033           
Impacted Files Coverage Δ
...turbosch/detekt/rules/style/ForbiddenMethodCall.kt 88.57% <ø> (ø)
...in/io/github/detekt/tooling/api/FunctionMatcher.kt 86.76% <66.66%> (+0.40%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 1ca3ab5...6a9f0ca. Read the comment docs.

@BraisGabin BraisGabin added this to the 1.20.0 milestone Jan 6, 2022
@BraisGabin
Copy link
Member Author

I have no idea what's going on here: https://github.com/detekt/detekt/runs/4726908840?check_suite_focus=true#step:4:499

I didn't change that class at all. Well, I did change it, but one year ago: 04/01/2021

@BraisGabin BraisGabin marked this pull request as draft January 8, 2022 11:48
@chao2zhang chao2zhang self-requested a review January 23, 2022 20:19
Base automatically changed from function-matcher to main January 25, 2022 08:41
@BraisGabin BraisGabin marked this pull request as ready for review January 25, 2022 08:44
@@ -48,7 +48,9 @@ class ForbiddenMethodCall(config: Config = Config.empty) : Rule(config) {
"Methods can be defined without full signature (i.e. `java.time.LocalDate.now`) which will report " +
"calls of all methods with this name or with full signature " +
"(i.e. `java.time.LocalDate(java.time.Clock)`) which would report only call " +
"with this concrete signature."
"with this concrete signature. If you want to forbid an extension function like" +
"`fun String.hello(a: Int)` you should add the receiver parameter as the first parameter like this: " +
Copy link
Member

Choose a reason for hiding this comment

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

This still feels like a stop gap solution to me. When we disable type resolution, and we only want to forbid extension methods like String.hello(a: Int), we would actually report both extension function String.hello(a: Int) and normal function hello(s: String, a: Int).

Additionally, detekt should be compatible with other kotlin platforms.

Copy link
Member Author

Choose a reason for hiding this comment

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

This still feels like a stop gap solution to me.

Agree, I can't think of any other way to fix this without adding breaking changes to the syntax. And, even adding breaking changes, I can only think of more complex syntax, I can't think on any elegant way to fix this. Any idea in this regard is more than welcome.

When we disable type resolution, and we only want to forbid extension methods like String.hello(a: Int), we would actually report both extension function String.hello(a: Int) and normal function hello(s: String, a: Int).

This rule (and feature in general) only works with Type Solving so this is not a real issue.

Additionally, detekt should be compatible with other kotlin platforms.

Agree. Right now it's noy an issue because we only support type solving for JVM. But in the future maybe in js you could have both functions and that would be a problem.

The biggest issue to me is that it's not intuitive for a user how to use this feature. And I think that it should. But as I said before to handle this case properly we need to add a breaking change in the syntax. And, even then, I don't know how to define the function signature in an easy and elegant way.

Copy link
Member

Choose a reason for hiding this comment

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

The biggest issue to me is that it's not intuitive for a user how to use this feature.

I think that's the major point here. TR being a requirement is not a problem, as if you want to use an advanced capability, you need to opt-in to TR.

But in the future maybe in js you could have both functions and that would be a problem.

It's already a concern, as the compiler plugin runs with TR on all the platforms.

But as I said before to handle this case properly we need to add a breaking change in the syntax. And, even then, I don't know how to define the function signature in an easy and elegant way.

What would be your suggested way?

Copy link
Member Author

Choose a reason for hiding this comment

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

What would be your suggested way?

The toString of a CallableDescriptor.original of this function:

package org.example

class A<T> {
  fun <R> String.hello(a: T -> R): T {
    TODO()
  }
}

is

public final fun <R> kotlin.String.hello(a: (T) -> R): T defined in org.example.A[SimpleFunctionDescriptorImpl@5b4f828d]

If I simplify that I get (the output of the function is not relevant for the signature):

<R> kotlin.String.hello((T) -> R) defined in org.example.A

T is not clear where it was defined so I would do something like this:

<R> kotlin.String.hello((T) -> R) defined in org.example.A<T>

Problems:

  • I don't like defined in but I don't know how to simplify it.
  • Create a parser for this can be difficult. And create proper error messages to explain to the user what they write wrong.

To compare, if we merge this PR (and the next one) the signature for that function would be:

org.example.A.hello(kotlin.String, (T) -> R)

Other option is to use the syntax of apiDump:

public final class io/github/detekt/test/utils/ResourcesKt {
	public static final fun readResourceContent (Ljava/lang/String;)Ljava/lang/String;
	public static final fun resource (Ljava/lang/String;)Ljava/net/URI;
	public static final fun resourceAsPath (Ljava/lang/String;)Ljava/nio/file/Path;
	public static final fun resourceUrl (Ljava/lang/String;)Ljava/net/URL;
}

But I really don't like this one.

Copy link
Member

Choose a reason for hiding this comment

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

Other option is to use the syntax of apiDump:

I was actually thinking about something like that. I know that is not super immediate to grasp. But at the same time it comes from the bytecode representation of a method, and it's sort of "consistent" with the rest of the tools in the ecosystem

Copy link
Member Author

Choose a reason for hiding this comment

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

In that one the issues raised by @chao2zhang applies too:

  • It's not easy to write
  • Only works for JVM (js doesn't need a ResourcesKt for the top level functions)
  • An extension functions has it's receiver as the first parameter (that's what I'm doing here)

I agree that using something standard would be great but I don't think that syntax would work for us. And I don't know of any other standard one...

Copy link
Member Author

Choose a reason for hiding this comment

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

How can we unblock this? To me we should think if this change improves of make worst this rule. I think that it's better than don't support extension functions. An example would like to forbid the usage of androidx.activity.compose.setContent in favor of my our own setContent and right now it's impossible and #4448 is other reason.

We can open an issue to talk about which should be the function signature syntax that we should use and then we can implement it. And once we take a decission we can implement it.

Copy link
Member

Choose a reason for hiding this comment

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

Yup I'm in for merging this. I think having the extension function receiver modelled as first parameter is a good compromise and aligns to the approach taken by other tools as well.

@BraisGabin
Copy link
Member Author

Can we take a decission here?

@chao2zhang
Copy link
Member

Would this work across all Kotlin Multiplatform? I would vote for merge if this is not particularly tailored to JVM

@BraisGabin
Copy link
Member Author

Would this work across all Kotlin Multiplatform? I would vote for merge if this is not particularly tailored to JVM

Yes, it should work for all of them.

@cortinico
Copy link
Member

I think having the extension function receiver modelled as first parameter is a good compromise and aligns to the approach taken by other tools as well.

☝️ That's my latest opinion. I'm in for merging this

"fun foo(a: String), true",
"fun Int.foo(), false",
"fun String.foo(a: Int), false",
"'fun foo(a: String, ba: Int)', false",
Copy link
Member

Choose a reason for hiding this comment

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

Are those single quotes around fun foo(a: String, ba: Int) required?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes they are. This string is parsed as a csv. And this function has two parameters so it needs a , . So, I need the single quotes.

Copy link
Contributor

Choose a reason for hiding this comment

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

you could use a different separator though (Random guy yelling from the side line) 😉

@BraisGabin BraisGabin merged commit d7ac501 into main Mar 24, 2022
@BraisGabin BraisGabin deleted the function-extension-matcher branch March 24, 2022 07:54
@cortinico cortinico added the notable changes Marker for notable changes in the changelog label Mar 24, 2022
chao2zhang pushed a commit that referenced this pull request Apr 8, 2022
* Add missing tests

* Match extension functions
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
notable changes Marker for notable changes in the changelog
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants