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

Infer method #6877

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open

Infer method #6877

wants to merge 11 commits into from

Conversation

ag91
Copy link
Contributor

@ag91 ag91 commented Oct 22, 2024

This attempts to increase the capabilities of the "Create New Symbol" action to allow new method definition.

Supported only Scala 2 for cases shown in tests: tests/cross/src/test/scala/tests/pc/InsertInferredMethodSuite.scala

Related discussion and feature request: scalameta/metals-feature-requests#298

/**
* Return the missing method
*/
public abstract CompletableFuture<List<TextEdit>> insertInferredMethod(OffsetParams params);
Copy link
Contributor

Choose a reason for hiding this comment

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

Could we reuse the codeAction interface, it should be universal for all actions

Copy link
Contributor Author

Choose a reason for hiding this comment

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

is there an example by any chance? Otherwise I will pick this up at the end, after making sure the cases I handled in the tests work running interactively (not all do yet!)

Copy link
Contributor

Choose a reason for hiding this comment

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

I was supposed to quickly create an example, but then figured we can refactor the old methods in #6881

It's more complicated for the older action, but for anything new(as soon as my PR is merged) it should be as easy as:

  1. Add a new CodeActionId string
  2. Implement maybeCodeActionId from CodeAction trait
  3. Add handling of new id to codeAction method
  4. Add id to list of supported ones
  5. Use codeAction method from compilers in handleCommand

Feel free to work on the meat of the issue in the compiler, we can get back to it later or I can fix that myself.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

right, I seem to have achieved what I wanted! Things are working now 🌞

@ag91
Copy link
Contributor Author

ag91 commented Oct 26, 2024

@tgodzik I have managed to handle the issues I experienced by running my feature interactively.
I am now tuning the action to allow edits in other documents via WorkspaceEdit (without inferring a method for classes wouldn't be that much useful... ).

Meanwhile, I have a question:
wouldn't make sense to have a logging utility to let user report bugs with the action I am adding?
By that I mean this is what you get when something goes wrong with an action:
SEVERE: Internal error: scala.concurrent.Future$$anon$2: Future.filter predicate is not satisfied java.util.concurrent.CompletionException: scala.concurrent.Future$$anon$2: Future.filter predicate is not satisfied Caused by: scala.concurrent.Future$$anon$2: Future.filter predicate is not satisfied

but for debugging the issue I need the pprint.log of the tree and the position and possibly the original code.
How would a user report an issue like this? Should I look into printing this information out?

In my experience I have had a bunch of issues with inferType and now I see why the metals.log didn't have any hints about what caused them (not necessarily the state of my project, but possibly because not all cases are handled xD)

@ag91
Copy link
Contributor Author

ag91 commented Oct 26, 2024

@tgodzik also:
managed to convert to a WorkspaceEdit. In my testing adding a method to an imported object that exists in another file works ONLY IF the file is already open (I am testing with Emacs).
Not sure if that is a known limitation or there is a trick I have to use to tell the LSP client to open the file and then apply the edits?

And could I get a hint on how to pass the MiMa action? I can fix the scalafix one. What else is needed to merge this in?

@tgodzik
Copy link
Contributor

tgodzik commented Oct 28, 2024

wouldn't make sense to have a logging utility to let user report bugs with the action I am adding?

For sure it's always better to add reports, we do have a utility for that with ReportContext and we can add any data there. Also, we should make sure if is not used in for comprehensions with futures as it is not too clear when the filter fails. Do you know where that happened?

managed to convert to a WorkspaceEdit. In my testing adding a method to an imported object that exists in another file works ONLY IF the file is already open (I am testing with Emacs).
Not sure if that is a known limitation or there is a trick I have to use to tell the LSP client to open the file and then apply the edits?

I would think workspace edit opens a file, but maybe it's a problem with uri? I can check

@tgodzik
Copy link
Contributor

tgodzik commented Oct 28, 2024

And could I get a hint on how to pass the MiMa action? I can fix the scalafix one. What else is needed to merge this in?

We should be ok once we use the codeAction interface. I can update it all before merging

@tgodzik
Copy link
Contributor

tgodzik commented Oct 28, 2024

I can't seem to make it work at all with .method(..) syntax, I can dig in later, but we could implement that later.

@ag91
Copy link
Contributor Author

ag91 commented Oct 29, 2024

I can't seem to make it work at all with .method(..) syntax, I can dig in later, but we could implement that later.

you were right, there was an issue with me not handling NullaryMethodType (not sure why that didn't show in the tests): solved in cc5b6e6

@ag91
Copy link
Contributor Author

ag91 commented Oct 29, 2024

I would think workspace edit opens a file, but maybe it's a problem with uri? I can check

Found out that this issue happens only when I restart metals. Editing a source without an open buffer works fine on a fresh session. Thanks!

@ag91
Copy link
Contributor Author

ag91 commented Oct 29, 2024

And could I get a hint on how to pass the MiMa action? I can fix the scalafix one. What else is needed to merge this in?

We should be ok once we use the codeAction interface. I can update it all before merging

Okay, I am removing the draft from this PR then.

@ag91 ag91 marked this pull request as ready for review October 29, 2024 01:20
@tgodzik
Copy link
Contributor

tgodzik commented Oct 29, 2024

Is it ok if I push the changes related to the new codeAction inteface? That's now merged to main

@ag91
Copy link
Contributor Author

ag91 commented Oct 29, 2024

Is it ok if I push the changes related to the new codeAction inteface? That's now merged to main

sure, please go ahead: I am planning to only change the codebase for review feedback for now

Copy link
Contributor

@tgodzik tgodzik left a comment

Choose a reason for hiding this comment

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

We should tighten the scope of this PR I think, code action like this should really only work within a single file as otherwise it will become much more complex.

@@ -158,211 +159,215 @@ public abstract CompletableFuture<List<AutoImportsResult>> autoImports(String na
public abstract CompletableFuture<List<TextEdit>> insertInferredType(OffsetParams params);

/**
* Return the text edits for inlining a value.
Copy link
Contributor

Choose a reason for hiding this comment

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

Any chance not to reformat the file? It makes it difficult to see what actually changed

Copy link
Contributor Author

Choose a reason for hiding this comment

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

oh, I think metals github action runs automatically before push (I had to apply to be able to push). Maybe you can try difftastic or ignore whitespaces in PR

Copy link
Contributor

Choose a reason for hiding this comment

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

It's fine, but I think the formatting was done by the editor actually, but maybe we never run any formatting on it before 🤔 Would probably be cleaner to have that as a separate commit because otherwise any blame in the file will point at you change 😅

Nil
}
// nonExistent(param1, param2)
// val a: Int = nonExistent(param1, param2) TODO
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it still TODO?

}

val uriTextEditPairs = typedTree match {
// case errorMethod if errorMethod.toString().contains("lol") =>
Copy link
Contributor

Choose a reason for hiding this comment

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

Might be worth removing any comments that no longer make sense

Copy link
Contributor Author

Choose a reason for hiding this comment

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

sorry, I tired myself out while reiterating over this change.
I will take a little time to address your feedback: thanks for that!

Ident(containingMethod),
arguments
) :: _ =>
val methodSymbol = context.lookupSymbol(containingMethod, _ => true)
Copy link
Contributor

Choose a reason for hiding this comment

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

Would be able to extract some methods? Might be easier to review

paramsString = argumentsString(arguments).getOrElse(""),
retType = None,
postProcess = method => {
if (
Copy link
Contributor

Choose a reason for hiding this comment

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

Looks like this could be extracted, isn't it the same as below and above?

Copy link
Contributor

@tgodzik tgodzik left a comment

Choose a reason for hiding this comment

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

Great work! I added a few comments.

There also seems to be an unhandled case:

ObjectName.otherMethodWithoutParens we do show the code action but fail to do anything. We should either not show the method (we can use trees for that and not show if we are in a select statement without and apply parent) or handle it in the compiler

/**
* Return the missing method
*/
public abstract CompletableFuture<WorkspaceEdit> insertInferredMethod(OffsetParams params);
Copy link
Contributor

Choose a reason for hiding this comment

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

This should have default implementation for Mima to pass, but I will change it later on to use codeAction, so don't worry about 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.

yes please, I haven't dived in the codeAction method sure that it would be easier for you to adapt.


// doesn't work currently, User(1) is not being typed
checkEdit(
"custom-type-advanced".ignore,
Copy link
Contributor

Choose a reason for hiding this comment

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

TODO -> reminder to create an issue when we merge

.asInstanceOf[NullaryMethodType]
.resultType
else containerSymbol.symbol.tpe) match {
case TypeRef(_, classSymbol, _) =>
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we also make sure it's the same file 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.

not necessary because the check is in makeClassMethodEdits which I made sure to call for each case that infers a method.

// this gives me the position of the class
typedTreeAt(containerClass.symbol.pos) match {
// class case
case ClassDef(
Copy link
Contributor

Choose a reason for hiding this comment

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

Could we use ImplDef superclass for both ClassDef and ModuleDef?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

it seems a good idea but a simple ClassDef -> ImplDef didn't compile.
I will look into the other issues you have highlighted since this feels minor and requires digging time.

*/
private def inferEditPosition(t: Template): Position = {
// get text via reflection because Template could be in a different file
val text = t.pos.source.content.map(_.toString).mkString
Copy link
Contributor

Choose a reason for hiding this comment

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

Since we only support single file we can just use text from the OffsetParams

Copy link
Contributor

@tgodzik tgodzik left a comment

Choose a reason for hiding this comment

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

I tested things a bit more and looks a couple of cases are not tested and do not work. I think overall we should reduce the cases handled to 4:

  • otherMethod(errorMethod(param))
  • errorMethod(param)
  • ident.errorMethod(param)
  • method(errorMethod) with no select

anything else seems very hard to handle and we can make this PR more focused. We can also make sure that this code action only shows up for these cases.

I hope you are not too discouraged. This is a pretty hard one, which is why I probably abandoned this after the initial tries.

Let me know if you need help, I can help rework this PR a bit

arguments
) :: _ =>
makeEditsForListApply(arguments, containingMethod, errorMethod)
// List(1,2,3).map(myFn)
Copy link
Contributor

Choose a reason for hiding this comment

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

This and the one below does not work unfortunately, it's not really possible to get a type of List(1,2,3).map the current implementation will just produce:

def otherMethod(arg0: Int, arg1: Int, arg2: Int) = ???

which is not valid. We can't even know how many parameters it will have. And also this might be a much more complex expression.

position = None
)
// List(1, 2, 3).map(nonExistent)
// List((1, 2)).map{case (x,y) => otherMethod(x,y)}
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this actually the snippet this handles? Looks like this is a simple apply containingMethod(arguments) The same as the one above

I don't see any check for being inside another method.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yes, this is handling both List cases: both the non argument case, and the unpacking case

@ag91
Copy link
Contributor Author

ag91 commented Nov 8, 2024

I hope you are not too discouraged. This is a pretty hard one, which is why I probably abandoned this after the initial tries.

It is alright: it is a mix of I had more time before because I was on holiday and lost a bit of enthusiasm because the action will not be as useful as I needed it to be.

I am happy to wrap the PR up with your feedback though: I will reduce the cases and polish, possibly during the weekend.
I will get this in so Metals users can benefit ☀️

@ag91
Copy link
Contributor Author

ag91 commented Nov 10, 2024

@tgodzik it turned out that handling the Object.definition was just adding a couple of cases
and the List(1,2,3).map(myFn) also seemed easy to patch up.
Hope we are closer at this point

@ag91 ag91 requested a review from tgodzik November 10, 2024 01:23
tgodzik and others added 6 commits November 10, 2024 01:23
some tests that I believe are unrelated are failing. Shall look into fixing those
once I push to my fork.
It now handles adding methods to class and object, lambda-0 case,
map(nonExistent) with and without arguments

fix: make action handleCommand with infer method

also select action on the ScalacDiagnostic.NotAMember

fix issues found by running interactively

with scala 2.13.15

move from TextEdit to WorkspaceEdit to allow edit in other files than current

when adding a file to a class, we may have the class imported but
defined in another file. TextEdit only works for current edits, while
we need to add the method stub in the other file.
WorkspaceEdit seems to be the way to go here.

satisfy scalafix

fix issues with action for class methods and extra curly brackets

remove unnecessary java formatting
this is to restrict the scope of this action, since it is unclear what
would happen for defining method for libraries we are using
logger.warning(
s"This case ($c) is not currently supported by the infer-method code action."
)
throw new RuntimeException()
Copy link
Contributor

Choose a reason for hiding this comment

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

We can use DisplayableException similar to inlineValue does it.

signature(
name = errorMethod.name.toString(),
paramsString =
Option(argumentsString(arguments.take(1)).getOrElse("")),
Copy link
Contributor

Choose a reason for hiding this comment

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

What if we have a method like:

object DoubleList{
  def apply(a : Int) = List( (a,b) )
}

class DoubList{
  def map(f: (Int, Int) => String) = ???
}


DoubleList(1).map(nonExistent) would create a wrong method. We can't know what is the type used in a method based on the arguments of the preceding apply.

Copy link
Contributor

@tgodzik tgodzik Nov 14, 2024

Choose a reason for hiding this comment

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

A very hacky way around it would be to create a function that take in everything like
def myErrorFunction(a: Any) : Nothing and with that rerun the presentation compiler to get the actual type of map. We do a similar thing with updating inferred type : https://github.com/scalameta/metals/blob/main/mtags/src/main/scala-2/scala/meta/internal/pc/InferredTypeProvider.scala#L89C9-L89C26

But maybe we could do it later, for now I would remove these cases and maybe document everything unsupported in a separate issue?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ignored the cases, thanks

Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks!

unimplemented("other-than-method")
}
case _ =>
unimplemented("other-than-ident-or-select")
Copy link
Contributor

Choose a reason for hiding this comment

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

We could add better explanations here and use the DisplayableError which would then show up for the users.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

this code is just a reference to find it back in code, I will make the logger into the DisplayableError.
I am unsure what would be a better explanation than saying "the case x is not currently supported for the action".
There could be many cases unsupported for these bits (it would take time to list them and not sure how that would help a user who is likely just disappointed things don't work here)

Copy link
Contributor

Choose a reason for hiding this comment

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

I think it's enough to make it into a sentence, so that it's clear what is not supported.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

okay I applied the changes.

@ag91
Copy link
Contributor Author

ag91 commented Nov 15, 2024

@tgodzik I think I have addressed all the comments. I think the last step is for you to address the MiMa issue and merge it when it is ready, no?
What a journey ☀️

@tgodzik
Copy link
Contributor

tgodzik commented Nov 16, 2024

@tgodzik I think I have addressed all the comments. I think the last step is for you to address the MiMa issue and merge it when it is ready, no? What a journey ☀️

I think we are almost ready, I will add some final touches next week most likely related to the code actions interface. Thanks for your hard work!

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.

2 participants