Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;

Expand All @@ -70,10 +68,11 @@ public class PathConfigs {
private static final URIResolverRegistry reg = URIResolverRegistry.getInstance();
private final Map<ISourceLocation, Pair<PathConfig, Instant>> currentPathConfigs = new ConcurrentHashMap<>();
private final PathConfigUpdater updater = new PathConfigUpdater(currentPathConfigs);
private final Projects projects = new Projects();
private final LoadingCache<ISourceLocation, ISourceLocation> translatedRoots =
Caffeine.newBuilder()
.expireAfterAccess(Duration.ofMinutes(20))
.build(PathConfigs::inferProjectRoot);
.build(projects::inferRoot);

private final Executor executor;
private final PathConfigDiagnostics diagnostics;
Expand All @@ -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) {
Expand Down Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* 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 Projects {
Comment thread
toinehartman marked this conversation as resolved.

/**
* Infers the shallowest possible root of the project that `origin` is in.
*/
public ISourceLocation inferRoot(ISourceLocation origin) {
var innerRoot = inferDeepestRoot(origin);
var outerRoot = inferDeepestRoot(URIUtil.getParentLocation(innerRoot));

if (!innerRoot.equals(outerRoot) && isSameProject(innerRoot, outerRoot)) {
Comment thread
toinehartman marked this conversation as resolved.
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;
}

// Inner root is for a 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 manifest = new RascalManifest();
var root = origin;
Comment thread
toinehartman marked this conversation as resolved.

while (!(manifest.hasManifest(root) || URIUtil.getParentLocation(root).equals(root))) {
root = URIUtil.getParentLocation(root);
}
Comment thread
toinehartman marked this conversation as resolved.
Outdated
return root;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {};

Expand All @@ -88,7 +88,7 @@ map[loc, set[Message]] checkFile(loc l, set[loc] workspaceFolders, start[Module]
while (tree <- checkForImports) {
step2("Calculating imports for <tree.top.header.name>", 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 = "<i.\module>";
Expand All @@ -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 += <currentProject, inferProjectRoot(mlpt.src.top)>;
dependencies += <currentProject, inferRoot(mlpt.src.top)>;
}
}
}
Expand All @@ -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 {<intercalate(", ", [*cyclicDependencies])>}. 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};
Expand Down Expand Up @@ -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) {
Expand All @@ -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;
}
Comment thread
DavyLandman marked this conversation as resolved.
@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 = {};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
* 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 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);
Comment thread
toinehartman marked this conversation as resolved.
Outdated
var root = projects.inferRoot(m);
assertEquals(project, root);
}

@Mock PathConfigDiagnostics diagnostics;
Comment thread
DavyLandman marked this conversation as resolved.
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());
}
}
3 changes: 3 additions & 0 deletions rascal-lsp/src/test/resources/project-a/META-INF/RASCAL.MF
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Manifest-Version: 0.0.1
Project-Name: project-a
Source: src/
Loading