Skip to content
Open
Show file tree
Hide file tree
Changes from all 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,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 {
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) {
origin = origin.top();
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.
// 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;
Comment thread
toinehartman marked this conversation as resolved.
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;
}

}
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,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;
Comment thread
DavyLandman marked this conversation as resolved.
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());
}
}
Loading
Loading