diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/model/PathConfigs.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/model/PathConfigs.java index 25fdd52db..a14b0d394 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/model/PathConfigs.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/model/PathConfigs.java @@ -26,6 +26,8 @@ */ package org.rascalmpl.vscode.lsp.rascal.model; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.LoadingCache; import java.io.IOException; import java.nio.file.attribute.FileTime; import java.time.Duration; @@ -41,7 +43,6 @@ import java.util.function.Consumer; import java.util.regex.Pattern; import java.util.stream.Collectors; - import org.apache.commons.lang3.tuple.Pair; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -53,9 +54,6 @@ import org.rascalmpl.uri.URIResolverRegistry; import org.rascalmpl.uri.URIUtil; -import com.github.benmanes.caffeine.cache.Caffeine; -import com.github.benmanes.caffeine.cache.LoadingCache; - import io.usethesource.vallang.IConstructor; import io.usethesource.vallang.ISourceLocation; @@ -70,10 +68,11 @@ public class PathConfigs { private static final URIResolverRegistry reg = URIResolverRegistry.getInstance(); private final Map> currentPathConfigs = new ConcurrentHashMap<>(); private final PathConfigUpdater updater = new PathConfigUpdater(currentPathConfigs); + private final Projects projects = new Projects(); private final LoadingCache translatedRoots = Caffeine.newBuilder() .expireAfterAccess(Duration.ofMinutes(20)) - .build(PathConfigs::inferProjectRoot); + .build(projects::inferRoot); private final Executor executor; private final PathConfigDiagnostics diagnostics; @@ -86,7 +85,7 @@ public PathConfigs(Executor executor, PathConfigDiagnostics diagnostics) { } public void expungePathConfig(ISourceLocation project) { - var projectRoot = inferProjectRoot(project); + var projectRoot = projects.inferRoot(project); try { updater.unregisterProject(project); } catch (IOException e) { @@ -262,42 +261,4 @@ private static boolean hasParentSection(URIResolverRegistry reg, ISourceLocation } } - /** - * Infers the root of the project that `member` is in. - */ - private static ISourceLocation inferProjectRoot(ISourceLocation member) { - ISourceLocation lastRoot = member; - ISourceLocation root; - do { - root = lastRoot; - lastRoot = inferDeepestProjectRoot(URIUtil.getParentLocation(root)); - } while (!lastRoot.equals(URIUtil.getParentLocation(root))); - return root; - } - - /** - * Infers the longest project root-like path that `member` is in. Might return a sub-directory of `target/`. - */ - private static ISourceLocation inferDeepestProjectRoot(ISourceLocation member) { - ISourceLocation current = member; - URIResolverRegistry reg = URIResolverRegistry.getInstance(); - if (!reg.isDirectory(current)) { - current = URIUtil.getParentLocation(current); - } - - while (current != null && reg.exists(current) && reg.isDirectory(current)) { - if (reg.exists(URIUtil.getChildLocation(current, "META-INF/RASCAL.MF"))) { - return current; - } - var parent = URIUtil.getParentLocation(current); - if (parent.equals(current)) { - // we went all the way up to the root - return reg.isDirectory(member) ? member : URIUtil.getParentLocation(member); - } - - current = parent; - } - - return current; - } } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/model/Projects.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/model/Projects.java new file mode 100644 index 000000000..32cd127c4 --- /dev/null +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/model/Projects.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2018-2025, NWO-I CWI and Swat.engineering + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package org.rascalmpl.vscode.lsp.rascal.model; + +import org.rascalmpl.interpreter.utils.RascalManifest; +import org.rascalmpl.uri.URIUtil; + +import io.usethesource.vallang.ISourceLocation; + +/** + * Tools for projects, like path computations. Non-static functions so they can be used in Rascal via `@javaClass` as well. + */ +public class Projects { + + /** + * Infers the shallowest possible root of the project that `origin` is in. + */ + public ISourceLocation inferRoot(ISourceLocation origin) { + origin = origin.top(); + var innerRoot = inferDeepestRoot(origin); + var outerRoot = inferDeepestRoot(URIUtil.getParentLocation(innerRoot)); + + if (!innerRoot.equals(outerRoot) && isSameProject(innerRoot, outerRoot)) { + // The roots are not equal, but refer to the same project: the inner root is somewhere inside the target folder. + // In that case, we need the outer root + return outerRoot; + } + + // (innerRoot.equals(outerRoot) || !isSameProject(innerRoot, outerRoot)) + // Inner is a nested project within outer; we want the root of the nested project. + return innerRoot; + } + + private boolean isSameProject(ISourceLocation root1, ISourceLocation root2) { + var mf = new RascalManifest(); + return mf.hasManifest(root1) && mf.getProjectName(root1).equals(mf.getProjectName(root2)); + } + + /** + * Infers the longest project root-like path that `member` is in. Might return a sub-directory of `target/`. + */ + private ISourceLocation inferDeepestRoot(ISourceLocation origin) { + var root = origin; + while (!new RascalManifest().hasManifest(root)) { + if (root.getPath().equals(URIUtil.URI_PATH_SEPARATOR)) { + // File system root; cannot recurse further + break; + } + root = URIUtil.getParentLocation(root); + } + return root; + } + +} diff --git a/rascal-lsp/src/main/rascal/lsp/lang/rascal/lsp/IDECheckerWrapper.rsc b/rascal-lsp/src/main/rascal/lsp/lang/rascal/lsp/IDECheckerWrapper.rsc index a0f19e4c6..e1a6212b6 100644 --- a/rascal-lsp/src/main/rascal/lsp/lang/rascal/lsp/IDECheckerWrapper.rsc +++ b/rascal-lsp/src/main/rascal/lsp/lang/rascal/lsp/IDECheckerWrapper.rsc @@ -79,7 +79,7 @@ map[loc, set[Message]] checkFile(loc l, set[loc] workspaceFolders, start[Module] openFileHeader = openFile.top.header.name; checkForImports = [openFile]; checkedForImports = {}; - initialProject = inferProjectRoot(l); + initialProject = inferRoot(l); rel[loc, loc] dependencies = {}; @@ -88,7 +88,7 @@ map[loc, set[Message]] checkFile(loc l, set[loc] workspaceFolders, start[Module] while (tree <- checkForImports) { step2("Calculating imports for ", 1); currentSrc = tree.src.top; - currentProject = inferProjectRoot(currentSrc); + currentProject = inferRoot(currentSrc); if (currentProject in workspaceFolders && currentProject.file notin {"rascal", "rascal-lsp"}) { for (i <- tree.top.header.imports, i has \module) { modName = ""; @@ -102,7 +102,7 @@ map[loc, set[Message]] checkFile(loc l, set[loc] workspaceFolders, start[Module] if (mlpt.src.top notin checkedForImports) { checkForImports += mlpt; jobTodo("Building dependency graph"); - dependencies += ; + dependencies += ; } } } @@ -123,7 +123,7 @@ map[loc, set[Message]] checkFile(loc l, set[loc] workspaceFolders, start[Module] if (cyclicDependencies != {}) { return (l : {error("Cyclic dependencies detected between projects {}. This is not supported. Fix your project setup.", l)}); } - modulesPerProject = classify(checkedForImports, loc(loc l) {return inferProjectRoot(l);}); + modulesPerProject = classify(checkedForImports, loc(loc l) {return inferRoot(l);}); msgs = []; upstreamDependencies = {project | project <- reverse(order(dependencies)), project in modulesPerProject, project != initialProject}; @@ -196,7 +196,7 @@ set[loc] locateRascalModules(str fqn, PathConfig pcfg, PathConfig(loc file) getP // Check the source directories return {fileLoc | dir <- pcfg.srcs, fileLoc := dir + fileName, exists(fileLoc)} // And libraries available in the current workspace - + {fileLoc | lib <- pcfg.libs, inWorkspace(workspaceFolders, lib), dir <- getPathConfig(inferProjectRoot(lib)).srcs, fileLoc := dir + fileName, exists(fileLoc)}; + + {fileLoc | lib <- pcfg.libs, inWorkspace(workspaceFolders, lib), dir <- getPathConfig(inferRoot(lib)).srcs, fileLoc := dir + fileName, exists(fileLoc)}; } loc targetToProject(loc l) { @@ -206,39 +206,9 @@ loc targetToProject(loc l) { return l; } -@memo @synopsis{Infers the root of the project that `member` is in.} -loc inferProjectRoot(loc member) { - parentRoot = member; - root = parentRoot; - - do { - root = parentRoot; - parentRoot = inferDeepestProjectRoot(root.parent); - } while (root.parent? && parentRoot != root.parent); - return root; -} - -@synopsis{Infers the longest project root-like path that `member` is in.} -@pitfalls{Might return a sub-directory of `target/`.} -loc inferDeepestProjectRoot(loc member) { - current = targetToProject(member); - if (!isDirectory(current)) { - current = current.parent; - } - - while (exists(current), isDirectory(current)) { - if (exists(current + "META-INF" + "RASCAL.MF")) { - return current; - } - if (!current.parent?) { - return isDirectory(member) ? member : member.parent; - } - current = current.parent; - } - - return current; -} +@javaClass{org.rascalmpl.vscode.lsp.rascal.model.Projects} +java loc inferRoot(loc member); map[loc, set[Message]] filterAndFix(list[ModuleMessages] messages, set[loc] workspaceFolders) { set[Message] empty = {}; diff --git a/rascal-lsp/src/test/java/org/rascalmpl/vscode/lsp/rascal/model/PathConfigsTest.java b/rascal-lsp/src/test/java/org/rascalmpl/vscode/lsp/rascal/model/PathConfigsTest.java new file mode 100644 index 000000000..7a98af711 --- /dev/null +++ b/rascal-lsp/src/test/java/org/rascalmpl/vscode/lsp/rascal/model/PathConfigsTest.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2018-2025, NWO-I CWI and Swat.engineering + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package org.rascalmpl.vscode.lsp.rascal.model; + +import java.io.IOException; +import java.util.concurrent.Executors; +import org.junit.Assert; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.mockito.Mock; +import org.rascalmpl.uri.URIResolverRegistry; +import org.rascalmpl.uri.URIUtil; + +import io.usethesource.vallang.ISourceLocation; + +public class PathConfigsTest { + + private static final URIResolverRegistry reg = URIResolverRegistry.getInstance(); + private static ISourceLocation absoluteProjectDir; + @Mock PathConfigDiagnostics diagnostics; + private PathConfigs configs; + + @BeforeClass + public static void initTests() throws IOException { + absoluteProjectDir = reg.logicalToPhysical(URIUtil.rootLocation("cwd")); + } + + @Before + public void setUp() { + configs = new PathConfigs(Executors.newCachedThreadPool(), diagnostics); + } + + private static void assertEquals(String message, ISourceLocation expected, ISourceLocation actual) { + Assert.assertEquals(message, URIUtil.getChildLocation(expected, ""), URIUtil.getChildLocation(actual, "")); + } + + @Test + public void pathConfigForLsp() { + var pcfg = configs.lookupConfig(absoluteProjectDir); + assertEquals("Path config root should equal project URI", absoluteProjectDir, pcfg.getProjectRoot()); + } + + @Test + public void pathConfigForLspModule() { + var pcfg = configs.lookupConfig(URIUtil.getChildLocation(absoluteProjectDir, "src/main/rascal/library/util/LanguageServer.rsc")); + assertEquals("Path config root should equal project URI", absoluteProjectDir, pcfg.getProjectRoot()); + } +} diff --git a/rascal-lsp/src/test/java/org/rascalmpl/vscode/lsp/rascal/model/ProjectsTest.java b/rascal-lsp/src/test/java/org/rascalmpl/vscode/lsp/rascal/model/ProjectsTest.java new file mode 100644 index 000000000..9f757a10e --- /dev/null +++ b/rascal-lsp/src/test/java/org/rascalmpl/vscode/lsp/rascal/model/ProjectsTest.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2018-2025, NWO-I CWI and Swat.engineering + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package org.rascalmpl.vscode.lsp.rascal.model; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.net.URISyntaxException; +import org.junit.BeforeClass; +import org.junit.Test; +import org.rascalmpl.uri.URIResolverRegistry; +import org.rascalmpl.uri.URIUtil; +import org.rascalmpl.values.IRascalValueFactory; + +import io.usethesource.vallang.ISourceLocation; + +public class ProjectsTest { + + private static final URIResolverRegistry reg = URIResolverRegistry.getInstance(); + private static final IRascalValueFactory VF = IRascalValueFactory.getInstance(); + private static ISourceLocation absoluteProjectDir; + private final Projects projects = new Projects(); + + /** + * Test {@link Projects::inferRoot} for a specific module within a project. + * @param project The project that contains the module. This is the expected value for the inferred root. + * @param modulePath The relative path of the module within the project. Does not need to actually exist. + * @param projectExists Whether the project actually exists. WARNING: If it does not exist, root inference probably returns the root of the file system of the project. + */ + private void checkRoot(ISourceLocation project, String modulePath, boolean projectExists, boolean moduleExists) { + assertFalse("Cannot check for existing module in non-existent project", !projectExists && moduleExists); + assertTrue("Project should exist", !projectExists || reg.exists(project)); + var m = URIUtil.getChildLocation(project, modulePath); + assertTrue("Module should exist", !moduleExists || reg.exists(m)); + var root = projects.inferRoot(m); + assertEquals("Inferred root should equal project URI", project, root); + } + + private void checkRoot(ISourceLocation project, String modulePath) { + checkRoot(project, modulePath, true, true); + } + + @BeforeClass + public static void initTests() throws IOException { + absoluteProjectDir = reg.logicalToPhysical(URIUtil.rootLocation("cwd")); + } + + @Test + public void lspRoot() { + checkRoot(absoluteProjectDir, "src/main/rascal/library/util/LanguageServer.rsc"); + } + + @Test + public void lspTargetRoot() { + checkRoot(absoluteProjectDir, "target/classes/library/util/LanguageServer.rsc"); + } + + @Test + public void nestedRoot() { + checkRoot(URIUtil.getChildLocation(absoluteProjectDir, "src/test/resources/project-a"), "src/Module.rsc", true, false); + } + + @Test + public void projectRoot() throws URISyntaxException { + checkRoot(VF.sourceLocation("project", "rascal-lsp", ""), "src/main/rascal/library/util/LanguageServer.rsc", false, false); + } + +} diff --git a/rascal-lsp/src/test/resources/project-a/META-INF/RASCAL.MF b/rascal-lsp/src/test/resources/project-a/META-INF/RASCAL.MF new file mode 100644 index 000000000..8182d1e49 --- /dev/null +++ b/rascal-lsp/src/test/resources/project-a/META-INF/RASCAL.MF @@ -0,0 +1,3 @@ +Manifest-Version: 0.0.1 +Project-Name: project-a +Source: src/