diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 1a5032dd7..4cb06cd94 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -125,32 +125,32 @@ jobs: if: contains(matrix.os, 'windows') working-directory: ./rascal-vscode-extension env: - DELAY_FACTOR: 10 + DELAY_FACTOR: 1 RASCAL_LSP_DEV_DEPLOY: true _JAVA_OPTIONS: '-Xmx5G' - run: npx extest setup-and-run out/test/vscode-suite/*.test.js --storage uitests + run: npx extest setup-and-run out/test/vscode-suite/ide.test.js --storage uitests - name: "UI test (mac)" shell: bash if: contains(matrix.os, 'macos') working-directory: ./rascal-vscode-extension env: - DELAY_FACTOR: 15 + DELAY_FACTOR: 1 RASCAL_LSP_DEV_DEPLOY: true _JAVA_OPTIONS: '-Xmx5G' - run: npx extest setup-and-run out/test/vscode-suite/*.test.js --storage uitests + run: npx extest setup-and-run out/test/vscode-suite/ide.test.js --storage uitests - name: "UI test (ubuntu)" shell: bash if: contains(matrix.os, 'ubuntu') || startsWith(matrix.os, 'ubicloud-standard') working-directory: ./rascal-vscode-extension env: - DELAY_FACTOR: 8 + DELAY_FACTOR: 1 RASCAL_LSP_DEV_DEPLOY: true _JAVA_OPTIONS: '-Xmx5G' # we have 16gb of memory, make sure LSP, REPL & DSL-LSP can start run: | sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 # workaround for issue with Ubuntu 24.04 https://github.com/redhat-developer/vscode-extension-tester/blob/main/KNOWN_ISSUES.md#openresources-not-working-with-apparmor-2404 - xvfb-run --auto-servernum --server-args='-screen 0 1920x1080x24' npx extest setup-and-run out/test/vscode-suite/*.test.js --storage uitests + xvfb-run --auto-servernum --server-args='-screen 0 1920x1080x24' npx extest setup-and-run out/test/vscode-suite/ide.test.js --storage uitests - name: Upload Screenshots uses: actions/upload-artifact@v7 diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java index d8c71386a..41540a636 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/BaseLanguageServer.java @@ -72,6 +72,7 @@ import org.rascalmpl.util.NamedThreadPool; import org.rascalmpl.vscode.lsp.log.LogRedirectConfiguration; import org.rascalmpl.vscode.lsp.parametric.LanguageRegistry.LanguageParameter; +import org.rascalmpl.vscode.lsp.rascal.jsonrpc.CheckProjectRequest; import org.rascalmpl.vscode.lsp.terminal.RemoteIDEServicesThread; import org.rascalmpl.vscode.lsp.uri.jsonrpc.messages.PathConfigParameter; import org.rascalmpl.vscode.lsp.util.Sets; @@ -287,6 +288,11 @@ public CompletableFuture sendUnregisterLanguage(LanguageParameter lang) { return CompletableFutureUtils.completedFuture(null, executor); } + @Override + public CompletableFuture checkProject(CheckProjectRequest req) { + return CompletableFuture.runAsync(() -> lspDocumentService.checkProject(req), executor); + } + @Override public CompletableFuture initialize(InitializeParams params) { logger.info("LSP connection started (connected to {} version {})", params.getClientInfo().getName(), params.getClientInfo().getVersion()); diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/IBaseLanguageServerExtensions.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/IBaseLanguageServerExtensions.java index fd75224e7..85108e457 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/IBaseLanguageServerExtensions.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/IBaseLanguageServerExtensions.java @@ -37,6 +37,7 @@ import org.rascalmpl.uri.remote.jsonrpc.ISourceLocationRequest; import org.rascalmpl.uri.remote.jsonrpc.SourceLocationResponse; import org.rascalmpl.vscode.lsp.parametric.LanguageRegistry.LanguageParameter; +import org.rascalmpl.vscode.lsp.rascal.jsonrpc.CheckProjectRequest; import org.rascalmpl.vscode.lsp.uri.jsonrpc.messages.PathConfigParameter; @JsonSegment("rascal") @@ -64,10 +65,12 @@ default CompletableFuture[]> supplyPathConfig(PathConfigParam @JsonNotification void setMinimumLogLevel(String level); - @JsonRequest("vfs/schemes") CompletableFuture fileSystemSchemes(); @JsonRequest("vfs/logical/resolve") CompletableFuture resolve(ISourceLocationRequest req); + + @JsonRequest + CompletableFuture checkProject(CheckProjectRequest req); } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/IBaseTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/IBaseTextDocumentService.java index 10c768ef2..91f9cface 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/IBaseTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/IBaseTextDocumentService.java @@ -30,6 +30,7 @@ import java.util.Collection; import java.util.List; import java.util.concurrent.CompletableFuture; + import org.eclipse.lsp4j.ClientCapabilities; import org.eclipse.lsp4j.CreateFilesParams; import org.eclipse.lsp4j.DeleteFilesParams; @@ -39,6 +40,8 @@ import org.eclipse.lsp4j.services.LanguageClient; import org.eclipse.lsp4j.services.TextDocumentService; import org.rascalmpl.vscode.lsp.parametric.LanguageRegistry.LanguageParameter; +import org.rascalmpl.vscode.lsp.rascal.jsonrpc.CheckProjectRequest; +import org.rascalmpl.vscode.lsp.util.concurrent.InterruptibleFuture; import io.usethesource.vallang.IValue; @@ -52,6 +55,7 @@ public interface IBaseTextDocumentService extends TextDocumentService, ITextDocu void initialized(); void registerLanguage(LanguageParameter lang); void unregisterLanguage(LanguageParameter lang); + InterruptibleFuture checkProject(CheckProjectRequest req); CompletableFuture executeCommand(String languageName, String command); Collection extensions(); diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java index d0dcb5f50..97e971046 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java @@ -122,6 +122,7 @@ import org.eclipse.lsp4j.services.LanguageClientAware; import org.rascalmpl.uri.URIResolverRegistry; import org.rascalmpl.uri.URIUtil; +import org.rascalmpl.uri.remote.jsonrpc.ISourceLocationRequest; import org.rascalmpl.values.IRascalValueFactory; import org.rascalmpl.values.parsetrees.ITree; import org.rascalmpl.values.parsetrees.TreeAdapter; @@ -148,6 +149,7 @@ import org.rascalmpl.vscode.lsp.rascal.conversion.Message; import org.rascalmpl.vscode.lsp.rascal.conversion.SelectionRanges; import org.rascalmpl.vscode.lsp.rascal.conversion.SemanticTokenizer; +import org.rascalmpl.vscode.lsp.rascal.jsonrpc.CheckProjectRequest; import org.rascalmpl.vscode.lsp.uri.LSPOpenFileRedirector; import org.rascalmpl.vscode.lsp.util.Maps; import org.rascalmpl.vscode.lsp.util.Versioned; @@ -1051,4 +1053,8 @@ public void cancelProgress(String progressId) { contributions.values().forEach(plex -> plex.cancelProgress(progressId)); } + + public InterruptibleFuture checkProject(CheckProjectRequest req) { + throw new UnsupportedOperationException("ParametricTextDocumentService cannot check Rascal projects"); + } } 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 327be0c48..9b8ad3225 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 @@ -45,6 +45,7 @@ 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; import org.checkerframework.checker.nullness.qual.Nullable; @@ -76,6 +77,7 @@ import org.rascalmpl.vscode.lsp.util.concurrent.InterruptibleFuture; import org.rascalmpl.vscode.lsp.util.locations.Locations; +import io.usethesource.vallang.IBool; import io.usethesource.vallang.IConstructor; import io.usethesource.vallang.IList; import io.usethesource.vallang.IMap; @@ -265,18 +267,30 @@ IFunction makeParseTreeGetter(Evaluator e) { }); } + private ISet getWorkspaceFolders() { + return workspaceService.workspaceFolders().stream().map(f -> Locations.toLoc(f.getUri())).collect(VF.setWriter()); + } - public InterruptibleFuture> compileFile(ISourceLocation file, PathConfig pcfg, + public InterruptibleFuture> checkFile(ISourceLocation file, PathConfig pcfg, Executor exec) { logger.debug("Running Rascal check for: {} with {}", file, pcfg); - var workspaceFolders = workspaceService.workspaceFolders().stream().map(f -> Locations.toLoc(f.getUri())).collect(VF.setWriter()); var shortModuleName = URIUtil.getLocationName(URIUtil.removeExtension(file)); return runEvaluator("Rascal check (" + shortModuleName +")", compilerEvaluator, - e -> translateCheckResults((IMap) e.call("checkFile", file, workspaceFolders, makeParseTreeGetter(e), makePathConfigGetter(e))), + e -> translateCheckResults((IMap) e.call("checkFile", file, getWorkspaceFolders(), makeParseTreeGetter(e), makePathConfigGetter(e))), Map.of(file, VF.set()), exec, false, client); } + public InterruptibleFuture checkProject(ISourceLocation projectRoot, IBool clean, Executor exec) { + logger.debug("Check Rascal project: {}", projectRoot); + + var shortName = URIUtil.getLocationName(projectRoot); + return runEvaluator("Rascal check project (" + shortName +")", compilerEvaluator, + e -> translateCheckResults((IMap) e.call("checkProject", projectRoot, clean, getWorkspaceFolders(), makeParseTreeGetter(e), makePathConfigGetter(e))), + Map.of(projectRoot, VF.set()), exec, false, client).thenAccept(r -> + rascalTextDocumentService.getFileFacts().reportTypeCheckerMessages(r)); + } + private @Nullable ISourceLocation getFileLoc(ITree moduleTree) { try { if (TreeAdapter.isTop(moduleTree)) { diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalTextDocumentService.java index 22cc34796..3127ec0e4 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalTextDocumentService.java @@ -38,6 +38,7 @@ import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.Stream; + import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -124,13 +125,16 @@ import org.rascalmpl.vscode.lsp.rascal.conversion.Message; import org.rascalmpl.vscode.lsp.rascal.conversion.SelectionRanges; import org.rascalmpl.vscode.lsp.rascal.conversion.SemanticTokenizer; +import org.rascalmpl.vscode.lsp.rascal.jsonrpc.CheckProjectRequest; import org.rascalmpl.vscode.lsp.rascal.model.FileFacts; import org.rascalmpl.vscode.lsp.rascal.model.SummaryBridge; import org.rascalmpl.vscode.lsp.uri.LSPOpenFileRedirector; import org.rascalmpl.vscode.lsp.util.Versioned; import org.rascalmpl.vscode.lsp.util.concurrent.CompletableFutureUtils; +import org.rascalmpl.vscode.lsp.util.concurrent.InterruptibleFuture; import org.rascalmpl.vscode.lsp.util.locations.Locations; import org.rascalmpl.vscode.lsp.util.locations.impl.TreeSearch; + import io.usethesource.vallang.IConstructor; import io.usethesource.vallang.IList; import io.usethesource.vallang.ISet; @@ -156,7 +160,6 @@ public RascalTextDocumentService(ExecutorService exec) { LSPOpenFileRedirector.getInstance().registerTextDocumentService(this); } - private LanguageClient availableClient() { if (client == null) { throw new IllegalStateException("Client has not been connected yet"); @@ -257,7 +260,6 @@ private void triggerAnalyzer(TextDocumentState state, Duration delay) { } } - @Override public void didClose(DidCloseTextDocumentParams params) { logger.debug("Close: {}", params.getTextDocument()); @@ -656,4 +658,9 @@ public FileFacts getFileFacts() { public void cancelProgress(String progressId) { exec.submit(() -> availableRascalServices().cancelProgress(progressId)); } + + @Override + public InterruptibleFuture checkProject(CheckProjectRequest req) { + return availableRascalServices().checkProject(req.getLocation(), req.getClean(), exec); + } } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/jsonrpc/CheckProjectRequest.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/jsonrpc/CheckProjectRequest.java new file mode 100644 index 000000000..5e2ef6fb8 --- /dev/null +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/jsonrpc/CheckProjectRequest.java @@ -0,0 +1,68 @@ +/* + * 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.jsonrpc; + +import java.util.Objects; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.rascalmpl.values.ValueFactoryFactory; + +import io.usethesource.vallang.IBool; +import io.usethesource.vallang.ISourceLocation; + +public class CheckProjectRequest { + private final ISourceLocation loc; + private final boolean clean; + + public CheckProjectRequest(ISourceLocation loc, boolean clean) { + this.loc = loc; + this.clean = clean; + } + + public ISourceLocation getLocation() { + return loc; + } + + public IBool getClean() { + return ValueFactoryFactory.getValueFactory().bool(clean); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (obj instanceof CheckProjectRequest) { + var other = (CheckProjectRequest) obj; + return Objects.equals(loc, other.loc) + && clean == other.clean; + } + return false; + } + + @Override + public int hashCode() { + return Objects.hash(loc, clean); + } +} 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 30314d890..5ce6f809f 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 @@ -57,6 +57,7 @@ import org.rascalmpl.vscode.lsp.util.locations.Locations; import io.usethesource.vallang.IConstructor; +import io.usethesource.vallang.ISet; import io.usethesource.vallang.ISourceLocation; public class FileFacts implements DiagnosticsReporter { @@ -113,6 +114,10 @@ public void reportParseErrors(ISourceLocation file, Versioned> getFile(file).reportParseErrors(msgs); } + public void reportTypeCheckerMessages(Map messages) { + Diagnostics.translateMessages(messages, Set.of("rsc"), cm).forEach((f, msgs) -> getFile(f).reportTypeCheckerMessages(msgs)); + } + private FileFact getFile(ISourceLocation l) { l = l.top(); ISourceLocation resolved = Locations.toClientLocation(l); @@ -224,7 +229,7 @@ public void invalidate() { summary.invalidate(); typeCheckerMessages.clear(); this.typeCheckResults.replace( - rascal.compileFile(file, confs.lookupConfig(file), exec) + rascal.checkFile(file, confs.lookupConfig(file), exec) .thenApply(m -> Diagnostics.translateMessages(m, Set.of("rsc"), cm)) ).thenAccept(m -> m.forEach((f, msgs) -> getFile(f).reportTypeCheckerMessages(msgs))); } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/Versioned.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/Versioned.java index db963c4a3..e0a331029 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/Versioned.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/Versioned.java @@ -32,6 +32,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; + import org.checkerframework.checker.nullness.qual.PolyNull; import org.rascalmpl.vscode.lsp.util.concurrent.CompletableFutureUtils; 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..417dcafbe 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 @@ -29,6 +29,7 @@ module lang::rascal::lsp::IDECheckerWrapper import IO; import List; +import Node; import Relation; import Set; import String; @@ -36,6 +37,7 @@ import ValueIO; import Location; import analysis::graphs::Graph; import util::FileSystem; +import util::IDEServices; import util::Monitor; import util::ParseErrorRecovery; @@ -55,23 +57,10 @@ import lang::rascalcore::check::ModuleLocations; map[loc, set[Message]] checkFile(loc l, set[loc] workspaceFolders, start[Module](loc file) getParseTree, PathConfig(loc file) getPathConfig) = job("Rascal check", map[loc, set[Message]](void(str, int) step) { - tuple[start[Module], set[Message]] getParseTreeOrErrors(loc l, str name, loc errorLocation) { - try { - t = getParseTree(l); - errors = hasParseErrors(t) - ? {error("Cannot typecheck this module, since dependency `` has parse error(s).", errorLocation, - causes=[error("Parse error around this position.", e.src) | e <- findBestParseErrors(t)])} - : {}; - return ; - } catch ParseError(loc err): { - return <(start[Module]) `module ModuleHadParseError`, {error("Cannot typecheck this module, since dependency `` has parse error(s).", errorLocation, causes=[error("Parse error(s).", err)])}>; - } - } - // Note: check further down parses again, possibly leading to a different tree if the contents changed in the meantime. // We cannot fix that here, unless we pass `getParseTree` to `check`. - = getParseTreeOrErrors(l, "unknown", l); - if ({} != parseErrors) { + = getParseTreeOrErrors(l, "unknown", l, getParseTree); + if ([] != parseErrors) { // No need to return the errors, since the language server will take care of parse errors in open modules return (); } @@ -84,20 +73,108 @@ map[loc, set[Message]] checkFile(loc l, set[loc] workspaceFolders, start[Module] rel[loc, loc] dependencies = {}; step("Dependency graph", 1); - job("Building dependency graph", bool (void (str, int) step2) { + = buildDependencyGraph(checkForImports, openFileHeader.src, workspaceFolders, getParseTree, getPathConfig); + + if ([] != dependencyMsgs) { + // Since we only reported errors on `l`, there is not need to analyze to which files the errors belong here. + return (l: {*dependencyMsgs}); + } + + cyclicDependencies = {p | <- (dependencies - ident(carrier(dependencies)))+}; + if (cyclicDependencies != {}) { + return (l : {error("Cyclic dependencies detected between projects {}. This is not supported. Fix your project setup.", l)}); + } + + step("Checking upstream dependencies ", 1); + msgs = checkDependencies(checkedForImports, dependencies, initialProject, workspaceFolders, getParseTree, getPathConfig); + + step("Checking module ", 1); + pcfg = getPathConfig(initialProject); + checkOutdatedPathConfig(pcfg); + msgs += check(calculateOutdated({f | f <- checkedForImports, initialProject := inferProjectRoot(f)}, pcfg) + [l], rascalCompilerConfig(pcfg)); + return filterAndFix(msgs, workspaceFolders); +}, totalWork=3); + +map[loc, set[Message]] checkProject(loc projectRoot, bool clean, set[loc] workspaceFolders, start[Module](loc file) getParseTree, PathConfig(loc file) getPathConfig) + = job("Rascal check project", map[loc, set[Message]](void(str, int) step) { + + pcfg = getPathConfig(projectRoot); + + rscFiles = sort({*find(src, "rsc") | src <- pcfg.srcs}); + + //`pt` and `errors` are explicitly typed because of https://github.com/usethesource/rascal/issues/2818 + parsed = (l : f | l <- rscFiles, f: := getParseTreeOrErrors(l, "unknown", projectRoot, getParseTree)); + + if (clean) { + for (f <- find(pcfg.bin, "tpl")) { + remove(f); + } + } + + checkForImports = [pt | l <- parsed, := parsed[l]]; + checkedForImports = {}; + initialProject = projectRoot; + + msgs = [*msgs | l <- parsed, <_pt, msgs> := parsed[l]]; + + rel[loc, loc] dependencies = {}; + + step("Dependency graph", 1); + = buildDependencyGraph(checkForImports, projectRoot, workspaceFolders, getParseTree, getPathConfig); + + msgs += dependencyMsgs; + + cyclicDependencies = {p | <- (dependencies - ident(carrier(dependencies)))+}; + if (cyclicDependencies != {}) { + msgs += [error("Cyclic dependencies detected between projects {}. This is not supported. Fix your project setup.", l) | l <- rscFiles]; + return msgs; + } + + step("Checking upstream dependencies", 1); + upstreamMessages = checkDependencies(checkedForImports, dependencies, initialProject, workspaceFolders, getParseTree, getPathConfig); + + msgs += upstreamMessages; + + step("Checking project ``", 1); + pcfg = getPathConfig(projectRoot); + checkOutdatedPathConfig(pcfg); + msgs += check(rscFiles, rascalCompilerConfig(pcfg)); + return filterAndFix(msgs, workspaceFolders); +}, totalWork=3); + +private tuple[start[Module], list[Message]] getParseTreeOrErrors(loc l, str name, loc errorLocation, start[Module](loc file) getParseTree) { + try { + t = getParseTree(l); + errors = hasParseErrors(t) + ? [error("Cannot typecheck this module, since dependency `` has parse error(s).", errorLocation, + causes=[error("Parse error around this position.", e.src) | e <- findBestParseErrors(t)])] + : []; + return ; + } catch ParseError(loc err): { + return <(start[Module]) `module ModuleHadParseError`, [error("Cannot typecheck this module, since dependency `` has parse error(s).", errorLocation, causes=[error("Parse error(s).", err)])]>; + } +} + +tuple[list[ModuleMessages] messages, set[loc] checkedForImports, rel[loc, loc] dependencies] buildDependencyGraph(list[start[Module]] checkForImports, loc errorLocation, set[loc] workspaceFolders, start[Module](loc file) getParseTree, PathConfig(loc file) getPathConfig) { + list[ModuleMessages] msgs = []; + set[loc] checkedForImports = {}; + rel[loc, loc] dependencies = {}; + + jobName = "Building dependency graph"; + job(jobName, bool (void (str, int) step) { while (tree <- checkForImports) { - step2("Calculating imports for ", 1); + step("Calculating imports for ", 1); currentSrc = tree.src.top; currentProject = inferProjectRoot(currentSrc); if (currentProject in workspaceFolders && currentProject.file notin {"rascal", "rascal-lsp"}) { for (i <- tree.top.header.imports, i has \module) { modName = ""; for (ml <- locateRascalModules(modName, getPathConfig(currentProject), getPathConfig, workspaceFolders)) { - if ( := getParseTreeOrErrors(ml, modName, openFileHeader.src)) { - if ({} !:= importErrors) { - parseErrors += importErrors; - checkedForImports += currentSrc; // do not check this module again - continue; // since there is an error in this module, we do not recurse into its imports + if ( := getParseTreeOrErrors(ml, modName, errorLocation, getParseTree)) { + if ([] !:= importErrors) { + msgs += program(ml, importErrors); + checkedForImports += currentSrc; + continue; } if (mlpt.src.top notin checkedForImports) { checkForImports += mlpt; @@ -112,26 +189,17 @@ map[loc, set[Message]] checkFile(loc l, set[loc] workspaceFolders, start[Module] checkForImports -= tree; } return true; - }, totalWork=1); - - if ({} != parseErrors) { - // Since we only reported errors on `l`, there is not need to analyze to which files the errors belong here. - return (l: parseErrors); - } + }, totalWork=size(checkForImports)); + return ; +} - cyclicDependencies = {p | <- (dependencies - ident(carrier(dependencies)))+}; - if (cyclicDependencies != {}) { - return (l : {error("Cyclic dependencies detected between projects {}. This is not supported. Fix your project setup.", l)}); - } +list[ModuleMessages] checkDependencies(set[loc] checkedForImports, rel[loc, loc] dependencies, loc initialProject, set[loc] workspaceFolders, start[Module](loc file) getParseTree, PathConfig(loc file) getPathConfig) { modulesPerProject = classify(checkedForImports, loc(loc l) {return inferProjectRoot(l);}); - msgs = []; - upstreamDependencies = {project | project <- reverse(order(dependencies)), project in modulesPerProject, project != initialProject}; - - step("Checking upstream dependencies ", 1); - job("Checking upstream dependencies", bool (void (str, int) step3) { + list[ModuleMessages] msgs = []; + job("Checking upstream dependencies", bool (void (str, int) step) { for (project <- upstreamDependencies) { - step3("Checked module in ``", 1); + step("Checked module in ``", 1); pcfg = getPathConfig(project); checkOutdatedPathConfig(pcfg); modulesToCheck = calculateOutdated(modulesPerProject[project], pcfg); @@ -141,13 +209,8 @@ map[loc, set[Message]] checkFile(loc l, set[loc] workspaceFolders, start[Module] } return true; }, totalWork=size(upstreamDependencies)); - - step("Checking module ", 1); - pcfg = getPathConfig(initialProject); - checkOutdatedPathConfig(pcfg); - msgs += check(calculateOutdated(modulesPerProject[initialProject], pcfg) + [l], rascalCompilerConfig(pcfg)); - return filterAndFix(msgs, workspaceFolders); -}, totalWork=3); + return msgs; +} private bool inWorkspace(set[loc] workspaceFolders, loc lib) { try { @@ -242,7 +305,7 @@ loc inferDeepestProjectRoot(loc member) { map[loc, set[Message]] filterAndFix(list[ModuleMessages] messages, set[loc] workspaceFolders) { set[Message] empty = {}; - map[loc, set[Message]] result = ( f.top : empty | program(f,_) <- messages); + map[loc, set[Message]] result = ( f.top : empty | program(f,_) <- messages, inWorkspace(workspaceFolders, f.top)); for (program(_, ms) <- messages, m <- ms, inWorkspace(workspaceFolders, m.at.top)) { result[m.at.top]?empty += {m}; } diff --git a/rascal-vscode-extension/package.json b/rascal-vscode-extension/package.json index ae368ab39..e657cbb54 100644 --- a/rascal-vscode-extension/package.json +++ b/rascal-vscode-extension/package.json @@ -59,6 +59,16 @@ "title": "Stop Rascal debugger for REPL", "icon": "$(debug-disconnect-compact)" }, + { + "command": "rascalmpl.checkProject", + "title": "Rascal: check project", + "icon": "./assets/images/rascal-logo-v2.1.svg" + }, + { + "command": "rascalmpl.cleanAndCheckProject", + "title": "Rascal: clean and check project", + "icon": "./assets/images/rascal-logo-v2.1.svg" + }, { "command": "rascalmpl.copySourceLocation", "title": "Copy Path as Rascal Location", @@ -71,6 +81,14 @@ "command": "rascalmpl.startDebuggerForRepl", "when": "false" }, + { + "command": "rascalmpl.checkProject", + "when": "false" + }, + { + "command": "rascalmpl.cleanAndCheckProject", + "when": "false" + }, { "command": "rascalmpl.copySourceLocation", "when": "false" @@ -101,6 +119,14 @@ } ], "explorer/context": [ + { + "command": "rascalmpl.checkProject", + "group": "1_modification" + }, + { + "command": "rascalmpl.cleanAndCheckProject", + "group": "1_modification" + }, { "command": "rascalmpl.copySourceLocation", "group": "6_copypath@21" diff --git a/rascal-vscode-extension/src/RascalExtension.ts b/rascal-vscode-extension/src/RascalExtension.ts index 0ff18c2c4..400fa4db4 100644 --- a/rascal-vscode-extension/src/RascalExtension.ts +++ b/rascal-vscode-extension/src/RascalExtension.ts @@ -30,13 +30,14 @@ import * as vscode from 'vscode'; import { integer, URI } from'vscode-languageclient'; import { checkForJVMUpdate, getJavaExecutable } from './auto-jvm/JavaLookup'; -import { RascalLanguageServer } from './lsp/RascalLanguageServer'; +import { RascalLanguageServer, toRascalUri } from './lsp/RascalLanguageServer'; import { LanguageParameter, ParameterizedLanguageServer } from './lsp/ParameterizedLanguageServer'; import { RascalTerminalLinkProvider } from './RascalTerminalLinkProvider'; import { VSCodeFileSystemInRascal } from './fs/VSCodeFileSystemInRascal'; import { RascalLibraryProvider } from './ux/LibraryNavigator'; import { FileType } from 'vscode'; import { RascalDebugViewProvider } from './dap/RascalDebugView'; +import { ISourceLocationRequest } from './fs/JsonRpcMessages'; export class RascalExtension implements vscode.Disposable { private readonly vfsServer: VSCodeFileSystemInRascal; @@ -55,6 +56,7 @@ export class RascalExtension implements vscode.Disposable { this.registerMainRun(); this.registerImportModule(); this.registerCopySourceLocationCommand(); + this.registerCheckProjectCommand(); void checkForJVMUpdate(); vscode.window.registerTreeDataProvider('rascalmpl-configuration-view', new RascalLibraryProvider(this.rascal.rascalClient, this.log)); @@ -313,12 +315,41 @@ export class RascalExtension implements vscode.Disposable { return jars.map(j => path.join(this.jarRootPath, j)).join(path.delimiter); } + private registerCheckProjectCommand() { + this.context.subscriptions.push(vscode.commands.registerCommand("rascalmpl.checkProject", this.checkProjectCommand(false))); + this.context.subscriptions.push(vscode.commands.registerCommand("rascalmpl.cleanAndCheckProject", this.checkProjectCommand(true))); + } + + private checkProjectCommand(clean: boolean) { + return async (uri: vscode.Uri) => { + const projectRoot = vscode.workspace.getWorkspaceFolder(uri); + if (!projectRoot) { + void vscode.window.showErrorMessage(`Rascal check project: could not deduce the project root directory for ${uri}`); + return; + } + const rascalMF = vscode.Uri.joinPath(projectRoot.uri, "META-INF", "RASCAL.MF"); + if (!await this.fileExists(rascalMF)) { + void vscode.window.showErrorMessage(`Rascal check project: ${projectRoot} is not a Rascal project`); + } + try { + await (await this.rascal.rascalClient).sendRequest("rascal/checkProject", { + loc: toRascalUri(projectRoot.uri), + clean: clean + }); + } catch (_e) { + void vscode.window.showErrorMessage(`Rascal check project: could not check ${projectRoot}`); + } + }; + } } interface IDEServicesConfiguration { port:integer; } +interface CheckProjectRequest extends ISourceLocationRequest { + clean: boolean; +} function gb(amount: integer) { diff --git a/rascal-vscode-extension/src/lsp/RascalLanguageServer.ts b/rascal-vscode-extension/src/lsp/RascalLanguageServer.ts index 9813b2dab..37d0dfb56 100644 --- a/rascal-vscode-extension/src/lsp/RascalLanguageServer.ts +++ b/rascal-vscode-extension/src/lsp/RascalLanguageServer.ts @@ -116,7 +116,7 @@ function addCoordinates(uri: string, coordinates: IRascalCoordinates | undefined // VS Code omits the leading two slashes from URIs if the autority is empty *and* the scheme is not equal to "file" // Rascal does not support this style of URIs, so we add the slashes before sending the URI over -function toRascalUri(uri: vscode.Uri): string { +export function toRascalUri(uri: vscode.Uri): string { const uriString = uri.toString(); if (uri.authority === "" && uri.scheme !== "file") { const colon = uri.scheme.length + 1; diff --git a/rascal-vscode-extension/src/test/vscode-suite/ide.test.ts b/rascal-vscode-extension/src/test/vscode-suite/ide.test.ts index 36f2d9b30..70ec1a6d0 100644 --- a/rascal-vscode-extension/src/test/vscode-suite/ide.test.ts +++ b/rascal-vscode-extension/src/test/vscode-suite/ide.test.ts @@ -27,9 +27,10 @@ import { expect } from 'chai'; import * as fs from 'fs/promises'; +import * as os from 'os'; import * as path from 'path'; -import { TextEditor, until, ViewSection, VSBrowser, WebDriver, Workbench } from 'vscode-extension-tester'; -import { Delays, IDEOperations, ignoreFails, isLanguageLoading, printRascalOutputOnFailure, ProtectedFiles, sleep, TestWorkspace } from './utils'; +import { TextEditor, TreeItem, until, ViewSection, VSBrowser, WebDriver, Workbench } from 'vscode-extension-tester'; +import { Delays, IDEOperations, ignoreFails, isLanguageLoading, printRascalOutputOnFailure, ProtectedFiles, RascalREPL, sleep, TestWorkspace } from './utils'; describe('IDE', function () { let browser: VSBrowser; @@ -291,4 +292,35 @@ describe('IDE', function () { await ide.checkNoDiagnosticsAnymore(); }); + + it.only("check project works", async function () { + // Context menu does not work on macOS + if (os.type() === "Darwin") { + this.skip(); + } + // Fix type error to avoid a failing "after each" hook in CI + const importeeEditor = await ide.openModule(TestWorkspace.importeeFile); + await ide.openModule(TestWorkspace.importeeFile); + await importeeEditor.setTextAtLine(2, "public int foo;"); + + await ide.screenshot("SCREENSHOT1"); + + const explorer = await (await bench.getActivityBar().getViewControl("Explorer"))!.openView(); + const workspace = await explorer.getContent().getSection("test (Workspace)"); + await workspace.expand(); + await ide.screenshot("SCREENSHOT2"); + const projectRoot = await workspace.findItem("test-project"); + await (await projectRoot!.openContextMenu()).select("Rascal: clean and check project"); + await ide.screenshot("SCREENSHOT3"); + const repl = new RascalREPL(bench, driver); + await repl.start(); + await repl.execute("import IO;"); + let n = 4; + await driver.wait(async () => { + await ide.screenshot(`SCREENSHOT${n}`); + n += 1; + await repl.execute("exists(|target://test-project/rascal/$Main.tpl|)"); + return repl.lastOutput === "bool: true"; + }, Delays.extremelySlow, "tpl for Main.rsc should exist by now"); + }); });