Dynamicaly typed variable references custom language plugin

Answered

I am developing a language plugin where, for the sake of simplicity for this post, I only have functions and variables. It is easy enough for me to define in grammar that function declaration (by using keywords) should implement PsiNameIdentifierOwner and function calls are its references. I have implemented my own findUsagesProvider and as well as the go to declaration functionality works as expected.

I have to preface that I dont have control over the language itself (the language is MVEL).

I am lost as to how to handle variables, as their types can be interpreted. Example for the issue I am facing:

x = 1;

x = "string";

y = x;

With my current code all instance of variable x would implement PsiNameIdentifierOwner and when evoking the go to declaration, it would point to every other instance of variable x. But expected is that it should jump to x = 1;

So far it seems it would not be possible while defining the grammar (I am using Grammar-Kit) as its not distinguishable. It leads me to believe I have to process the whole tree after parsing and somehow and somewhere I have to add logic to define that the statement x = 1; PsiElement x should implement PsiNameIdentifierOwner. I am wondering if there is an expected way to handle such cases. By means of an extension or something else.

 

4 comments
Comment actions Permalink

Hi,

I understand that the problem is that the first x assignment is a declaration that you would like to resolve when going to the declaration, but now all x assignments are resolved. Is it correct?

If not, please clarify, it would be perfect to see some screenshots.

1
Comment actions Permalink

Hi,

What happens if you try to find usages for second or third x?

Also, could you please show your FindUsagesProvider and PsiReference.resolve() and PsiReference.isReferenceTo() implementions?

1
Comment actions Permalink

Hi,

Yes that is correct. As I ctrl + click on "x" on line 5, it gives me a choice of several declarations as seen in the following screenshot.

 

I will provide grammar rules that I currently have:

 
{
...
implements
="org.intellij.sdk.language.psi.MVELCompositeElement"
extends="org.intellij.sdk.language.psi.impl.MVELCompositeElementImpl"
...
implements
("FunctionName|VariableName")="org.intellij.sdk.language.psi.MVELNamedElement"
mixin("FunctionName|VariableName")="org.intellij.sdk.language.psi.impl.MVELNamedElementImpl"
...
}
...
FunctionCallName
::= Identifier LPAREN RPAREN
private FunctionStatement ::= def FunctionName
FunctionName ::= Identifier LPAREN RPAREN

VariableName ::= Identifier

private Identifier ::= IDENTIFIER

Here I have clearly defined that function definition is a separate rule than a function call, by expecting the keyword "def" before function declaration name.

Here is a screenshot of the functions working as expected:

 

As for variables, I have only one rule VariableName and as I currently see that I can either add my NamedElement implementation for all variables or not add it at all. Ideally I would want to add my NamedElement only to the first occurrence of this variable with the same name.


Here is my reference class, I gather all PsiElements of MVELVariableName class and add them as a result. I understand that this is why I get the result in the first screenshot, as it will point to every result created here.

public class MVELReference extends PsiReferenceBase<PsiElement> implements PsiPolyVariantReference {
...
@Override
public ResolveResult @NotNull [] multiResolve(boolean incompleteCode) {
if (myElement instanceof MVELFunctionCallName) {
...
}

if (myElement instanceof MVELVariableName) {
MVELFile file = (MVELFile) myElement.getContainingFile();

if (file != null) {
Collection<MVELVariableName> children = PsiTreeUtil.findChildrenOfType(file, MVELVariableName.class);
List<ResolveResult> results = new ArrayList<>();
for (MVELVariableName child : children) {
if (child.getName().equals(((MVELVariableName) myElement).getName())) {
results.add(new PsiElementResolveResult(child));
}
}
return results.toArray(new ResolveResult[results.size()]);
}
}

return EMPTY_RESOLVE_RESULT;
}
...
}

 

Now what I have come up with is to get the first element like in the next snippet and only provide that as a result, which in turn when ctrl + cliking the variable will automatically jump to result provided.

if (myElement instanceof MVELVariableName) {
MVELFile file = (MVELFile) myElement.getContainingFile();

if (file != null) {
Collection<MVELVariableName> children = PsiTreeUtil.findChildrenOfType(file, MVELVariableName.class);
if (!children.isEmpty()) {
MVELVariableName firstChild = children.iterator().next();
return new ResolveResult[]{new PsiElementResolveResult(firstChild)};
}
}
}

This does the job,  ctrl + cliking on any of the variables but the first now move the carret to the first occurance. And Clicking on the first one provides me the usages. Before the code change I didn in the above snipped, it did not provide any usages only the declarations.


Now the issue is when I place the carret on the line 3, only that occurance of the variable is highlighted. Same for line 5.

But if I put the carret on line 1, they all are highlighted

I would guess this is because they all are NamedElemets and only the first "x" has any references poinitng to it. The rest do not have any references themselves, so nothing to highlight? Why does it not fallback to checking if the NamedElemet on line 3 is a reference to line 1 and then highlighting everything it?


For the function example, if I place my carret on line 15 every occurance gets highlighted. In this case on line 15 the function call "test()" is just a reference.

I would take if the function call on line 15 would also be a NameElement it would act the same as in my variable example, correct?

This is why I am asking if there is a built in way of handling this, as I am clearly not doing it right.

0
Comment actions Permalink

Hi!

I have updated the code that gathers PsiElementResolveResult for variables to check for the variables with the same name, but that would not change anything in this example.

if (myElement instanceof MVELVariableName) {
MVELFile file = (MVELFile) myElement.getContainingFile();

if (file != null) {
Collection<MVELVariableName> children = PsiTreeUtil.findChildrenOfType(file, MVELVariableName.class);
if (!children.isEmpty()) {
for (MVELVariableName child : children) {
if (child.getName().equals(((MVELVariableName) myElement).getName())) {
return new ResolveResult[]{new PsiElementResolveResult(child)};
}
}
}
}
}


To answer you questions.

Find usage on second and third x:

results in this view:

FindUsagesProvider:

public class MVELFindUsagesProvider implements FindUsagesProvider {

@Nullable
@Override
public WordsScanner getWordsScanner() {
return new DefaultWordsScanner(new MVELLexerAdapter(),
TokenSet.create(MVELTypes.FUNCTION_NAME),
TokenSet.create(MVELTypes.VARIABLE_NAME),
TokenSet.create(MVELTypes.COMMENT),
TokenSet.EMPTY);
}

@Override
public boolean canFindUsagesFor(@NotNull PsiElement psiElement) {
return psiElement instanceof MVELNamedElement;
}

@Nullable
@Override
public String getHelpId(@NotNull PsiElement psiElement) {
return null;
}

@NotNull
@Override
public String getType(@NotNull PsiElement element) {
return ElementDescriptionUtil.getElementDescription(element, UsageViewTypeLocation.INSTANCE);
}

@NotNull
@Override
public String getDescriptiveName(@NotNull PsiElement element) {
return ElementDescriptionUtil.getElementDescription(element, UsageViewLongNameLocation.INSTANCE);
}

@NotNull
@Override
public String getNodeText(@NotNull PsiElement element, boolean useFullName) {
return ElementDescriptionUtil.getElementDescription(element, UsageViewNodeTextLocation.INSTANCE);
}

}


PsiReference.resolve():

@Override
public @Nullable PsiElement resolve() {
ResolveResult[] resolveResults = multiResolve(false);
return resolveResults.length > 0 ? resolveResults[0].getElement() : null;
}

 


I havent implemented my PsiReference.isReferenceTo(). I havent played around this method yet.
...
As I write this I am debuging the PsiReference.isReferenceTo() which lead me to the isEquivalentTo() method.
Implementing it, for it to comparing the names of each element seems to have helped.

@Override
public boolean isEquivalentTo(PsiElement another) {
return this.getName().equals(((MVELCompositeElement) another).getName()) &&
this.getNode().getElementType().equals(another.getNode().getElementType());
}

It now highlights everything as expected!


I am grateful for the guidance, thank you Karol. I believe you have helped me with my initial issue. Now I have to play around scope handling and I noticed it also finds the usage of the declaration, but I feel thats a different issue. Thank you again!

0

Please sign in to leave a comment.