diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalLanguageServices.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalLanguageServices.java index 89becf0c3..b796576e3 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalLanguageServices.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalLanguageServices.java @@ -31,6 +31,7 @@ import java.io.IOException; import java.io.StringReader; +import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -40,6 +41,7 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; +import java.util.function.Function; import java.util.stream.Collectors; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -130,17 +132,92 @@ public RascalLanguageServices(RascalTextDocumentService docService, BaseWorkspac this.workspaceService = workspaceService; } - public InterruptibleFuture<@Nullable IConstructor> getSummary(ISourceLocation occ, PathConfig pcfg) { + static String pathToModuleName(ISourceLocation l) { + var p = l.getPath(); + if (isInsideJar(l)) { + p = jarFilePath(l); + } + return p.substring(1, p.lastIndexOf('.')).replace("/", "::"); + } + + private static boolean isInsideJar(ISourceLocation l) { + return l.getScheme().startsWith("jar+"); + } + + static @Nullable ISourceLocation libraryTplLocation(ISourceLocation modPath) { try { - IString moduleName = VF.string(pcfg.getModuleName(occ)); - return runEvaluator("Rascal makeSummary", semanticEvaluator, eval -> { - IConstructor result = (IConstructor) eval.call("makeSummary", moduleName, pcfg.asConstructor()); - return result != null && result.asWithKeywordParameters().hasParameters() ? result : null; - }, null, exec, false, client); - } catch (IOException e) { - logger.error("Error looking up module name from source location {}", occ, e); - return InterruptibleFuture.completedFuture(null, exec); + var tplFolder = libraryTplRoot(modPath); + if (tplFolder != null) { + var modPrefix = libraryModulePrefix(modPath); + var tplFileName = "$" + URIUtil.getLocationName(URIUtil.changeExtension(modPath, "tpl")); + return URIUtil.getChildLocation(URIUtil.getChildLocation(tplFolder, modPrefix), tplFileName); + } + } catch (URISyntaxException e) { + logger.error("Error while finding TPL for {}", modPath, e); } + return null; + } + + private static @Nullable ISourceLocation libraryTplRoot(ISourceLocation modPath) throws URISyntaxException { + modPath = Locations.toPhysicalIfPossible(modPath); // resolve logical paths like `std:///` + if (isInsideJar(modPath)) { + return URIUtil.getChildLocation(jarBasePath(modPath), "rascal"); + } else if ("mvn".equals(modPath.getScheme())) { + return URIUtil.changePath(modPath, "rascal"); + } + return null; + } + + private static String libraryModulePrefix(ISourceLocation modPath) { + modPath = URIUtil.getParentLocation(modPath); + if (isInsideJar(modPath)) { + // For a file within a JAR, return the sub-path within the JAR + return jarFilePath(modPath); + } + // Otherwise, return just the sub-path + return modPath.getPath(); + } + + private static ISourceLocation jarBasePath(ISourceLocation l) throws URISyntaxException { + if (!isInsideJar(l)) { + throw new IllegalArgumentException("Location should have scheme jar+...: " + l); + } + + var path = l.getPath(); + return URIUtil.changePath(l, path.substring(0, path.lastIndexOf('!') + 1)); + } + + private static String jarFilePath(ISourceLocation l) { + if (!isInsideJar(l)) { + throw new IllegalArgumentException("Location should have scheme jar+...: " + l); + } + + var path = l.getPath(); + return path.substring(path.lastIndexOf('!') + 1); + } + + public InterruptibleFuture<@Nullable IConstructor> getSummary(ISourceLocation occ, Function computePathConfig) { + Function computeSummary; + var tplLoc = libraryTplLocation(occ); + if (tplLoc != null) { + computeSummary = eval -> (IConstructor) eval.call("makeSummary", VF.string(pathToModuleName(occ)), tplLoc); + } else { + computeSummary = eval -> { + try { + var pcfg = computePathConfig.apply(occ); + var moduleName = VF.string(pcfg.getModuleName(occ)); + return (IConstructor) eval.call("makeSummary", moduleName, pcfg.asConstructor()); + } catch (IOException e) { + logger.error("Error looking up module name for source location {}", occ, e); + return null; + } + }; + } + + return runEvaluator("Rascal makeSummary", semanticEvaluator, eval -> { + var result = computeSummary.apply(eval); + return result != null && result.asWithKeywordParameters().hasParameters() ? result : null; + }, null, exec, false, client); } private static Map translateCheckResults(IMap messages) { diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/model/FileFacts.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/model/FileFacts.java index 07a2bc1a7..88061ba0c 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/model/FileFacts.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/model/FileFacts.java @@ -159,7 +159,7 @@ public ActualFileFact(ISourceLocation file, Executor exec) { // only run get summary after the typechecker for this file is done running, because it needs the TPL // (we cannot now global running type checkers, that is a different subject) return InterruptibleFuture.flatten(typeCheckResults.get() - .>thenApply(o -> rascal.getSummary(file, confs.lookupConfig(file))), exec) + .>thenApply(o -> rascal.getSummary(file, confs::lookupConfig)), exec) .<@Nullable SummaryBridge>thenApply(s -> s == null ? null : new SummaryBridge(file, s, cm)); }); } diff --git a/rascal-lsp/src/test/java/org/rascalmpl/vscode/lsp/rascal/RascalLanguageServicesTest.java b/rascal-lsp/src/test/java/org/rascalmpl/vscode/lsp/rascal/RascalLanguageServicesTest.java new file mode 100644 index 000000000..ff37ca8b2 --- /dev/null +++ b/rascal-lsp/src/test/java/org/rascalmpl/vscode/lsp/rascal/RascalLanguageServicesTest.java @@ -0,0 +1,87 @@ +/* + * 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; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.net.URISyntaxException; +import org.junit.Test; +import org.rascalmpl.uri.URIUtil; +import org.rascalmpl.values.IRascalValueFactory; + +import io.usethesource.vallang.ISourceLocation; + +public class RascalLanguageServicesTest { + + private static final IRascalValueFactory VF = IRascalValueFactory.getInstance(); + + private void moduleNameTest(ISourceLocation sourcePath, String modPath, String modName) throws URISyntaxException { + var actualName = RascalLanguageServices.pathToModuleName(URIUtil.getChildLocation(sourcePath, modPath)); + assertEquals(modName, actualName); + } + + @Test + public void stdModuleName() throws URISyntaxException { + moduleNameTest(URIUtil.rootLocation("std"), "IO.rsc", "IO"); + moduleNameTest(URIUtil.rootLocation("std"), "util/Maybe.rsc", "util::Maybe"); + } + + @Test + public void mvnModuleName() throws URISyntaxException { + moduleNameTest(VF.sourceLocation("mvn", "org.rascalmpl--rascal--0.42.2", ""), "String.rsc", "String"); + moduleNameTest(VF.sourceLocation("mvn", "org.rascalmpl--typepal--0.16.6", ""), "analysis/typepal/Collector.rsc", "analysis::typepal::Collector"); + } + + @Test + public void jarModuleName() throws URISyntaxException { + moduleNameTest(VF.sourceLocation("jar+file", "", "rascal-lsp.jar!/"), "util/LanguageServer.rsc", "util::LanguageServer"); + } + + @Test + public void stdTplLoc() throws URISyntaxException { + var src = VF.sourceLocation("std", "", "util/Maybe.rsc"); + var actualTpl = RascalLanguageServices.libraryTplLocation(src); + assertEquals("jar+file", actualTpl.getScheme()); + assertTrue(actualTpl.getPath().endsWith(".jar!/rascal/util/$Maybe.tpl")); + } + + @Test + public void mvnTplLoc() throws URISyntaxException { + var src = VF.sourceLocation("mvn", "org.rascalmpl--typepal--0.16.6", "analysis/typepal/Collector.rsc"); + var actualTpl = RascalLanguageServices.libraryTplLocation(src); + assertEquals(VF.sourceLocation("mvn", "org.rascalmpl--typepal--0.16.6", "rascal/analysis/typepal/$Collector.tpl"), actualTpl); + } + + @Test + public void jarFileTplLoc() throws URISyntaxException { + var src = VF.sourceLocation("jar+file", "", "some/path/to/rascal-lsp.jar!/util/LanguageServer.rsc"); + var actualTpl = RascalLanguageServices.libraryTplLocation(src); + assertEquals(VF.sourceLocation("jar+file", "", "some/path/to/rascal-lsp.jar!/rascal/util/$LanguageServer.tpl"), actualTpl); + } + +}