From 0aba9b1707129217e77b14fbb95b27717f394a38 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Wed, 1 Apr 2026 15:57:33 +0200 Subject: [PATCH 01/16] Add tests for various project root and path config scenarios. --- .../vscode/lsp/rascal/model/PathConfigs.java | 2 +- .../lsp/rascal/model/PathConfigsTest.java | 103 ++++++++++++++++++ .../resources/project-a/META-INF/RASCAL.MF | 3 + 3 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 rascal-lsp/src/test/java/org/rascalmpl/vscode/lsp/rascal/model/PathConfigsTest.java create mode 100644 rascal-lsp/src/test/resources/project-a/META-INF/RASCAL.MF 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..2eaf59c57 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 @@ -265,7 +265,7 @@ private static boolean hasParentSection(URIResolverRegistry reg, ISourceLocation /** * Infers the root of the project that `member` is in. */ - private static ISourceLocation inferProjectRoot(ISourceLocation member) { + /*package*/ static ISourceLocation inferProjectRoot(ISourceLocation member) { ISourceLocation lastRoot = member; ISourceLocation root; do { 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..27fa59d68 --- /dev/null +++ b/rascal-lsp/src/test/java/org/rascalmpl/vscode/lsp/rascal/model/PathConfigsTest.java @@ -0,0 +1,103 @@ +/* + * 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 java.io.IOException; +import java.net.URISyntaxException; +import java.util.concurrent.Executors; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.rascalmpl.uri.URIResolverRegistry; +import org.rascalmpl.values.IRascalValueFactory; + +import io.usethesource.vallang.ISourceLocation; + +public class PathConfigsTest { + private static final IRascalValueFactory VF = IRascalValueFactory.getInstance(); + private static final URIResolverRegistry reg = URIResolverRegistry.getInstance(); + + + private static void checkRoot(ISourceLocation project, String modulePath) throws URISyntaxException { + var m = VF.sourceLocation(project.getScheme(), project.getAuthority(), project.getPath() + "/" + modulePath); + var root = PathConfigs.inferProjectRoot(m); + assertEquals(project, root); + } + + @Mock PathConfigDiagnostics diagnostics; + private PathConfigs configs; + + @Before + public void setUp() { + configs = new PathConfigs(Executors.newCachedThreadPool(), diagnostics); + } + + @Test + public void standardRoot() throws URISyntaxException { + checkRoot(VF.sourceLocation("std", "", ""), "IO.rsc"); + } + + @Test + public void nestedStandardRoot() throws URISyntaxException { + checkRoot(VF.sourceLocation("std", "", ""), "util/Maybe.rsc"); + } + + @Test + public void lspRoot() throws URISyntaxException { + checkRoot(VF.sourceLocation("cwd", "", ""), "src/main/rascal/library/util/LanguageServer.src"); + } + + @Test + public void lspTargetRoot() throws URISyntaxException { + checkRoot(VF.sourceLocation("cwd", "", ""), "target/classes/library/util/LanguageServer.rsc"); + } + + @Test + public void nestedProjectRoot() throws URISyntaxException { + checkRoot(VF.sourceLocation("cwd", "", "src/test/resources/project-a"), "src/Module.rsc"); + } + + @Test + public void pathConfigForStandardModule() throws URISyntaxException, IOException { + var pcfg = configs.lookupConfig(VF.sourceLocation("std", "", "IO.rsc")); + assertEquals(reg.logicalToPhysical(VF.sourceLocation("std", "", "")), pcfg.getProjectRoot()); + } + + @Test + public void pathConfigForLsp() throws URISyntaxException, IOException { + var pcfg = configs.lookupConfig(VF.sourceLocation("project", "rascal-lsp", "")); + assertEquals(reg.logicalToPhysical(VF.sourceLocation("project", "rascal-lsp", "")), pcfg.getProjectRoot()); + } + + @Test + public void pathConfigForLspModule() throws URISyntaxException, IOException { + var pcfg = configs.lookupConfig(VF.sourceLocation("project", "rascal-lsp", "src/main/rascal/library/util/LanguageServer.src")); + assertEquals(reg.logicalToPhysical(VF.sourceLocation("project", "rascal-lsp", "")), pcfg.getProjectRoot()); + } +} 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/ From 79d2a2a039d5fd995f9936ed28c0179999d22f66 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Wed, 1 Apr 2026 16:30:47 +0200 Subject: [PATCH 02/16] Fix path config computation for various edge cases. --- .../vscode/lsp/rascal/model/PathConfigs.java | 65 ++++++++++--------- 1 file changed, 33 insertions(+), 32 deletions(-) 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 2eaf59c57..70b5eff17 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,20 +43,18 @@ 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; import org.checkerframework.checker.nullness.qual.Nullable; +import org.rascalmpl.interpreter.utils.RascalManifest; import org.rascalmpl.library.Prelude; import org.rascalmpl.library.util.PathConfig; import org.rascalmpl.library.util.PathConfig.RascalConfigMode; import org.rascalmpl.uri.ISourceLocationWatcher.ISourceLocationChanged; 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 org.rascalmpl.values.IRascalValueFactory; import io.usethesource.vallang.IConstructor; import io.usethesource.vallang.ISourceLocation; @@ -66,6 +66,7 @@ public class PathConfigs { private static final Logger logger = LogManager.getLogger(PathConfigs.class); private static final long UPDATE_DELAY = TimeUnit.SECONDS.toNanos(5); + private static final IRascalValueFactory VF = IRascalValueFactory.getInstance(); private static final URIResolverRegistry reg = URIResolverRegistry.getInstance(); private final Map> currentPathConfigs = new ConcurrentHashMap<>(); @@ -263,41 +264,41 @@ private static boolean hasParentSection(URIResolverRegistry reg, ISourceLocation } /** - * Infers the root of the project that `member` is in. + * Infers the root of the project that `origin` is in. + * + * Prefer deepest nested project, but ignore manifest in target folders. */ - /*package*/ 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; + /*package*/ static ISourceLocation inferProjectRoot(ISourceLocation origin) { + System.out.println("Computing root for " + origin); + var innerRoot = inferDeepestProjectRoot(origin); + var outerRoot = inferDeepestProjectRoot(URIUtil.getParentLocation(innerRoot)); + + while (!innerRoot.equals(outerRoot) && isSameProject(innerRoot, outerRoot)) { + innerRoot = outerRoot; + outerRoot = inferDeepestProjectRoot(URIUtil.getParentLocation(innerRoot)); + } + + return isSameProject(innerRoot, outerRoot) + ? outerRoot // Inner root is for the same project, but inside the target folder + : innerRoot // Inner root is for a nested project + ; + } + + private static 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 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); - } + private static ISourceLocation inferDeepestProjectRoot(ISourceLocation origin) { + var manifest = new RascalManifest(); + var root = origin; - current = parent; + while (!(manifest.hasManifest(root) || URIUtil.getParentLocation(root).equals(root))) { + root = URIUtil.getParentLocation(root); } - - return current; + return root; } } From 55594e5d37e26db5f9591c78570055440edc473d Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Wed, 1 Apr 2026 16:35:30 +0200 Subject: [PATCH 03/16] Re-use project root computation in Rascal. --- .../vscode/lsp/rascal/model/PathConfigs.java | 46 +----------- .../vscode/lsp/rascal/model/ProjectRoots.java | 72 +++++++++++++++++++ .../lsp/lang/rascal/lsp/IDECheckerWrapper.rsc | 34 +-------- .../lsp/rascal/model/PathConfigsTest.java | 5 +- 4 files changed, 80 insertions(+), 77 deletions(-) create mode 100644 rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/model/ProjectRoots.java 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 70b5eff17..d019aa999 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 @@ -47,14 +47,12 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.checkerframework.checker.nullness.qual.Nullable; -import org.rascalmpl.interpreter.utils.RascalManifest; import org.rascalmpl.library.Prelude; import org.rascalmpl.library.util.PathConfig; import org.rascalmpl.library.util.PathConfig.RascalConfigMode; import org.rascalmpl.uri.ISourceLocationWatcher.ISourceLocationChanged; import org.rascalmpl.uri.URIResolverRegistry; import org.rascalmpl.uri.URIUtil; -import org.rascalmpl.values.IRascalValueFactory; import io.usethesource.vallang.IConstructor; import io.usethesource.vallang.ISourceLocation; @@ -66,15 +64,15 @@ public class PathConfigs { private static final Logger logger = LogManager.getLogger(PathConfigs.class); private static final long UPDATE_DELAY = TimeUnit.SECONDS.toNanos(5); - private static final IRascalValueFactory VF = IRascalValueFactory.getInstance(); private static final URIResolverRegistry reg = URIResolverRegistry.getInstance(); private final Map> currentPathConfigs = new ConcurrentHashMap<>(); private final PathConfigUpdater updater = new PathConfigUpdater(currentPathConfigs); + private final ProjectRoots projectRoots = new ProjectRoots(); private final LoadingCache translatedRoots = Caffeine.newBuilder() .expireAfterAccess(Duration.ofMinutes(20)) - .build(PathConfigs::inferProjectRoot); + .build(projectRoots::inferProjectRoot); private final Executor executor; private final PathConfigDiagnostics diagnostics; @@ -87,7 +85,7 @@ public PathConfigs(Executor executor, PathConfigDiagnostics diagnostics) { } public void expungePathConfig(ISourceLocation project) { - var projectRoot = inferProjectRoot(project); + var projectRoot = projectRoots.inferProjectRoot(project); try { updater.unregisterProject(project); } catch (IOException e) { @@ -263,42 +261,4 @@ private static boolean hasParentSection(URIResolverRegistry reg, ISourceLocation } } - /** - * Infers the root of the project that `origin` is in. - * - * Prefer deepest nested project, but ignore manifest in target folders. - */ - /*package*/ static ISourceLocation inferProjectRoot(ISourceLocation origin) { - System.out.println("Computing root for " + origin); - var innerRoot = inferDeepestProjectRoot(origin); - var outerRoot = inferDeepestProjectRoot(URIUtil.getParentLocation(innerRoot)); - - while (!innerRoot.equals(outerRoot) && isSameProject(innerRoot, outerRoot)) { - innerRoot = outerRoot; - outerRoot = inferDeepestProjectRoot(URIUtil.getParentLocation(innerRoot)); - } - - return isSameProject(innerRoot, outerRoot) - ? outerRoot // Inner root is for the same project, but inside the target folder - : innerRoot // Inner root is for a nested project - ; - } - - private static 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 static ISourceLocation inferDeepestProjectRoot(ISourceLocation origin) { - var manifest = new RascalManifest(); - var root = origin; - - while (!(manifest.hasManifest(root) || URIUtil.getParentLocation(root).equals(root))) { - root = URIUtil.getParentLocation(root); - } - return root; - } } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/model/ProjectRoots.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/model/ProjectRoots.java new file mode 100644 index 000000000..26322bbbb --- /dev/null +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/model/ProjectRoots.java @@ -0,0 +1,72 @@ +/* + * 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; + +public class ProjectRoots { + + /** + * Infers the shallowest possible root of the project that `origin` is in. + */ + public ISourceLocation inferProjectRoot(ISourceLocation origin) { + var innerRoot = inferDeepestProjectRoot(origin); + var outerRoot = inferDeepestProjectRoot(URIUtil.getParentLocation(innerRoot)); + + while (!innerRoot.equals(outerRoot) && isSameProject(innerRoot, outerRoot)) { + innerRoot = outerRoot; + outerRoot = inferDeepestProjectRoot(URIUtil.getParentLocation(innerRoot)); + } + + return isSameProject(innerRoot, outerRoot) + ? outerRoot // Inner root is for the same project, but might be inside the target folder + : innerRoot // Inner root is for a nested project + ; + } + + 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 inferDeepestProjectRoot(ISourceLocation origin) { + var manifest = new RascalManifest(); + var root = origin; + + while (!(manifest.hasManifest(root) || URIUtil.getParentLocation(root).equals(root))) { + 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..9ab70869b 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 @@ -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.ProjectRoots} +java loc inferProjectRoot(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 index 27fa59d68..23fa5762a 100644 --- 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 @@ -43,10 +43,11 @@ public class PathConfigsTest { private static final IRascalValueFactory VF = IRascalValueFactory.getInstance(); private static final URIResolverRegistry reg = URIResolverRegistry.getInstance(); + private final ProjectRoots roots = new ProjectRoots(); - private static void checkRoot(ISourceLocation project, String modulePath) throws URISyntaxException { + private void checkRoot(ISourceLocation project, String modulePath) throws URISyntaxException { var m = VF.sourceLocation(project.getScheme(), project.getAuthority(), project.getPath() + "/" + modulePath); - var root = PathConfigs.inferProjectRoot(m); + var root = roots.inferProjectRoot(m); assertEquals(project, root); } From f3c985c3b0e79bdefa294018f1d167a56bd6639f Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Wed, 1 Apr 2026 16:40:16 +0200 Subject: [PATCH 04/16] Rename roots class and functions for less verbosity. --- .../vscode/lsp/rascal/model/PathConfigs.java | 6 +++--- .../model/{ProjectRoots.java => Projects.java} | 12 ++++++------ .../lsp/lang/rascal/lsp/IDECheckerWrapper.rsc | 14 +++++++------- .../vscode/lsp/rascal/model/PathConfigsTest.java | 4 ++-- 4 files changed, 18 insertions(+), 18 deletions(-) rename rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/model/{ProjectRoots.java => Projects.java} (86%) 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 d019aa999..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 @@ -68,11 +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 ProjectRoots projectRoots = new ProjectRoots(); + private final Projects projects = new Projects(); private final LoadingCache translatedRoots = Caffeine.newBuilder() .expireAfterAccess(Duration.ofMinutes(20)) - .build(projectRoots::inferProjectRoot); + .build(projects::inferRoot); private final Executor executor; private final PathConfigDiagnostics diagnostics; @@ -85,7 +85,7 @@ public PathConfigs(Executor executor, PathConfigDiagnostics diagnostics) { } public void expungePathConfig(ISourceLocation project) { - var projectRoot = projectRoots.inferProjectRoot(project); + var projectRoot = projects.inferRoot(project); try { updater.unregisterProject(project); } catch (IOException e) { diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/model/ProjectRoots.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/model/Projects.java similarity index 86% rename from rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/model/ProjectRoots.java rename to rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/model/Projects.java index 26322bbbb..19df0a4eb 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/model/ProjectRoots.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/model/Projects.java @@ -31,18 +31,18 @@ import io.usethesource.vallang.ISourceLocation; -public class ProjectRoots { +public class Projects { /** * Infers the shallowest possible root of the project that `origin` is in. */ - public ISourceLocation inferProjectRoot(ISourceLocation origin) { - var innerRoot = inferDeepestProjectRoot(origin); - var outerRoot = inferDeepestProjectRoot(URIUtil.getParentLocation(innerRoot)); + public ISourceLocation inferRoot(ISourceLocation origin) { + var innerRoot = inferDeepestRoot(origin); + var outerRoot = inferDeepestRoot(URIUtil.getParentLocation(innerRoot)); while (!innerRoot.equals(outerRoot) && isSameProject(innerRoot, outerRoot)) { innerRoot = outerRoot; - outerRoot = inferDeepestProjectRoot(URIUtil.getParentLocation(innerRoot)); + outerRoot = inferDeepestRoot(URIUtil.getParentLocation(innerRoot)); } return isSameProject(innerRoot, outerRoot) @@ -59,7 +59,7 @@ private boolean isSameProject(ISourceLocation root1, ISourceLocation root2) { /** * Infers the longest project root-like path that `member` is in. Might return a sub-directory of `target/`. */ - private ISourceLocation inferDeepestProjectRoot(ISourceLocation origin) { + private ISourceLocation inferDeepestRoot(ISourceLocation origin) { var manifest = new RascalManifest(); var root = origin; 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 9ab70869b..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) { @@ -207,8 +207,8 @@ loc targetToProject(loc l) { } @synopsis{Infers the root of the project that `member` is in.} -@javaClass{org.rascalmpl.vscode.lsp.rascal.model.ProjectRoots} -java loc inferProjectRoot(loc member); +@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 index 23fa5762a..cacc77aec 100644 --- 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 @@ -43,11 +43,11 @@ public class PathConfigsTest { private static final IRascalValueFactory VF = IRascalValueFactory.getInstance(); private static final URIResolverRegistry reg = URIResolverRegistry.getInstance(); - private final ProjectRoots roots = new ProjectRoots(); + private final Projects projects = new Projects(); private void checkRoot(ISourceLocation project, String modulePath) throws URISyntaxException { var m = VF.sourceLocation(project.getScheme(), project.getAuthority(), project.getPath() + "/" + modulePath); - var root = roots.inferProjectRoot(m); + var root = projects.inferRoot(m); assertEquals(project, root); } From 4195c1853f8a73592153ecc0f3480c121ba66830 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Wed, 1 Apr 2026 17:24:21 +0200 Subject: [PATCH 05/16] Simplify while -> if --- .../java/org/rascalmpl/vscode/lsp/rascal/model/Projects.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 19df0a4eb..cd24448c5 100644 --- 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 @@ -40,7 +40,7 @@ public ISourceLocation inferRoot(ISourceLocation origin) { var innerRoot = inferDeepestRoot(origin); var outerRoot = inferDeepestRoot(URIUtil.getParentLocation(innerRoot)); - while (!innerRoot.equals(outerRoot) && isSameProject(innerRoot, outerRoot)) { + if (!innerRoot.equals(outerRoot) && isSameProject(innerRoot, outerRoot)) { innerRoot = outerRoot; outerRoot = inferDeepestRoot(URIUtil.getParentLocation(innerRoot)); } From 4505478ac1f47fe9497a8f08dd6f8d9263af7be5 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Wed, 1 Apr 2026 17:25:34 +0200 Subject: [PATCH 06/16] Rewrite ternary -> if. --- .../rascalmpl/vscode/lsp/rascal/model/Projects.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) 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 index cd24448c5..f5f622725 100644 --- 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 @@ -45,10 +45,13 @@ public ISourceLocation inferRoot(ISourceLocation origin) { outerRoot = inferDeepestRoot(URIUtil.getParentLocation(innerRoot)); } - return isSameProject(innerRoot, outerRoot) - ? outerRoot // Inner root is for the same project, but might be inside the target folder - : innerRoot // Inner root is for a nested project - ; + if (isSameProject(innerRoot, outerRoot)) { + // Inner root is for the same project, but might be inside the target folder + return outerRoot; + } + + // Inner root is for a nested project + return innerRoot; } private boolean isSameProject(ISourceLocation root1, ISourceLocation root2) { From cbb77d7f064b8d8371ad0b5af7e1b2c9add8c255 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Thu, 2 Apr 2026 17:29:59 +0200 Subject: [PATCH 07/16] Docs, performance and rewrites (h/t @DavyLandman). --- .../vscode/lsp/rascal/model/Projects.java | 22 +++++++++++-------- .../lsp/rascal/model/PathConfigsTest.java | 3 ++- 2 files changed, 15 insertions(+), 10 deletions(-) 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 index f5f622725..d422a3231 100644 --- 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 @@ -31,26 +31,26 @@ 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)) { - innerRoot = outerRoot; - outerRoot = inferDeepestRoot(URIUtil.getParentLocation(innerRoot)); - } - - if (isSameProject(innerRoot, outerRoot)) { - // Inner root is for the same project, but might be inside the target folder - return outerRoot; + // The roots are not equal, but refer to the same project: the inner root is somewhere inside the target folder. + // In that case, find a root one level up. + return inferDeepestRoot(URIUtil.getParentLocation(outerRoot)); } - // Inner root is for a nested project + // Inner root is a nested project within outer root return innerRoot; } @@ -66,7 +66,11 @@ private ISourceLocation inferDeepestRoot(ISourceLocation origin) { var manifest = new RascalManifest(); var root = origin; - while (!(manifest.hasManifest(root) || URIUtil.getParentLocation(root).equals(root))) { + while (!manifest.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/test/java/org/rascalmpl/vscode/lsp/rascal/model/PathConfigsTest.java b/rascal-lsp/src/test/java/org/rascalmpl/vscode/lsp/rascal/model/PathConfigsTest.java index cacc77aec..025370d97 100644 --- 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 @@ -35,6 +35,7 @@ import org.junit.Test; import org.mockito.Mock; import org.rascalmpl.uri.URIResolverRegistry; +import org.rascalmpl.uri.URIUtil; import org.rascalmpl.values.IRascalValueFactory; import io.usethesource.vallang.ISourceLocation; @@ -46,7 +47,7 @@ public class PathConfigsTest { private final Projects projects = new Projects(); private void checkRoot(ISourceLocation project, String modulePath) throws URISyntaxException { - var m = VF.sourceLocation(project.getScheme(), project.getAuthority(), project.getPath() + "/" + modulePath); + var m = URIUtil.getChildLocation(project, modulePath); var root = projects.inferRoot(m); assertEquals(project, root); } From 9d2f2506f2701349f9e90a9eabfb77e09d74efb6 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Fri, 3 Apr 2026 10:36:18 +0200 Subject: [PATCH 08/16] Use absolute project dir in tests. --- .../lsp/rascal/model/PathConfigsTest.java | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) 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 index 025370d97..dd01579f8 100644 --- 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 @@ -32,6 +32,7 @@ import java.net.URISyntaxException; import java.util.concurrent.Executors; import org.junit.Before; +import org.junit.BeforeClass; import org.junit.Test; import org.mockito.Mock; import org.rascalmpl.uri.URIResolverRegistry; @@ -54,6 +55,12 @@ private void checkRoot(ISourceLocation project, String modulePath) throws URISyn @Mock PathConfigDiagnostics diagnostics; private PathConfigs configs; + private static ISourceLocation absoluteProjectDir; + + @BeforeClass + public static void initTests() throws URISyntaxException, IOException { + absoluteProjectDir = reg.logicalToPhysical(URIUtil.rootLocation("cwd")); + } @Before public void setUp() { @@ -72,17 +79,17 @@ public void nestedStandardRoot() throws URISyntaxException { @Test public void lspRoot() throws URISyntaxException { - checkRoot(VF.sourceLocation("cwd", "", ""), "src/main/rascal/library/util/LanguageServer.src"); + checkRoot(absoluteProjectDir, "src/main/rascal/library/util/LanguageServer.src"); } @Test public void lspTargetRoot() throws URISyntaxException { - checkRoot(VF.sourceLocation("cwd", "", ""), "target/classes/library/util/LanguageServer.rsc"); + checkRoot(absoluteProjectDir, "target/classes/library/util/LanguageServer.rsc"); } @Test public void nestedProjectRoot() throws URISyntaxException { - checkRoot(VF.sourceLocation("cwd", "", "src/test/resources/project-a"), "src/Module.rsc"); + checkRoot(URIUtil.getChildLocation(absoluteProjectDir, "src/test/resources/project-a"), "src/Module.rsc"); } @Test @@ -93,13 +100,13 @@ public void pathConfigForStandardModule() throws URISyntaxException, IOException @Test public void pathConfigForLsp() throws URISyntaxException, IOException { - var pcfg = configs.lookupConfig(VF.sourceLocation("project", "rascal-lsp", "")); - assertEquals(reg.logicalToPhysical(VF.sourceLocation("project", "rascal-lsp", "")), pcfg.getProjectRoot()); + var pcfg = configs.lookupConfig(absoluteProjectDir); + assertEquals(absoluteProjectDir, pcfg.getProjectRoot()); } @Test public void pathConfigForLspModule() throws URISyntaxException, IOException { - var pcfg = configs.lookupConfig(VF.sourceLocation("project", "rascal-lsp", "src/main/rascal/library/util/LanguageServer.src")); - assertEquals(reg.logicalToPhysical(VF.sourceLocation("project", "rascal-lsp", "")), pcfg.getProjectRoot()); + var pcfg = configs.lookupConfig(URIUtil.getChildLocation(absoluteProjectDir, "src/main/rascal/library/util/LanguageServer.src")); + assertEquals(absoluteProjectDir, pcfg.getProjectRoot()); } } From b94cb1cf0cdfd28837cc6bfe4dc5388a59b6398d Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Fri, 3 Apr 2026 10:45:56 +0200 Subject: [PATCH 09/16] Always compare paths with separator at the end. --- .../rascalmpl/vscode/lsp/rascal/model/PathConfigsTest.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) 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 index dd01579f8..f197e1192 100644 --- 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 @@ -26,11 +26,10 @@ */ package org.rascalmpl.vscode.lsp.rascal.model; -import static org.junit.Assert.assertEquals; - import java.io.IOException; import java.net.URISyntaxException; import java.util.concurrent.Executors; +import org.junit.Assert; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; @@ -67,6 +66,10 @@ public void setUp() { configs = new PathConfigs(Executors.newCachedThreadPool(), diagnostics); } + private static void assertEquals(ISourceLocation expected, ISourceLocation actual) { + Assert.assertEquals(URIUtil.getChildLocation(expected, ""), URIUtil.getChildLocation(actual, "")); + } + @Test public void standardRoot() throws URISyntaxException { checkRoot(VF.sourceLocation("std", "", ""), "IO.rsc"); From 840ad2ccfa24db8ee5e8cf32bc3b83c306cc7501 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Fri, 3 Apr 2026 10:56:57 +0200 Subject: [PATCH 10/16] Fix nested project root logic. --- .../org/rascalmpl/vscode/lsp/rascal/model/Projects.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) 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 index d422a3231..c3a7b9574 100644 --- 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 @@ -46,11 +46,12 @@ public ISourceLocation inferRoot(ISourceLocation origin) { 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, find a root one level up. - return inferDeepestRoot(URIUtil.getParentLocation(outerRoot)); + // In that case, we need the outer root + return outerRoot; } - // Inner root is a nested project within outer root + // (innerRoot.equals(outerRoot) || !isSameProject(innerRoot, outerRoot)) + // Inner is a nested project within outer; we want the root of the nested project. return innerRoot; } From 49b9df760e22eb2c98ed7b191080fb77aaa06167 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Fri, 3 Apr 2026 11:26:59 +0200 Subject: [PATCH 11/16] Improve and clean up tests. --- .../lsp/rascal/model/PathConfigsTest.java | 49 +++++++++++++------ 1 file changed, 34 insertions(+), 15 deletions(-) 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 index f197e1192..1b90fbb8c 100644 --- 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 @@ -26,6 +26,8 @@ */ package org.rascalmpl.vscode.lsp.rascal.model; +import static org.junit.Assert.assertTrue; + import java.io.IOException; import java.net.URISyntaxException; import java.util.concurrent.Executors; @@ -46,18 +48,30 @@ public class PathConfigsTest { private final Projects projects = new Projects(); - private void checkRoot(ISourceLocation project, String modulePath) throws URISyntaxException { + /** + * 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) { + assertTrue(!projectExists || reg.exists(project)); var m = URIUtil.getChildLocation(project, modulePath); + assertTrue(!(projectExists && moduleExists) || reg.exists(m)); var root = projects.inferRoot(m); assertEquals(project, root); } + private void checkRoot(ISourceLocation project, String modulePath) { + checkRoot(project, modulePath, true, true); + } + @Mock PathConfigDiagnostics diagnostics; private PathConfigs configs; private static ISourceLocation absoluteProjectDir; @BeforeClass - public static void initTests() throws URISyntaxException, IOException { + public static void initTests() throws IOException { absoluteProjectDir = reg.logicalToPhysical(URIUtil.rootLocation("cwd")); } @@ -71,45 +85,50 @@ private static void assertEquals(ISourceLocation expected, ISourceLocation actua } @Test - public void standardRoot() throws URISyntaxException { - checkRoot(VF.sourceLocation("std", "", ""), "IO.rsc"); + public void standardRoot() { + checkRoot(URIUtil.rootLocation("std"), "IO.rsc"); } @Test - public void nestedStandardRoot() throws URISyntaxException { - checkRoot(VF.sourceLocation("std", "", ""), "util/Maybe.rsc"); + public void nestedStandardRoot() { + checkRoot(URIUtil.rootLocation("std"), "util/Maybe.rsc"); } @Test - public void lspRoot() throws URISyntaxException { - checkRoot(absoluteProjectDir, "src/main/rascal/library/util/LanguageServer.src"); + public void lspRoot() { + checkRoot(absoluteProjectDir, "src/main/rascal/library/util/LanguageServer.rsc"); } @Test - public void lspTargetRoot() throws URISyntaxException { + public void lspTargetRoot() { checkRoot(absoluteProjectDir, "target/classes/library/util/LanguageServer.rsc"); } @Test - public void nestedProjectRoot() throws URISyntaxException { - checkRoot(URIUtil.getChildLocation(absoluteProjectDir, "src/test/resources/project-a"), "src/Module.rsc"); + 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); } @Test - public void pathConfigForStandardModule() throws URISyntaxException, IOException { + public void pathConfigForStandardModule() throws IOException, URISyntaxException { var pcfg = configs.lookupConfig(VF.sourceLocation("std", "", "IO.rsc")); assertEquals(reg.logicalToPhysical(VF.sourceLocation("std", "", "")), pcfg.getProjectRoot()); } @Test - public void pathConfigForLsp() throws URISyntaxException, IOException { + public void pathConfigForLsp() { var pcfg = configs.lookupConfig(absoluteProjectDir); assertEquals(absoluteProjectDir, pcfg.getProjectRoot()); } @Test - public void pathConfigForLspModule() throws URISyntaxException, IOException { - var pcfg = configs.lookupConfig(URIUtil.getChildLocation(absoluteProjectDir, "src/main/rascal/library/util/LanguageServer.src")); + public void pathConfigForLspModule() { + var pcfg = configs.lookupConfig(URIUtil.getChildLocation(absoluteProjectDir, "src/main/rascal/library/util/LanguageServer.rsc")); assertEquals(absoluteProjectDir, pcfg.getProjectRoot()); } } From abc744acd8c8d19a74206a54da82f606587a99ed Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Fri, 3 Apr 2026 13:05:12 +0200 Subject: [PATCH 12/16] Consider Java project roots as well. --- .../vscode/lsp/rascal/model/Projects.java | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) 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 index c3a7b9574..571b86f96 100644 --- 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 @@ -27,6 +27,7 @@ package org.rascalmpl.vscode.lsp.rascal.model; import org.rascalmpl.interpreter.utils.RascalManifest; +import org.rascalmpl.uri.URIResolverRegistry; import org.rascalmpl.uri.URIUtil; import io.usethesource.vallang.ISourceLocation; @@ -36,6 +37,8 @@ */ public class Projects { + private static final URIResolverRegistry reg = URIResolverRegistry.getInstance(); + /** * Infers the shallowest possible root of the project that `origin` is in. */ @@ -64,10 +67,8 @@ private boolean isSameProject(ISourceLocation root1, ISourceLocation 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 manifest = new RascalManifest(); var root = origin; - - while (!manifest.hasManifest(root)) { + while (!isProjectRoot(root)) { if (root.getPath().equals(URIUtil.URI_PATH_SEPARATOR)) { // File system root; cannot recurse further break; @@ -77,4 +78,18 @@ private ISourceLocation inferDeepestRoot(ISourceLocation origin) { return root; } + /** + * Determines whether a location is a project root. + * + * Note: this considers any Maven project as a possible project root. This might not give the desired result for a + * non-Rascal Maven project nested within a Rascal project. We could parse and resolve the POM here to check for + * a Rascal dependency, but that has the following drawbacks: + * 1. It is expensive. + * 2. It will not work for Rascal projects that depend on pure Java Maven projects -- like bird-nescio-tests. + */ + private boolean isProjectRoot(ISourceLocation l) { + return new RascalManifest().hasManifest(l) + || reg.exists(URIUtil.getChildLocation(l, "pom.xml")); + } + } From 436ca05237c569e420934600d95d75f68a52cf72 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Fri, 3 Apr 2026 13:05:51 +0200 Subject: [PATCH 13/16] Check project and module existence in tests by default. --- .../lsp/rascal/model/PathConfigsTest.java | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) 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 index 1b90fbb8c..c71f350cb 100644 --- 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 @@ -26,6 +26,7 @@ */ package org.rascalmpl.vscode.lsp.rascal.model; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import java.io.IOException; @@ -55,11 +56,12 @@ public class PathConfigsTest { * @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) { - assertTrue(!projectExists || reg.exists(project)); + 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(!(projectExists && moduleExists) || reg.exists(m)); + assertTrue("Module should exist", !moduleExists || reg.exists(m)); var root = projects.inferRoot(m); - assertEquals(project, root); + assertEquals("Inferred root should equal project URI", project, root); } private void checkRoot(ISourceLocation project, String modulePath) { @@ -80,8 +82,8 @@ public void setUp() { configs = new PathConfigs(Executors.newCachedThreadPool(), diagnostics); } - private static void assertEquals(ISourceLocation expected, ISourceLocation actual) { - Assert.assertEquals(URIUtil.getChildLocation(expected, ""), URIUtil.getChildLocation(actual, "")); + private static void assertEquals(String message, ISourceLocation expected, ISourceLocation actual) { + Assert.assertEquals(message, URIUtil.getChildLocation(expected, ""), URIUtil.getChildLocation(actual, "")); } @Test @@ -117,18 +119,18 @@ public void projectRoot() throws URISyntaxException { @Test public void pathConfigForStandardModule() throws IOException, URISyntaxException { var pcfg = configs.lookupConfig(VF.sourceLocation("std", "", "IO.rsc")); - assertEquals(reg.logicalToPhysical(VF.sourceLocation("std", "", "")), pcfg.getProjectRoot()); + assertEquals("Path config root should equal project URI", reg.logicalToPhysical(VF.sourceLocation("std", "", "")), pcfg.getProjectRoot()); } @Test public void pathConfigForLsp() { var pcfg = configs.lookupConfig(absoluteProjectDir); - assertEquals(absoluteProjectDir, pcfg.getProjectRoot()); + 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(absoluteProjectDir, pcfg.getProjectRoot()); + assertEquals("Path config root should equal project URI", absoluteProjectDir, pcfg.getProjectRoot()); } } From 254a7b3b8157e070fbadf1b928de16345078c7e9 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Mon, 6 Apr 2026 10:02:03 +0200 Subject: [PATCH 14/16] Only consider Java project roots. --- .../vscode/lsp/rascal/model/Projects.java | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) 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 index 571b86f96..074ceb409 100644 --- 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 @@ -68,7 +68,7 @@ private boolean isSameProject(ISourceLocation root1, ISourceLocation root2) { */ private ISourceLocation inferDeepestRoot(ISourceLocation origin) { var root = origin; - while (!isProjectRoot(root)) { + while (!new RascalManifest().hasManifest(root)) { if (root.getPath().equals(URIUtil.URI_PATH_SEPARATOR)) { // File system root; cannot recurse further break; @@ -78,18 +78,4 @@ private ISourceLocation inferDeepestRoot(ISourceLocation origin) { return root; } - /** - * Determines whether a location is a project root. - * - * Note: this considers any Maven project as a possible project root. This might not give the desired result for a - * non-Rascal Maven project nested within a Rascal project. We could parse and resolve the POM here to check for - * a Rascal dependency, but that has the following drawbacks: - * 1. It is expensive. - * 2. It will not work for Rascal projects that depend on pure Java Maven projects -- like bird-nescio-tests. - */ - private boolean isProjectRoot(ISourceLocation l) { - return new RascalManifest().hasManifest(l) - || reg.exists(URIUtil.getChildLocation(l, "pom.xml")); - } - } From 520ddd07800668adffe568ccf1e7576f477fe415 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Tue, 14 Apr 2026 15:50:33 +0200 Subject: [PATCH 15/16] Do no compute roots for non-source projects. --- .../vscode/lsp/rascal/model/Projects.java | 3 --- .../vscode/lsp/rascal/model/PathConfigsTest.java | 16 ---------------- 2 files changed, 19 deletions(-) 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 index 074ceb409..32cd127c4 100644 --- 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 @@ -27,7 +27,6 @@ package org.rascalmpl.vscode.lsp.rascal.model; import org.rascalmpl.interpreter.utils.RascalManifest; -import org.rascalmpl.uri.URIResolverRegistry; import org.rascalmpl.uri.URIUtil; import io.usethesource.vallang.ISourceLocation; @@ -37,8 +36,6 @@ */ public class Projects { - private static final URIResolverRegistry reg = URIResolverRegistry.getInstance(); - /** * Infers the shallowest possible root of the project that `origin` is in. */ 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 index c71f350cb..040a03e04 100644 --- 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 @@ -86,16 +86,6 @@ private static void assertEquals(String message, ISourceLocation expected, ISour Assert.assertEquals(message, URIUtil.getChildLocation(expected, ""), URIUtil.getChildLocation(actual, "")); } - @Test - public void standardRoot() { - checkRoot(URIUtil.rootLocation("std"), "IO.rsc"); - } - - @Test - public void nestedStandardRoot() { - checkRoot(URIUtil.rootLocation("std"), "util/Maybe.rsc"); - } - @Test public void lspRoot() { checkRoot(absoluteProjectDir, "src/main/rascal/library/util/LanguageServer.rsc"); @@ -116,12 +106,6 @@ public void projectRoot() throws URISyntaxException { checkRoot(VF.sourceLocation("project", "rascal-lsp", ""), "src/main/rascal/library/util/LanguageServer.rsc", false, false); } - @Test - public void pathConfigForStandardModule() throws IOException, URISyntaxException { - var pcfg = configs.lookupConfig(VF.sourceLocation("std", "", "IO.rsc")); - assertEquals("Path config root should equal project URI", reg.logicalToPhysical(VF.sourceLocation("std", "", "")), pcfg.getProjectRoot()); - } - @Test public void pathConfigForLsp() { var pcfg = configs.lookupConfig(absoluteProjectDir); From 4a357dd97c2e4b30d21b88ec1257ad2778bab979 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Tue, 14 Apr 2026 16:07:38 +0200 Subject: [PATCH 16/16] Split tests over classes. --- .../lsp/rascal/model/PathConfigsTest.java | 51 +--------- .../vscode/lsp/rascal/model/ProjectsTest.java | 94 +++++++++++++++++++ 2 files changed, 96 insertions(+), 49 deletions(-) create mode 100644 rascal-lsp/src/test/java/org/rascalmpl/vscode/lsp/rascal/model/ProjectsTest.java 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 index 040a03e04..7a98af711 100644 --- 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 @@ -26,11 +26,7 @@ */ package org.rascalmpl.vscode.lsp.rascal.model; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - import java.io.IOException; -import java.net.URISyntaxException; import java.util.concurrent.Executors; import org.junit.Assert; import org.junit.Before; @@ -39,38 +35,15 @@ import org.mockito.Mock; import org.rascalmpl.uri.URIResolverRegistry; import org.rascalmpl.uri.URIUtil; -import org.rascalmpl.values.IRascalValueFactory; import io.usethesource.vallang.ISourceLocation; public class PathConfigsTest { - private static final IRascalValueFactory VF = IRascalValueFactory.getInstance(); - private static final URIResolverRegistry reg = URIResolverRegistry.getInstance(); - - 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); - } + private static final URIResolverRegistry reg = URIResolverRegistry.getInstance(); + private static ISourceLocation absoluteProjectDir; @Mock PathConfigDiagnostics diagnostics; private PathConfigs configs; - private static ISourceLocation absoluteProjectDir; @BeforeClass public static void initTests() throws IOException { @@ -86,26 +59,6 @@ private static void assertEquals(String message, ISourceLocation expected, ISour Assert.assertEquals(message, URIUtil.getChildLocation(expected, ""), URIUtil.getChildLocation(actual, "")); } - @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); - } - @Test public void pathConfigForLsp() { var pcfg = configs.lookupConfig(absoluteProjectDir); 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); + } + +}