diff --git a/src/main/java/net/prominic/groovyls/compiler/util/GroovyASTUtils.java b/src/main/java/net/prominic/groovyls/compiler/util/GroovyASTUtils.java index 58d0403..d0640f2 100644 --- a/src/main/java/net/prominic/groovyls/compiler/util/GroovyASTUtils.java +++ b/src/main/java/net/prominic/groovyls/compiler/util/GroovyASTUtils.java @@ -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; @@ -59,7 +50,11 @@ public static ASTNode getEnclosingNodeOfType(ASTNode offsetNode, Class getReferences(ASTNode node, ASTNodeVisitor ast) { + public static List 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 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) { @@ -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; + } } \ No newline at end of file diff --git a/src/main/java/net/prominic/groovyls/providers/ReferenceProvider.java b/src/main/java/net/prominic/groovyls/providers/ReferenceProvider.java index 8bafb8b..2e5340b 100644 --- a/src/main/java/net/prominic/groovyls/providers/ReferenceProvider.java +++ b/src/main/java/net/prominic/groovyls/providers/ReferenceProvider.java @@ -54,9 +54,12 @@ public CompletableFuture> provideReferences(TextDocumen return CompletableFuture.completedFuture(Collections.emptyList()); } - List references = GroovyASTUtils.getReferences(offsetNode, ast); + List references = GroovyASTUtils.getReferences(offsetNode, ast, position); List 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()); diff --git a/src/main/java/net/prominic/groovyls/providers/RenameProvider.java b/src/main/java/net/prominic/groovyls/providers/RenameProvider.java index cce9d9e..e2e3f19 100644 --- a/src/main/java/net/prominic/groovyls/providers/RenameProvider.java +++ b/src/main/java/net/prominic/groovyls/providers/RenameProvider.java @@ -81,7 +81,7 @@ public CompletableFuture provideRename(RenameParams renameParams) return CompletableFuture.completedFuture(workspaceEdit); } - List references = GroovyASTUtils.getReferences(offsetNode, ast); + List references = GroovyASTUtils.getReferences(offsetNode, ast, position); references.forEach(node -> { URI uri = ast.getURI(node); if (uri == null) { diff --git a/src/test/java/net/prominic/groovyls/GroovyServicesReferenceTests.java b/src/test/java/net/prominic/groovyls/GroovyServicesReferenceTests.java new file mode 100644 index 0000000..fdee6aa --- /dev/null +++ b/src/test/java/net/prominic/groovyls/GroovyServicesReferenceTests.java @@ -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 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 = 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 locations = services.references(new ReferenceParams(textDocument, position,new ReferenceContext(true))).get(); + Assertions.assertEquals(3, locations.size()); + List 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 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 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 locations = services.references(new ReferenceParams(textDocument, position,new ReferenceContext(true))).get(); + Assertions.assertEquals(2, locations.size()); + Optional 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 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 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 locations = services.references(new ReferenceParams(textDocument, position,new ReferenceContext(true))).get(); + Assertions.assertEquals(3, locations.size()); + List 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 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); + + } +} diff --git a/src/test/java/net/prominic/groovyls/GroovyServicesTypeDefinitionTests.java b/src/test/java/net/prominic/groovyls/GroovyServicesTypeDefinitionTests.java index 5611c52..6f80564 100644 --- a/src/test/java/net/prominic/groovyls/GroovyServicesTypeDefinitionTests.java +++ b/src/test/java/net/prominic/groovyls/GroovyServicesTypeDefinitionTests.java @@ -255,6 +255,34 @@ void testMemberMethodTypeDefinitionFromDeclaration() throws Exception { Assertions.assertEquals(3, location.getRange().getEnd().getLine()); Assertions.assertEquals(1, location.getRange().getEnd().getCharacter()); } + @Test + void testMemberAdjacentMethodTypeDefinitionFromDeclaration() throws Exception { + Path filePath = srcRoot.resolve("Definitions.groovy"); + String uri = filePath.toUri().toString(); + StringBuilder contents = new StringBuilder(); + contents.append("class TypeDefinitions {\n"); + contents.append(" public TypeDefinitions memberMethod() {\n"); + contents.append(" }\n"); + contents.append("}\n"); + contents.append("\n"); + contents.append("class Util {\n"); + contents.append(" TypeDefinitions getTypeDef(){\n"); + contents.append(" }\n"); + contents.append("}\n"); + TextDocumentItem textDocumentItem = new TextDocumentItem(uri, LANGUAGE_GROOVY, 1, contents.toString()); + services.didOpen(new DidOpenTextDocumentParams(textDocumentItem)); + TextDocumentIdentifier textDocument = new TextDocumentIdentifier(uri); + Position position = new Position(6, 12); + List locations = services.typeDefinition(new TypeDefinitionParams(textDocument, position)) + .get().getLeft(); + Assertions.assertEquals(1, locations.size()); + Location location = locations.get(0); + Assertions.assertEquals(uri, location.getUri()); + Assertions.assertEquals(0, location.getRange().getStart().getLine()); + Assertions.assertEquals(0, location.getRange().getStart().getCharacter()); + Assertions.assertEquals(3, location.getRange().getEnd().getLine()); + Assertions.assertEquals(1, location.getRange().getEnd().getCharacter()); + } @Test void testMemberMethodTypeDefinitionFromCall() throws Exception {