diff --git a/README.md b/README.md index 12b9a186..f8c661ed 100644 --- a/README.md +++ b/README.md @@ -106,7 +106,8 @@ directory under the folder `.ralph-lsp/ralph.json`. The file contains the follow "ignoreUnusedFieldsWarnings": false, "ignoreUnusedPrivateFunctionsWarnings": false, "ignoreUpdateFieldsCheckWarnings": false, - "ignoreCheckExternalCallerWarnings": false + "ignoreCheckExternalCallerWarnings": false, + "ignoreUnusedFunctionReturnWarnings": false }, "contractPath": "contracts" } 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 0481220c..430c8b8b 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 @@ -84,4 +84,28 @@ object AstExtra { } + /** + * Fetches the type identifier for a given global definition. + * + * @param ast The type for which to fetch the identifier. + * @return `Some(TypeId)` if the type has an associated [[Ast.TypeId]], otherwise [[None]]. + */ + def getTypeId(ast: Ast.GlobalDefinition): Option[Ast.TypeId] = + ast match { + case ast: Ast.ContractWithState => + Some(ast.ident) + + case ast: Ast.Struct => + Some(ast.id) + + case ast: Ast.EnumDef[_] => + Some(ast.id) + + case asset: Ast.AssetScript => + Some(asset.ident) + + case _: Ast.ConstantVarDef[_] => + None + } + } diff --git a/compiler-access/src/main/scala/org/alephium/ralph/lsp/access/compiler/ast/Tree.scala b/compiler-access/src/main/scala/org/alephium/ralph/lsp/access/compiler/ast/Tree.scala index 00b2f1ca..86593376 100644 --- a/compiler-access/src/main/scala/org/alephium/ralph/lsp/access/compiler/ast/Tree.scala +++ b/compiler-access/src/main/scala/org/alephium/ralph/lsp/access/compiler/ast/Tree.scala @@ -74,22 +74,7 @@ object Tree { NodeBuilder.buildRootNode(ast) def typeId(): Option[Ast.TypeId] = - ast match { - case ast: Ast.ContractWithState => - Some(ast.ident) - - case ast: Ast.Struct => - Some(ast.id) - - case ast: Ast.EnumDef[_] => - Some(ast.id) - - case asset: Ast.AssetScript => - Some(asset.ident) - - case _: Ast.ConstantVarDef[_] => - None - } + AstExtra.getTypeId(ast) } diff --git a/compiler-access/src/main/scala/org/alephium/ralph/lsp/access/compiler/ast/node/NodeBuilder.scala b/compiler-access/src/main/scala/org/alephium/ralph/lsp/access/compiler/ast/node/NodeBuilder.scala index a47c3e36..3245be0c 100644 --- a/compiler-access/src/main/scala/org/alephium/ralph/lsp/access/compiler/ast/node/NodeBuilder.scala +++ b/compiler-access/src/main/scala/org/alephium/ralph/lsp/access/compiler/ast/node/NodeBuilder.scala @@ -34,39 +34,39 @@ object NodeBuilder extends StrictLogging { val rootSiblings = ast match { case ast: Ast.TxScript => - buildOne(ast.ident) ++ - buildMany(ast.templateVars) ++ - buildMany(ast.funcs) + Seq(buildParent(ast.ident)) ++ + buildParents(ast.templateVars) ++ + buildParents(ast.funcs) case ast: Ast.Contract => - buildOne(ast.stdInterfaceId) ++ - buildOne(ast.ident) ++ - buildMany(ast.templateVars) ++ - buildMany(ast.fields) ++ - buildMany(ast.funcs) ++ - buildMany(ast.events) ++ - buildMany(ast.constantVars) ++ - buildMany(ast.enums) ++ - buildMany(ast.inheritances) ++ - buildMany(ast.maps) + buildParents(ast.stdInterfaceId.toSeq) ++ + Seq(buildParent(ast.ident)) ++ + buildParents(ast.templateVars) ++ + buildParents(ast.fields) ++ + buildParents(ast.funcs) ++ + buildParents(ast.events) ++ + buildParents(ast.constantVars) ++ + buildParents(ast.enums) ++ + buildParents(ast.inheritances) ++ + buildParents(ast.maps) case ast: Ast.ContractInterface => - buildOne(ast.stdId) ++ - buildOne(ast.ident) ++ - buildMany(ast.funcs) ++ - buildMany(ast.events) ++ - buildMany(ast.inheritances) + buildParents(ast.stdId.toSeq) ++ + Seq(buildParent(ast.ident)) ++ + buildParents(ast.funcs) ++ + buildParents(ast.events) ++ + buildParents(ast.inheritances) case ast: Ast.Struct => - buildOne(ast.id) ++ - buildMany(ast.fields) + buildParent(ast.id) +: + buildParents(ast.fields) case ast: Ast.EnumDef[_] => - buildOne(ast.id) ++ - buildMany(ast.fields) + buildParent(ast.id) +: + buildParents(ast.fields) case ast: Ast.ConstantVarDef[_] => - buildOne(ast) + Seq(buildParent(ast)) case _: Ast.AssetScript => // AssetScript is not parsed. This will be supported in the future. @@ -84,13 +84,30 @@ object NodeBuilder extends StrictLogging { ) } - private def buildOne(product: Any): List[Node[Ast.Positioned, Ast.Positioned]] = - product match { + /** + * Constructs a parent [[Node]] for the given positioned AST parent. + * + * @param parent The positioned AST node to process. + * @return A Node representing the given parent and its children. + */ + private def buildParent(parent: Ast.Positioned): Node[Positioned, Positioned] = { + val children = processChildren(parent) + Node(parent, children) + } + + /** + * Processes the children of the given parent node as independent parent nodes. + * + * @param parent The parent node whose children are to be processed. + * @return A list of nodes representing the children of the given parent node. + */ + private def processChildren(parent: Any): List[Node[Ast.Positioned, Ast.Positioned]] = + parent match { case product: Product => product .productIterator .toList - .collect(positionedProducts) + .collect(processParent) .flatten case item => @@ -98,35 +115,45 @@ object NodeBuilder extends StrictLogging { List.empty } - private def buildMany(products: Seq[Any]): Seq[Node[Ast.Positioned, Ast.Positioned]] = + /** + * Constructs parent [[Node]] objects for each object of type [[Ast.Positioned]] in the given sequence of products. + * + * @param products A sequence of items to process, each representing a parent node. + * @return A sequence of Nodes, each representing a parent node. + */ + private def buildParents(products: Seq[Any]): Seq[Node[Ast.Positioned, Ast.Positioned]] = products - .collect(positionedProducts) + .collect(processParent) .flatten - private def positionedProducts: PartialFunction[Any, Seq[Node[Ast.Positioned, Ast.Positioned]]] = { - case positioned: Positioned => - val children = buildOne(positioned) - List(Node(positioned, children)) + /** + * Processes a product containing various types within an AST, checking for the existence + * of an [[Ast.Positioned]] type and processing it accordingly. + * + * @return A partial function that processes input of any type and returns a sequence of Nodes, + * each representing a parent node. + */ + private def processParent: PartialFunction[Any, Seq[Node[Ast.Positioned, Ast.Positioned]]] = { + case parent: Positioned => + List(buildParent(parent)) case (left: Positioned, right: Positioned) => - val leftChildren = buildOne(left) - val rightChildren = buildOne(right) List( - Node(left, leftChildren), - Node(right, rightChildren) + buildParent(left), + buildParent(right) ) - case Some(positioned: Positioned) => - positionedProducts(positioned) + case positions: Seq[_] => + buildParents(positions) - case Some(tuple @ (_: Positioned, _: Positioned)) => - positionedProducts(tuple) + case Some(product) => + processParent(product) - case positions: Seq[_] => - buildMany(positions) + case Right(product) => + processParent(product) - case Some(positions: Seq[_]) => - buildMany(positions) + case Left(product) => + processParent(product) } } diff --git a/compiler-access/src/test/scala/org/alephium/ralph/lsp/access/util/TestCodeUtil.scala b/compiler-access/src/test/scala/org/alephium/ralph/lsp/access/util/TestCodeUtil.scala index 5df272b3..c366f612 100644 --- a/compiler-access/src/test/scala/org/alephium/ralph/lsp/access/util/TestCodeUtil.scala +++ b/compiler-access/src/test/scala/org/alephium/ralph/lsp/access/util/TestCodeUtil.scala @@ -22,7 +22,7 @@ import org.scalatest.Assertions.fail object TestCodeUtil { /** Use this in your test-case for */ - private val SEARCH_INDICATOR = + val SEARCH_INDICATOR = "@@" def codeLines(code: String): Array[String] = diff --git a/presentation-compiler/src/main/scala/org/alephium/ralph/lsp/pc/PC.scala b/presentation-compiler/src/main/scala/org/alephium/ralph/lsp/pc/PC.scala index e21a1649..77a41d29 100644 --- a/presentation-compiler/src/main/scala/org/alephium/ralph/lsp/pc/PC.scala +++ b/presentation-compiler/src/main/scala/org/alephium/ralph/lsp/pc/PC.scala @@ -91,7 +91,8 @@ object PC extends StrictImplicitLogging { Workspace.buildChanged( buildURI = fileURI, code = code, - workspace = aware + workspace = aware, + buildErrors = pcState.buildErrors ) val newPCState = diff --git a/presentation-compiler/src/main/scala/org/alephium/ralph/lsp/pc/search/CodeProvider.scala b/presentation-compiler/src/main/scala/org/alephium/ralph/lsp/pc/search/CodeProvider.scala index 13186414..bfc95bcb 100644 --- a/presentation-compiler/src/main/scala/org/alephium/ralph/lsp/pc/search/CodeProvider.scala +++ b/presentation-compiler/src/main/scala/org/alephium/ralph/lsp/pc/search/CodeProvider.scala @@ -22,6 +22,7 @@ import org.alephium.ralph.lsp.pc.log.ClientLogger import org.alephium.ralph.lsp.pc.search.completion.{Suggestion, CodeCompletionProvider} import org.alephium.ralph.lsp.pc.search.gotodef.GoToDefinitionProvider import org.alephium.ralph.lsp.pc.sourcecode.{SourceLocation, SourceCodeState} +import org.alephium.ralph.lsp.pc.util.URIUtil import org.alephium.ralph.lsp.pc.workspace.{WorkspaceState, WorkspaceSearcher} import java.net.URI @@ -76,6 +77,93 @@ object CodeProvider { workspace: WorkspaceState.IsSourceAware )(implicit provider: CodeProvider[A], logger: ClientLogger): Option[Either[CompilerMessage.Error, Iterator[A]]] = + // if the fileURI belongs to the workspace, then search just within that workspace + if (URIUtil.contains(workspace.build.contractURI, fileURI)) + searchWorkspace[A]( + line = line, + character = character, + fileURI = fileURI, + workspace = workspace + ) + else // else search all source files + searchWorkspaceAndDependencies[A]( + line = line, + character = character, + fileURI = fileURI, + workspace = workspace + ) + + /** + * Executes search on dependencies and the workspace that can use this dependency. + * + * @param character Character offset on a line in a document (zero-based). + * @param fileURI The text document's uri. + * @param workspace Current workspace state. + * @tparam A The type to search. + */ + private def searchWorkspaceAndDependencies[A]( + line: Int, + character: Int, + fileURI: URI, + workspace: WorkspaceState.IsSourceAware + )(implicit provider: CodeProvider[A], + logger: ClientLogger): Option[Either[CompilerMessage.Error, Iterator[A]]] = + // Search on dependencies should only run for go-to definitions requests. Code-completion is ignored. + if (provider == CodeProvider.goToDefinition) + workspace + .build + .dependencies + .find { // find the dependency where this search was executed + dependency => + URIUtil.contains(dependency.build.contractURI, fileURI) + } + .flatMap { + dependency => + // merge all source files of all dependencies, because dependencies themselves could be interdependent. + val dependencySourceCode = + workspace + .build + .dependencies + .flatMap(_.sourceCode) + + // merge all dependencies and workspace source-files. + val mergedSourceCode = + workspace.sourceCode ++ dependencySourceCode + + // create one workspace with all source-code. + val mergedWorkspace = + WorkspaceState.UnCompiled( + build = dependency.build, + sourceCode = mergedSourceCode + ) + + // execute search on that one workspace + searchWorkspace[A]( + line = line, + character = character, + fileURI = fileURI, + workspace = mergedWorkspace + ) + } + else + None + + /** + * Execute search at cursor position within the current workspace state. + * + * @param line Line position in a document (zero-based). + * @param character Character offset on a line in a document (zero-based). + * @param fileURI The text document's uri. + * @param workspace Current workspace state. + * @tparam A The type to search. + */ + private def searchWorkspace[A]( + line: Int, + character: Int, + fileURI: URI, + workspace: WorkspaceState.IsSourceAware + )(implicit provider: CodeProvider[A], + logger: ClientLogger): Option[Either[CompilerMessage.Error, Iterator[A]]] = WorkspaceSearcher .findParsed( // find the parsed file where this search was executed. fileURI = fileURI, diff --git a/presentation-compiler/src/main/scala/org/alephium/ralph/lsp/pc/search/gotodef/GoToFuncId.scala b/presentation-compiler/src/main/scala/org/alephium/ralph/lsp/pc/search/gotodef/GoToFuncId.scala index 8c2f2347..a6bcdfc5 100644 --- a/presentation-compiler/src/main/scala/org/alephium/ralph/lsp/pc/search/gotodef/GoToFuncId.scala +++ b/presentation-compiler/src/main/scala/org/alephium/ralph/lsp/pc/search/gotodef/GoToFuncId.scala @@ -130,7 +130,13 @@ private[search] object GoToFuncId extends StrictImplicitLogging { WorkspaceSearcher.collectFunctions(builtInWorkspace.parsed) case None => - Iterator.empty + // there is no dependency on the built-in library with this workspace, + // but maybe the workspace itself is a built-in library that has usages within itself. + // TODO: Each workspace build should contain a DependencyID, + // so this collection of workspace functions runs only when required. + WorkspaceSearcher + .collectAllFunctions(workspace) + .filter(_.ast.id.isBuiltIn) } else WorkspaceSearcher.collectFunctions( @@ -155,26 +161,38 @@ private[search] object GoToFuncId extends StrictImplicitLogging { private def goToFunctionUsage( funcId: Ast.FuncId, sourceCode: SourceLocation.Code, - workspace: WorkspaceState.IsSourceAware): Iterator[SourceLocation.Node[Ast.Positioned]] = { - val children = - WorkspaceSearcher.collectImplementingChildren(sourceCode, workspace) + workspace: WorkspaceState.IsSourceAware): Iterator[SourceLocation.Node[Ast.Positioned]] = + if (funcId.isBuiltIn) { + val allTrees = + WorkspaceSearcher.collectAllTrees(workspace) - // Direct calls can only occur within the scope of inheritance. - val directCallUsages = goToDirectCallFunctionUsage( funcId = funcId, - children = children.childTrees + children = allTrees ) + } else { + val children = + WorkspaceSearcher.collectImplementingChildren( + sourceCode = sourceCode, + workspace = workspace + ) - // Contract call can occur anywhere where an instance of the Contract can be created. - val contractCallUsages = - goToContractCallFunctionUsage( - funcId = funcId, - children = children - ) + // Direct calls can only occur within the scope of inheritance. + val directCallUsages = + goToDirectCallFunctionUsage( + funcId = funcId, + children = children.childTrees + ) - directCallUsages ++ contractCallUsages - } + // Contract call can occur anywhere where an instance of the Contract can be created. + val contractCallUsages = + goToContractCallFunctionUsage( + funcId = funcId, + children = children + ) + + directCallUsages ++ contractCallUsages + } /** * Navigates to all direct function call usages for the specified [[Ast.FuncId]]. diff --git a/presentation-compiler/src/main/scala/org/alephium/ralph/lsp/pc/search/gotodef/GoToTypeId.scala b/presentation-compiler/src/main/scala/org/alephium/ralph/lsp/pc/search/gotodef/GoToTypeId.scala index 4510fb66..87dc5298 100644 --- a/presentation-compiler/src/main/scala/org/alephium/ralph/lsp/pc/search/gotodef/GoToTypeId.scala +++ b/presentation-compiler/src/main/scala/org/alephium/ralph/lsp/pc/search/gotodef/GoToTypeId.scala @@ -17,6 +17,7 @@ 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.pc.sourcecode.{SourceLocation, SourceCodeSearcher} import org.alephium.ralph.lsp.pc.workspace.{WorkspaceState, WorkspaceSearcher} @@ -70,6 +71,12 @@ private object GoToTypeId { .iterator .flatMap(goToEventDefUsage(eventDef, _)) + case Node(globalDef: Ast.GlobalDefinition, _) if AstExtra.getTypeId(globalDef) contains typeIdNode.data => + goToTypeIdUsage( + selectedTypId = typeIdNode.data, + workspace = workspace + ) + case _ => // For everything else find Contracts, Interfaces, or TxScripts with the given type ID. goToCodeId( @@ -245,4 +252,30 @@ private object GoToTypeId { .collectTypes(workspace, includeNonImportedCode = false) .filter(_.ast == typeId) + /** + * Navigate to all [[Ast.TypeId]] usages excluding itself. + * + * @param selectedTypId The selected typed ID. + * This must be the actual selected [[Ast.TypeId]] instance from the tree. + * @param workspace The Workspace where the implementation of the type ID may exist. + * @return An iterator over [[Ast.TypeId]] usages. + */ + private def goToTypeIdUsage( + selectedTypId: Ast.TypeId, + workspace: WorkspaceState.IsSourceAware): Iterator[SourceLocation.Node[Ast.TypeId]] = + WorkspaceSearcher + .collectAllTrees(workspace) + .iterator + .flatMap { + code => + code.tree.rootNode.walkDown.collect { + // this typeId should equal the searched typeId and a different object then itself. + case Node(typeId: Ast.TypeId, _) if typeId == selectedTypId && typeId.ne(selectedTypId) => + SourceLocation.Node( + ast = typeId, + source = code + ) + } + } + } diff --git a/presentation-compiler/src/main/scala/org/alephium/ralph/lsp/pc/workspace/Workspace.scala b/presentation-compiler/src/main/scala/org/alephium/ralph/lsp/pc/workspace/Workspace.scala index a923055a..6f8f7552 100644 --- a/presentation-compiler/src/main/scala/org/alephium/ralph/lsp/pc/workspace/Workspace.scala +++ b/presentation-compiler/src/main/scala/org/alephium/ralph/lsp/pc/workspace/Workspace.scala @@ -25,6 +25,7 @@ import org.alephium.ralph.lsp.pc.util.URIUtil import org.alephium.ralph.lsp.pc.workspace.build.dependency.DependencyID import org.alephium.ralph.lsp.pc.workspace.build.typescript.TSBuild import org.alephium.ralph.lsp.pc.workspace.build.{BuildError, Build, BuildState} +import org.alephium.ralph.lsp.pc.workspace.build.dependency.downloader.DependencyDownloader import java.net.URI import scala.collection.immutable.ArraySeq @@ -96,7 +97,8 @@ private[pc] object Workspace extends StrictImplicitLogging { Build.parseAndCompile( buildURI = workspace.buildURI, code = None, - currentBuild = None + currentBuild = None, + dependencyDownloaders = DependencyDownloader.natives() ) // Build `alephium.config.ts` using `ralph.json`'s compilation result. @@ -129,7 +131,8 @@ private[pc] object Workspace extends StrictImplicitLogging { Build.parseAndCompile( buildURI = code.fileURI, code = code.text, - currentBuild = None + currentBuild = None, + dependencyDownloaders = DependencyDownloader.natives() ) initialise(build) @@ -142,7 +145,8 @@ private[pc] object Workspace extends StrictImplicitLogging { Build.parseAndCompile( buildURI = workspace.buildURI, code = None, - currentBuild = None + currentBuild = None, + dependencyDownloaders = DependencyDownloader.natives() ) initialise(build) @@ -173,7 +177,8 @@ private[pc] object Workspace extends StrictImplicitLogging { Build.parseAndCompile( buildURI = currentBuild.buildURI, code = newBuildCode, - currentBuild = currentBuild + currentBuild = currentBuild, + dependencyDownloaders = DependencyDownloader.natives() ) getOrElse currentBuild // if the build code is the same and existing build, then compile using existing build. newBuild match { @@ -398,25 +403,30 @@ private[pc] object Workspace extends StrictImplicitLogging { } /** - * Process changes to a valid build file. + * Processes changes to a valid build file (`ralph.json` or `alephium.config.ts`). * - * If the build file is valid, this drops existing compilations - * and starts a fresh workspace. + * If the build file is valid and a change has occurred, this function drops existing compilation + * and initialises a new workspace, and re-builds it. * - * @param buildURI Location of the build file. - * @param code Build file's content. + * @param buildURI Location of the build file. + * @param code Content of the build file, if available, otherwise it's read from the disk, if required. + * @param workspace The current workspace with a successfully compiled JSON build file (`ralph.json`). + * @param buildErrors Provided when there are errors in the latest JSON build file (`ralph.json`). + * @return None if no change has occurred, otherwise, either a build error or an updated workspace state. */ def buildChanged( buildURI: URI, code: Option[String], - workspace: WorkspaceState.IsSourceAware + workspace: WorkspaceState.IsSourceAware, + buildErrors: Option[BuildState.Errored] )(implicit file: FileAccess, compiler: CompilerAccess, logger: ClientLogger): Option[Either[BuildError, WorkspaceState.IsSourceAware]] = if (workspace.workspaceURI.resolve(buildURI) == workspace.tsBuildURI) // Request is for the `alephium.config.ts` build file. TSBuild.build( code = code, - currentBuild = workspace.build + // Prefer a build with errors over a successfully compiled workspace build to ensure the most recent build is processed. + currentBuild = buildErrors getOrElse workspace.build ) match { case Left(error) => // TypeScript build reported error. @@ -428,7 +438,8 @@ private[pc] object Workspace extends StrictImplicitLogging { buildChanged( buildURI = parsed.buildURI, code = Some(parsed.code), // the newly persisted `ralph.json` file content is known, provide the code so no disk read occurs. - workspace = workspace + workspace = workspace, + buildErrors = buildErrors ) case Right(None) => @@ -440,7 +451,8 @@ private[pc] object Workspace extends StrictImplicitLogging { Build.parseAndCompile( buildURI = buildURI, code = code, - currentBuild = workspace.build + currentBuild = workspace.build, + dependencyDownloaders = DependencyDownloader.natives() ) match { case Some(newBuild) => newBuild match { diff --git a/presentation-compiler/src/main/scala/org/alephium/ralph/lsp/pc/workspace/WorkspaceSearcher.scala b/presentation-compiler/src/main/scala/org/alephium/ralph/lsp/pc/workspace/WorkspaceSearcher.scala index 3eacf4ba..df6d1161 100644 --- a/presentation-compiler/src/main/scala/org/alephium/ralph/lsp/pc/workspace/WorkspaceSearcher.scala +++ b/presentation-compiler/src/main/scala/org/alephium/ralph/lsp/pc/workspace/WorkspaceSearcher.scala @@ -141,6 +141,17 @@ object WorkspaceSearcher { * Consider using other [[collectFunctions]] functions for more targeted collections. */ def collectFunctions(workspace: WorkspaceState.Parsed): Iterator[SourceLocation.Node[Ast.FuncDef[StatefulContext]]] = + collectAllFunctions(workspace) + + /** + * Collects ALL function definitions within the provided parsed workspace state. + * + * Note: Collecting all functions within a large workspace can be expensive. + * + * @param workspace The parsed workspace state from which to collect function definitions. + * @return An iterator containing all function implementations. + */ + def collectAllFunctions(workspace: WorkspaceState.IsSourceAware): Iterator[SourceLocation.Node[Ast.FuncDef[StatefulContext]]] = collectTrees(workspace, includeNonImportedCode = false) .iterator .flatMap(SourceCodeSearcher.collectFunctions) diff --git a/presentation-compiler/src/main/scala/org/alephium/ralph/lsp/pc/workspace/build/Build.scala b/presentation-compiler/src/main/scala/org/alephium/ralph/lsp/pc/workspace/build/Build.scala index 3442f4f1..92656b6d 100644 --- a/presentation-compiler/src/main/scala/org/alephium/ralph/lsp/pc/workspace/build/Build.scala +++ b/presentation-compiler/src/main/scala/org/alephium/ralph/lsp/pc/workspace/build/Build.scala @@ -22,6 +22,7 @@ import org.alephium.ralph.lsp.access.compiler.message.SourceIndexExtra import org.alephium.ralph.lsp.access.file.FileAccess import org.alephium.ralph.lsp.pc.log.ClientLogger import org.alephium.ralph.lsp.pc.workspace.build.config.{RalphcConfigState, RalphcConfig} +import org.alephium.ralph.lsp.pc.workspace.build.dependency.downloader.DependencyDownloader import org.alephium.ralph.lsp.pc.workspace.build.dependency.{DependencyDB, Dependency} import java.net.URI @@ -126,7 +127,8 @@ object Build { /** Compile a parsed build */ def compile( parsed: BuildState.IsParsed, - currentBuild: Option[BuildState.IsCompiled] + currentBuild: Option[BuildState.IsCompiled], + dependencyDownloaders: ArraySeq[DependencyDownloader] )(implicit file: FileAccess, compiler: CompilerAccess, logger: ClientLogger): BuildState.IsCompiled = @@ -146,7 +148,8 @@ object Build { def compileDependencies() = Dependency.compile( parsed = parsed, - currentBuild = currentBuild + currentBuild = currentBuild, + downloaders = dependencyDownloaders ) // parse successful. Perform compilation! @@ -164,7 +167,8 @@ object Build { /** Parse and compile from disk */ def parseAndCompile( buildURI: URI, - currentBuild: Option[BuildState.IsCompiled] + currentBuild: Option[BuildState.IsCompiled], + dependencyDownloaders: ArraySeq[DependencyDownloader] )(implicit file: FileAccess, compiler: CompilerAccess, logger: ClientLogger): BuildState.IsCompiled = @@ -182,7 +186,8 @@ object Build { if (exists) compile( parsed = parse(buildURI), - currentBuild = currentBuild + currentBuild = currentBuild, + dependencyDownloaders = dependencyDownloaders ) else createDefaultBuildFile( @@ -196,7 +201,8 @@ object Build { // default build file created! Parse and compile it! parseAndCompile( buildURI = buildURI, - currentBuild = currentBuild + currentBuild = currentBuild, + dependencyDownloaders = dependencyDownloaders ) } } @@ -205,7 +211,8 @@ object Build { def parseAndCompile( buildURI: URI, code: String, - currentBuild: Option[BuildState.IsCompiled] + currentBuild: Option[BuildState.IsCompiled], + dependencyDownloaders: ArraySeq[DependencyDownloader] )(implicit file: FileAccess, compiler: CompilerAccess, logger: ClientLogger): BuildState.IsCompiled = { @@ -218,14 +225,16 @@ object Build { compile( parsed = parsed, - currentBuild = currentBuild + currentBuild = currentBuild, + dependencyDownloaders = dependencyDownloaders ) } def parseAndCompile( buildURI: URI, code: Option[String], - currentBuild: Option[BuildState.IsCompiled] + currentBuild: Option[BuildState.IsCompiled], + dependencyDownloaders: ArraySeq[DependencyDownloader] )(implicit file: FileAccess, compiler: CompilerAccess, logger: ClientLogger): BuildState.IsCompiled = @@ -234,14 +243,16 @@ object Build { parseAndCompile( buildURI = buildURI, code = code, - currentBuild = currentBuild + currentBuild = currentBuild, + dependencyDownloaders = dependencyDownloaders ) case None => // Code is not known. Parse and validate it from disk. parseAndCompile( buildURI = buildURI, - currentBuild = currentBuild + currentBuild = currentBuild, + dependencyDownloaders = dependencyDownloaders ) } @@ -251,7 +262,8 @@ object Build { def parseAndCompile( buildURI: URI, code: Option[String], - currentBuild: BuildState.Compiled + currentBuild: BuildState.Compiled, + dependencyDownloaders: ArraySeq[DependencyDownloader] )(implicit file: FileAccess, compiler: CompilerAccess, logger: ClientLogger): Option[BuildState.IsCompiled] = @@ -275,7 +287,8 @@ object Build { Build.parseAndCompile( buildURI = buildURI, code = code, - currentBuild = Some(currentBuild) + currentBuild = Some(currentBuild), + dependencyDownloaders = dependencyDownloaders ) match { case newBuild: BuildState.Compiled => // if the new build-file is the same as current build-file, return it as diff --git a/presentation-compiler/src/main/scala/org/alephium/ralph/lsp/pc/workspace/build/dependency/Dependency.scala b/presentation-compiler/src/main/scala/org/alephium/ralph/lsp/pc/workspace/build/dependency/Dependency.scala index 4b969751..b635adb9 100644 --- a/presentation-compiler/src/main/scala/org/alephium/ralph/lsp/pc/workspace/build/dependency/Dependency.scala +++ b/presentation-compiler/src/main/scala/org/alephium/ralph/lsp/pc/workspace/build/dependency/Dependency.scala @@ -47,7 +47,8 @@ object Dependency { */ def compile( parsed: BuildState.Parsed, - currentBuild: Option[BuildState.IsCompiled] + currentBuild: Option[BuildState.IsCompiled], + downloaders: ArraySeq[DependencyDownloader] )(implicit file: FileAccess, compiler: CompilerAccess, logger: ClientLogger): BuildState.IsCompiled = { @@ -73,7 +74,7 @@ object Dependency { downloadAndCompileDependencies( parsed = parsed, absoluteDependencyPath = absoluteDependenciesPath, - dependencyDownloaders = DependencyDownloader.all() + dependencyDownloaders = downloaders ) } diff --git a/presentation-compiler/src/main/scala/org/alephium/ralph/lsp/pc/workspace/build/dependency/downloader/BuiltInFunctionDownloader.scala b/presentation-compiler/src/main/scala/org/alephium/ralph/lsp/pc/workspace/build/dependency/downloader/BuiltInFunctionDownloader.scala index 5e622b7a..b751ca6d 100644 --- a/presentation-compiler/src/main/scala/org/alephium/ralph/lsp/pc/workspace/build/dependency/downloader/BuiltInFunctionDownloader.scala +++ b/presentation-compiler/src/main/scala/org/alephium/ralph/lsp/pc/workspace/build/dependency/downloader/BuiltInFunctionDownloader.scala @@ -27,7 +27,10 @@ import org.alephium.ralph.{SourceIndex, BuiltIn} import java.nio.file.Path import scala.collection.immutable.ArraySeq -private object BuiltInFunctionDownloader extends DependencyDownloader { +object BuiltInFunctionDownloader extends DependencyDownloader.Native { + + override def dependencyID: DependencyID.BuiltIn.type = + DependencyID.BuiltIn /** * Downloads built-in function source files. @@ -40,7 +43,7 @@ private object BuiltInFunctionDownloader extends DependencyDownloader { errorIndex: SourceIndex )(implicit logger: ClientLogger): Either[ArraySeq[CompilerMessage.AnyError], WorkspaceState.UnCompiled] = { val workspaceDir = - dependencyPath resolve DependencyID.BuiltIn.dirName + dependencyPath resolve dependencyID.dirName val sourceCode = toSourceCodeState( diff --git a/presentation-compiler/src/main/scala/org/alephium/ralph/lsp/pc/workspace/build/dependency/downloader/DependencyDownloader.scala b/presentation-compiler/src/main/scala/org/alephium/ralph/lsp/pc/workspace/build/dependency/downloader/DependencyDownloader.scala index 84b5f2fa..971e6a97 100644 --- a/presentation-compiler/src/main/scala/org/alephium/ralph/lsp/pc/workspace/build/dependency/downloader/DependencyDownloader.scala +++ b/presentation-compiler/src/main/scala/org/alephium/ralph/lsp/pc/workspace/build/dependency/downloader/DependencyDownloader.scala @@ -20,6 +20,7 @@ import org.alephium.ralph.lsp.access.compiler.message.CompilerMessage import org.alephium.ralph.lsp.pc.log.{ClientLogger, StrictImplicitLogging} import org.alephium.ralph.lsp.pc.workspace.WorkspaceState import org.alephium.ralph.lsp.pc.workspace.build.config.{RalphcConfigState, RalphcConfig} +import org.alephium.ralph.lsp.pc.workspace.build.dependency.DependencyID import org.alephium.ralph.lsp.pc.workspace.build.error.ErrorEmptyErrorsOnDownload import org.alephium.ralph.lsp.pc.workspace.build.{Build, BuildState} import org.alephium.ralph.{SourceIndex, CompilerOptions} @@ -32,6 +33,8 @@ import scala.collection.immutable.ArraySeq */ trait DependencyDownloader extends StrictImplicitLogging { self => + def dependencyID: DependencyID + /** * Downloads dependency code to an un-compiled workspace. * @@ -79,8 +82,13 @@ trait DependencyDownloader extends StrictImplicitLogging { self => object DependencyDownloader { + /** + * Indicates dependencies that are native to Ralph - `std` and builtin`. + */ + trait Native extends DependencyDownloader + /** All dependency downloaders */ - def all(): ArraySeq[DependencyDownloader] = + def natives(): ArraySeq[DependencyDownloader.Native] = ArraySeq( StdInterfaceDownloader, BuiltInFunctionDownloader diff --git a/presentation-compiler/src/main/scala/org/alephium/ralph/lsp/pc/workspace/build/dependency/downloader/StdInterfaceDownloader.scala b/presentation-compiler/src/main/scala/org/alephium/ralph/lsp/pc/workspace/build/dependency/downloader/StdInterfaceDownloader.scala index 09590ae1..b5b59793 100644 --- a/presentation-compiler/src/main/scala/org/alephium/ralph/lsp/pc/workspace/build/dependency/downloader/StdInterfaceDownloader.scala +++ b/presentation-compiler/src/main/scala/org/alephium/ralph/lsp/pc/workspace/build/dependency/downloader/StdInterfaceDownloader.scala @@ -31,7 +31,10 @@ import scala.io.Source import scala.jdk.CollectionConverters.{IteratorHasAsScala, MapHasAsJava} import scala.util.{Using, Success, Failure} -private object StdInterfaceDownloader extends DependencyDownloader with StrictImplicitLogging { +object StdInterfaceDownloader extends DependencyDownloader.Native with StrictImplicitLogging { + + override def dependencyID: DependencyID.Std.type = + DependencyID.Std /** * Download the Std package and return an un-compiled workspace for compilation. @@ -48,7 +51,7 @@ private object StdInterfaceDownloader extends DependencyDownloader with StrictIm ) match { case Right(interfacesSource) => val workspaceDir = - dependencyPath resolve DependencyID.Std.dirName + dependencyPath resolve dependencyID.dirName // a default build file. val build = @@ -86,7 +89,7 @@ private object StdInterfaceDownloader extends DependencyDownloader with StrictIm )(implicit logger: ClientLogger): Either[ErrorDownloadingDependency, List[SourceCodeState.UnCompiled]] = Using.Manager { use => - val stdURL = getClass.getResource(s"/${DependencyID.Std.dirName}") + val stdURL = getClass.getResource(s"/${dependencyID.dirName}") val stdPath = if (stdURL.getProtocol == "file") { Paths.get(stdURL.toURI) @@ -101,7 +104,7 @@ private object StdInterfaceDownloader extends DependencyDownloader with StrictIm interfaceFiles.map { file => val code = use(Source.fromInputStream(Files.newInputStream(file), "UTF-8")).getLines().mkString("\n") - val filePath = dependencyPath.resolve(Paths.get(DependencyID.Std.dirName).resolve(file.getFileName.toString)) + val filePath = dependencyPath.resolve(Paths.get(dependencyID.dirName).resolve(file.getFileName.toString)) SourceCodeState.UnCompiled( fileURI = filePath.toUri, code = code @@ -114,7 +117,7 @@ private object StdInterfaceDownloader extends DependencyDownloader with StrictIm case Failure(throwable) => val error = ErrorDownloadingDependency( - dependencyID = DependencyID.Std.dirName, + dependencyID = dependencyID.dirName, throwable = throwable, index = errorIndex ) diff --git a/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/search/TestCodeProvider.scala b/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/search/TestCodeProvider.scala index b8f2e8f8..1fb53572 100644 --- a/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/search/TestCodeProvider.scala +++ b/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/search/TestCodeProvider.scala @@ -16,7 +16,7 @@ package org.alephium.ralph.lsp.pc.search -import org.alephium.ralph.lsp.TestFile +import org.alephium.ralph.lsp.TestCommon import org.alephium.ralph.lsp.access.compiler.CompilerAccess import org.alephium.ralph.lsp.access.compiler.message.CompilerMessage import org.alephium.ralph.lsp.access.file.FileAccess @@ -25,54 +25,38 @@ import org.alephium.ralph.lsp.pc.client.TestClientLogger import org.alephium.ralph.lsp.pc.log.ClientLogger import org.alephium.ralph.lsp.pc.search.completion.Suggestion import org.alephium.ralph.lsp.pc.sourcecode.{SourceLocation, TestSourceCode, SourceCodeState} -import org.alephium.ralph.lsp.pc.workspace.build.TestBuild -import org.alephium.ralph.lsp.pc.workspace.build.dependency.DependencyID +import org.alephium.ralph.lsp.pc.workspace.build.{TestRalphc, BuildState, TestBuild} +import org.alephium.ralph.lsp.pc.workspace.build.dependency.{DependencyID, TestDependency} +import org.alephium.ralph.lsp.pc.workspace.build.dependency.downloader.{StdInterfaceDownloader, DependencyDownloader, BuiltInFunctionDownloader} import org.alephium.ralph.lsp.pc.workspace.{WorkspaceState, TestWorkspace, Workspace} -import org.scalacheck.Gen import org.scalatest.Assertion import org.scalatest.EitherValues._ import org.scalatest.OptionValues._ import org.scalatest.matchers.should.Matchers._ -import java.nio.file.Paths +import java.net.URI import scala.collection.immutable.ArraySeq object TestCodeProvider { /** - * Runs completion where `@@` is. + * Runs code completion where `@@` is positioned. * - * For example: The following runs completion between the double quotes. - * {{{ - * TestCompleter(""" import "@@" """) - * }}} + * @param code The code to run code completion on. + * @return A list of code completion suggestions. */ - private def apply[A](code: String)(implicit provider: CodeProvider[A]): (Iterator[A], SourceCodeState.IsCodeAware, WorkspaceState.IsParsedAndCompiled) = { - val (linePosition, _, codeWithoutAtSymbol) = TestCodeUtil.indicatorPosition(code) - - // run completion at that line and character - val (searchResult, workspace) = - TestCodeProvider( - line = linePosition.line, - character = linePosition.character, - code = codeWithoutAtSymbol - ) - - workspace.sourceCode should have size 1 - - // delete the workspace - TestWorkspace delete workspace - - (searchResult.value, workspace.sourceCode.head.asInstanceOf[SourceCodeState.IsCodeAware], workspace) - - } + def suggest(code: String): List[Suggestion] = + TestCodeProvider[Suggestion]( + code = code, + dependencyDownloaders = DependencyDownloader.natives() + )._1.toList /** * Runs GoTo definition where `@@` is located * and expects the go-to location to be the text * between the symbols `>>...<<`. * - * If the go-to symbols are not provided, then it expects empty result. + * If the go-to symbols are not provided, then it expects an empty result. * * @param code The containing `@@` and `>>...<<` symbols. */ @@ -82,7 +66,10 @@ object TestCodeProvider { // Execute go-to definition. val (searchResult, sourceCode, _) = - TestCodeProvider[SourceLocation.GoTo](codeWithoutGoToSymbols) + TestCodeProvider[SourceLocation.GoTo]( + code = codeWithoutGoToSymbols, + dependencyDownloaders = ArraySeq.empty + ) // Expect GoToLocations to also contain the fileURI val expectedGoToLocations = @@ -105,8 +92,12 @@ object TestCodeProvider { } /** + * Tests directly on the `builtin` native library. + * * Runs go-to definition where `@@` is positioned, expecting - * the resulting go-to definition to be a built-in function. + * the resulting go-to definition to be a built-in function + * contained in the `builtin` library downloaded by native dependency + * downloader [[BuiltInFunctionDownloader]]. * * @param code The code with the search indicator '@@'. * @param expected Expected resulting built-in function. @@ -120,16 +111,20 @@ object TestCodeProvider { string => (string, string) }, - dependencyId = DependencyID.BuiltIn + downloader = BuiltInFunctionDownloader ) /** + * Tests directly on the `std` native library. + * * Runs go-to definition where `@@` is positioned, expecting - * the resulting go-to definition to be in a std file. + * the resulting go-to definition to be in a std file + * contained in the `std` library downloaded by native dependency + * downloader [[StdInterfaceDownloader]]. * - * @param code The code with the search indicator '@@'. - * @param expected An optional tuple where the first element is the expected line and the second - * is the highlighted token in that line. + * @param code The code with the search indicator '@@'. + * @param expected An optional tuple where the first element is the expected line, + * and the second is the highlighted token in that line. */ def goToStd( code: String, @@ -137,35 +132,153 @@ object TestCodeProvider { goToDependency( code = code, expected = expected, - dependencyId = DependencyID.Std + downloader = StdInterfaceDownloader ) + /** + * Runs go-to definition on a custom dependency and workspace source-code. + * Other go-to functions test directly on native `std` and `builtin` libraries, + * but this allows creating custom dependency code. + * + * @param dependencyId The dependency ID to assign to the custom dependency. + * @param dependency The dependency code to write to the dependency. + * @param workspace The developer's workspace code. + * @return + */ + def goTo( + dependencyId: DependencyID, + dependency: String, + workspace: String): Unit = { + implicit val clientLogger: ClientLogger = TestClientLogger + implicit val file: FileAccess = FileAccess.disk + implicit val compiler: CompilerAccess = CompilerAccess.ralphc + + // The indicator's gotta be in either the dependency or the workspace code. + val isIndicatorInDependency = + dependency contains TestCodeUtil.SEARCH_INDICATOR + + val (indicatorPosition, _, codeWithoutIndicatorMarker) = + if (isIndicatorInDependency) + TestCodeUtil.indicatorPosition(dependency) // maybe the indicator in dependency code + else + TestCodeUtil.indicatorPosition(workspace) // otherwise, the indicator must be in dependency code + + val (dependencyLineRange, dependencyCodeWithoutRangeMarkers, _, _) = + if (isIndicatorInDependency) + TestCodeUtil.lineRanges(codeWithoutIndicatorMarker) // indicator existed in dependency code, use codeWithoutIndicatorMarker + else + TestCodeUtil.lineRanges(dependency) // no indicator in dependency code, use dependency directly + + val (workspaceCodeRange, workspaceCodeWithoutRangeMarkers, _, _) = + if (isIndicatorInDependency) + TestCodeUtil.lineRanges(workspace) // no indicator in workspace code, use workspace directly + else + TestCodeUtil.lineRanges(codeWithoutIndicatorMarker) // indicator existed in workspace code, use codeWithoutIndicatorMarker + + // collect all line ranges, they could be in both dependency and workspace code + val expectedRanges = + dependencyLineRange ++ workspaceCodeRange + + // build a custom dependency from dependency code + val downloader = + TestDependency.buildDependencyDownloader( + depId = dependencyId, + depCode = dependencyCodeWithoutRangeMarkers + ) + + // create a build file + val build = + TestCommon + .genName + .flatMap { + dependenciesFolderName => + TestBuild + .genCompiledOK( + // since custom dependencies are being written; always set a dependency folder name, + // so dependency files get generated local to the workspace, + // otherwise they will get written to `~/.ralph-lsp/*`. + config = TestRalphc.genRalphcParsedConfig(dependenciesFolderName = Some(dependenciesFolderName)), + dependencyDownloaders = ArraySeq(downloader) + ) + } + .sample + .get + + // the one dependency is written with custom code. + build.dependencies should have size 1 + build.dependencies.head.sourceCode should have size 1 + val dependencySourceFile = build.dependencies.head.sourceCode.head + dependencySourceFile.code shouldBe dependencyCodeWithoutRangeMarkers + + // generate an on-disk workspace source-file. + val sourceCode = + TestSourceCode + .genOnDiskAndPersist( + build = build, + code = workspaceCodeWithoutRangeMarkers + ) + .sample + .get + + // use the selected fileURI to be the one with @@ indicator. + val selectedFileURI = + if (isIndicatorInDependency) + dependencySourceFile.fileURI + else + sourceCode.fileURI + + // run test + val (searchResult, testWorkspace) = + TestCodeProvider[SourceLocation.GoTo]( + line = indicatorPosition.line, + character = indicatorPosition.character, + selectedFileURI = selectedFileURI, + build = build, + workspaceSourceCode = sourceCode + ) + + testWorkspace.sourceCode should have size 1 + testWorkspace.build.dependencies should have size 1 + + val actualLineRanges = searchResult.value.flatMap(_.toLineRange()).toList + actualLineRanges should contain theSameElementsAs expectedRanges + + TestWorkspace delete testWorkspace + () + } + /** * Runs go-to definition where @@ is positioned, expecting * the resulting go-to definition to be within a dependency workspace. * - * @param code The code with the search indicator '@@'. - * @param expected An optional tuple where the first element is the expected line and the second - * is the highlighted token in that line. - * @param dependencyId The dependency ID to test on. + * @param code The code with the search indicator '@@'. + * @param expected An optional tuple where the first element is the expected line, + * and the second is the highlighted token in that line. + * @param downloader The native dependency to download, and to test on. + * These must be of type [[DependencyDownloader.Native]] + * as they can be written to `~/ralph-lsp`. + * We don't want generated libraries being written to `~/ralph-lsp`. */ private def goToDependency( code: String, expected: Option[(String, String)], - dependencyId: DependencyID): Assertion = { + downloader: DependencyDownloader.Native): Assertion = { val (_, codeWithoutGoToSymbols, _, _) = TestCodeUtil.lineRanges(code) // Execute go-to definition. val (searchResult, _, workspace) = - TestCodeProvider[SourceLocation.GoTo](codeWithoutGoToSymbols) + TestCodeProvider[SourceLocation.GoTo]( + code = codeWithoutGoToSymbols, + dependencyDownloaders = ArraySeq(downloader) + ) expected match { case Some((expectedLine, expectedHighlightedToken)) => val expectedResults = workspace .build - .findDependency(dependencyId) + .findDependency(downloader.dependencyID) .to(ArraySeq) .flatMap(_.sourceCode) .filter(_.code.contains(expectedLine)) // filter source-files that contain this code function. @@ -211,74 +324,98 @@ object TestCodeProvider { } /** - * Runs code completion where `@@` is positioned. - * - * @param code The code to run code completion on. - * @return A list of code completion suggestions. - */ - def suggest(code: String): List[Suggestion] = - TestCodeProvider[Suggestion](code) - ._1 - .toList - - /** - * Run test completion. + * Runs code-provider on the given code that contains the selection indicator '@@'. * - * @param line The target line number - * @param character The target character within the line - * @param code The code to run completion on. - * @return Suggestions and the created workspace. + * @param code The code with the search indicator '@@'. + * @param dependencyDownloaders The native dependency to download, and to test on. + * These must be of type [[DependencyDownloader.Native]] + * as they can be written to `~/ralph-lsp`. + * We don't want generated libraries being written to `~/ralph-lsp`. */ private def apply[A]( - line: Int, - character: Int, - code: Gen[String] - )(implicit provider: CodeProvider[A]): (Either[CompilerMessage.Error, Iterator[A]], WorkspaceState.IsParsedAndCompiled) = { + code: String, + dependencyDownloaders: ArraySeq[DependencyDownloader.Native] + )(implicit provider: CodeProvider[A]): (Iterator[A], SourceCodeState.IsCodeAware, WorkspaceState.IsParsedAndCompiled) = { implicit val clientLogger: ClientLogger = TestClientLogger implicit val file: FileAccess = FileAccess.disk implicit val compiler: CompilerAccess = CompilerAccess.ralphc + val (linePosition, _, codeWithoutAtSymbol) = + TestCodeUtil.indicatorPosition(code) + // create a build file val build = TestBuild - .genCompiledOK() - .sample - .get - - // Generate a source-file name within the contract URI - val sourceFile = - TestFile - .genFileURI(rootFolder = Paths.get(build.contractURI)) + .genCompiledOK(dependencyDownloaders = dependencyDownloaders) .sample .get - // write the source code - val (sourceCode, _) = + // generate source-code for the code + val sourceCode = TestSourceCode .genOnDiskAndPersist( - fileURI = sourceFile, - code = code.sample.get + build = build, + code = codeWithoutAtSymbol ) .sample .get + // run completion at that line and character + val (searchResult, workspace) = + TestCodeProvider( + line = linePosition.line, + character = linePosition.character, + selectedFileURI = sourceCode.fileURI, + build = build, + workspaceSourceCode = sourceCode + ) + + workspace.sourceCode should have size 1 + + // delete the workspace + TestWorkspace delete workspace + + (searchResult.value, workspace.sourceCode.head.asInstanceOf[SourceCodeState.IsCodeAware], workspace) + + } + + /** + * Runs test on the given [[CodeProvider]], at the given line and character number. + * + * @param line The target line number + * @param character The target character within the line + * @param selectedFileURI The URI of the source-file where this search is to be executed. + * @param workspaceSourceCode The source to write to the test workspace. + * @return Suggestions and the created workspace. + */ + private def apply[A]( + line: Int, + character: Int, + selectedFileURI: URI, + build: BuildState.Compiled, + workspaceSourceCode: SourceCodeState.OnDisk + )(implicit provider: CodeProvider[A], + client: ClientLogger, + file: FileAccess, + compiler: CompilerAccess): (Either[CompilerMessage.Error, Iterator[A]], WorkspaceState.IsParsedAndCompiled) = { + // create a workspace for the build file val workspace = WorkspaceState.UnCompiled( build = build, - sourceCode = ArraySeq(sourceCode) + sourceCode = ArraySeq(workspaceSourceCode) ) // parse and compile workspace val compiledWorkspace = Workspace.parseAndCompile(workspace) - // execute completion. + // execute code-provider. val completionResult = CodeProvider.search( line = line, character = character, - fileURI = sourceCode.fileURI, + fileURI = selectedFileURI, workspace = compiledWorkspace ) diff --git a/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/search/gotodef/GoToBuiltInFunctionsSpec.scala b/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/search/gotodef/GoToBuiltInFunctionsSpec.scala index ce9b61b7..cf877f8a 100644 --- a/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/search/gotodef/GoToBuiltInFunctionsSpec.scala +++ b/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/search/gotodef/GoToBuiltInFunctionsSpec.scala @@ -17,6 +17,7 @@ package org.alephium.ralph.lsp.pc.search.gotodef import org.alephium.ralph.lsp.pc.search.TestCodeProvider._ +import org.alephium.ralph.lsp.pc.workspace.build.dependency.DependencyID import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpec @@ -38,17 +39,46 @@ class GoToBuiltInFunctionsSpec extends AnyWordSpec with Matchers { } "return non-empty" when { - "assert!" in { - goToBuiltIn( - code = """ - |Contract Test() { - | pub fn function() -> () { - | @@assert!() - | } - |} - |""".stripMargin, - expected = Some("""fn assert!(condition:Bool, errorCode:U256) -> ()""") - ) + "assert!" when { + "native builtin library" in { + // Expect go-to definition to work directly on the native builtin library + goToBuiltIn( + code = """ + |Contract Test() { + | pub fn function() -> () { + | @@assert!() + | } + |} + |""".stripMargin, + expected = Some("""fn assert!(condition:Bool, errorCode:U256) -> ()""") + ) + } + + "custom builtin library" in { + // Expect go-to definition to work on the following custom builtin code + goTo( + dependencyId = DependencyID.BuiltIn, + // the custom builtin library + dependency = """ + |Interface TestBuiltIn { + | fn hello!() -> () + | + | >>fn assert!() -> ()<< + | + | fn blah!() -> () + |} + |""".stripMargin, + // the developer's workspace code + workspace = """ + |Contract Test() { + | pub fn function() -> () { + | @@assert!() + | } + |} + |""".stripMargin + ) + } + } "verifyAbsoluteLocktime!" in { diff --git a/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/search/gotodef/GoToConstantSpec.scala b/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/search/gotodef/GoToConstantSpec.scala index 496e8735..4beb5163 100644 --- a/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/search/gotodef/GoToConstantSpec.scala +++ b/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/search/gotodef/GoToConstantSpec.scala @@ -151,6 +151,37 @@ class GoToConstantSpec extends AnyWordSpec with Matchers { |""".stripMargin } } + + "constant is used within array type definition" when { + "global constant" in { + goTo { + """ + |const SIZE@@ = 2 + | + |Contract Test() { + | fn test() -> [U256; >>SIZE<<] { + | return [0; >>SIZE<<] + | } + |} + |""".stripMargin + } + } + + "local constant" in { + goTo { + """ + |Contract Test() { + | + | const SIZE@@ = 2 + | + | fn test() -> [U256; >>SIZE<<] { + | return [0; >>SIZE<<] + | } + |} + |""".stripMargin + } + } + } } } diff --git a/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/search/gotodef/GoToConstantUsagesSpec.scala b/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/search/gotodef/GoToConstantUsagesSpec.scala index ddef8fe1..9ebdd6a0 100644 --- a/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/search/gotodef/GoToConstantUsagesSpec.scala +++ b/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/search/gotodef/GoToConstantUsagesSpec.scala @@ -68,7 +68,7 @@ class GoToConstantUsagesSpec extends AnyWordSpec with Matchers { | const MyCons@@tant = 0 | const MyConstant_B = 1 | - | pub fn function() -> () { + | pub fn function() -> [U256; >>MyConstant<<] { | let my_constant = >>MyConstant<< | let my_constant2 = >>MyConstant<< | let my_constant3 = MyConstant_B @@ -76,6 +76,7 @@ class GoToConstantUsagesSpec extends AnyWordSpec with Matchers { | let my_constant4 = >>MyConstant<< | let my_constant5 = MyConstant_B | } + | return [0; >>MyConstant<<] | } |} |""".stripMargin @@ -100,7 +101,7 @@ class GoToConstantUsagesSpec extends AnyWordSpec with Matchers { | |Contract $contractName() { | - | pub fn function() -> () { + | pub fn function() -> [U256; >>MyConstant<<] { | let my_constant = >>MyConstant<< | let my_constant2 = >>MyConstant<< | let my_constant3 = MyConstant_B @@ -108,6 +109,7 @@ class GoToConstantUsagesSpec extends AnyWordSpec with Matchers { | let my_constant4 = >>MyConstant<< | let my_constant5 = MyConstant_B | } + | return [0; >>MyConstant<<] | } |} |""".stripMargin @@ -132,25 +134,28 @@ class GoToConstantUsagesSpec extends AnyWordSpec with Matchers { | | const MyCons@@tant = 0 | - | fn function0() -> () { + | fn function0() -> [U256; >>MyConstant<<] { | let my_constant2 = >>MyConstant<< | let my_constant3 = MyConstant_B + | return [0; >>MyConstant<<] | } |} | |Contract Parent1() extends Parent() { | - | pub fn function1() -> () { + | pub fn function1() -> [U256; >>MyConstant<<] { | let my_constant2 = >>MyConstant<< | let my_constant3 = MyConstant_B + | return [0; >>MyConstant<<] | } |} | |Contract Child() extends Parent1() { | - | pub fn function2() -> () { + | pub fn function2() -> [U256; >>MyConstant<<] { | let my_constant2 = >>MyConstant<< | let my_constant3 = MyConstant_B + | return [0; >>MyConstant<<] | } |} |""".stripMargin @@ -167,25 +172,28 @@ class GoToConstantUsagesSpec extends AnyWordSpec with Matchers { | |Abstract Contract Parent() { | - | fn function0() -> () { + | fn function0() -> [U256; >>MyConstant<<] { | let my_constant2 = >>MyConstant<< | let my_constant3 = MyConstant_B + | return [0; >>MyConstant<<] | } |} | |Contract Parent1() extends Parent() { | - | pub fn function1() -> () { + | pub fn function1() -> [U256; >>MyConstant<<] { | let my_constant2 = >>MyConstant<< | let my_constant3 = MyConstant_B + | return [0; >>MyConstant<<] | } |} | |Contract Child() extends Parent1() { | - | pub fn function2() -> () { + | pub fn function2() -> [U256; >>MyConstant<<] { | let my_constant2 = >>MyConstant<< | let my_constant3 = MyConstant_B + | return [0; >>MyConstant<<] | } |} | diff --git a/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/search/gotodef/GoToFunctionUsageSpec.scala b/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/search/gotodef/GoToFunctionUsageSpec.scala index b17c380e..a8148248 100644 --- a/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/search/gotodef/GoToFunctionUsageSpec.scala +++ b/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/search/gotodef/GoToFunctionUsageSpec.scala @@ -17,6 +17,7 @@ package org.alephium.ralph.lsp.pc.search.gotodef import org.alephium.ralph.lsp.pc.search.TestCodeProvider._ +import org.alephium.ralph.lsp.pc.workspace.build.dependency.DependencyID import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpec @@ -154,6 +155,64 @@ class GoToFunctionUsageSpec extends AnyWordSpec with Matchers { ) } + "dependency function usage exists" when { + "only in workspace code" in { + goTo( + dependencyId = DependencyID.BuiltIn, + // the custom builtin library + dependency = """ + |Interface TestBuiltIn { + | fn hello!() -> () + | + | fn assert!@@() -> () + | + | fn blah!() -> () + |} + |""".stripMargin, + // the developer's workspace code + workspace = """ + |Contract Test() { + | pub fn function() -> () { + | >>assert!()<< + | } + |} + |""".stripMargin + ) + + } + + "within the dependency itself and also the workspace" in { + goTo( + dependencyId = DependencyID.BuiltIn, + // the custom builtin library + dependency = """ + |Interface TestBuiltIn { + | fn hello!() -> () + | + | fn assert!@@() -> () + | + | fn blah!() -> () + |} + | + |Contract Test() { + | pub fn function() -> () { + | >>assert!(true, 0)<< + | } + |} + |""".stripMargin, + // the developer's workspace code (no usage) + workspace = """ + |Contract Test() { + | pub fn function() -> () { + | >>assert!(false, 1)<< + | } + |} + |""".stripMargin + ) + + } + } + } } diff --git a/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/search/gotodef/GoToTypeIdContractUsageSpec.scala b/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/search/gotodef/GoToTypeIdContractUsageSpec.scala new file mode 100644 index 00000000..b6eeb474 --- /dev/null +++ b/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/search/gotodef/GoToTypeIdContractUsageSpec.scala @@ -0,0 +1,83 @@ +// 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.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpec +import org.alephium.ralph.lsp.pc.search.TestCodeProvider._ + +class GoToTypeIdContractUsageSpec extends AnyWordSpec with Matchers { + + "return empty" when { + "no usage exists" in { + goTo { + """ + |Contract This@@() { + | + | fn main() -> () { } + | + |} + | + |""".stripMargin + } + } + } + + "return non-empty" when { + "usage exists" when { + "within itself" in { + goTo { + """ + |Contract This@@( + | this: >>This<<) + | extends >>This<<() { + | + | fn main(param: >>This<<) -> () { + | let test = >>This<<.encodeFields!() + | } + | + |} + |""".stripMargin + } + } + + "within another Contract" in { + goTo { + """ + |Contract This@@() { + | + | fn main(param: Another) -> () { + | let another = Another.encodeFields!() + | } + | + |} + | + |Contract Another(this: >>This<<) + | extends >>This<<() { + | + | fn main(param: >>This<<) -> () { + | let this = >>This<<.encodeFields!() + | } + | + |} + |""".stripMargin + } + } + } + } + +} diff --git a/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/search/gotodef/GoToTypeIdStructUsageSpec.scala b/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/search/gotodef/GoToTypeIdStructUsageSpec.scala new file mode 100644 index 00000000..7f89f704 --- /dev/null +++ b/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/search/gotodef/GoToTypeIdStructUsageSpec.scala @@ -0,0 +1,78 @@ +// 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.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpec +import org.alephium.ralph.lsp.pc.search.TestCodeProvider._ + +class GoToTypeIdStructUsageSpec extends AnyWordSpec with Matchers { + + "return empty" when { + "no usage exists" in { + goTo { + """ + |struct Foo@@ { + | x: U256, + | y: U256 + |} + |""".stripMargin + } + } + } + + "return non-empty" when { + "usage exists" when { + "within itself" in { + goTo { + """ + |struct Foo@@ { + | x: U256, + | mut foo: >>Foo<< + |} + |""".stripMargin + } + } + + "within another struct" in { + goTo { + """ + |struct Foo@@ { x: U256 } + |struct Bar { mut foo: >>Foo<< } + |""".stripMargin + } + } + + "within a contract" in { + goTo { + """ + |struct Foo@@ { x: U256 } + | + |Contract Test(foo: >>Foo<<) { + | + | fn function(foo: >>Foo<<) -> () { + | let foo = >>Foo<< { x: 1 } + | } + | + |} + |""".stripMargin + } + } + } + } + +} diff --git a/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/sourcecode/TestSourceCode.scala b/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/sourcecode/TestSourceCode.scala index 04bb8a85..76f818cf 100644 --- a/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/sourcecode/TestSourceCode.scala +++ b/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/sourcecode/TestSourceCode.scala @@ -53,6 +53,26 @@ object TestSourceCode { (persistedOnDisk, code) } + /** Generates an on-disk source file within the `contractURI` of the given build */ + def genOnDiskAndPersist( + build: BuildState.Compiled, + code: Gen[String]): Gen[SourceCodeState.OnDisk] = + // Generate a source-file name within the contract URI + for { + // Generate a source-file name within the contract URI + sourceFile <- + TestFile.genFileURI( + rootFolder = Paths.get(build.contractURI) + ) + + // write the source code + (sourceCode, _) <- + genOnDiskAndPersist( + fileURI = sourceFile, + code = code.sample.get + ) + } yield sourceCode + /** */ def genOnDiskForRoot(rootURI: Gen[URI] = genFolderURI()): Gen[SourceCodeState.OnDisk] = @@ -60,7 +80,7 @@ object TestSourceCode { rootURI <- rootURI workspacePath = Gen.const(Paths.get(rootURI)) fileURI = genFileURI(rootFolder = workspacePath) - sourceCode <- TestSourceCode.genOnDisk(fileURI) + sourceCode <- genOnDisk(fileURI) } yield { // assert that SourceCode URI is a child of rootURI URIUtil.contains(rootURI, sourceCode.fileURI) shouldBe true @@ -73,7 +93,7 @@ object TestSourceCode { build <- build workspacePath = Paths.get(build.workspaceURI).resolve(build.config.contractPath) fileURI = genFileURI(rootFolder = workspacePath) - sourceCode <- TestSourceCode.genOnDisk(fileURI) + sourceCode <- genOnDisk(fileURI) } yield sourceCode /** diff --git a/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/workspace/TestWorkspace.scala b/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/workspace/TestWorkspace.scala index 62e146c7..418a0eca 100644 --- a/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/workspace/TestWorkspace.scala +++ b/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/workspace/TestWorkspace.scala @@ -22,7 +22,7 @@ import org.alephium.ralph.lsp.access.file.FileAccess import org.alephium.ralph.lsp.pc.log.ClientLogger import org.alephium.ralph.lsp.pc.sourcecode.TestSourceCode._ import org.alephium.ralph.lsp.pc.sourcecode.{TestSourceCode, SourceCodeState} -import org.alephium.ralph.lsp.pc.workspace.build.TestBuild +import org.alephium.ralph.lsp.pc.workspace.build.{BuildState, TestBuild} import org.alephium.ralph.lsp.{TestCode, TestFile} import org.scalacheck.Gen @@ -67,6 +67,16 @@ object TestWorkspace { client: ClientLogger): Gen[WorkspaceState.Parsed] = genParsed(code: _*).map(_.asInstanceOf[WorkspaceState.Parsed]) + def genParsedOK( + build: Gen[BuildState.Compiled], + code: Seq[String] + )(implicit compiler: CompilerAccess, + file: FileAccess): Gen[WorkspaceState.Parsed] = + genParsed( + build = build, + code = code + ).map(_.asInstanceOf[WorkspaceState.Parsed]) + /** * Generates a parsed or errored workspace from the provided source code. * @@ -78,7 +88,26 @@ object TestWorkspace { )(implicit compiler: CompilerAccess, file: FileAccess, client: ClientLogger): Gen[WorkspaceState.IsParsed] = - genUnCompiled(code: _*) map { + genParsed( + build = TestBuild.genCompiledOK(), + code = code.toSeq + ) + + /** + * Generates a parsed or errored workspace from the provided source code. + * + * @param code The source code to parse. + * @return The workspace containing the parser result. + */ + def genParsed( + build: Gen[BuildState.Compiled], + code: Seq[String] + )(implicit compiler: CompilerAccess, + file: FileAccess): Gen[WorkspaceState.IsParsed] = + genUnCompiled( + build = build, + code = code + ) map { unCompiled => // try parsing the workspace val workspace = @@ -90,6 +119,56 @@ object TestWorkspace { workspace } + def genCompiledOK( + code: String* + )(implicit compiler: CompilerAccess, + file: FileAccess, + client: ClientLogger): Gen[WorkspaceState.Compiled] = + genCompiled(code: _*).map(_.asInstanceOf[WorkspaceState.Compiled]) + + def genCompiled( + code: String* + )(implicit compiler: CompilerAccess, + file: FileAccess, + client: ClientLogger): Gen[WorkspaceState.IsCompiled] = + genParsedOK(code: _*) map { + parsed => + Workspace.compile(parsed) + } + + def genCompiledOK( + build: Gen[BuildState.Compiled], + code: Seq[String] + )(implicit compiler: CompilerAccess, + file: FileAccess): Gen[WorkspaceState.Compiled] = + genCompiled( + build = build, + code = code + ).map(_.asInstanceOf[WorkspaceState.Compiled]) + + def genCompiled( + build: Gen[BuildState.Compiled], + code: Seq[String] + )(implicit compiler: CompilerAccess, + file: FileAccess): Gen[WorkspaceState.IsCompiled] = + genParsedOK( + build = build, + code = code + ) map { + parsed => + Workspace.compile(parsed) + } + + def genUnCompiled( + code: String* + )(implicit compiler: CompilerAccess, + file: FileAccess, + client: ClientLogger): Gen[WorkspaceState.UnCompiled] = + genUnCompiled( + build = TestBuild.genCompiledOK(), + code = code.toSeq + ) + /** * Generates an un-compiled workspace from the provided source code. * @@ -97,12 +176,9 @@ object TestWorkspace { * @return An un-compiled workspace. */ def genUnCompiled( - code: String* - )(implicit compiler: CompilerAccess, - file: FileAccess, - client: ClientLogger): Gen[WorkspaceState.UnCompiled] = - // Generate a build file - TestBuild.genCompiledOK() map { + build: Gen[BuildState.Compiled], + code: Seq[String]): Gen[WorkspaceState.UnCompiled] = + build map { build => val sourceFiles = code diff --git a/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/workspace/WorkspaceBuildIncrementallySpec.scala b/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/workspace/WorkspaceBuildIncrementallySpec.scala index c0892443..4fbf3a57 100644 --- a/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/workspace/WorkspaceBuildIncrementallySpec.scala +++ b/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/workspace/WorkspaceBuildIncrementallySpec.scala @@ -23,6 +23,7 @@ import org.alephium.ralph.lsp.access.file.FileAccess import org.alephium.ralph.lsp.pc.client.TestClientLogger import org.alephium.ralph.lsp.pc.log.ClientLogger import org.alephium.ralph.lsp.pc.sourcecode.TestSourceCode +import org.alephium.ralph.lsp.pc.workspace.build.dependency.downloader.DependencyDownloader import org.alephium.ralph.lsp.pc.workspace.build.error.{ErrorBuildFileNotFound, ErrorInvalidBuildSyntax} import org.alephium.ralph.lsp.pc.workspace.build.{Build, BuildState, TestBuild} import org.scalacheck.Gen @@ -196,7 +197,8 @@ class WorkspaceBuildIncrementallySpec extends AnyWordSpec with Matchers with Sca Build .compile( parsed = build, - currentBuild = None + currentBuild = None, + dependencyDownloaders = DependencyDownloader.natives() ) .asInstanceOf[BuildState.Compiled] diff --git a/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/workspace/WorkspaceInitialiseSpec.scala b/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/workspace/WorkspaceInitialiseSpec.scala index ede3131c..39161d63 100644 --- a/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/workspace/WorkspaceInitialiseSpec.scala +++ b/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/workspace/WorkspaceInitialiseSpec.scala @@ -53,7 +53,8 @@ class WorkspaceInitialiseSpec extends AnyWordSpec with Matchers with ScalaCheckD val errored = Build.compile( parsed = parsed, - currentBuild = None + currentBuild = None, + dependencyDownloaders = ArraySeq.empty )(FileAccess.disk, compiler, clientLogger) // errored because contractsURI is not a persisted folder diff --git a/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/workspace/build/BuildSpec.scala b/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/workspace/build/BuildSpec.scala index d32e65ed..fc9e2960 100644 --- a/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/workspace/build/BuildSpec.scala +++ b/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/workspace/build/BuildSpec.scala @@ -50,7 +50,7 @@ class BuildSpec extends AnyWordSpec with Matchers with ScalaCheckDrivenPropertyC TestBuild .genParsed() .map(persist) - .map(Build.compile(_, None)(FileAccess.disk, CompilerAccess.ralphc, clientLogger)) + .map(Build.compile(_, None, ArraySeq.empty)(FileAccess.disk, CompilerAccess.ralphc, clientLogger)) .map(_.asInstanceOf[BuildState.Compiled]) forAll(outSideBuildGen, insideBuildGen) { @@ -72,7 +72,8 @@ class BuildSpec extends AnyWordSpec with Matchers with ScalaCheckDrivenPropertyC .parseAndCompile( buildURI = outsideBuild.buildURI, code = buildCode, - currentBuild = insideBuild + currentBuild = insideBuild, + dependencyDownloaders = ArraySeq.empty ) .value @@ -103,7 +104,7 @@ class BuildSpec extends AnyWordSpec with Matchers with ScalaCheckDrivenPropertyC TestBuild .genParsed() .map(persist) - .map(Build.compile(_, None)(FileAccess.disk, CompilerAccess.ralphc, clientLogger)) + .map(Build.compile(_, None, ArraySeq.empty)(FileAccess.disk, CompilerAccess.ralphc, clientLogger)) .map(_.asInstanceOf[BuildState.Compiled]) val generator = @@ -144,7 +145,8 @@ class BuildSpec extends AnyWordSpec with Matchers with ScalaCheckDrivenPropertyC .parseAndCompile( buildURI = build.buildURI, code = buildCode, - currentBuild = currentBuild + currentBuild = currentBuild, + dependencyDownloaders = ArraySeq.empty ) .value @@ -190,7 +192,8 @@ class BuildSpec extends AnyWordSpec with Matchers with ScalaCheckDrivenPropertyC val actual = Build.parseAndCompile( buildURI = buildURI, - currentBuild = None + currentBuild = None, + dependencyDownloaders = ArraySeq.empty ) val expected = diff --git a/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/workspace/build/TestBuild.scala b/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/workspace/build/TestBuild.scala index 6f8f06c9..86876df9 100644 --- a/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/workspace/build/TestBuild.scala +++ b/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/workspace/build/TestBuild.scala @@ -24,12 +24,14 @@ import org.alephium.ralph.lsp.pc.log.ClientLogger import org.alephium.ralph.lsp.pc.sourcecode.{TestSourceCode, SourceCodeState} import org.alephium.ralph.lsp.pc.workspace.build.TestRalphc.genRalphcParsedConfig import org.alephium.ralph.lsp.pc.workspace.build.config.{RalphcConfigState, RalphcConfig} +import org.alephium.ralph.lsp.pc.workspace.build.dependency.downloader.DependencyDownloader import org.alephium.ralph.lsp.{TestCode, TestFile} import org.scalacheck.Gen import org.scalatest.matchers.should.Matchers._ import java.net.URI import java.nio.file.Paths +import scala.collection.immutable.ArraySeq /** Build specific generators */ object TestBuild { @@ -58,18 +60,21 @@ object TestBuild { /** Generate a successfully compiled BuildState */ def genCompiledOK( workspaceURI: Gen[URI] = genFolderURI(), - config: Gen[RalphcConfigState.Parsed] = genRalphcParsedConfig() + config: Gen[RalphcConfigState.Parsed] = genRalphcParsedConfig(), + dependencyDownloaders: ArraySeq[DependencyDownloader] = DependencyDownloader.natives() )(implicit file: FileAccess, compiler: CompilerAccess, logger: ClientLogger): Gen[BuildState.Compiled] = genCompiled( workspaceURI = workspaceURI, - config = config + config = config, + dependencyDownloaders = dependencyDownloaders ).map(_.asInstanceOf[BuildState.Compiled]) def genCompiled( workspaceURI: Gen[URI] = genFolderURI(), - config: Gen[RalphcConfigState.Parsed] = genRalphcParsedConfig() + config: Gen[RalphcConfigState.Parsed] = genRalphcParsedConfig(), + dependencyDownloaders: ArraySeq[DependencyDownloader] = DependencyDownloader.natives() )(implicit file: FileAccess, compiler: CompilerAccess, logger: ClientLogger): Gen[BuildState.IsCompiled] = @@ -80,7 +85,8 @@ object TestBuild { parsed => Build.compile( parsed = persist(parsed), - currentBuild = None + currentBuild = None, + dependencyDownloaders = dependencyDownloaders ) } diff --git a/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/workspace/build/TestRalphc.scala b/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/workspace/build/TestRalphc.scala index fe59437d..663e4ba2 100644 --- a/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/workspace/build/TestRalphc.scala +++ b/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/workspace/build/TestRalphc.scala @@ -32,13 +32,15 @@ object TestRalphc { ignoreUnusedPrivateFunctionsWarnings <- Arbitrary.arbitrary[Boolean] ignoreUpdateFieldsCheckWarnings <- Arbitrary.arbitrary[Boolean] ignoreCheckExternalCallerWarnings <- Arbitrary.arbitrary[Boolean] + ignoreUnusedFunctionReturnWarnings <- Arbitrary.arbitrary[Boolean] } yield CompilerOptions( ignoreUnusedConstantsWarnings = ignoreUnusedConstantsWarnings, ignoreUnusedVariablesWarnings = ignoreUnusedVariablesWarnings, ignoreUnusedFieldsWarnings = ignoreUnusedFieldsWarnings, ignoreUnusedPrivateFunctionsWarnings = ignoreUnusedPrivateFunctionsWarnings, ignoreUpdateFieldsCheckWarnings = ignoreUpdateFieldsCheckWarnings, - ignoreCheckExternalCallerWarnings = ignoreCheckExternalCallerWarnings + ignoreCheckExternalCallerWarnings = ignoreCheckExternalCallerWarnings, + ignoreUnusedFunctionReturnWarnings = ignoreUnusedFunctionReturnWarnings ) def genRalphcParsedConfig( diff --git a/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/workspace/build/config/RalphcConfigSpec.scala b/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/workspace/build/config/RalphcConfigSpec.scala index 50cdcbe4..ea5a8fd1 100644 --- a/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/workspace/build/config/RalphcConfigSpec.scala +++ b/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/workspace/build/config/RalphcConfigSpec.scala @@ -22,6 +22,7 @@ import org.alephium.ralph.lsp.pc.client.TestClientLogger import org.alephium.ralph.lsp.pc.log.ClientLogger import org.alephium.ralph.lsp.pc.workspace.build.{Build, BuildState} import org.alephium.ralph.lsp.pc.workspace.build.dependency.Dependency +import org.alephium.ralph.lsp.pc.workspace.build.dependency.downloader.DependencyDownloader import org.alephium.ralph.lsp.pc.workspace.build.error.ErrorEmptyBuildFile import org.alephium.ralph.lsp.pc.workspace.{WorkspaceState, TestWorkspace} import org.alephium.ralphc.Config @@ -51,7 +52,8 @@ class RalphcConfigSpec extends AnyWordSpec with Matchers { | "ignoreUnusedFieldsWarnings": false, | "ignoreUnusedPrivateFunctionsWarnings": false, | "ignoreUpdateFieldsCheckWarnings": false, - | "ignoreCheckExternalCallerWarnings": false + | "ignoreCheckExternalCallerWarnings": false, + | "ignoreUnusedFunctionReturnWarnings": false | }, | "contractPath": "contracts", | "artifactPath": "artifacts", @@ -74,7 +76,8 @@ class RalphcConfigSpec extends AnyWordSpec with Matchers { | "ignoreUnusedFieldsWarnings": false, | "ignoreUnusedPrivateFunctionsWarnings": false, | "ignoreUpdateFieldsCheckWarnings": false, - | "ignoreCheckExternalCallerWarnings": false + | "ignoreCheckExternalCallerWarnings": false, + | "ignoreUnusedFunctionReturnWarnings": false | }, | "contractPath": "contracts", | "artifactPath": "artifacts" @@ -98,7 +101,8 @@ class RalphcConfigSpec extends AnyWordSpec with Matchers { | "ignoreUnusedFieldsWarnings": false, | "ignoreUnusedPrivateFunctionsWarnings": false, | "ignoreUpdateFieldsCheckWarnings": false, - | "ignoreCheckExternalCallerWarnings": false + | "ignoreCheckExternalCallerWarnings": false, + | "ignoreUnusedFunctionReturnWarnings": false | }, | "contractPath": "contracts", | "artifactPath": "artifacts" @@ -120,7 +124,8 @@ class RalphcConfigSpec extends AnyWordSpec with Matchers { | "ignoreUnusedFieldsWarnings": false, | "ignoreUnusedPrivateFunctionsWarnings": false, | "ignoreUpdateFieldsCheckWarnings": false, - | "ignoreCheckExternalCallerWarnings": false + | "ignoreCheckExternalCallerWarnings": false, + | "ignoreUnusedFunctionReturnWarnings": false | }, | "contractPath": "contracts" |} @@ -156,7 +161,7 @@ class RalphcConfigSpec extends AnyWordSpec with Matchers { implicit val compiler: CompilerAccess = CompilerAccess.ralphc - // create workspace structure with config file + // create workspace structure with a config file def doTest( dependencyPath: Option[String], artifactPath: Option[String]) = { @@ -199,7 +204,8 @@ class RalphcConfigSpec extends AnyWordSpec with Matchers { val readConfig = Build.parseAndCompile( buildURI = expectedBuildPath.toUri, - currentBuild = None + currentBuild = None, + dependencyDownloaders = DependencyDownloader.natives() ) // The code of the parsed config (user inputted) is expected, not the compiled config's code. @@ -229,7 +235,8 @@ class RalphcConfigSpec extends AnyWordSpec with Matchers { Dependency .compile( parsed = parsedBuild, - currentBuild = None + currentBuild = None, + downloaders = DependencyDownloader.natives() ) .asInstanceOf[BuildState.Compiled] diff --git a/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/workspace/build/dependency/TestDependency.scala b/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/workspace/build/dependency/TestDependency.scala index cc075c8a..59125561 100644 --- a/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/workspace/build/dependency/TestDependency.scala +++ b/presentation-compiler/src/test/scala/org/alephium/ralph/lsp/pc/workspace/build/dependency/TestDependency.scala @@ -16,15 +16,22 @@ package org.alephium.ralph.lsp.pc.workspace.build.dependency +import org.alephium.ralph.SourceIndex import org.alephium.ralph.lsp.access.compiler.CompilerAccess +import org.alephium.ralph.lsp.access.compiler.message.CompilerMessage import org.alephium.ralph.lsp.access.file.FileAccess import org.alephium.ralph.lsp.pc.client.TestClientLogger import org.alephium.ralph.lsp.pc.log.ClientLogger -import org.alephium.ralph.lsp.pc.workspace.build.config.{RalphcConfig, RalphcConfigState} -import org.alephium.ralph.lsp.pc.workspace.build.{Build, BuildState} +import org.alephium.ralph.lsp.pc.util.URIUtil +import org.alephium.ralph.lsp.pc.workspace.{WorkspaceState, TestWorkspace} +import org.alephium.ralph.lsp.pc.workspace.build.config.{RalphcConfigState, RalphcConfig} +import org.alephium.ralph.lsp.pc.workspace.build.dependency.downloader.{StdInterfaceDownloader, DependencyDownloader} +import org.alephium.ralph.lsp.pc.workspace.build.{Build, BuildState, TestBuild} import org.scalatest.matchers.should.Matchers._ +import org.scalatest.OptionValues._ -import java.nio.file.Paths +import java.nio.file.{Path, Paths} +import scala.collection.immutable.ArraySeq object TestDependency { @@ -52,15 +59,64 @@ object TestDependency { Dependency .compile( parsed = parsed, - currentBuild = None + currentBuild = None, + downloaders = ArraySeq(StdInterfaceDownloader) ) .asInstanceOf[BuildState.Compiled] - // dependency should exists in the build - dependencyBuild.dependencies should have size 2 + // only the `std` dependency should exist in the build + dependencyBuild.dependencies should have size 1 + dependencyBuild.findDependency(DependencyID.Std) shouldBe defined + URIUtil.getFileName(dependencyBuild.dependencies.head.workspaceURI) shouldBe DependencyID.Std.dirName // return the build (the build contains the std workspace) dependencyBuild } + /** + * Builds a custom dependency downloader for the given code. + * + * @param depId The ID to assign to the dependency. + * @param depCode The code to include within the dependency. + * @return A downloader that generates an uncompiled dependency workspace. + */ + def buildDependencyDownloader( + depId: DependencyID, + depCode: String + )(implicit file: FileAccess, + compiler: CompilerAccess): DependencyDownloader = + new DependencyDownloader { + override def dependencyID: DependencyID = + depId + + protected override def _download( + dependencyPath: Path, + errorIndex: SourceIndex + )(implicit logger: ClientLogger): Either[ArraySeq[CompilerMessage.AnyError], WorkspaceState.UnCompiled] = { + // Generate a build file for this dependency + val build = + TestBuild + .genCompiledOK( + // the dependency's folder name should be the ID's name + workspaceURI = dependencyPath.resolve(depId.dirName).toUri, + // the dependency itself does not have other dependencies + dependencyDownloaders = ArraySeq.empty + ) + .sample + .value + + // Generate a workspace from the build file. + val dependencyWorkspace = + TestWorkspace + .genUnCompiled( + build = build, + code = Seq(depCode) + ) + .sample + .value + + Right(dependencyWorkspace) + } + } + } diff --git a/project/Dependencies.scala b/project/Dependencies.scala index fa11fbb6..3e5e3579 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -15,7 +15,7 @@ object Dependencies { lazy val scalaMock = "org.scalamock" %% "scalamock" % "6.0.0" % Test /** Core */ - lazy val ralphc = "org.alephium" %% "alephium-ralphc" % "3.4.0" excludeAll ( + lazy val ralphc = "org.alephium" %% "alephium-ralphc" % "3.5.1" excludeAll ( ExclusionRule(organization = "org.rocksdb"), ExclusionRule(organization = "io.prometheus"), ExclusionRule(organization = "org.alephium", name = "alephium-api_2.13"),