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

Improved References #86

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 85 additions & 18 deletions src/main/java/net/prominic/groovyls/compiler/util/GroovyASTUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,11 @@
////////////////////////////////////////////////////////////////////////////////
package net.prominic.groovyls.compiler.util;

import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.*;
import java.util.stream.Collectors;

import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.FieldNode;
import org.codehaus.groovy.ast.ImportNode;
import org.codehaus.groovy.ast.MethodNode;
import org.codehaus.groovy.ast.ModuleNode;
import org.codehaus.groovy.ast.Parameter;
import org.codehaus.groovy.ast.PropertyNode;
import org.codehaus.groovy.ast.Variable;
import net.prominic.lsp.utils.Ranges;
import org.codehaus.groovy.ast.*;
import org.codehaus.groovy.ast.expr.ArgumentListExpression;
import org.codehaus.groovy.ast.expr.BinaryExpression;
import org.codehaus.groovy.ast.expr.ClassExpression;
Expand All @@ -59,7 +50,11 @@ public static ASTNode getEnclosingNodeOfType(ASTNode offsetNode, Class<? extends
if (nodeType.isInstance(current)) {
return current;
}
current = astVisitor.getParent(current);
ASTNode newParent = astVisitor.getParent(current);
if(newParent!=null && newParent.equals(current)){
break;
}
current = newParent;
}
return null;
}
Expand Down Expand Up @@ -133,15 +128,42 @@ public static ASTNode getTypeDefinition(ASTNode node, ASTNodeVisitor astVisitor)
return null;
}

public static List<ASTNode> getReferences(ASTNode node, ASTNodeVisitor ast) {
public static List<ASTNode> getReferences(ASTNode node, ASTNodeVisitor ast, Position currentPosition) {
ASTNode definitionNode = getDefinition(node, true, ast);
if (definitionNode == null) {
return Collections.emptyList();
}
return ast.getNodes().stream().filter(otherNode -> {
ASTNode otherDefinition = getDefinition(otherNode, false, ast);
return definitionNode.equals(otherDefinition) && node.getLineNumber() != -1 && node.getColumnNumber() != -1;
}).collect(Collectors.toList());

if(node.getLineNumber() == -1 || node.getColumnNumber() == -1){
return new ArrayList<>();
}


if((definitionNode instanceof Variable) && currentPosition !=null){
ClassNode variableType = tryToResolveOriginalClassNode(((Variable) definitionNode).getOriginType(),true,ast);
FieldNode variableField = ((PropertyNode) definitionNode).getField(); //Get field from property

Range typeRange = variableType==null?null:GroovyLanguageServerUtils.astNodeToRange(variableType);
Range fieldRange = variableField==null?null:GroovyLanguageServerUtils.astNodeToRange(variableField);

// Give preference to variable where possible
if(fieldRange !=null && Ranges.contains(fieldRange,currentPosition)){
definitionNode = variableField;
}else if(typeRange!=null && Ranges.contains(typeRange,currentPosition)){
definitionNode = variableField;
}
}

ArrayList<ASTNode> outNodes = new ArrayList<>();
for (ASTNode otherNode : ast.getNodes()){
if(otherNode.getLineNumber()!=-1 && otherNode.getColumnNumber() != -1){
ASTNode otherDefinition = getDefinition(otherNode,false,ast);
if(otherDefinition!=null && isAnnotatedNodeEqual(definitionNode,otherDefinition,ast)){
outNodes.add(otherNode);
}
}
}
return outNodes;
}

private static ClassNode tryToResolveOriginalClassNode(ClassNode node, boolean strict, ASTNodeVisitor ast) {
Expand Down Expand Up @@ -373,4 +395,49 @@ public static Range findAddImportRange(ASTNode offsetNode, ASTNodeVisitor astVis
Position position = new Position(nodeRange.getEnd().getLine() + 1, 0);
return new Range(position, position);
}

static boolean isAnnotatedNodeEqual(ASTNode declaringNode, ASTNode otherNode,ASTNodeVisitor ast){
if(Objects.equals(declaringNode,otherNode)){
return true;
}else if (declaringNode instanceof MethodNode) {
if(otherNode instanceof MethodNode){
MethodNode dn = (MethodNode) declaringNode;
MethodNode on = (MethodNode) otherNode;
return on.getName().equals(dn.getName()) && on.getDeclaringClass().equals(dn.getDeclaringClass())
&& on.getLineNumber() == dn.getLineNumber() && on.getColumnNumber() == dn.getColumnNumber()
&& on.getLastLineNumber() == dn.getLastLineNumber() && on.getLastColumnNumber() == dn.getLastColumnNumber();

}
} else if (declaringNode instanceof FieldNode) {
if (otherNode instanceof FieldNode){
FieldNode dn = (FieldNode) declaringNode;
FieldNode on = (FieldNode) otherNode;
return on.getName().equals(dn.getName()) && on.getOriginType().equals(dn.getOriginType())
&& on.getOwner().equals(dn.getOwner())
&& on.getLineNumber() == dn.getLineNumber() && on.getColumnNumber() == dn.getColumnNumber()
&& on.getLastLineNumber() == dn.getLastLineNumber() && on.getLastColumnNumber() == dn.getLastColumnNumber();
} else if (otherNode instanceof PropertyNode) {
FieldNode dn = (FieldNode) declaringNode;
FieldNode on = ((PropertyNode) otherNode).getField();
if(on!=null) {
return on.getName().equals(dn.getName()) && on.getOriginType().equals(dn.getOriginType())
&& on.getOwner().equals(dn.getOwner())
&& on.getLineNumber() == dn.getLineNumber() && on.getColumnNumber() == dn.getColumnNumber()
&& on.getLastLineNumber() == dn.getLastLineNumber() && on.getLastColumnNumber() == dn.getLastColumnNumber();
}
}
else if(otherNode instanceof PropertyExpression){
FieldNode dn = (FieldNode) declaringNode;
FieldNode on = GroovyASTUtils.getFieldFromExpression((PropertyExpression) otherNode,ast);
if(on!=null) {
return on.getName().equals(dn.getName()) && on.getOriginType().equals(dn.getOriginType())
&& on.getOwner().equals(dn.getOwner())
&& on.getLineNumber() == dn.getLineNumber() && on.getColumnNumber() == dn.getColumnNumber()
&& on.getLastLineNumber() == dn.getLastLineNumber() && on.getLastColumnNumber() == dn.getLastColumnNumber();
}
}
}

return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,12 @@ public CompletableFuture<List<? extends Location>> provideReferences(TextDocumen
return CompletableFuture.completedFuture(Collections.emptyList());
}

List<ASTNode> references = GroovyASTUtils.getReferences(offsetNode, ast);
List<ASTNode> references = GroovyASTUtils.getReferences(offsetNode, ast, position);
List<Location> locations = references.stream().map(node -> {
URI uri = ast.getURI(node);
if(uri == null){
return null;
}
return GroovyLanguageServerUtils.astNodeToLocation(node, uri);
}).filter(location -> location != null).collect(Collectors.toList());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ public CompletableFuture<WorkspaceEdit> provideRename(RenameParams renameParams)
return CompletableFuture.completedFuture(workspaceEdit);
}

List<ASTNode> references = GroovyASTUtils.getReferences(offsetNode, ast);
List<ASTNode> references = GroovyASTUtils.getReferences(offsetNode, ast, position);
references.forEach(node -> {
URI uri = ast.getURI(node);
if (uri == null) {
Expand Down
238 changes: 238 additions & 0 deletions src/test/java/net/prominic/groovyls/GroovyServicesReferenceTests.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
package net.prominic.groovyls;

import net.prominic.groovyls.config.CompilationUnitFactory;
import org.eclipse.lsp4j.*;
import org.eclipse.lsp4j.services.LanguageClient;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;

public class GroovyServicesReferenceTests {
private static final String LANGUAGE_GROOVY = "groovy";
private static final String PATH_WORKSPACE = "./build/test_workspace/";
private static final String PATH_SRC = "./src/main/groovy";

private GroovyServices services;
private Path workspaceRoot;
private Path srcRoot;

@BeforeEach
void setup() {
workspaceRoot = Paths.get(System.getProperty("user.dir")).resolve(PATH_WORKSPACE);
srcRoot = workspaceRoot.resolve(PATH_SRC);
if (!Files.exists(srcRoot)) {
srcRoot.toFile().mkdirs();
}

services = new GroovyServices(new CompilationUnitFactory());
services.setWorkspaceRoot(workspaceRoot);
services.connect(new LanguageClient() {

@Override
public void telemetryEvent(Object object) {

}

@Override
public CompletableFuture<MessageActionItem> showMessageRequest(ShowMessageRequestParams requestParams) {
return null;
}

@Override
public void showMessage(MessageParams messageParams) {

}

@Override
public void publishDiagnostics(PublishDiagnosticsParams diagnostics) {

}

@Override
public void logMessage(MessageParams message) {

}
});
}

@AfterEach
void tearDown() {
services = null;
workspaceRoot = null;
srcRoot = null;
}


@Test
void getUsagesOfMethodFromClass() throws Exception{
List<TextDocumentItem> textDocumentItem = getTextDocumentForUsage(srcRoot);
services.didOpen(new DidOpenTextDocumentParams(textDocumentItem.get(0)));
services.didOpen(new DidOpenTextDocumentParams(textDocumentItem.get(1)));
TextDocumentIdentifier textDocument = new TextDocumentIdentifier(textDocumentItem.get(0).getUri());
Position position = new Position(1, 15);
List<? extends Location> locations = services.references(new ReferenceParams(textDocument, position,new ReferenceContext(true))).get();
Assertions.assertEquals(3, locations.size());
List<Location> locationList = locations.stream().filter(r->r.getUri().equals(textDocumentItem.get(1).getUri())).collect(Collectors.toList());
Location location = locationList.get(0);
Assertions.assertEquals(textDocumentItem.get(1).getUri(), location.getUri());
Assertions.assertEquals(7, location.getRange().getStart().getLine());
Assertions.assertEquals(59, location.getRange().getStart().getCharacter());
Assertions.assertEquals(7, location.getRange().getEnd().getLine());
Assertions.assertEquals(76, location.getRange().getEnd().getCharacter());
Location location2 = locationList.get(1);
Assertions.assertEquals(textDocumentItem.get(1).getUri(), location2.getUri());
Assertions.assertEquals(11, location2.getRange().getStart().getLine());
Assertions.assertEquals(21, location2.getRange().getStart().getCharacter());
Assertions.assertEquals(11, location2.getRange().getEnd().getLine());
Assertions.assertEquals(38, location2.getRange().getEnd().getCharacter());
}

private static List<TextDocumentItem> getTextDocumentForUsage(Path srcRoot) {

Path filePath = srcRoot.resolve("MyClass.groovy");
String uri = filePath.toUri().toString();
StringBuilder contents = new StringBuilder();

contents.append("class MyClass{\n");
contents.append(" String getMyClassVersion(){\n");
contents.append(" return \"1.0.0\";\n");
contents.append(" }\n");
contents.append("}\n");

TextDocumentItem textDocumentMyClass = new TextDocumentItem(uri, LANGUAGE_GROOVY, 1, contents.toString());

Path userfilePath = srcRoot.resolve("User.groovy");
String userfileuri = userfilePath.toUri().toString();
StringBuilder userFileContents = new StringBuilder();

userFileContents.append("class User{\n");
userFileContents.append(" MyClass myclass;\n");
userFileContents.append("\n");
userFileContents.append(" User(MyClass ref){\n");
userFileContents.append(" myclass = ref;\n");
userFileContents.append(" }\n");
userFileContents.append(" void doStuff(){\n");
userFileContents.append(" String out = \"The version of my class is \" + myclass.getMyClassVersion();\n");
userFileContents.append(" }\n");
userFileContents.append("\n");
userFileContents.append(" String getLocalClassVersion() {\n");
userFileContents.append(" return myclass.getMyClassVersion();\n");
userFileContents.append(" }\n");
userFileContents.append("}\n");

TextDocumentItem textDocumentUser = new TextDocumentItem(userfileuri, LANGUAGE_GROOVY, 1, userFileContents.toString());

return Arrays.asList(textDocumentMyClass,textDocumentUser);
}


@Test
void getUsagesOfMethodFromClassObjectDeclaration() throws Exception{
List<TextDocumentItem> textDocumentItems = getTextDocumentForUsageObjectDeclaration(srcRoot);

services.didOpen(new DidOpenTextDocumentParams(textDocumentItems.get(0)));
services.didOpen(new DidOpenTextDocumentParams(textDocumentItems.get(1)));
TextDocumentIdentifier textDocument = new TextDocumentIdentifier(textDocumentItems.get(0).getUri());
Position position = new Position(1, 15);
List<? extends Location> locations = services.references(new ReferenceParams(textDocument, position,new ReferenceContext(true))).get();
Assertions.assertEquals(2, locations.size());
Optional<? extends Location> location = locations.stream().filter(it-> it.getUri().equals(textDocumentItems.get(1).getUri())).findFirst();
Assertions.assertTrue(location.isPresent());
Assertions.assertEquals(textDocumentItems.get(1).getUri(), location.get().getUri());
Assertions.assertEquals(3, location.get().getRange().getStart().getLine());
Assertions.assertEquals(9, location.get().getRange().getStart().getCharacter());
Assertions.assertEquals(3, location.get().getRange().getEnd().getLine());
Assertions.assertEquals(26, location.get().getRange().getEnd().getCharacter());
}
private static List<TextDocumentItem> getTextDocumentForUsageObjectDeclaration(Path srcRoot){
Path filePath = srcRoot.resolve("MyClass.groovy");
String uri = filePath.toUri().toString();
StringBuilder contents = new StringBuilder();

contents.append("class MyClass{\n");
contents.append(" String getMyClassVersion(){\n");
contents.append(" return \"1.0.0\";\n");
contents.append(" }\n");
contents.append("}\n");
TextDocumentItem textDocumentMyClass = new TextDocumentItem(uri, LANGUAGE_GROOVY, 1, contents.toString());

Path userfilePath = srcRoot.resolve("User.groovy");
String userfileuri = userfilePath.toUri().toString();
StringBuilder userFileContents = new StringBuilder();

userFileContents.append("class User{\n");
userFileContents.append(" void doStuff(){\n");
userFileContents.append(" MyClass mc = new MyClass();\n");
userFileContents.append(" mc.getMyClassVersion();\n");
userFileContents.append(" }\n");
userFileContents.append("}\n");
TextDocumentItem textDocumentUser = new TextDocumentItem(userfileuri, LANGUAGE_GROOVY, 1, userFileContents.toString());

return Arrays.asList(textDocumentMyClass,textDocumentUser);

}


@Test
void getUsagesOfVariableFromClassDeclaration() throws Exception{
List<TextDocumentItem> textDocumentItems = getTextDocumentForUsageVariable(srcRoot);

services.didOpen(new DidOpenTextDocumentParams(textDocumentItems.get(0)));
services.didOpen(new DidOpenTextDocumentParams(textDocumentItems.get(1)));
TextDocumentIdentifier textDocument = new TextDocumentIdentifier(textDocumentItems.get(0).getUri());
Position position = new Position(1, 13);
List<? extends Location> locations = services.references(new ReferenceParams(textDocument, position,new ReferenceContext(true))).get();
Assertions.assertEquals(3, locations.size());
List<? extends Location> locationFiltered = locations.stream().filter(it->it.getUri().equals(textDocumentItems.get(1).getUri())).collect(Collectors.toList());

Assertions.assertEquals(locationFiltered.size(),2);
Location location1 = locationFiltered.get(0);
Assertions.assertEquals(3, location1.getRange().getStart().getLine());
Assertions.assertEquals(26, location1.getRange().getStart().getCharacter());
Assertions.assertEquals(3, location1.getRange().getEnd().getLine());
Assertions.assertEquals(33, location1.getRange().getEnd().getCharacter());

Location location2 = locationFiltered.get(1);
Assertions.assertEquals(4, location2.getRange().getStart().getLine());
Assertions.assertEquals(48, location2.getRange().getStart().getCharacter());
Assertions.assertEquals(4, location2.getRange().getEnd().getLine());
Assertions.assertEquals(55, location2.getRange().getEnd().getCharacter());
}
private static List<TextDocumentItem> getTextDocumentForUsageVariable(Path srcRoot){
Path filePath = srcRoot.resolve("MyClass.groovy");
String uri = filePath.toUri().toString();
StringBuilder contents = new StringBuilder();
contents.append("class MyClass{\n");
contents.append(" String version = \"1.0\";\n");
contents.append("}\n");
contents.append("\n");
contents.append("\n");
TextDocumentItem textDocumentMyClass = new TextDocumentItem(uri, LANGUAGE_GROOVY, 1, contents.toString());

Path userfilePath = srcRoot.resolve("User.groovy");
String userfileuri = userfilePath.toUri().toString();
StringBuilder userFileContents = new StringBuilder();

userFileContents.append("class User{\n");
userFileContents.append(" void doStuff(){\n");
userFileContents.append(" MyClass mc = new MyClass();\n");
userFileContents.append(" String version = mc.version;\n");
userFileContents.append(" return \"The version of my class is \" + mc.version;\n");
userFileContents.append(" }\n");
userFileContents.append("}\n");
TextDocumentItem textDocumentUser = new TextDocumentItem(userfileuri, LANGUAGE_GROOVY, 1, userFileContents.toString());

return Arrays.asList(textDocumentMyClass,textDocumentUser);

}
}
Loading