Is there any way to add JavaScript type inference logic?

Answered

I originally posted this on the Slack intellij-platform channel, but when that came up empty Yann Cebron recommended that I ask the same question here:

Another JavaScript extensibility question... Is there any way to assist with JavaScript type inference, specifically the types of default import names? In other words, if an ES6 class has:
import name from 'module';
can I help the IDE know the data type of name when it's not immediately apparent from module given how module is resolved? In this case some of my imported modules don't resolve to actual JavaScript/TypeScript files. Instead they resolve to declarations in another language, though with an easy translation between the other language's resolved declarations and those in JavaScript. I just need an opportunity to provide that translation as part of the type inference engine.I thought I might be able to do this by implementing the JavaScript frameworkIndexingHandler EP, but I can't seem to find the method(s) to override to do what I want.Any insights are greatly appreciated!
12 comments
Comment actions Permalink

I think the better way to solve this is to do two things:

  1. Make 'module' be resolved to your special file or PSI element. If it's already done, then
  2. Implement FrameworkIndexingHandler#addTypeFromResolveResult(JSTypeEvaluator, JSEvaluateContext, PsiElement). In your implementation you should check that 'result' is you special element, and if it is, create JSType (e.g. JSRecordTypeImpl) and pass it to JSTypeEvaluator.
0
Comment actions Permalink

Thanks, Konstantin. I'm still struggling a bit. In the following synthetic ES6 import statement:

import bar from 'foo/bar';

I'm properly injecting references into foo/bar in the ES6 from clause that resolve to the correct non-JS elements. When I tried to implement FrameworkIndexingHandler#addTypeFromResolveResult(), it appears that it's trying to add a type for the ES6ImportedBinding element, not the ES6FromClause element. I tried to add references to that element, but it doesn't implement ContributedReferenceHost so you can't use a reference provider/contributor.

I've also noticed that addTypeFromResolveResult() only seems to be called for import specifier lists, i.e., for "import { foo, bar } from 'foo/bar';" and not "import bar from 'foo/bar';". I really need to be able to provide type information for the latter. Perhaps if I could fix the first issue the second wouldn't be a problem?

Did I misunderstand something? Any additional tips are greatly appreciated!

Regards,

Scott

0
Comment actions Permalink

Oh, to have your reference in 'foo/bar' working for type evaluation, you need to add an extension of

com.intellij.lang.javascript.psi.resolve.JSModuleReferenceContributor

Like here. And don't forget to register it in your plugin.xml with the extension point name JavaScript.moduleReferenceContributor.

0
Comment actions Permalink

Thanks, Konstantin. Sorry if I confused things. Yes, I have implementations of moduleReferenceContributor working properly for all of these imports. That part is solid. In an import statement like:

import bar from 'foo/bar';

both "foo" and "bar" in the from clause have working references. The problem is that the imported binding for "bar" does not, and I can't figure out how to get a reference into it because it won't accept a reference contributor.

Moreover, it doesn't seem like a FrameworkIndexingHandler#addTypeFromResolveResult implementation would help there anyway because it doesn't seem to kick in for ES6ImportedBinding. I've only ever seen it called for ES6ImportSpecifier.

Thoughts?

0
Comment actions Permalink

It should be processed in ES6TypeEvaluator#addTypeFromDialectSpecificElements. ((ES6ImportedExportedDefaultBinding)resolveResult).findReferencedElements() is called there and passes referenced element to processor later. To find referenced elements, ES6PsiUtil#resolveDefaultExport is called, please check that it returns default export or a file.

0
Comment actions Permalink

Gotcha. Let me set some breakpoints and see if I can figure out how much of that is getting invoked for my import statements. Hopefully that will lead me to some thoughts on what I'm missing or potentially doing wrong right now. My guess is that I'll be back with more questions, but let me do my own homework first.

0
Comment actions Permalink

Yeah, unfortunately I'm still in a similar spot. Let me set things up a bit better than I have previously. I want to be able to handle an ES6 import like:

import someMethodAlias from 'SomeType.someMethod';

where there is a type called "SomeType" with a method called "someMethod" from my plugin's custom language, e.g.:

public class SomeType {
public String someMethod(String param1) {}
}

In the ES6 import's from clause, "SomeType" resolves to the named element for the class SomeType, and "someMethod" resolves to the named element for the method someMethod in that type. That all works properly right now.

Of course I want for "someMethodAlias" to behave as a JavaScript "proxy" (from a type inference standpoint) for SomeType.someMethod. Ideally when the developer types the following in the same ES6 file with the import:

let result = someMethod('foo');

I want for the IDE to know that the type of "result" is string, and if the user were to open parameter info inside the parens of the "someMethod" invocation, I'd like for it to show "param1: string".

That's the goal. Right now I have implemented both code completion and reference contribution (I'll stop saying "injection" because I know that means something different) for the ES6 import statement's from clause.

Based on your latest response, I set a breakpoint in ES6TypeEvaluator#addTypeFromDialectSpecificElements. When I add the line:

let result = someMethod('foo');

I do hit that breakpoint with resolveResult=ES6ImportedBinding:someMethod. So far, so good. Stepping through, it goes into the ultimate else clause:

} else {
Collection<PsiElement> referencedElements = ((ES6ImportedExportedDefaultBinding)resolveResult).findReferencedElements();

and findReferencedElements() returns an empty collection.

This is why I said that I'm in a similar spot...I can't find a way to add a reference to this ES6ImportedBinding. I think if I could do that, my implementation of FrameworkIndexingHandler#addTypeFromResolveResult would be invoked and I could translate the custom language type for this symbol into the corresponding ES6 type.

Let's start there and hopefully we can figure out how to get that resolved. Once that's resolved I'll have a similar question about whether/how I can tell the JavaScript evaluation context that this is a function/method with parameters and the types/names of those parameters.

Thanks in advance!

0
Comment actions Permalink

UPDATE: Hold tight...I think I figured out how to add references to the ES6ImportedBinding elements using another moduleReferenceContributor. Hopefully I'm on a path to success now!

0
Comment actions Permalink

Woohoo! I have the first part--really just the simple case--working:

Now I need to handle the more complex cases of "proxying" methods in my language as functions/methods in JavaScript including the signature, and I need to see if I can handle on-the-fly "proxying" of composite data types vs. the primitives that I'm doing now.

It's significant progress, though! I'm sure I'll have more questions for you, Konstantin, but thanks so much for the guidance so far!

0
Comment actions Permalink

Okay, next question, Konstantin. Is there a way that I can indicate to the JavaScript plugin that an imported symbol is a function/method and provide information about its parameters? In other words, if I have the following import:

import doSomething from 'SomeType.doSomething';

that resolves to the following method in my plugin's custom language:

public class SomeType {
public String doSomething(String withSomethingElse) {}
}

I would like for the imported symbol "doSomething" to be treated as if it were:

/**
* @param withSomethingElse {string}
* @return {string}
*/
function doSomething(withSomethingElse) {}

when code completions are offered, parameter info is displayed, etc.

Right now I've only been able to designate the return type as string, and that only works when resolving variants of "doSomething" and not "doSomething()".

Is this possible, or have I exhausted the possibilities now with addTypeFromResolveResult()?

0
Comment actions Permalink

I'm glad that it worked for you!

To fix parameter info, you need to ensure that doSomething in import statement is resolved to the method of the class. ((ES6ImportedExportedDefaultBinding)resolveResult).findReferencedElements() is also called for parameter info, but from JSStubBasedPsiTreeUtil#calculateMeaningfulElements. The entry point for this is JSParameterInfoHandler#findElementForParameterInfo

0
Comment actions Permalink

Konstantin, just to clarify, in this case doSomething does not resolve to a JavaScript function or method. It resolves to a method in another language where the underlying framework is responsible for proxying requests from JavaScript into the other language. If I understand your last comment, it sounds like the reference target must be a JavaScript function or method, no? Or is there some EP I can use to specialize the behavior of JSParameterInfoHandler#findElementForParameterInfo?

0

Please sign in to leave a comment.