diff --git a/compiler-access/src/main/scala/org/alephium/ralph/lsp/access/compiler/ast/AstExtra.scala b/compiler-access/src/main/scala/org/alephium/ralph/lsp/access/compiler/ast/AstExtra.scala index 3abf2407b..9e9b1720a 100644 --- a/compiler-access/src/main/scala/org/alephium/ralph/lsp/access/compiler/ast/AstExtra.scala +++ b/compiler-access/src/main/scala/org/alephium/ralph/lsp/access/compiler/ast/AstExtra.scala @@ -17,6 +17,7 @@ package org.alephium.ralph.lsp.access.compiler.ast import org.alephium.protocol.vm.StatelessContext +import org.alephium.ralph.lsp.access.compiler.message.SourceIndexExtra import org.alephium.ralph.{Type, Ast} object AstExtra { @@ -47,6 +48,36 @@ object AstExtra { false } + /** + * Checks if the `current` AST's position is before the `anchor` AST's position. + * + * @param current The AST whose position is being tested. + * @param anchor The AST with which the position of `current` is compared. + * @return `true` if `current`'s position is before `anchor`'s position, `false` otherwise. + */ + def isBehind( + current: Ast.Positioned, + anchor: Ast.Positioned): Boolean = + SourceIndexExtra.isBehind( + current = current.sourceIndex, + anchor = anchor.sourceIndex + ) + + /** + * Checks if the `current` AST's position is after the `anchor` AST's position. + * + * @param current The AST whose position is being tested. + * @param anchor The AST with which the position of `current` is compared. + * @return `true` if `current`'s position is after `anchor`'s position, `false` otherwise. + */ + def isAhead( + current: Ast.Positioned, + anchor: Ast.Positioned): Boolean = + SourceIndexExtra.isAhead( + current = current.sourceIndex, + anchor = anchor.sourceIndex + ) + /** * Fetches the type identifier for a given type. * diff --git a/compiler-access/src/main/scala/org/alephium/ralph/lsp/access/compiler/message/SourceIndexExtra.scala b/compiler-access/src/main/scala/org/alephium/ralph/lsp/access/compiler/message/SourceIndexExtra.scala index aeab5ea42..39e1abc79 100644 --- a/compiler-access/src/main/scala/org/alephium/ralph/lsp/access/compiler/message/SourceIndexExtra.scala +++ b/compiler-access/src/main/scala/org/alephium/ralph/lsp/access/compiler/message/SourceIndexExtra.scala @@ -71,6 +71,51 @@ object SourceIndexExtra { fileURI = fileURI ) + def contains( + parent: Option[SourceIndex], + child: Option[SourceIndex]): Boolean = + (parent, child) match { + case (Some(parent), Some(child)) => + parent contains child + + case (_, _) => + false + } + + /** + * Checks if the `current` position is before the `anchor`'s position. + * + * @param current The SourceIndex whose position is being tested. + * @param anchor The SourceIndex with which the position of `current` is compared. + * @return `true` if `current`'s position is before `anchor`'s position, `false` otherwise. + */ + def isBehind( + current: Option[SourceIndex], + anchor: Option[SourceIndex]): Boolean = + current + .zip(anchor) + .exists { + case (current, anchor) => + current isBehind anchor + } + + /** + * Checks if the `current` SourceIndex's position is after the `anchor` SourceIndex's position. + * + * @param current The SourceIndex whose position is being tested. + * @param anchor The SourceIndex with which the position of `current` is compared. + * @return `true` if `current`'s position is after `anchor`'s position, `false` otherwise. + */ + def isAhead( + current: Option[SourceIndex], + anchor: Option[SourceIndex]): Boolean = + current + .zip(anchor) + .exists { + case (current, anchor) => + current isAhead anchor + } + implicit class SourceIndexExtension(val sourceIndex: SourceIndex) extends AnyVal { def from: Int = @@ -86,6 +131,27 @@ object SourceIndexExtra { def contains(index: Int): Boolean = index >= from && index <= to + def contains(child: SourceIndex): Boolean = + (sourceIndex.fileURI, child.fileURI) match { + case (Some(parentURI), Some(childURI)) if parentURI == childURI => + sourceIndex contains child.from + + case (_, _) => + false + } + + def isBehind(that: SourceIndex): Boolean = + isBehind(that.from) + + def isBehind(index: Int): Boolean = + sourceIndex.from < index + + def isAhead(that: SourceIndex): Boolean = + isAhead(that.from) + + def isAhead(index: Int): Boolean = + sourceIndex.from > index + /** Offset this SourceIndex */ def +(right: Int): SourceIndex = sourceIndex.copy(index = from + right) diff --git a/presentation-compiler/src/main/scala/org/alephium/ralph/lsp/pc/search/gotodef/GoToDefIdent.scala b/presentation-compiler/src/main/scala/org/alephium/ralph/lsp/pc/search/gotodef/GoToDefIdent.scala index 3482fecde..6d30fbf26 100644 --- a/presentation-compiler/src/main/scala/org/alephium/ralph/lsp/pc/search/gotodef/GoToDefIdent.scala +++ b/presentation-compiler/src/main/scala/org/alephium/ralph/lsp/pc/search/gotodef/GoToDefIdent.scala @@ -331,12 +331,13 @@ private object GoToDefIdent extends StrictImplicitLogging { .iterator .flatMap { block => - block - .walkDown - .collect { - case Node(varDef: Ast.VarDef[_], _) if AstExtra.containsNamedVar(varDef, identNode.data) => - SourceLocation.Node(varDef, sourceCode) - } + ScopeWalker.walk( + from = block, + anchor = identNode.data + ) { + case Node(varDef: Ast.VarDef[_], _) if AstExtra.containsNamedVar(varDef, identNode.data) => + SourceLocation.Node(varDef, sourceCode) + } } /** diff --git a/presentation-compiler/src/main/scala/org/alephium/ralph/lsp/pc/search/gotodef/ScopeWalker.scala b/presentation-compiler/src/main/scala/org/alephium/ralph/lsp/pc/search/gotodef/ScopeWalker.scala new file mode 100644 index 000000000..cd34b59d0 --- /dev/null +++ b/presentation-compiler/src/main/scala/org/alephium/ralph/lsp/pc/search/gotodef/ScopeWalker.scala @@ -0,0 +1,74 @@ +// Copyright 2024 The Alephium Authors +// This file is part of the alephium project. +// +// The library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the library. If not, see http://www.gnu.org/licenses/. + +package org.alephium.ralph.lsp.pc.search.gotodef + +import org.alephium.ralph.Ast +import org.alephium.ralph.lsp.access.compiler.ast.AstExtra +import org.alephium.ralph.lsp.access.compiler.ast.node.Node +import org.alephium.ralph.lsp.access.compiler.message.SourceIndexExtra + +import scala.collection.mutable.ListBuffer + +private object ScopeWalker { + + /** + * Navigates the nodes within the scope of the `anchor` node, starting from the `from` node. + * + * @param from The node where the search starts. + * @param anchor The node which is being scoped and where the search ends. + * If the collected result is empty, nodes after the `anchor`'s position + * are processed until at least one item is collected. + * @param pf Only the Nodes defined by this partial function are collected. + * @return Nodes within the scope of the anchor AST. + */ + def walk[T]( + from: Node[Ast.Positioned, Ast.Positioned], + anchor: Ast.Positioned + )(pf: PartialFunction[Node[Ast.Positioned, Ast.Positioned], T]): Iterable[T] = { + val found = ListBuffer.empty[T] + var walker = from.walkDown + + while (walker.hasNext) + walker.next() match { + // Check: Is this a scoped node that does not contain the anchor node within its scope? If yes, drop all its child nodes. + // format: off + case block @ Node(_: Ast.While[_] | _: Ast.ForLoop[_] | _: Ast.IfBranch[_] | _: Ast.ElseBranch[_], _) if !SourceIndexExtra.contains(block.data.sourceIndex, anchor.sourceIndex) => + // format: on + walker = walker dropWhile { // drop all child nodes + next => + SourceIndexExtra.contains( + parent = block.data.sourceIndex, + child = next.data.sourceIndex + ) + } + + // Check: + // - Is this node (i.e., within the scope) defined by the partial-function? + // - And is it before the anchor node? + // - If it's defined after the anchor node (node in scope), then only add it if currently collected items are empty. + case node @ Node(ast, _) if pf.isDefinedAt(node) && (AstExtra.isBehind(ast, anchor) || found.isEmpty) => + found addOne pf(node) + + case _ => + // ignore the rest + } + + found + + } + +} diff --git a/presentation-compiler/src/main/scala/org/alephium/ralph/lsp/pc/search/gotoref/GoToRefIdent.scala b/presentation-compiler/src/main/scala/org/alephium/ralph/lsp/pc/search/gotoref/GoToRefIdent.scala index 11947e8c2..b91d283c6 100644 --- a/presentation-compiler/src/main/scala/org/alephium/ralph/lsp/pc/search/gotoref/GoToRefIdent.scala +++ b/presentation-compiler/src/main/scala/org/alephium/ralph/lsp/pc/search/gotoref/GoToRefIdent.scala @@ -21,6 +21,7 @@ import org.alephium.ralph.lsp.access.compiler.ast.Tree import org.alephium.ralph.lsp.access.compiler.ast.node.Node import org.alephium.ralph.lsp.access.compiler.message.SourceIndexExtra.SourceIndexExtension import org.alephium.ralph.lsp.pc.log.{ClientLogger, StrictImplicitLogging} +import org.alephium.ralph.lsp.pc.search.CodeProvider import org.alephium.ralph.lsp.pc.sourcecode.SourceLocation import org.alephium.ralph.lsp.pc.workspace.{WorkspaceState, WorkspaceSearcher} @@ -96,7 +97,8 @@ private object GoToRefIdent extends StrictImplicitLogging { val result = goToLocalVariableUsage( fromNode = namedVarNode.upcast(namedVar), - sourceCode = sourceCode + sourceCode = sourceCode, + workspace = workspace ) IncludeDeclaration.add( @@ -227,15 +229,18 @@ private object GoToRefIdent extends StrictImplicitLogging { */ private def goToLocalVariableUsage( fromNode: Node[Ast.NamedVar, Ast.Positioned], - sourceCode: SourceLocation.Code): Iterator[SourceLocation.Node[Ast.Ident]] = + sourceCode: SourceLocation.Code, + workspace: WorkspaceState.IsSourceAware + )(implicit logger: ClientLogger): Iterator[SourceLocation.Node[Ast.Ident]] = goToNearestBlockInScope(fromNode, sourceCode.tree) .iterator .flatMap { from => goToVariableUsages( - ident = fromNode.data.ident, + definition = fromNode.data.ident, from = from, - sourceCode = sourceCode + sourceCode = sourceCode, + workspace = workspace ) } @@ -250,14 +255,16 @@ private object GoToRefIdent extends StrictImplicitLogging { private def goToArgumentUsage( argumentNode: Node[Ast.Argument, Ast.Positioned], sourceCode: SourceLocation.Code, - workspace: WorkspaceState.IsSourceAware): Iterator[SourceLocation.Node[Ast.Ident]] = + workspace: WorkspaceState.IsSourceAware + )(implicit logger: ClientLogger): Iterator[SourceLocation.Node[Ast.Ident]] = goToNearestFuncDef(argumentNode) match { case Some(functionNode) => // It's a function argument, search within this function's body. goToVariableUsages( - ident = argumentNode.data.ident, + definition = argumentNode.data.ident, from = functionNode, - sourceCode = sourceCode + sourceCode = sourceCode, + workspace = workspace ) case None => @@ -280,7 +287,8 @@ private object GoToRefIdent extends StrictImplicitLogging { private def goToTemplateArgumentUsage( argument: Ast.Argument, sourceCode: SourceLocation.Code, - workspace: WorkspaceState.IsSourceAware): Iterator[SourceLocation.Node[Ast.Ident]] = { + workspace: WorkspaceState.IsSourceAware + )(implicit logger: ClientLogger): Iterator[SourceLocation.Node[Ast.Ident]] = { val contractInheritanceUsage = goToArgumentsUsageInInheritanceDefinition( argument = argument, @@ -339,7 +347,8 @@ private object GoToRefIdent extends StrictImplicitLogging { private def goToTemplateArgumentUsageWithinInheritance( argument: Ast.Argument, sourceCode: SourceLocation.Code, - workspace: WorkspaceState.IsSourceAware): Iterator[SourceLocation.Node[Ast.Ident]] = + workspace: WorkspaceState.IsSourceAware + )(implicit logger: ClientLogger): Iterator[SourceLocation.Node[Ast.Ident]] = WorkspaceSearcher .collectImplementingChildren(sourceCode, workspace) .childTrees @@ -347,9 +356,10 @@ private object GoToRefIdent extends StrictImplicitLogging { .flatMap { sourceCode => goToVariableUsages( - ident = argument.ident, + definition = argument.ident, from = sourceCode.tree.rootNode, - sourceCode = sourceCode + sourceCode = sourceCode, + workspace = workspace ) } @@ -460,7 +470,8 @@ private object GoToRefIdent extends StrictImplicitLogging { private def goToConstantUsage( constantDef: Ast.ConstantVarDef[_], sourceCode: SourceLocation.Code, - workspace: WorkspaceState.IsSourceAware): Iterator[SourceLocation.Node[Ast.Ident]] = { + workspace: WorkspaceState.IsSourceAware + )(implicit logger: ClientLogger): Iterator[SourceLocation.Node[Ast.Ident]] = { val children = if (sourceCode.tree.ast == constantDef) // Is a global constant, fetch all workspace trees. @@ -476,9 +487,10 @@ private object GoToRefIdent extends StrictImplicitLogging { .flatMap { sourceCode => goToVariableUsages( - ident = constantDef.ident, + definition = constantDef.ident, from = sourceCode.tree.rootNode, - sourceCode = sourceCode + sourceCode = sourceCode, + workspace = workspace ) } } @@ -486,25 +498,73 @@ private object GoToRefIdent extends StrictImplicitLogging { /** * Navigate to all variable usages for the given variable identifier. * - * @param ident The variable identifier to search for. + * @param definition The variable identifier to search for. * @param from The node to search within, walking downwards. * @return An array sequence of variable usage IDs. */ private def goToVariableUsages( - ident: Ast.Ident, + definition: Ast.Ident, from: Node[Ast.Positioned, Ast.Positioned], - sourceCode: SourceLocation.Code): Iterator[SourceLocation.Node[Ast.Ident]] = + sourceCode: SourceLocation.Code, + workspace: WorkspaceState.IsSourceAware + )(implicit logger: ClientLogger): Iterator[SourceLocation.Node[Ast.Ident]] = from .walkDown .collect { // find all the selections matching the variable name. - case Node(variable: Ast.Variable[_], _) if variable.id == ident => + case Node(variable: Ast.Variable[_], _) if variable.id == definition => SourceLocation.Node(variable.id, sourceCode) // collect all assignments - case Node(variable: Ast.AssignmentTarget[_], _) if variable.ident == ident => + case Node(variable: Ast.AssignmentTarget[_], _) if variable.ident == definition => SourceLocation.Node(variable.ident, sourceCode) } + .filter { + reference => + // So far the collected references match the name, but they also must match the actual definition in scope. + isReferenceForDefinition( + definition = definition, + reference = reference, + sourceCode = sourceCode, + workspace = workspace + ) + } + + /** + * Checks if the input `reference` is in fact a reference for the input `definition`'s instance. + * There could be duplicate definitions, a go-to-definition test can confirm this. + * + * @param definition The definition to expect. + * @param reference The reference to test. + * @param sourceCode The parsed state of the source-code where the search is executed. + * @param workspace The workspace where this search was executed and where all the source trees exist. + * @return True if definitions were a match, otherwise false. + */ + private def isReferenceForDefinition( + definition: Ast.Ident, + reference: SourceLocation.Node[Ast.Ident], + sourceCode: SourceLocation.Code, + workspace: WorkspaceState.IsSourceAware + )(implicit logger: ClientLogger): Boolean = + reference.ast.sourceIndex exists { + referenceSourceIndex => + CodeProvider + .goToDefinition + .search( + cursorIndex = referenceSourceIndex.index, + sourceCode = sourceCode.parsed, + workspace = workspace, + searchSettings = () + ) + .exists { + case SourceLocation.File(_) => + false + + case SourceLocation.Node(foundDefinition, _) => + // The following could be tested with `ast eq ident` + foundDefinition == definition && foundDefinition.sourceIndex == definition.sourceIndex + } + } /** * Navigate to the nearest function definition for which the given child node is in scope. diff --git a/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/search/gotodef/GoToAssignmentsInContractSpec.scala b/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/search/gotodef/GoToAssignmentsInContractSpec.scala index 1672074ec..58958524e 100644 --- a/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/search/gotodef/GoToAssignmentsInContractSpec.scala +++ b/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/search/gotodef/GoToAssignmentsInContractSpec.scala @@ -98,7 +98,7 @@ class GoToAssignmentsInContractSpec extends AnyWordSpec with Matchers { | pub fn function(mut >>counter<<: U256) -> () { | let mut >>counter<< = 0 | counte@@r = counter + 1 - | for (let mut >>counter<< = 0; counter <= 4; counter = counter + 1) { + | for (let mut counter = 0; counter <= 4; counter = counter + 1) { | counter = counter + 1 | } | } diff --git a/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/search/gotodef/GoToAssignmentsInTxScriptSpec.scala b/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/search/gotodef/GoToAssignmentsInTxScriptSpec.scala index 57f375ece..6a4c7a751 100644 --- a/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/search/gotodef/GoToAssignmentsInTxScriptSpec.scala +++ b/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/search/gotodef/GoToAssignmentsInTxScriptSpec.scala @@ -41,7 +41,7 @@ class GoToAssignmentsInTxScriptSpec extends AnyWordSpec with Matchers { |TxScript GoToAssignment(>>counter<<: U256) { | let mut >>counter<< = 0 | counte@@r = counter + 1 - | for (let mut >>counter<< = 0; counter <= 4; counter = counter + 1) { + | for (let mut counter = 0; counter <= 4; counter = counter + 1) { | counter = counter + 1 | } |} diff --git a/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/search/gotodef/GoToLocalVariableSpec.scala b/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/search/gotodef/GoToLocalVariableSpec.scala index ac1b98ab6..ad67feeaf 100644 --- a/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/search/gotodef/GoToLocalVariableSpec.scala +++ b/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/search/gotodef/GoToLocalVariableSpec.scala @@ -111,7 +111,7 @@ class GoToLocalVariableSpec extends AnyWordSpec with Matchers { | pub fn function() -> () { | let >>varA<< = 123 | let varB = var@@A - | let >>varA<< = ABC + | let varA = ABC | } | |} @@ -127,7 +127,7 @@ class GoToLocalVariableSpec extends AnyWordSpec with Matchers { | pub fn function(>>varA<<: Bool) -> () { | let >>varA<< = 123 | let varB = var@@A - | for (let mut >>varA<< = 0; varA <= 4; varA = varA + 1) { + | for (let mut varA = 0; varA <= 4; varA = varA + 1) { | function(true) | } | } diff --git a/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/search/gotodef/ScopeWalkerSpec.scala b/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/search/gotodef/ScopeWalkerSpec.scala new file mode 100644 index 000000000..4c261bbdd --- /dev/null +++ b/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/search/gotodef/ScopeWalkerSpec.scala @@ -0,0 +1,265 @@ +// Copyright 2024 The Alephium Authors +// This file is part of the alephium project. +// +// The library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the library. If not, see http://www.gnu.org/licenses/. + +package org.alephium.ralph.lsp.pc.search.gotodef + +import org.alephium.ralph.lsp.pc.search.TestCodeProvider._ +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpec + +/** Tests go-to definition on scoping rules defined in [[ScopeWalker]] */ +class ScopeWalkerSpec extends AnyWordSpec with Matchers { + + "allow variable access" when { + "defined outside the scope of a block" when { + "defined before usage" in { + goToDefinition( + """ + |Contract Test() { + | + | pub fn test() -> () { + | let >>variable<< = 1 + | let >>variable<< = 1 + | while (true) { + | let copyVar = variabl@@e + | } + | } + | + |} + |""".stripMargin + ) + } + + "defined after usage" should { + "go-to the first definition" in { + goToDefinition( + """ + |Contract Test() { + | + | pub fn test() -> () { + | while (true) { + | let copyVar = variabl@@e + | } + | let >>variable<< = 1 + | let variable = 1 + | } + | + |} + |""".stripMargin + ) + } + + "prioritise local definition over outside definition" in { + goToDefinition( + """ + |Contract Test() { + | + | pub fn test() -> () { + | while (true) { + | let >>variable<< = 1 + | let copyVar = variabl@@e + | } + | let variable = 1 + | let variable = 1 + | } + | + |} + |""".stripMargin + ) + } + } + + "defined in a for loop" in { + goToDefinition( + """ + |Contract Test() { + | + | pub fn test() -> () { + | while (true) { + | for (let mut >>index<< = 0; index < 2; index = index + 1) { + | let copyIndex = inde@@x + | } + | } + | } + | + |} + |""".stripMargin + ) + } + } + + "defined within one of the nested blocks and accessed in another" when { + "define before usage" in { + goToDefinition( + """ + |Contract Test() { + | + | pub fn test() -> () { + | while (true) { + | let >>variable<< = 1 + | let >>variable<< = 1 + | while (true) { + | let b = inner2 + | for (let mut index = 0; index < 2; index = index + 1) { + | while (true) { + | if (true) { + | let copyVar = variabl@@e + | } + | } + | } + | } + | } + | } + | + |} + |""".stripMargin + ) + } + + "define after usage" in { + goToDefinition( + """ + |Contract Test() { + | + | pub fn test() -> () { + | while (true) { + | while (true) { + | let b = inner2 + | while (true) { + | while (true) { + | let copyVar = variabl@@e + | } + | } + | } + | let >>variable<< = 1 + | let variable = 1 + | } + | } + | + |} + |""".stripMargin + ) + } + + "define in while loop and accessed in for loop" in { + goToDefinition( + """ + |Contract Test() { + | + | pub fn test() -> () { + | while (true) { + | while (true) { + | let b = inner2 + | while (true) { + | for (let mut index = 0; index < 2; index = index + 1) { + | let copyVar = variabl@@e + | } + | } + | } + | let >>variable<< = 1 + | let variable = 1 + | } + | } + | + |} + |""".stripMargin + ) + } + } + + } + + "disallow variable access" when { + "definition is in a different scope" when { + "defined before usage" in { + goToDefinition( + """ + |Contract Test() { + | + | pub fn test() -> () { + | while(true) { + | let variable = 1 + | } + | while (true) { + | let copyVar = variabl@@e + | } + | } + | + |} + |""".stripMargin + ) + } + + "defined after usage" in { + goToDefinition( + """ + |Contract Test() { + | + | pub fn test() -> () { + | while (true) { + | let copyVar = variabl@@e + | } + | for (let mut index = 0; index < 2; index = index + 1) { + | let variable = 1 + | } + | } + | + |} + |""".stripMargin + ) + } + + "defined after usage but in an inner scope" in { + goToDefinition( + """ + |Contract Test() { + | + | pub fn test() -> () { + | while (true) { + | let copyVar = variabl@@e + | for (let mut index = 0; index < 2; index = index + 1) { + | let variable = 1 + | } + | } + | } + | + |} + |""".stripMargin + ) + } + + "defined in a for loop" in { + goToDefinition( + """ + |Contract Test() { + | + | pub fn test() -> () { + | while (true) { + | let copyIndex = inde@@x + | for (let mut index = 0; index < 2; index = index + 1) { + | let variable = 1 + | } + | } + | } + | + |} + |""".stripMargin + ) + } + } + } + +} diff --git a/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/search/gotoref/GoToLocalVariableUsageSpec.scala b/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/search/gotoref/GoToLocalVariableUsageSpec.scala index 752d6a1af..7c82ad4e1 100644 --- a/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/search/gotoref/GoToLocalVariableUsageSpec.scala +++ b/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/search/gotoref/GoToLocalVariableUsageSpec.scala @@ -104,7 +104,7 @@ class GoToLocalVariableUsageSpec extends AnyWordSpec with Matchers { | >>counter<< + 1) { | return >>counter<< | } - | return >>counter<< + | return counter | } |} | diff --git a/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/search/gotoref/ScopeWalkerUsageSpec.scala b/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/search/gotoref/ScopeWalkerUsageSpec.scala new file mode 100644 index 000000000..d06bbcc42 --- /dev/null +++ b/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/search/gotoref/ScopeWalkerUsageSpec.scala @@ -0,0 +1,250 @@ +// Copyright 2024 The Alephium Authors +// This file is part of the alephium project. +// +// The library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the library. If not, see http://www.gnu.org/licenses/. + +package org.alephium.ralph.lsp.pc.search.gotoref + +import org.alephium.ralph.lsp.pc.search.TestCodeProvider._ +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpec + +/** + * Inverse test for [[org.alephium.ralph.lsp.pc.search.gotodef.ScopeWalkerSpec]] + * that implements the same tests, but for references. + */ +class ScopeWalkerUsageSpec extends AnyWordSpec with Matchers { + + "allow variable access" when { + "defined outside the scope of a block" when { + "defined before usage" in { + goToReferencesForAll(">>variable<<".r, ">>variabl@@e<<")( + """ + |Contract Test() { + | + | pub fn test() -> () { + | let variab@@le = 1 + | while (true) { + | let copyVar = >>variable<< + | } + | let variable = 1 + | } + | + |} + |""".stripMargin + ) + } + + "defined after usage" should { + "go-to the first definition" in { + goToReferencesForAll(">>variable<<".r, ">>variabl@@e<<")( + """ + |Contract Test() { + | + | pub fn test() -> () { + | while (true) { + | let copyVar = >>variable<< + | } + | let variab@@le = 1 + | let variable = 1 + | } + | + |} + |""".stripMargin + ) + } + + "prioritise local definition over outside definition" in { + goToReferencesForAll(">>variable<<".r, ">>variabl@@e<<")( + """ + |Contract Test() { + | + | pub fn test() -> () { + | while (true) { + | let variabl@@e = 1 + | let copyVar = >>variable<< + | } + | let variable = 1 + | let variable = 1 + | } + | + |} + |""".stripMargin + ) + } + } + + "defined in a for loop" in { + goToReferencesForAll(">>index<<".r, ">>inde@@x<<")( + """ + |Contract Test() { + | + | pub fn test() -> () { + | while (true) { + | for (let mut inde@@x = 0; + | >>index<< < 2; + | >>index<< = + | >>index<< + 1) { + | let copyIndex = >>index<< + | } + | // this not in the search result + | let copyIndex = index + | } + | } + | + |} + |""".stripMargin + ) + } + } + + "defined within one of the nested blocks and accessed in another" when { + "define before usage" in { + goToReferencesForAll(">>variable<<".r, ">>variabl@@e<<")( + """ + |Contract Test() { + | + | pub fn test() -> () { + | while (true) { + | let variab@@le = 1 + | while (true) { + | let b = inner2 + | for (let mut index = 0; index < 2; index = >>variable<< + 1) { + | while (true) { + | if (true) { + | let copyVar = >>variable<< + | } + | } + | } + | } + | } + | } + | + |} + |""".stripMargin + ) + } + + "define after usage" in { + goToReferencesForAll(">>variable<<".r, ">>variabl@@e<<")( + """ + |Contract Test() { + | + | pub fn test() -> () { + | while (true) { + | while (true) { + | let b = inner2 + | while (true) { + | while (true) { + | let copyVar = >>variable<< + | } + | } + | } + | let variabl@@e = 1 + | let variable = 1 + | } + | } + | + |} + |""".stripMargin + ) + } + } + + } + + "disallow variable access" when { + "definition is in a different scope" when { + "defined before usage" in { + goToReferences( + """ + |Contract Test() { + | + | pub fn test() -> () { + | while(true) { + | let variab@@le = 1 + | } + | while (true) { + | let copyVar = variable + | } + | } + | + |} + |""".stripMargin + ) + } + + "defined after usage" in { + goToReferences( + """ + |Contract Test() { + | + | pub fn test() -> () { + | while (true) { + | let copyVar = variable + | } + | for (let mut index = 0; index < 2; index = index + 1) { + | let varia@@ble = 1 + | } + | } + | + |} + |""".stripMargin + ) + } + + "defined after usage but in an inner scope" in { + goToReferences( + """ + |Contract Test() { + | + | pub fn test() -> () { + | while (true) { + | let copyVar = variable + | for (let mut index = 0; index < 2; index = index + 1) { + | let varia@@ble = 1 + | } + | } + | } + | + |} + |""".stripMargin + ) + } + + "defined in a for loop" in { + goToReferencesForAll(">>index<<".r, ">>inde@@x<<")( + """ + |Contract Test() { + | + | pub fn test() -> () { + | while (true) { + | let copyIndex = index + | for (let mut ind@@ex = 0; + | >>index<< < 2; + | >>index<< = + | >>index<< + 1) { + | let variable = 1 + | } + | } + | } + | + |} + |""".stripMargin + ) + } + } + } + +}