diff --git a/rascal-lsp/pom.xml b/rascal-lsp/pom.xml
index 9a1013e21..cef39b926 100644
--- a/rascal-lsp/pom.xml
+++ b/rascal-lsp/pom.xml
@@ -33,7 +33,7 @@
2.21.0-SNAPSHOT
UTF-8
- 5.10.3
+ 4.13.1
3.3.0
2.23.1
0.23.1
@@ -68,36 +68,17 @@
org.rascalmpl
rascal-core
- 0.12.3
+ 0.12.4
org.rascalmpl
typepal
- 0.13.4
-
-
- org.junit.jupiter
- junit-jupiter-api
- ${junit.version}
- test
-
-
- org.junit.jupiter
- junit-jupiter-engine
- ${junit.version}
- test
+ 0.14.0
junit
junit
- 4.13.1
- test
-
-
-
- org.junit.vintage
- junit-vintage-engine
${junit.version}
test
@@ -169,6 +150,14 @@
org.apache.maven.plugins
maven-surefire-plugin
${maven-surefire-plugin.version}
+
+
+
+ org.apache.maven.surefire
+ surefire-junit47
+ 3.3.0
+
+
org.rascalmpl
@@ -181,8 +170,9 @@
${project.basedir}/src/main/rascal
- ${project.basedir}/src/main/rascal/lang/rascal/lsp/Rename.rsc
- ${project.basedir}/src/main/rascal/lang/rascal/tests/Rename.rsc
+ ${project.basedir}/src/main/rascal/lang/rascal/lsp/refactor/Rename.rsc
+ ${project.basedir}/src/main/rascal/lang/rascal/lsp/refactor/WorkspaceInfo.rsc
+ ${project.basedir}/src/main/rascal/lang/rascal/tests/rename
|lib://rascal-lsp|
false
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 d3ed3320e..4dd0e378b 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
@@ -38,6 +38,7 @@
import java.util.concurrent.CompletableFuture;
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;
@@ -50,6 +51,7 @@
import org.rascalmpl.interpreter.Evaluator;
import org.rascalmpl.library.util.PathConfig;
import org.rascalmpl.values.IRascalValueFactory;
+import org.rascalmpl.values.functions.IFunction;
import org.rascalmpl.values.parsetrees.ITree;
import org.rascalmpl.values.parsetrees.TreeAdapter;
import org.rascalmpl.vscode.lsp.BaseWorkspaceService;
@@ -65,6 +67,9 @@
import io.usethesource.vallang.IString;
import io.usethesource.vallang.IValue;
import io.usethesource.vallang.IValueFactory;
+import io.usethesource.vallang.type.Type;
+import io.usethesource.vallang.type.TypeFactory;
+import io.usethesource.vallang.type.TypeStore;
public class RascalLanguageServices {
private static final IValueFactory VF = IRascalValueFactory.getInstance();
@@ -75,6 +80,10 @@ public class RascalLanguageServices {
private final CompletableFuture semanticEvaluator;
private final CompletableFuture compilerEvaluator;
+ private final TypeFactory tf = TypeFactory.getInstance();
+ private final TypeStore store = new TypeStore();
+ private final Type getPathConfigType = tf.functionType(tf.abstractDataType(store, "PathConfig"), tf.tupleType(tf.sourceLocationType()), tf.tupleEmpty());
+
private final ExecutorService exec;
private final IBaseLanguageClient client;
@@ -86,7 +95,7 @@ public RascalLanguageServices(RascalTextDocumentService docService, BaseWorkspac
var monitor = new RascalLSPMonitor(client, logger);
outlineEvaluator = makeFutureEvaluator(exec, docService, workspaceService, client, "Rascal outline", monitor, null, false, "lang::rascal::lsp::Outline");
- semanticEvaluator = makeFutureEvaluator(exec, docService, workspaceService, client, "Rascal summary", monitor, null, true, "lang::rascalcore::check::Summary", "lang::rascal::lsp::Rename");
+ semanticEvaluator = makeFutureEvaluator(exec, docService, workspaceService, client, "Rascal semantics", monitor, null, true, "lang::rascalcore::check::Summary", "lang::rascal::lsp::refactor::Rename");
compilerEvaluator = makeFutureEvaluator(exec, docService, workspaceService, client, "Rascal compiler", monitor, null, true, "lang::rascalcore::check::Checker");
}
@@ -181,7 +190,7 @@ public InterruptibleFuture getOutline(IConstructor module) {
}
- public InterruptibleFuture getRename(ITree module, Position cursor, Set workspaceFolders, PathConfig pcfg, String newName, ColumnMaps columns) {
+ public InterruptibleFuture getRename(ITree module, Position cursor, Set workspaceFolders, Function getPathConfig, String newName, ColumnMaps columns) {
var line = cursor.getLine() + 1;
var moduleLocation = TreeAdapter.getLocation(module);
var translatedOffset = columns.get(moduleLocation).translateInverseColumn(line, cursor.getCharacter(), false);
@@ -189,7 +198,8 @@ public InterruptibleFuture getRename(ITree module, Position cursor, Set {
try {
- return (IList) eval.call("renameRascalSymbol", module, cursorTree, VF.set(workspaceFolders.toArray(ISourceLocation[]::new)), addResources(pcfg), VF.string(newName));
+ IFunction rascalGetPathConfig = eval.getFunctionValueFactory().function(getPathConfigType, (t, u) -> addResources(getPathConfig.apply((ISourceLocation) t[0])));
+ return (IList) eval.call("rascalRenameSymbol", cursorTree, VF.set(workspaceFolders.toArray(ISourceLocation[]::new)), VF.string(newName), rascalGetPathConfig);
} catch (Throw e) {
if (e.getException() instanceof IConstructor) {
var exception = (IConstructor)e.getException();
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 0fbb719c5..5e41f55ad 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
@@ -286,7 +286,7 @@ public CompletableFuture rename(RenameParams params) {
return file.getCurrentTreeAsync()
.thenApply(Versioned::get)
.handle((t, r) -> (t == null ? (file.getMostRecentTree().get()) : t))
- .thenCompose(tr -> rascalServices.getRename(tr, params.getPosition(), workspaceFolders, facts.getPathConfig(file.getLocation()), params.getNewName(), columns).get())
+ .thenCompose(tr -> rascalServices.getRename(tr, params.getPosition(), workspaceFolders, facts::getPathConfig, params.getNewName(), columns).get())
.thenApply(c -> new WorkspaceEdit(DocumentChanges.translateDocumentChanges(this, c)))
;
}
diff --git a/rascal-lsp/src/main/rascal/lang/rascal/lsp/Rename.rsc b/rascal-lsp/src/main/rascal/lang/rascal/lsp/Rename.rsc
deleted file mode 100644
index fd402225a..000000000
--- a/rascal-lsp/src/main/rascal/lang/rascal/lsp/Rename.rsc
+++ /dev/null
@@ -1,249 +0,0 @@
-@license{
-Copyright (c) 2018-2023, 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.
-}
-@bootstrapParser
-module lang::rascal::lsp::Rename
-
-/**
- * Rename refactoring
- *
- * Implements rename refactoring according to the LSP.
- * Renaming collects information generated by the typechecker for the module/workspace, finds all definitions and
- * uses matching the position of the cursor, and computes file changes needed to rename these to the user-input name.
- */
-
-import Exception;
-import IO;
-import List;
-import Location;
-import Node;
-import ParseTree;
-import Relation;
-import Set;
-import String;
-
-import lang::rascal::\syntax::Rascal;
-
-import lang::rascalcore::check::Checker;
-import lang::rascalcore::check::Import;
-
-import analysis::typepal::TypePal;
-import analysis::typepal::TModel;
-
-import analysis::diff::edits::TextEdits;
-
-import vis::Text;
-
-import util::Maybe;
-import util::Reflective;
-
-alias Capture = tuple[loc def, loc use];
-
-data IllegalRenameReason
- = invalidName(str name)
- | doubleDeclaration(loc old, set[loc] new)
- | captureChange(set[Capture] captures)
- ;
-
-data RenameException
- = illegalRename(loc location, set[IllegalRenameReason] reason)
- | unsupportedRename(rel[loc location, str message] issues)
- | unexpectedFailure(str message)
-;
-
-private set[loc] getUses(TModel tm, loc def) {
- return invert(getUseDef(tm))[def];
-}
-
-private set[Define] getDefines(TModel tm, loc use) {
- return {tm.definitions[d] | d <- tm.useDef[use]};
-}
-
-void throwIfNotEmpty(&T(&A) R, &A arg) {
- if (arg != {}) {
- throw R(arg);
- }
-}
-
-set[IllegalRenameReason] checkLegalName(str name) {
- try {
- parse(#Name, escapeName(name));
- return {};
- } catch ParseError(_): {
- return {invalidName(name)};
- }
-}
-
-set[IllegalRenameReason] checkCausesDoubleDeclarations(TModel tm, set[Define] currentDefs, set[Define] newDefs) {
- // Is newName already resolvable from a scope where is currently declared?
- rel[loc old, loc new] doubleDeclarations = { | Define cD <- currentDefs
- , Define nD <- newDefs
- , isContainedIn(cD.defined, nD.scope)
- , !rascalMayOverload({cD.defined, nD.defined}, tm.definitions)
- };
-
- return {doubleDeclaration(old, doubleDeclarations[old]) | old <- doubleDeclarations.old};
-}
-
-set[Define] findImplicitDefinitions(TModel tm, start[Module] m, set[Define] newDefs) {
- set[loc] maybeImplicitDefs = {l | /QualifiedName n := m, just(l) := locationOfName(n)};
- return {def | Define def <- newDefs, (def.idRole is variableId && def.defined in tm.useDef<0>)
- || (def.idRole is patternVariableId && def.defined in maybeImplicitDefs)};
-}
-
-set[IllegalRenameReason] checkCausesCaptures(TModel tm, start[Module] m, set[Define] currentDefs, set[loc] currentUses, set[Define] newDefs) {
- set[Define] newNameImplicitDefs = findImplicitDefinitions(tm, m, newDefs);
- set[Define] newNameExplicitDefs = newDefs - newNameImplicitDefs;
-
- // Will this rename turn an implicit declaration of `newName` into a use of a current declaration?
- set[Capture] implicitDeclBecomesUseOfCurrentDecl =
- { | Define nD <- newNameImplicitDefs
- , <- currentDefs
- , isContainedIn(nD.defined, cS)
- };
-
- // Will this rename hide a used definition of `oldName` behind an existing definition of `newName` (shadowing)?
- set[Capture] currentUseShadowedByRename =
- { | Define nD <- newDefs
- , <- ident(currentUses) o tm.useDef o tm.defines
- , isContainedIn(cU, nD.scope)
- , isStrictlyContainedIn(nD.scope, cS)
- };
-
- // Will this rename hide a used definition of `newName` behind a definition of `oldName` (shadowing)?
- set[Capture] newUseShadowedByRename =
- { | Define nD <- newDefs
- , nU <- invert(tm.useDef)[newDefs.defined]
- , Define cD <- currentDefs
- , isContainedIn(cD.scope, nD.scope)
- , isContainedIn(nU, cD.scope)
- };
-
- allCaptures =
- implicitDeclBecomesUseOfCurrentDecl
- + currentUseShadowedByRename
- + newUseShadowedByRename;
-
- return allCaptures == {} ? {} : {captureChange(allCaptures)};
-}
-
-void checkLegalRename(TModel tm, start[Module] m, loc cursorLoc, set[Define] currentDefs, set[loc] currentUses, str newName) {
- set[Define] newNameDefs = {def | def:<_, newName, _, _, _, _> <- tm.defines};
-
- checkUnsupported(m.src, currentDefs);
-
- set[IllegalRenameReason] reasons =
- checkLegalName(newName)
- + checkCausesDoubleDeclarations(tm, currentDefs, newNameDefs)
- + checkCausesCaptures(tm, m, currentDefs, currentUses, newNameDefs)
- ;
-
- if (reasons != {}) {
- throw illegalRename(cursorLoc, reasons);
- }
-}
-
-void checkUnsupported(loc moduleLoc, set[Define] defsToRename) {
- Maybe[str] isUnsupportedDefinition(Define _: ) = just("Global function definitions might span multiple modules; unsupported for now.")
- when moduleLoc == scope; // function is defined in module scope
- default Maybe[str] isUnsupportedDefinition(Define _) = nothing();
-
- throwIfNotEmpty(unsupportedRename, { | def <- defsToRename, just(msg) := isUnsupportedDefinition(def)});
-}
-
-str escapeName(str name) = name in getRascalReservedIdentifiers() ? "\\" : name;
-
-// Find the smallest declaration that contains `nameLoc`
-loc findDeclarationAroundName(TModel tm, loc nameLoc) =
- (|unknown:///| | l < it && isContainedIn(nameLoc, l) ? l : it | l <- tm.defines.defined);
-
-// Find the smallest trees of defined non-terminal type with a source location in `useDefs`
-set[loc] findNames(start[Module] m, set[loc] useDefs) {
- set[loc] names = {};
- visit(m.top) {
- case t: appl(prod(_, _, _), _): {
- if (t.src in useDefs && just(nameLoc) := locationOfName(t)) {
- names += nameLoc;
- }
- }
- }
-
- if (size(names) != size(useDefs)) {
- throw unsupportedRename({."> | l <- useDefs - names});
- }
-
- return names;
-}
-
-Maybe[loc] locationOfName(Name n) = just(n.src);
-Maybe[loc] locationOfName(QualifiedName qn) = just((qn.names[-1]).src);
-Maybe[loc] locationOfName(FunctionDeclaration f) = just(f.signature.name.src);
-default Maybe[loc] locationOfName(Tree t) = nothing();
-
-list[DocumentEdit] renameRascalSymbol(start[Module] m, Tree cursor, set[loc] workspaceFolders, TModel tm, str newName) {
- loc cursorLoc = cursor.src;
-
- set[Define] defs = {};
- if (cursorLoc in tm.useDef<0>) {
- // Cursor is at a use
- defs = getDefines(tm, cursorLoc);
- } else {
- // Cursor is at a name within a declaration
- loc cursorAtDef = findDeclarationAroundName(tm, cursorLoc);
- defs = tm.definitions[cursorAtDef] + getDefines(tm, cursorAtDef);
- }
-
- set[loc] uses = ({} | it + getUses(tm, def) | def <- defs.defined);
-
- checkLegalRename(tm, m, cursorLoc, defs, uses, newName);
-
- rel[loc file, loc useDefs] useDefsPerFile = { | loc useDef <- uses + defs};
- list[DocumentEdit] changes = [changed(file, [replace(nameLoc, escapeName(newName)) | nameLoc <- findNames(m, useDefsPerFile[file])]) | loc file <- useDefsPerFile.file];;
- // TODO If the cursor was a module name, we need to rename files as well
- list[DocumentEdit] renames = [];
-
- return changes + renames;
-}
-
-// Compute the edits for the complete workspace here
-list[DocumentEdit] renameRascalSymbol(start[Module] m, Tree cursor, set[loc] workspaceFolders, PathConfig pcfg, str newName) {
- str moduleName = getModuleName(m.src.top, pcfg);
-
- ModuleStatus ms = rascalTModelForLocs([m.src.top], getRascalCoreCompilerConfig(pcfg), dummy_compile1);
-
- TModel tm = tmodel();
- try
- tm = convertTModel2PhysicalLocs(ms.tmodels[moduleName]);
- catch NoSuchKey(): {
- throw unsupportedRename({});
- }
- return renameRascalSymbol(m, cursor, workspaceFolders, tm, newName);
-}
-
-//// WORKAROUNDS
-
-// Workaround to be able to pattern match on the emulated `src` field
-data Tree (loc src = |unknown:///|(0,0,<0,0>,<0,0>));
diff --git a/rascal-lsp/src/main/rascal/lang/rascal/lsp/refactor/Exception.rsc b/rascal-lsp/src/main/rascal/lang/rascal/lsp/refactor/Exception.rsc
new file mode 100644
index 000000000..5121a1990
--- /dev/null
+++ b/rascal-lsp/src/main/rascal/lang/rascal/lsp/refactor/Exception.rsc
@@ -0,0 +1,65 @@
+@license{
+Copyright (c) 2018-2023, 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.
+}
+module lang::rascal::lsp::refactor::Exception
+
+import analysis::typepal::TModel;
+
+import Message;
+import Set;
+
+alias Capture = tuple[loc def, loc use];
+
+data IllegalRenameReason
+ = invalidName(str name)
+ | doubleDeclaration(loc old, set[loc] new)
+ | captureChange(set[Capture] captures)
+ | definitionsOutsideWorkspace(set[loc] defs)
+ ;
+
+data RenameException
+ = illegalRename(str message, set[IllegalRenameReason] reason)
+ | unsupportedRename(str message, rel[loc location, str message] issues = {})
+ | unexpectedFailure(str message)
+ ;
+
+str describe(invalidName(name)) = "\'\' is not a valid identifier";
+str describe(doubleDeclaration(_, _)) = "it causes double declarations";
+str describe(captureChange(_)) = "it changes program semantics";
+str describe(definitionsOutsideWorkspace(_)) = "it renames definitions outside of currently open projects";
+
+void throwAnyErrors(TModel tm) {
+ throwAnyErrors(tm.messages);
+}
+
+void throwAnyErrors(set[Message] msgs) {
+ throwAnyErrors(toList(msgs));
+}
+
+void throwAnyErrors(list[Message] msgs) {
+ errors = {msg | msg <- msgs, msg is error};
+ if (errors != {}) throw errors;
+}
diff --git a/rascal-lsp/src/main/rascal/lang/rascal/lsp/refactor/Rename.rsc b/rascal-lsp/src/main/rascal/lang/rascal/lsp/refactor/Rename.rsc
new file mode 100644
index 000000000..4508c5ee9
--- /dev/null
+++ b/rascal-lsp/src/main/rascal/lang/rascal/lsp/refactor/Rename.rsc
@@ -0,0 +1,549 @@
+@license{
+Copyright (c) 2018-2023, 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.
+}
+@bootstrapParser
+module lang::rascal::lsp::refactor::Rename
+
+/**
+ * Rename refactoring
+ *
+ * Implements rename refactoring according to the LSP.
+ * Renaming collects information generated by the typechecker for the module/workspace, finds all definitions and
+ * uses matching the position of the cursor, and computes file changes needed to rename these to the user-input name.
+ */
+
+import Exception;
+import IO;
+import List;
+import Location;
+import Map;
+import ParseTree;
+import Relation;
+import Set;
+import String;
+
+import lang::rascal::\syntax::Rascal;
+
+import lang::rascalcore::check::Checker;
+import lang::rascalcore::check::Import;
+import lang::rascalcore::check::RascalConfig;
+
+import analysis::typepal::TypePal;
+import analysis::typepal::Collector;
+
+extend lang::rascal::lsp::refactor::Exception;
+import lang::rascal::lsp::refactor::Util;
+import lang::rascal::lsp::refactor::WorkspaceInfo;
+
+import analysis::diff::edits::TextEdits;
+
+import vis::Text;
+
+import util::FileSystem;
+import util::Maybe;
+import util::Monitor;
+import util::Reflective;
+
+// Rascal compiler-specific extension
+void throwAnyErrors(list[ModuleMessages] mmsgs) {
+ for (mmsg <- mmsgs) {
+ throwAnyErrors(mmsg);
+ }
+}
+
+// Rascal compiler-specific extension
+void throwAnyErrors(program(_, msgs)) {
+ throwAnyErrors(msgs);
+}
+
+set[IllegalRenameReason] rascalCheckLegalName(str name) {
+ try {
+ parse(#Name, rascalEscapeName(name));
+ return {};
+ } catch ParseError(_): {
+ return {invalidName(name)};
+ }
+}
+
+private set[IllegalRenameReason] rascalCheckDefinitionsOutsideWorkspace(WorkspaceInfo ws, set[loc] defs) =
+ { definitionsOutsideWorkspace(d) | set[loc] d <- groupRangeByDomain({ | loc d <- defs, f := d.top, f notin ws.sourceFiles}) };
+
+private set[IllegalRenameReason] rascalCheckCausesDoubleDeclarations(WorkspaceInfo ws, set[loc] currentDefs, set[Define] newDefs, str newName) {
+ // Is newName already resolvable from a scope where is currently declared?
+ rel[loc old, loc new] doubleDeclarations = { | <- (currentDefs * newDefs)
+ , isContainedIn(cD, nD.scope)
+ , !rascalMayOverload({cD, nD.defined}, ws.definitions)
+ };
+
+ rel[loc old, loc new] doubleFieldDeclarations = {
+ | Define _: <- definitionsRel(ws)[currentDefs]
+ // The scope of a field def is the surrounding data def
+ , loc dataDef <- rascalGetOverloadedDefs(ws, {curFieldScope}, rascalMayOverloadSameName)
+ , loc nD <- (newDefs)[fieldId()] & (ws.defines)[fieldId(), dataDef]
+ };
+
+ rel[loc old, loc new] doubleTypeParamDeclarations = {
+ | loc cD <- currentDefs
+ , ws.facts[cD]?
+ , cT: aparameter(_, _) := ws.facts[cD]
+ , Define fD: <_, _, _, _, _, defType(afunc(_, funcParams:/cT, _))> <- ws.defines
+ , isContainedIn(cD, fD.defined)
+ , <- toRel(ws.facts)
+ , isContainedIn(nD, fD.defined)
+ , /nT := funcParams
+ };
+
+ return {doubleDeclaration(old, doubleDeclarations[old]) | old <- (doubleDeclarations + doubleFieldDeclarations + doubleTypeParamDeclarations).old};
+}
+
+private set[IllegalRenameReason] rascalCheckCausesCaptures(WorkspaceInfo ws, start[Module] m, set[loc] currentDefs, set[loc] currentUses, set[Define] newDefs) {
+ set[Define] rascalFindImplicitDefinitions(WorkspaceInfo ws, start[Module] m, set[Define] newDefs) {
+ set[loc] maybeImplicitDefs = {l | /QualifiedName n := m, just(l) := rascalLocationOfName(n)};
+ return {def | Define def <- newDefs, (def.idRole is variableId && def.defined in ws.useDef<0>)
+ || (def.idRole is patternVariableId && def.defined in maybeImplicitDefs)};
+ }
+
+ set[Define] newNameImplicitDefs = rascalFindImplicitDefinitions(ws, m, newDefs);
+
+ // Will this rename turn an implicit declaration of `newName` into a use of a current declaration?
+ set[Capture] implicitDeclBecomesUseOfCurrentDecl =
+ { | Define nD <- newNameImplicitDefs
+ , loc cD <- currentDefs
+ , isContainedIn(nD.defined, ws.definitions[cD].scope)
+ };
+
+ // Will this rename hide a used definition of `oldName` behind an existing definition of `newName` (shadowing)?
+ set[Capture] currentUseShadowedByRename =
+ { | Define nD <- newDefs
+ , <- ident(currentUses) o ws.useDef o ws.defines
+ , isContainedIn(cU, nD.scope)
+ , isStrictlyContainedIn(nD.scope, cS)
+ };
+
+ // Will this rename hide a used definition of `newName` behind a definition of `oldName` (shadowing)?
+ set[Capture] newUseShadowedByRename =
+ { | Define nD <- newDefs
+ , loc cD <- currentDefs
+ , loc cS := ws.definitions[cD].scope
+ , isContainedIn(cS, nD.scope)
+ , nU <- defUse(ws)[newDefs.defined]
+ , isContainedIn(nU, cS)
+ };
+
+ allCaptures =
+ implicitDeclBecomesUseOfCurrentDecl
+ + currentUseShadowedByRename
+ + newUseShadowedByRename;
+
+ return allCaptures == {} ? {} : {captureChange(allCaptures)};
+}
+
+private set[IllegalRenameReason] rascalCollectIllegalRenames(WorkspaceInfo ws, start[Module] m, set[loc] currentDefs, set[loc] currentUses, str newName) {
+ set[Define] newNameDefs = {def | Define def:<_, newName, _, _, _, _> <- ws.defines};
+
+ return
+ rascalCheckLegalName(newName)
+ + rascalCheckDefinitionsOutsideWorkspace(ws, currentDefs)
+ + rascalCheckCausesDoubleDeclarations(ws, currentDefs, newNameDefs, newName)
+ + rascalCheckCausesCaptures(ws, m, currentDefs, currentUses, newNameDefs)
+ ;
+}
+
+private str rascalEscapeName(str name) = name in getRascalReservedIdentifiers() ? "\\" : name;
+
+// Find the smallest trees of defined non-terminal type with a source location in `useDefs`
+private set[loc] rascalFindNamesInUseDefs(start[Module] m, set[loc] useDefs) {
+ map[loc, loc] useDefNameAt = ();
+ useDefsToDo = useDefs;
+ visit(m.top) {
+ case t: appl(prod(_, _, _), _): {
+ if (t.src in useDefsToDo && just(nameLoc) := rascalLocationOfName(t)) {
+ useDefNameAt[t.src] = nameLoc;
+ useDefsToDo -= t.src;
+ }
+ }
+ }
+
+ if (useDefsToDo != {}) {
+ throw unsupportedRename("Rename unsupported", issues={."> | l <- useDefsToDo});
+ }
+
+ return range(useDefNameAt);
+}
+
+Maybe[loc] rascalLocationOfName(Name n) = just(n.src);
+Maybe[loc] rascalLocationOfName(QualifiedName qn) = just((qn.names[-1]).src);
+Maybe[loc] rascalLocationOfName(FunctionDeclaration f) = just(f.signature.name.src);
+Maybe[loc] rascalLocationOfName(Variable v) = just(v.name.src);
+Maybe[loc] rascalLocationOfName(KeywordFormal kw) = just(kw.name.src);
+Maybe[loc] rascalLocationOfName(Declaration d) = just(d.name.src) when d is annotation
+ || d is \tag;
+Maybe[loc] rascalLocationOfName(Declaration d) = rascalLocationOfName(d.user.name) when d is \alias
+ || d is dataAbstract
+ || d is \data;
+Maybe[loc] rascalLocationOfName(TypeVar tv) = just(tv.name.src);
+Maybe[loc] rascalLocationOfName(Header h) = rascalLocationOfName(h.name);
+default Maybe[loc] rascalLocationOfName(Tree t) = nothing();
+
+private tuple[set[IllegalRenameReason] reasons, list[TextEdit] edits] computeTextEdits(WorkspaceInfo ws, start[Module] m, set[loc] defs, set[loc] uses, str name) {
+ if (reasons := rascalCollectIllegalRenames(ws, m, defs, uses, name), reasons != {}) {
+ return ;
+ }
+
+ replaceName = rascalEscapeName(name);
+ return <{}, [replace(l, replaceName) | l <- rascalFindNamesInUseDefs(m, defs + uses)]>;
+}
+
+private tuple[set[IllegalRenameReason] reasons, list[TextEdit] edits] computeTextEdits(WorkspaceInfo ws, loc moduleLoc, set[loc] defs, set[loc] uses, str name) =
+ computeTextEdits(ws, parseModuleWithSpacesCached(moduleLoc), defs, uses, name);
+
+private bool rascalIsFunctionLocalDefs(WorkspaceInfo ws, set[loc] defs) {
+ for (d <- defs) {
+ if (Define fun: <_, _, _, _, _, defType(afunc(_, _, _))> <- ws.defines
+ , isContainedIn(ws.definitions[d].scope, fun.defined)) {
+ continue;
+ }
+ return false;
+ }
+ return true;
+}
+
+private bool rascalIsFunctionLocal(WorkspaceInfo ws, cursor(def(), cursorLoc, _)) =
+ rascalIsFunctionLocalDefs(ws, rascalGetOverloadedDefs(ws, {cursorLoc}, rascalMayOverloadSameName));
+private bool rascalIsFunctionLocal(WorkspaceInfo ws, cursor(use(), cursorLoc, _)) =
+ rascalIsFunctionLocalDefs(ws, rascalGetOverloadedDefs(ws, getDefs(ws, cursorLoc), rascalMayOverloadSameName));
+private bool rascalIsFunctionLocal(WorkspaceInfo _, cursor(typeParam(), _, _)) = true;
+private default bool rascalIsFunctionLocal(_, _) = false;
+
+Maybe[AType] rascalAdtCommonKeywordFieldType(WorkspaceInfo ws, str fieldName, Define _:<_, _, _, dataId(), _, DefInfo defInfo>) {
+ if (defInfo.commonKeywordFields?
+ , kwf:(KeywordFormal) ` = ` <- defInfo.commonKeywordFields
+ , "" == fieldName) {
+ if (ft:just(_) := getFact(ws, kwf.src)) return ft;
+ throw "Unknown field type for ";
+ }
+ return nothing();
+}
+
+Maybe[AType] rascalConsKeywordFieldType(str fieldName, Define _:<_, _, _, constructorId(), _, defType(acons(_, _, kwFields))>) {
+ if (kwField(fieldType, fieldName, _) <- kwFields) return just(fieldType);
+ return nothing();
+}
+
+Maybe[AType] rascalConsFieldType(str fieldName, Define _:<_, _, _, constructorId(), _, defType(acons(_, fields, _))>) {
+ if (field <- fields, field.alabel == fieldName) return just(field);
+ return nothing();
+}
+
+private CursorKind rascalGetDataFieldCursorKind(WorkspaceInfo ws, loc container, loc cursorLoc, str cursorName) {
+ for (Define dt <- rascalGetADTDefinitions(ws, container)
+ , adtType := dt.defInfo.atype) {
+ if (just(fieldType) := rascalAdtCommonKeywordFieldType(ws, cursorName, dt)) {
+ // Case 4 or 5 (or 0): common keyword field
+ return dataCommonKeywordField(dt.defined, fieldType);
+ }
+
+ for (Define d: <_, _, _, constructorId(), _, defType(acons(adtType, _, _))> <- ws.defines) {
+ if (just(fieldType) := rascalConsKeywordFieldType(cursorName, d)) {
+ // Case 3 (or 0): keyword field
+ return dataKeywordField(dt.defined, fieldType);
+ } else if (just(fieldType) := rascalConsFieldType(cursorName, d)) {
+ // Case 2 (or 0): positional field
+ return dataField(dt.defined, fieldType);
+ }
+ }
+ }
+
+ set[loc] fromDefs = cursorLoc in ws.useDef<1> ? {cursorLoc} : getDefs(ws, cursorLoc);
+ throw illegalRename("Cannot rename \'\'; it is not defined in this workspace", {definitionsOutsideWorkspace(fromDefs)});
+}
+
+private CursorKind rascalGetCursorKind(WorkspaceInfo ws, loc cursorLoc, str cursorName, rel[loc l, CursorKind kind] locsContainingCursor, rel[loc field, loc container] fields, rel[loc kw, loc container] keywords) {
+ loc c = min(locsContainingCursor.l);
+ switch (locsContainingCursor[c]) {
+ case {moduleName(), *_}: {
+ return moduleName();
+ }
+ case {keywordParam(), dataKeywordField(_, _), *_}: {
+ if ({loc container} := keywords[c]) {
+ return rascalGetDataFieldCursorKind(ws, container, cursorLoc, cursorName);
+ }
+ }
+ case {collectionField(), dataField(_, _), dataKeywordField(_, _), dataCommonKeywordField(_, _), *_}: {
+ /* Possible cases:
+ 0. We are on a field use/access (of either a data or collection field, in an expression/assignment/pattern(?))
+ 1. We are on a collection field
+ 2. We are on a positional field definition (inside a constructor variant, inside a data def)
+ 3. We are on a keyword field definition (inside a constructor variant)
+ 4. We are on a common keyword field definition (inside a data def)
+ 5. We are on a (common) keyword argument (inside a constructor call)
+ */
+
+ // Let's figure out what kind of field we are exactly
+ if ({loc container} := fields[c], maybeContainerType := getFact(ws, container)) {
+ if ((just(containerType) := maybeContainerType && rascalIsCollectionType(containerType))
+ || maybeContainerType == nothing()) {
+ // Case 1 (or 0): collection field
+ return collectionField();
+ }
+ return rascalGetDataFieldCursorKind(ws, container, cursorLoc, cursorName);
+ }
+ }
+ case {def(), *_}: {
+ // Cursor is at a definition
+ Define d = ws.definitions[c];
+ if (d.idRole is fieldId
+ , Define adt: <_, _, _, dataId(), _, _> <- ws.defines
+ , isStrictlyContainedIn(c, adt.defined)) {
+ return rascalGetDataFieldCursorKind(ws, adt.defined, cursorLoc, cursorName);
+ }
+ return def();
+ }
+ case {use(), *_}: {
+ set[loc] defs = getDefs(ws, c);
+ set[Define] defines = {ws.definitions[d] | d <- defs, ws.definitions[d]?};
+
+ if (d <- defs, just(amodule(_)) := getFact(ws, d)) {
+ // Cursor is at an import
+ return moduleName();
+ } else if (u <- ws.useDef<0>
+ , isContainedIn(cursorLoc, u)
+ , u.end > cursorLoc.end
+ // If the cursor is on a variable, we expect a module variable (`moduleVariable()`); not a local (`variableId()`)
+ , {variableId()} !:= (ws.defines)[getDefs(ws, u)]
+ ) {
+ // Cursor is at a qualified name
+ return moduleName();
+ } else if (defines != {}) {
+ // The cursor is at a use with corresponding definitions.
+ return use();
+ } else if (just(at) := getFact(ws, c)
+ , aparameter(cursorName, _) := at) {
+ // The cursor is at a type parameter
+ return typeParam();
+ }
+ }
+ case {k}: {
+ return k;
+ }
+ }
+
+ throw unsupportedRename("Could not retrieve information for \'\' at .");
+}
+
+private Cursor rascalGetCursor(WorkspaceInfo ws, Tree cursorT) {
+ loc cursorLoc = cursorT.src;
+ str cursorName = "";
+
+ rel[loc field, loc container] fields = {
+ | /Tree t := parseModuleWithSpacesCached(cursorLoc.top)
+ , just() := rascalGetFieldLocs(cursorName, t)
+ , loc fieldLoc <- fieldLocs
+ };
+
+ rel[loc kw, loc container] keywords = {
+ | /Tree t := parseModuleWithSpacesCached(cursorLoc.top)
+ , just() := rascalGetKeywordLocs(cursorName, t)
+ , loc kwLoc <- kwLocs
+ };
+
+ Maybe[loc] smallestFieldContainingCursor = findSmallestContaining(fields.field, cursorLoc);
+ Maybe[loc] smallestKeywordContainingCursor = findSmallestContaining(keywords.kw, cursorLoc);
+
+ rel[loc l, CursorKind kind] locsContainingCursor = {
+
+ | <- {
+ // Uses
+ , cursorLoc), use()>
+ // Defs with an identifier equals the name under the cursor
+ , )[cursorName], cursorLoc), def()>
+ // Type parameters
+ ,
+ // Any kind of field; we'll decide which exactly later
+ ,
+ ,
+ ,
+ ,
+ // Any kind of keyword param; we'll decide which exactly later
+ ,
+ ,
+ ,
+ // Module name declaration, where the cursor location is in the module header
+ ,
+ }
+ };
+
+ if (locsContainingCursor == {}) {
+ throw unsupportedRename("Renaming \'\' at is not supported.");
+ }
+
+ CursorKind kind = rascalGetCursorKind(ws, cursorLoc, cursorName, locsContainingCursor, fields, keywords);
+ return cursor(kind, min(locsContainingCursor.l), cursorName);
+}
+
+private set[Name] rascalNameToEquivalentNames(str name) = {
+ [Name] name,
+ startsWith(name, "\\") ? [Name] name : [Name] "\\"
+};
+
+private bool rascalContainsName(loc l, str name) {
+ m = parseModuleWithSpacesCached(l);
+ for (n <- rascalNameToEquivalentNames(name)) {
+ if (/n := m) return true;
+ }
+ return false;
+}
+
+@synopsis{
+ Rename the Rascal symbol under the cursor. Renames all related (overloaded) definitions and uses of those definitions.
+ Renaming is not supported for some symbols.
+}
+@description {
+ Rename the Rascal symbol under the cursor, across all currently open projects in the workspace.
+ The following symbols are supported.
+ - Variables
+ - Pattern variables
+ - Parameters (positional, keyword)
+ - Functions
+ - Annotations (on values)
+ - Collection fields (tuple, relations)
+ - Modules
+ - Aliases
+ - Data types
+ - Type parameters
+ - Data constructors
+ - Data constructor fields (fields, keyword fields, common keyword fields)
+
+ The following symbols are currently unsupported.
+ - Annotations (on functions)
+
+ *Name resolution*
+ A renaming triggers the typechecker on the currently open file to determine the scope of the renaming.
+ If the renaming is not function-local, it might trigger the type checker on all files in the workspace to find rename candidates.
+ A renaming requires all files in which the name is used to be without errors.
+
+ *Overloading*
+ Considers recognizes overloaded definitions and renames those as well.
+
+ Functions will be considered overloaded when they have the same name, even when the arity or type signature differ.
+ This means that the following functions defitions will be renamed in unison:
+ ```
+ list[&T] concat(list[&T] _, list[&T] _) = _;
+ set[&T] concat(set[&T] _, set[&T] _) = _;
+ set[&T] concat(set[&T] _, set[&T] _, set[&T] _) = _;
+ ```
+
+ *Validity checking*
+ Once all rename candidates have been resolved, validity of the renaming will be checked. A rename is valid iff
+ 1. It does not introduce errors.
+ 2. It does not change the semantics of the application.
+ 3. It does not change definitions outside of the current workspace.
+}
+list[DocumentEdit] rascalRenameSymbol(Tree cursorT, set[loc] workspaceFolders, str newName, PathConfig(loc) getPathConfig)
+ = job("renaming to ", list[DocumentEdit](void(str, int) step) {
+ loc cursorLoc = cursorT.src;
+ str cursorName = "";
+
+ step("collecting workspace information", 1);
+ WorkspaceInfo ws = workspaceInfo(
+ // Get path config
+ getPathConfig,
+ // Preload
+ ProjectFiles() {
+ return { <
+ max([f | f <- workspaceFolders, isPrefixOf(f, cursorLoc)]),
+ cursorLoc.top
+ > };
+ },
+ // Full load
+ ProjectFiles() {
+ return {
+ | folder <- workspaceFolders
+ , PathConfig pcfg := getPathConfig(folder)
+ , srcFolder <- pcfg.srcs
+ , file <- find(srcFolder, "rsc")
+ , file != cursorLoc.top // because we loaded that during preload
+ // If we do not find any occurrences of the name under the cursor in a module,
+ // we are not interested in it at all, and will skip loading its TPL.
+ , rascalContainsName(file, cursorName)
+ };
+ },
+ // Load TModel for loc
+ set[TModel](ProjectFiles projectFiles) {
+ set[TModel] tmodels = {};
+
+ for (projectFolder <- projectFiles.projectFolder, \files := projectFiles[projectFolder]) {
+ PathConfig pcfg = getPathConfig(projectFolder);
+ RascalCompilerConfig ccfg = rascalCompilerConfig(pcfg)[forceCompilationTopModule = true]
+ [verbose = false]
+ [logPathConfig = false];
+ for (file <- \files) {
+ ms = rascalTModelForLocs([file], ccfg, dummy_compile1);
+ tmodels += {convertTModel2PhysicalLocs(tm) | m <- ms.tmodels, tm := ms.tmodels[m]};
+ }
+ }
+ return tmodels;
+ }
+ );
+
+ step("preloading minimal workspace information", 1);
+ ws = preLoad(ws);
+
+ step("analyzing name at cursor", 1);
+ cur = rascalGetCursor(ws, cursorT);
+
+ step("loading required type information", 1);
+ if (!rascalIsFunctionLocal(ws, cur)) {
+ ws = loadWorkspace(ws);
+ }
+
+ step("collecting uses of \'\'", 1);
+ = rascalGetDefsUses(ws, cur, rascalMayOverloadSameName);
+
+ rel[loc file, loc defines] defsPerFile = { | d <- defs};
+ rel[loc file, loc uses] usesPerFile = { | u <- uses};
+
+ set[loc] \files = defsPerFile.file + usesPerFile.file;
+
+ step("checking rename validity", 1);
+ map[loc, tuple[set[IllegalRenameReason] reasons, list[TextEdit] edits]] moduleResults =
+ (file: | file <- \files, := computeTextEdits(ws, file, defsPerFile[file], usesPerFile[file], newName));
+
+ if (reasons := union({moduleResults[file].reasons | file <- moduleResults}), reasons != {}) {
+ list[str] reasonDescs = toList({describe(r) | r <- reasons});
+ throw illegalRename("Rename is not valid, because:\n - ", reasons);
+ }
+
+ list[DocumentEdit] changes = [changed(file, moduleResults[file].edits) | file <- moduleResults];
+ list[DocumentEdit] renames = [renamed(from, to) | <- getRenames(newName)];
+
+ return changes + renames;
+}, totalWork = 6);
+
+//// WORKAROUNDS
+
+// Workaround to be able to pattern match on the emulated `src` field
+data Tree (loc src = |unknown:///|(0,0,<0,0>,<0,0>));
diff --git a/rascal-lsp/src/main/rascal/lang/rascal/lsp/refactor/Util.rsc b/rascal-lsp/src/main/rascal/lang/rascal/lsp/refactor/Util.rsc
new file mode 100644
index 000000000..d062a5df2
--- /dev/null
+++ b/rascal-lsp/src/main/rascal/lang/rascal/lsp/refactor/Util.rsc
@@ -0,0 +1,118 @@
+@license{
+Copyright (c) 2018-2023, 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.
+}
+module lang::rascal::lsp::refactor::Util
+
+import IO;
+import List;
+import Location;
+import Message;
+import String;
+
+import util::Maybe;
+import util::Reflective;
+
+import lang::rascal::\syntax::Rascal;
+
+@synopsis{
+ Finds the smallest location in `wrappers` than contains `l`. If none contains `l`, returns `nothing().`
+ Accepts a predicate deciding containment as an optional argument.
+}
+Maybe[loc] findSmallestContaining(set[loc] wrappers, loc l, bool(loc, loc) containmentPred = isContainedIn) {
+ Maybe[loc] result = nothing();
+ for (w <- wrappers, containmentPred(l, w)) {
+ switch (result) {
+ case just(loc current): if (w < current) result = just(w);
+ case nothing(): result = just(w);
+ }
+ }
+ return result;
+}
+
+@synopsis{
+ Resizes a location by removing a prefix and/or suffix.
+}
+loc trim(loc l, int removePrefix = 0, int removeSuffix = 0) {
+ assert l.begin.line == l.end.line :
+ "Cannot trim a multi-line location";
+ return l[offset = l.offset + removePrefix]
+ [length = l.length - removePrefix - removeSuffix]
+ [begin = ]
+ [end = ];
+}
+
+@synopsis{
+ Decides if `prefix` is a prefix of `l`.
+}
+bool isPrefixOf(loc prefix, loc l) = l.scheme == prefix.scheme
+ && l.authority == prefix.authority
+ && startsWith(l.path, endsWith(prefix.path, "/") ? prefix.path : prefix.path + "/");
+
+@synopsis{
+ A cached wrapper for the Rascal whole-module parse function.
+}
+start[Module] parseModuleWithSpacesCached(loc l) {
+ @memo{expireAfter(minutes=5)} start[Module] parseModuleWithSpacesCached(loc l, datetime _) = parseModuleWithSpaces(l);
+ return parseModuleWithSpacesCached(l, lastModified(l));
+}
+
+Maybe[&B] flatMap(nothing(), Maybe[&B](&A) _) = nothing();
+Maybe[&B] flatMap(just(&A a), Maybe[&B](&A) f) = f(a);
+
+str toString(error(msg, l)) = "[error] \'\' at ";
+str toString(error(msg)) = "[error] \'\'";
+str toString(warning(msg, l)) = "[warning] \'\' at ";
+str toString(info(msg, l)) = "[info] \'\' at ";
+
+str toString(list[Message] msgs, int indent = 1) =
+ intercalate("\n", ([] | it + " <}>- " | msg <- msgs));
+
+str toString(map[str, list[Message]] moduleMsgs) =
+ intercalate("\n", ([] | it + "Messages for :\n" | m <- moduleMsgs));
+
+rel[&K, &V] groupBy(set[&V] s, &K(&V) pred) =
+ { | v <- s};
+
+@synopsis{
+ Predicate to sort locations by length.
+}
+bool isShorter(loc l1, loc l2) = l1.length < l2.length;
+
+bool isShorterTuple(tuple[loc, &T] t1, tuple[loc, &T] t2) = isShorter(t1[0], t2[0]);
+
+@synopsis{
+ Predicate to sort locations by offset.
+}
+bool byOffset(loc l1, loc l2) = l1.offset < l2.offset;
+
+@synopsis{
+ Predicate to reverse a sort order.
+}
+bool(&T, &T) desc(bool(&T, &T) f) {
+ return bool(&T t1, &T t2) {
+ return f(t2, t1);
+ };
+}
diff --git a/rascal-lsp/src/main/rascal/lang/rascal/lsp/refactor/WorkspaceInfo.rsc b/rascal-lsp/src/main/rascal/lang/rascal/lsp/refactor/WorkspaceInfo.rsc
new file mode 100644
index 000000000..9c1d79292
--- /dev/null
+++ b/rascal-lsp/src/main/rascal/lang/rascal/lsp/refactor/WorkspaceInfo.rsc
@@ -0,0 +1,595 @@
+@license{
+Copyright (c) 2018-2023, 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.
+}
+@bootstrapParser
+module lang::rascal::lsp::refactor::WorkspaceInfo
+
+import Relation;
+
+import analysis::typepal::TModel;
+
+import lang::rascalcore::check::Checker;
+
+import lang::rascal::\syntax::Rascal;
+
+import util::Maybe;
+import util::Reflective;
+
+import lang::rascal::lsp::refactor::Exception;
+import lang::rascal::lsp::refactor::Util;
+
+import List;
+import Location;
+import Map;
+import Set;
+import String;
+
+data CursorKind
+ = use()
+ | def()
+ | typeParam()
+ | collectionField()
+ | dataField(loc dataTypeDef, AType fieldType)
+ | dataKeywordField(loc dataTypeDef, AType fieldType)
+ | dataCommonKeywordField(loc dataTypeDef, AType fieldType)
+ | keywordParam()
+ | moduleName()
+ ;
+
+data Cursor
+ = cursor(CursorKind kind, loc l, str name)
+ ;
+
+alias MayOverloadFun = bool(set[loc] defs, map[loc, Define] defines);
+alias FileRenamesF = rel[loc old, loc new](str newName);
+alias DefsUsesRenames = tuple[set[loc] defs, set[loc] uses, FileRenamesF renames];
+alias ProjectFiles = rel[loc projectFolder, loc file];
+
+/**
+ * This is a subset of the fields from analysis::typepal::TModel, specifically tailored to refactorings.
+ * WorkspaceInfo comes with a set of functions that allow (incrementally) loading information from multiple TModels, and doing (cached) queries on this data.
+ */
+data WorkspaceInfo (
+ // Instance fields
+ // Read-only
+ rel[loc use, loc def] useDef = {},
+ set[Define] defines = {},
+ set[loc] sourceFiles = {},
+ map[loc, Define] definitions = (),
+ map[loc, AType] facts = (),
+ Scopes scopes = (),
+ Paths paths = {},
+ set[loc] projects = {}
+) = workspaceInfo(
+ PathConfig(loc) getPathConfig,
+ ProjectFiles() preloadFiles,
+ ProjectFiles() allFiles,
+ set[TModel](ProjectFiles) tmodelsForLocs
+);
+
+WorkspaceInfo loadLocs(WorkspaceInfo ws, ProjectFiles projectFiles) {
+ for (tm <- ws.tmodelsForLocs(projectFiles)) {
+ ws = loadTModel(ws, tm);
+ }
+
+ // In addition to data from the TModel, we keep track of which projects/modules we loaded.
+ ws.sourceFiles += projectFiles.file;
+ ws.projects += projectFiles.projectFolder;
+
+ return ws;
+}
+
+WorkspaceInfo preLoad(WorkspaceInfo ws) {
+ return loadLocs(ws, ws.preloadFiles());
+}
+
+WorkspaceInfo loadWorkspace(WorkspaceInfo ws) {
+ return loadLocs(ws, ws.allFiles());
+}
+
+WorkspaceInfo loadTModel(WorkspaceInfo ws, TModel tm) {
+ try {
+ throwAnyErrors(tm);
+ } catch set[Message] errors: {
+ throw unsupportedRename("Cannot rename: some files in workspace have errors.\n", issues={<(error.at ? |unknown:///|), error.msg> | error <- errors});
+ }
+
+ ws.useDef += tm.useDef;
+ ws.defines += tm.defines;
+ ws.definitions += tm.definitions;
+ ws.facts += tm.facts;
+ ws.scopes += tm.scopes;
+ ws.paths += tm.paths;
+
+ return ws;
+}
+
+loc getProjectFolder(WorkspaceInfo ws, loc l) {
+ if (project <- ws.projects, isPrefixOf(project, l)) {
+ return project;
+ }
+
+ throw "Could not find project containing ";
+}
+
+@memo{maximumSize(1), expireAfter(minutes=5)}
+rel[loc, loc] defUse(WorkspaceInfo ws) = invert(ws.useDef);
+
+@memo{maximumSize(1), expireAfter(minutes=5)}
+map[AType, set[loc]] factsInvert(WorkspaceInfo ws) = invert(ws.facts);
+
+set[loc] getUses(WorkspaceInfo ws, loc def) = defUse(ws)[def];
+
+set[loc] getUses(WorkspaceInfo ws, set[loc] defs) = defUse(ws)[defs];
+
+set[loc] getDefs(WorkspaceInfo ws, loc use) = ws.useDef[use];
+
+Maybe[AType] getFact(WorkspaceInfo ws, loc l) = l in ws.facts ? just(ws.facts[l]) : nothing();
+
+@memo{maximumSize(1), expireAfter(minutes=5)}
+set[loc] getModuleScopes(WorkspaceInfo ws) = invert(ws.scopes)[|global-scope:///|];
+
+@memo{maximumSize(1), expireAfter(minutes=5)}
+map[loc, loc] getModuleScopePerFile(WorkspaceInfo ws) = (scope.top: scope | loc scope <- getModuleScopes(ws));
+
+@memo{maximumSize(1), expireAfter(minutes=5)}
+rel[loc from, loc to] rascalGetTransitiveReflexiveModulePaths(WorkspaceInfo ws) {
+ rel[loc from, loc to] moduleI = ident(getModuleScopes(ws));
+ rel[loc from, loc to] imports = (ws.paths)[importPath()];
+ rel[loc from, loc to] extends = (ws.paths)[extendPath()];
+
+ return (moduleI + imports) // 0 or 1 imports
+ o (moduleI + extends+) // 0 or more extends
+ ;
+}
+
+@memo{maximumSize=1, minutes=5}
+rel[loc from, loc to] rascalGetTransitiveReflexiveScopes(WorkspaceInfo ws) = toRel(ws.scopes)*;
+
+@memo{maximumSize=10, minutes=5}
+set[loc] rascalReachableModules(WorkspaceInfo ws, set[loc] froms) {
+ rel[loc from, loc scope] fromScopes = {};
+ for (from <- froms) {
+ if (scope <- ws.scopes<1>, isContainedIn(from, scope)) {
+ fromScopes += ;
+ }
+ }
+ rel[loc from, loc modScope] reachable =
+ fromScopes
+ o rascalGetTransitiveReflexiveScopes(ws)
+ o rascalGetTransitiveReflexiveModulePaths(ws);
+
+ return {s.top | s <- reachable.modScope};
+}
+
+@memo{maximumSize=1, minutes=5}
+rel[loc, Define] definitionsRel(WorkspaceInfo ws) = toRel(ws.definitions);
+
+set[Define] rascalReachableDefs(WorkspaceInfo ws, set[loc] defs) {
+ rel[loc from, loc to] modulePaths = rascalGetTransitiveReflexiveModulePaths(ws);
+ rel[loc from, loc to] scopes = rascalGetTransitiveReflexiveScopes(ws);
+ rel[loc from, Define define] reachableDefs =
+ (ws.defines)[defs] // pairs
+ o scopes // All scopes surrounding defs
+ o modulePaths // Transitive-reflexive paths from scope to reachable modules
+ o ws.defines // Definitions in these scopes
+ o definitionsRel(ws); // Full define tuples
+ return reachableDefs.define; // We are only interested in reached defines; not *from where* they were reached
+}
+
+set[loc] rascalGetOverloadedDefs(WorkspaceInfo ws, set[loc] defs, MayOverloadFun mayOverloadF) {
+ if (defs == {}) return {};
+ set[loc] overloadedDefs = defs;
+
+ set[IdRole] roles = definitionsRel(ws)[defs].idRole;
+
+ // Pre-conditions
+ assert size(roles) == 1:
+ "Initial defs are of different roles!";
+ assert mayOverloadF(overloadedDefs, ws.definitions):
+ "Initial defs are invalid overloads!";
+
+ IdRole role = getFirstFrom(roles);
+ map[loc file, loc scope] moduleScopePerFile = getModuleScopePerFile(ws);
+ rel[loc def, loc scope] defUseScopes = { | <- ws.useDef};
+ rel[loc from, loc to] modulePaths = rascalGetTransitiveReflexiveModulePaths(ws);
+ rel[loc def, loc scope] defScopes = ws.defines+;
+ rel[loc scope, loc defined] scopeDefs =
+ (ws.defines)+ // Follow definition scopes ...
+ o ((ws.defines)[role]) // Until we arrive at a definition with the same role ...
+ ;
+
+ rel[loc from, loc to] fromDefPaths =
+ (defScopes + defUseScopes) // 1. Look up scopes of defs and scopes of their uses
+ o modulePaths // 2. Follow import/extend relations to reachable scopes
+ o scopeDefs // 3. Find definitions in the reached scope, and definitions within those definitions (transitively)
+ ;
+
+ rel[loc from, loc to] defPaths = fromDefPaths + invert(fromDefPaths);
+
+ solve(overloadedDefs) {
+ rel[loc from, loc to] reachableDefs = ident(overloadedDefs) o defPaths;
+
+ overloadedDefs += {d
+ | loc d <- reachableDefs<1>
+ , mayOverloadF(overloadedDefs + d, ws.definitions)
+ };
+ }
+
+ return overloadedDefs;
+}
+
+private rel[loc, loc] NO_RENAMES(str _) = {};
+private int qualSepSize = size("::");
+
+bool rascalIsCollectionType(AType at) = at is arel || at is alrel || at is atuple;
+bool rascalIsConstructorType(AType at) = at is acons;
+bool rascalIsDataType(AType at) = at is aadt;
+
+bool rascalMayOverloadSameName(set[loc] defs, map[loc, Define] definitions) {
+ if (l <- defs, !definitions[l]?) return false;
+ set[str] names = {definitions[l].id | l <- defs};
+ if (size(names) > 1) return false;
+
+ map[loc, Define] potentialOverloadDefinitions = (l: d | l <- definitions, d := definitions[l], d.id in names);
+ return rascalMayOverload(defs, potentialOverloadDefinitions);
+}
+
+set[Define] rascalGetADTDefinitions(WorkspaceInfo ws, loc lhs) {
+ set[loc] fromDefs = (ws.definitions[lhs]? || lhs in ws.useDef<1>)
+ ? {lhs}
+ : getDefs(ws, lhs)
+ ;
+
+ AType lhsType = ws.facts[lhs];
+ if (rascalIsConstructorType(lhsType)) {
+ return {adt
+ | loc cD <- rascalGetOverloadedDefs(ws, fromDefs, rascalMayOverloadSameName)
+ , Define cons: <_, _, _, constructorId(), _, _> := ws.definitions[cD]
+ , AType consAdtType := cons.defInfo.atype.adt
+ , Define adt: <_, _, _, dataId(), _, defType(consAdtType)> <- rascalReachableDefs(ws, {cons.defined})
+ };
+ } else if (rascalIsDataType(lhsType)) {
+ return {adt
+ | set[loc] overloads := rascalGetOverloadedDefs(ws, fromDefs, rascalMayOverloadSameName)
+ , Define adt: <_, _, _, dataId(), _, defType(lhsType)> <- rascalReachableDefs(ws, overloads)
+ };
+ }
+
+ return {};
+}
+
+set[loc] rascalGetKeywordFormalUses(WorkspaceInfo ws, set[loc] defs, str cursorName) {
+ set[loc] uses = {};
+
+ for (d <- defs
+ , f <- ws.facts
+ , isStrictlyContainedIn(d, f)
+ , afunc(retType, _, [*_, kwField(kwAType, cursorName, _), *_]) := ws.facts[f]
+ , funcName <- getUses(ws, f)
+ ) {
+ loc funcCall = min({l | l <- factsInvert(ws)[retType], l.offset == funcName.offset});
+ uses += {name.src | /Name name := parseModuleWithSpacesCached(funcCall.top)
+ , isStrictlyContainedIn(name.src, funcCall)
+ , "" == kwAType.alabel};
+ }
+
+ return uses;
+}
+
+set[loc] rascalGetKeywordFormals((KeywordFormals) ``, str _) = {};
+set[loc] rascalGetKeywordFormals((KeywordFormals) ` <{KeywordFormal ","}+ keywordFormals>`, str cursorName) =
+ rascalGetKeywordFormalList(keywordFormals, cursorName);
+
+set[loc] rascalGetKeywordFormalList([{KeywordFormal ","}+] keywordFormals, str cursorName) =
+ { kwFormal.name.src
+ | kwFormal <- keywordFormals
+ , "" == cursorName};
+
+set[loc] rascalGetKeywordArgs(none(), str _) = {};
+set[loc] rascalGetKeywordArgs(\default(_, keywordArgs), str cursorName) =
+ { kwArg.name.src
+ | kwArg <- keywordArgs
+ , "" == cursorName};
+
+set[loc] rascalGetKeywordFieldUses(WorkspaceInfo ws, set[loc] defs, str cursorName) {
+ set[loc] uses = getUses(ws, defs);
+
+ set[Define] reachableDefs = rascalReachableDefs(ws, defs);
+
+ for (d <- defs
+ , Define dataDef: <_, _, _, dataId(), _, _> <- reachableDefs
+ , isStrictlyContainedIn(d, dataDef.defined)
+ , Define consDef: <_, _, _, constructorId(), _, _> <- reachableDefs
+ , isStrictlyContainedIn(consDef.defined, dataDef.defined)
+ ) {
+ if (AType fieldType := ws.definitions[d].defInfo.atype) {
+ set[loc] reachableModules = rascalReachableModules(ws, {d});
+ if (<{}, consUses, _> := rascalGetFieldDefsUses(ws, reachableModules, dataDef.defInfo.atype, fieldType, cursorName)) {
+ uses += consUses;
+ }
+ } else {
+ throw unsupportedRename("Unknown type for definition at : ");
+ }
+ }
+
+ return uses;
+}
+
+DefsUsesRenames rascalGetDefsUses(WorkspaceInfo ws, cursor(use(), l, cursorName), MayOverloadFun mayOverloadF) {
+ defs = rascalGetOverloadedDefs(ws, getDefs(ws, l), mayOverloadF);
+ uses = getUses(ws, defs);
+
+ if (keywordFormalId() in {ws.definitions[d].idRole | d <- defs}) {
+ uses += rascalGetKeywordFormalUses(ws, defs, cursorName);
+ uses += rascalGetKeywordFieldUses(ws, defs, cursorName);
+ }
+
+ return ;
+}
+
+DefsUsesRenames rascalGetDefsUses(WorkspaceInfo ws, cursor(def(), l, cursorName), MayOverloadFun mayOverloadF) {
+ set[loc] initialUses = getUses(ws, l);
+ set[loc] initialDefs = {l} + {*ds | u <- initialUses, ds := getDefs(ws, u)};
+ defs = rascalGetOverloadedDefs(ws, initialDefs, mayOverloadF);
+ uses = getUses(ws, defs);
+
+ if (keywordFormalId() in {ws.definitions[d].idRole | d <- defs}) {
+ uses += rascalGetKeywordFormalUses(ws, defs, cursorName);
+ uses += rascalGetKeywordFieldUses(ws, defs, cursorName);
+ }
+
+ return ;
+}
+
+DefsUsesRenames rascalGetDefsUses(WorkspaceInfo ws, cursor(typeParam(), cursorLoc, cursorName), MayOverloadFun _) {
+ AType at = ws.facts[cursorLoc];
+ set[loc] defs = {};
+ set[loc] useDefs = {};
+ for (Define d: <_, _, _, _, _, defType(afunc(_, /at, _))> <- ws.defines, isContainedIn(cursorLoc, d.defined)) {
+ // From here on, we can assume that all locations are in the same file, because we are dealing with type parameters and filtered on `isContainedIn`
+ facts = { | l <- ws.facts
+ , at := ws.facts[l]
+ , isContainedIn(l, d.defined)};
+
+ formals = {l | <- facts, f.alabel != ""};
+
+ // Given the location/offset of `&T`, find the location/offset of `T`
+ offsets = sort({l.offset | l <- facts<0>});
+ nextOffsets = toMapUnique(zip2(prefix(offsets), tail(offsets)));
+
+ loc sentinel = |unknown:///|(0, 0, <0, 0>, <0, 0>);
+ defs += {min([f | f <- facts<0>, f.offset == nextOffset]) | formal <- formals, nextOffsets[formal.offset]?, nextOffset := nextOffsets[formal.offset]};
+
+ useDefs += {trim(l, removePrefix = l.length - size(cursorName))
+ | l <- facts<0>
+ , !ws.definitions[l]? // If there is a definition at this location, this is a formal argument name
+ , !any(ud <- ws.useDef[l], ws.definitions[ud]?) // If there is a definition for the use at this location, this is a use of a formal argument
+ , !any(flInner <- facts<0>, isStrictlyContainedIn(flInner, l)) // Filter out any facts that contain other facts
+ };
+
+
+ }
+
+ return ;
+}
+
+DefsUsesRenames rascalGetDefsUses(WorkspaceInfo ws, cursor(dataField(loc adtLoc, AType fieldType), cursorLoc, cursorName), MayOverloadFun mayOverloadF) {
+ set[loc] initialDefs = {};
+ if (cursorLoc in ws.useDef<0>) {
+ initialDefs = getDefs(ws, cursorLoc);
+ } else if (cursorLoc in ws.defines) {
+ initialDefs = {cursorLoc};
+ } else if (just(AType adtType) := getFact(ws, cursorLoc)) {
+ set[Define] reachableDefs = rascalReachableDefs(ws, {cursorLoc});
+ initialDefs = {
+ kwDef.defined
+ | Define dataDef: <_, _, _, dataId(), _, defType(adtType)> <- rascalGetADTDefinitions(ws, cursorLoc)
+ , Define kwDef: <_, _, cursorName, keywordFormalId(), _, _> <- reachableDefs
+ , isStrictlyContainedIn(kwDef.defined, dataDef.defined)
+ };
+ } else {
+ throw unsupportedRename("Cannot rename data field \'\' from ");
+ }
+
+ set[loc] defs = rascalGetOverloadedDefs(ws, initialDefs, mayOverloadF);
+ set[loc] uses = getUses(ws, defs) + rascalGetKeywordFieldUses(ws, defs, cursorName);
+
+ return ;
+}
+
+bool debug = false;
+
+DefsUsesRenames rascalGetDefsUses(WorkspaceInfo ws, cursor(cursorKind, cursorLoc, cursorName), MayOverloadFun mayOverloadF) {
+ if (cursorKind is dataKeywordField || cursorKind is dataCommonKeywordField) {
+ set[loc] defs = {};
+ set[loc] uses = {};
+
+ set[loc] adtDefs = rascalGetOverloadedDefs(ws, {cursorKind.dataTypeDef}, mayOverloadF);
+ set[Define] reachableDefs = rascalReachableDefs(ws, adtDefs);
+ set[loc] reachableModules = rascalReachableModules(ws, reachableDefs.defined);
+
+ for (Define _:<_, _, _, constructorId(), _, defType(AType consType)> <- reachableDefs) {
+ = rascalGetFieldDefsUses(ws, reachableModules, consType, cursorKind.fieldType, cursorName);
+ defs += ds;
+ uses += us;
+ }
+ for (Define _:<_, _, _, IdRole idRole, _, defType(acons(AType dataType, _, _))> <- reachableDefs
+ , idRole != dataId()) {
+ += rascalGetFieldDefsUses(ws, reachableModules, dataType, cursorKind.fieldType, cursorName);
+ defs += ds;
+ uses += us;
+ }
+
+ return ;
+ }
+
+ fail;
+}
+
+DefsUsesRenames rascalGetDefsUses(WorkspaceInfo ws, cursor(collectionField(), cursorLoc, cursorName), MayOverloadFun _) {
+ bool isTupleField(AType fieldType) = fieldType.alabel == "";
+
+ lrel[loc, AType] factsBySize = sort(toRel(ws.facts), isShorterTuple);
+ AType cursorType = avoid();
+
+ if (ws.facts[cursorLoc]?) {
+ cursorType = ws.facts[cursorLoc];
+ } else if (just(loc fieldAccess) := findSmallestContaining(ws.facts<0>, cursorLoc)
+ , just(AType collectionType) := getFact(ws, fieldAccess)) {
+ cursorType = collectionType;
+ }
+
+ if ( <- factsBySize
+ , isStrictlyContainedIn(cursorLoc, l)
+ , rascalIsCollectionType(collUseType)
+ , collUseType.elemType is atypeList) {
+ // We are at a collection definition site
+ return rascalGetFieldDefsUses(ws, rascalReachableModules(ws, {cursorLoc}), collUseType, cursorType, cursorName);
+ }
+
+ // We can find the collection type by looking for the first use to the left of the cursor that has a collection type
+ lrel[loc use, loc def] usesToLeft = reverse(sort({ | <- ws.useDef, isSameFile(u, cursorLoc), u.offset < cursorLoc.offset}));
+ if (<_, d> <- usesToLeft, define := ws.definitions[d], defType(AType collDefType) := define.defInfo, rascalIsCollectionType(collDefType)) {
+ // We are at a use site, where the field element type is wrapped in a `aset` of `alist` constructor
+ return rascalGetFieldDefsUses(ws, rascalReachableModules(ws, {define.defined}), collDefType, isTupleField(cursorType) ? cursorType.elmType : cursorType, cursorName);
+ } else {
+ throw unsupportedRename("Could not find a collection definition corresponding to the field at the cursor.");
+ }
+}
+
+Maybe[tuple[loc, set[loc], bool]] rascalGetFieldLocs(str fieldName, (Expression) `.`) =
+ just() when fieldName == "";
+
+Maybe[tuple[loc, set[loc], bool]] rascalGetFieldLocs(str fieldName, (Assignable) `.`) =
+ just() when fieldName == "";
+
+Maybe[tuple[loc, set[loc], bool]] rascalGetFieldLocs(str fieldName, (Expression) `\< <{Field ","}+ fields> \>`) {
+ fieldLocs = {field.src
+ | field <- fields
+ , field is name
+ , "" == fieldName
+ };
+
+ return fieldLocs != {} ? just() : nothing();
+}
+
+Maybe[tuple[loc, set[loc], bool]] rascalGetFieldLocs(str fieldName, (Expression) `[ = ]`) =
+ just() when fieldName == "";
+
+Maybe[tuple[loc, set[loc], bool]] rascalGetFieldLocs(str fieldName, (StructuredType) `[<{TypeArg ","}+ args>]`) {
+ fieldLocs = {name.src | (TypeArg) ` ` <- args, fieldName == ""};
+ return fieldLocs != {} ? just() : nothing();
+}
+
+default Maybe[tuple[loc, set[loc], bool]] rascalGetFieldLocs(str fieldName, Tree _) = nothing();
+
+Maybe[tuple[loc, set[loc], bool]] rascalGetKeywordLocs(str fieldName, (Expression) `(<{Expression ","}* _> )`) =
+ just();
+
+
+Maybe[tuple[loc, set[loc], bool]] rascalGetKeywordLocs(str fieldName, (Pattern) `(<{Pattern ","}* _> )`) =
+ just();
+
+Maybe[tuple[loc, set[loc], bool]] rascalGetKeywordLocs(str fieldName, (Variant) `(<{TypeArg ","}* _> )`) =
+ just();
+
+Maybe[tuple[loc, set[loc], bool]] rascalGetKeywordLocs(str fieldName, d:(Declaration) ` data (<{KeywordFormal ","}+ kwFormalList>) = <{Variant "|"}+ _>;`) =
+ just();
+
+Maybe[tuple[loc, set[loc], bool]] rascalGetKeywordLocs(str fieldName, d:(Declaration) ` data (<{KeywordFormal ","}+ kwFormalList>);`) =
+ just();
+
+default Maybe[tuple[loc, set[loc], bool]] rascalGetKeywordLocs(str _, Tree _) = nothing();
+
+private DefsUsesRenames rascalGetFieldDefsUses(WorkspaceInfo ws, set[loc] reachableModules, AType containerType, AType fieldType, str cursorName) {
+ set[loc] containerFacts = {f | f <- factsInvert(ws)[containerType], f.top in reachableModules};
+ rel[loc file, loc u] factsByModule = groupBy(containerFacts, loc(loc l) { return l.top; });
+
+ set[loc] defs = {};
+ set[loc] uses = {};
+ for (file <- factsByModule.file) {
+ fileFacts = factsByModule[file];
+ for (/Tree t := parseModuleWithSpacesCached(file)
+ , just() := rascalGetFieldLocs(cursorName, t) || just() := rascalGetKeywordLocs(cursorName, t)) {
+ if((StructuredType) `[<{TypeArg ","}+ _>]` := t) {
+ if(at := ws.facts[t.src]
+ , containerType.elemType?
+ , containerType.elemType == at.elemType) {
+ defs += {f | f <- fields, just(fieldType) := getFact(ws, f)};
+ }
+ } else if (isDef) {
+ defs += fields;
+ } else if (any(f <- fileFacts, isContainedIn(f, lhs))) {
+ uses += fields;
+ }
+ }
+ }
+
+ return ;
+}
+
+DefsUsesRenames rascalGetDefsUses(WorkspaceInfo ws, cursor(moduleName(), cursorLoc, cursorName), MayOverloadFun _) {
+ loc moduleFile = |unknown:///|;
+ if (d <- ws.useDef[cursorLoc], amodule(_) := ws.facts[d]) {
+ // Cursor is at an import
+ moduleFile = d.top;
+ } else if (just(l) := findSmallestContaining(ws.facts<0>, cursorLoc), amodule(_) := ws.facts[l]) {
+ // Cursor is at a module header
+ moduleFile = l.top;
+ } else {
+ // Cursor is at the module part of a qualified use
+ if ( <- ws.useDef, u.begin <= cursorLoc.begin, u.end == cursorLoc.end) {
+ moduleFile = d.top;
+ } else {
+ throw unsupportedRename("Could not find cursor location in TPL.");
+ }
+ }
+
+ modName = getModuleName(moduleFile, ws.getPathConfig(getProjectFolder(ws, moduleFile)));
+
+ defs = {parseModuleWithSpacesCached(moduleFile).top.header.name.names[-1].src};
+
+ imports = {u | u <- ws.useDef<0>, amodule(modName) := ws.facts[u]};
+ qualifiedUses = {
+ // We compute the location of the module name in the qualified name at `u`
+ // some::qualified::path::to::Foo::SomeVar
+ // \____________________________/\/\_____/
+ // moduleNameSize ^ qualSepSize ^ ^ idSize
+ trim(u, removePrefix = moduleNameSize - size(cursorName)
+ , removeSuffix = idSize + qualSepSize)
+ | <- ws.useDef o definitionsRel(ws)
+ , idSize := size(d.id)
+ , u.length > idSize // There might be a qualified prefix
+ , moduleNameSize := size(modName)
+ , u.length == moduleNameSize + qualSepSize + idSize
+ };
+ uses = imports + qualifiedUses;
+
+ rel[loc, loc] getRenames(str newName) = {.rsc"]> | d <- defs, file := d.top};
+
+ return ;
+}
diff --git a/rascal-lsp/src/main/rascal/lang/rascal/tests/Rename.rsc b/rascal-lsp/src/main/rascal/lang/rascal/tests/Rename.rsc
deleted file mode 100644
index a7b7836b2..000000000
--- a/rascal-lsp/src/main/rascal/lang/rascal/tests/Rename.rsc
+++ /dev/null
@@ -1,505 +0,0 @@
-@license{
-Copyright (c) 2018-2023, 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.
-}
-module lang::rascal::tests::Rename
-
-import lang::rascal::lsp::Rename;
-
-import Exception;
-import IO;
-import String;
-
-import lang::rascal::\syntax::Rascal;
-
-import lang::rascalcore::check::Checker;
-import lang::rascalcore::check::BasicRascalConfig;
-import lang::rascalcore::check::RascalConfig;
-
-import analysis::diff::edits::TextEdits;
-
-import util::Math;
-import util::Reflective;
-
-test bool freshName() = {0} == testRenameOccurrences("
- 'int foo = 8;
- 'int qux = 10;
-");
-
-test bool shadowVariableInInnerScope() = {0} == testRenameOccurrences("
- 'int foo = 8;
- '{
- ' int bar = 9;
- '}
-");
-
-test bool parameterShadowsVariable() = {0} == testRenameOccurrences("
- 'int foo = 8;
- 'int f(int bar) {
- ' return bar;
- '}
-");
-
-@expected{illegalRename}
-test bool implicitVariableDeclarationInSameScopeBecomesUse() = testRename("
- 'int foo = 8;
- 'bar = 9;
-");
-
-@expected{illegalRename}
-test bool implicitVariableDeclarationInInnerScopeBecomesUse() = testRename("
- 'int foo = 8;
- '{
- ' bar = 9;
- '}
-");
-
-@expected{illegalRename}
-test bool doubleVariableDeclaration() = testRename("
- 'int foo = 8;
- 'int bar = 9;
-");
-
-@expected{illegalRename}
-test bool doubleVariableAndParameterDeclaration() = testRename("
- 'int f(int foo) {
- ' int bar = 9;
- ' return foo + bar;
- '}
-");
-
-test bool adjacentScopes() = {0} == testRenameOccurrences("
- '{
- ' int foo = 8;
- '}
- '{
- ' int bar = 9;
- '}
-");
-
-@expected{illegalRename}
-test bool implicitPatterVariableInSameScopeBecomesUse() = testRename("
- 'int foo = 8;
- 'bar := 9;
-");
-
-@expected{illegalRename}
-test bool implicitNestedPatterVariableInSameScopeBecomesUse() = testRename("
- 'int foo = 8;
- '\ := \<9, 99\>;
-");
-
-@expected{illegalRename}
-test bool implicitPatterVariableInInnerScopeBecomesUse() = testRename("
- 'int foo = 8;
- 'if (bar := 9) {
- ' temp = 2 * bar;
- '}
-");
-
-test bool explicitPatternVariableInInnerScope() = {0} == testRenameOccurrences("
- 'int foo = 8;
- 'if (int bar := 9) {
- ' bar = 2 * bar;
- '}
-");
-
-test bool becomesPatternInInnerScope() = {0} == testRenameOccurrences("
- 'int foo = 8;
- 'if (bar : int _ := 9) {
- ' bar = 2 * bar;
- '}
-");
-
-@expected{illegalRename}
-test bool implicitPatternVariableBecomesInInnerScope() = testRename("
- 'int foo = 8;
- 'if (bar : _ := 9) {
- ' bar = 2 * foo;
- '}
-");
-
-@expected{illegalRename}
-test bool explicitPatternVariableBecomesInInnerScope() = testRename("
- 'int foo = 8;
- 'if (bar : int _ := 9) {
- ' bar = 2 * foo;
- '}
-");
-
-@expected{illegalRename}
-test bool shadowDeclaration() = testRename("
- 'int foo = 8;
- 'if (int bar := 9) {
- ' foo = 2 * bar;
- '}
-");
-
-// Although this is fine statically, it will cause runtime errors when `bar` is called
-// > A value of type int is not something you can call like a function, a constructor or a closure.
-@expected{illegalRename}
-test bool doubleVariableAndFunctionDeclaration() = testRename("
- 'int foo = 8;
- 'void bar() {}
-");
-
-// Although this is fine statically, it will cause runtime errors when `bar` is called
-// > A value of type int is not something you can call like a function, a constructor or a closure.
-@expected{illegalRename}
-test bool doubleFunctionAndVariableDeclaration() = testRename("
- 'void bar() {}
- 'foo = 8;
-");
-
-@expected{illegalRename}
-test bool doubleFunctionAndNestedVariableDeclaration() = testRename("
- 'bool bar() = true;
- 'void f() {
- ' int foo = 0;
- '}
-");
-
-@expected{illegalRename}
-test bool captureFunctionParameter() = testRename("
- 'int f(int foo) {
- ' int bar = 9;
- ' return foo + bar;
- '}
-");
-
-test bool paremeterShadowsParameter1() = {0, 3} == testRenameOccurrences("
- 'int f1(int foo) {
- ' int f2(int foo) {
- ' int baz = 9;
- ' return foo + baz;
- ' }
- ' return f2(foo);
- '}
-");
-
-test bool paremeterShadowsParameter2() = {1, 2} == testRenameOccurrences("
- 'int f1(int foo) {
- ' int f2(int foo) {
- ' int baz = 9;
- ' return foo + baz;
- ' }
- ' return f2(foo);
- '}
-", cursorAtOldNameOccurrence = 1);
-
-@expected{illegalRename}
-test bool paremeterShadowsParameter3() = testRename("
- 'int f(int bar) {
- ' int g(int baz) {
- ' int h(int foo) {
- ' return bar;
- ' }
- ' return h(baz);
- ' }
- ' return g(bar);
- '}
-");
-
-test bool nestedFunctionParameter() = {0, 1} == testRenameOccurrences("
- 'int f(int foo, int baz) {
- ' return foo;
- '}
-");
-
-test bool nestedRecursiveFunctionName() = {0, 1, 2, 3} == testRenameOccurrences("
- 'int fib(int n) {
- ' switch (n) {
- ' case 0: {
- ' return 1;
- ' }
- ' case 1: {
- ' return 1;
- ' }
- ' default: {
- ' return fib(n - 1) + fib(n - 2);
- ' }
- ' }
- '}
- '
- 'fib(7);
-", oldName = "fib", newName = "fibonacci", cursorAtOldNameOccurrence = -1);
-
-@expected{unsupportedRename}
-test bool recursiveFunctionName() = {0, 1, 2, 3} == testRenameOccurrences("fib(7);", decls = "
- 'int fib(int n) {
- ' switch (n) {
- ' case 0: {
- ' return 1;
- ' }
- ' case 1: {
- ' return 1;
- ' }
- ' default: {
- ' return fib(n - 1) + fib(n - 2);
- ' }
- ' }
- '}
-", oldName = "fib", newName = "fibonacci", cursorAtOldNameOccurrence = -1);
-
-test bool nestedPublicFunction() = {0, 1} == testRenameOccurrences("
- 'public int foo(int f) {
- ' return f;
- '}
- 'foo(1);
-");
-
-test bool nestedDefaultFunction() = {0, 1} == testRenameOccurrences("
- 'int foo(int f) {
- ' return f;
- '}
- 'foo(1);
-");
-
-test bool nestedPrivateFunction() = {0, 1} == testRenameOccurrences("
- 'private int foo(int f) {
- ' return f;
- '}
- 'foo(1);
-");
-
-test bool outerNestedFunctionParameter() = {0, 3} == testRenameOccurrences("
- 'int f(int foo) {
- ' int g(int foo) {
- ' return foo;
- ' }
- ' return f(foo);
- '}
-");
-
-test bool innerNestedFunctionParameter() = {1, 2} == testRenameOccurrences("
- 'int f(int foo) {
- ' int g(int foo) {
- ' return foo;
- ' }
- ' return f(foo);
- '}
-", cursorAtOldNameOccurrence = 1);
-
-@expected{unsupportedRename}
-test bool publicFunction() = testRename("foo(1);", decls = "
- 'public int foo(int f) {
- ' return f;
- '}
-");
-
-@expected{unsupportedRename}
-test bool defaultFunction() = testRename("", decls = "
- 'int foo(int f) {
- ' return f;
- '}
-");
-
-@expected{unsupportedRename}
-test bool privateFunction() = testRename("", decls = "
- 'private int foo(int f) {
- ' return f;
- '}
-");
-
-test bool publicFunctionParameter() = {0, 1} == testRenameOccurrences("", decls = "
- 'public int f(int foo) {
- ' return foo;
- '}
-");
-
-test bool defaultFunctionParameter() = {0, 1} == testRenameOccurrences("", decls = "
- 'int f(int foo) {
- ' return foo;
- '}
-");
-
-test bool privateFunctionParameter() = {0, 1} == testRenameOccurrences("", decls = "
- 'private int f(int foo) {
- ' return foo;
- '}
-");
-
-@expected{unsupportedRename}
-test bool nestedKeywordParameter() = {0, 1, 2} == testRenameOccurrences("
- 'int f(int foo = 8) = foo;
- 'int x = f(foo = 10);
-");
-
-@expected{unsupportedRename}
-test bool keywordParameter() = testRename(
- "int x = f(foo = 10);"
- decls="int f(int foo = 8) = foo;"
-);
-
-@expected{illegalRename} test bool doubleParameterDeclaration1() = testRename("int f(int foo, int bar) = 1;");
-@expected{illegalRename} test bool doubleParameterDeclaration2() = testRename("int f(int bar, int foo) = 1;");
-
-@expected{illegalRename} test bool doubleNormalAndKeywordParameterDeclaration1() = testRename("int f(int foo, int bar = 9) = 1;");
-@expected{illegalRename} test bool doubleNormalAndKeywordParameterDeclaration2() = testRename("int f(int bar, int foo = 8) = 1;");
-
-@expected{illegalRename} test bool doubleKeywordParameterDeclaration1() = testRename("int f(int foo = 8, int bar = 9) = 1;");
-@expected{illegalRename} test bool doubleKeywordParameterDeclaration2() = testRename("int f(int bar = 9, int foo = 8) = 1;");
-
-test bool renameParamToConstructorName() = {0, 1} == testRenameOccurrences(
- "int f(int foo) = foo;",
- decls = "data Bar = bar();"
-);
-
-@expected{illegalRename}
-test bool renameParamToUsedConstructorName() = testRename(
- "Bar f(int foo) = bar(foo);",
- decls = "data Bar = bar(int x);"
-);
-
-test bool renameToReservedName() {
- edits = getEdits("int foo = 8;", 0, "foo", "int");
-
- newNames = {name | e <- edits, changed(_, replaces) := e
- , r <- replaces, replace(_, name) := r};
-
- return newNames == {"\\int"};
-}
-
-@expected{illegalRename}
-test bool renameToUsedReservedName() = testRename("
- 'int \\int = 0;
- 'int foo = 8;
-", newName = "int");
-
-@expected{illegalRename}
-test bool newNameIsNonAlphaNumeric() = testRename("int foo = 8;", newName = "b@r");
-
-@expected{illegalRename}
-test bool newNameIsNumber() = testRename("int foo = 8;", newName = "8");
-
-@expected{illegalRename}
-test bool newNameHasNumericPrefix() = testRename("int foo = 8;", newName = "8abc");
-
-@expected{illegalRename}
-test bool newNameIsEscapedInvalid() = testRename("int foo = 8;", newName = "\\8int");
-
-
-
-//// Fixtures and utility functions
-
-private PathConfig testPathConfig = pathConfig(
- bin=|memory://tests/rename/bin|,
- libs=[|lib://rascal|],
- srcs=[|memory://tests/rename/src|],
- resources=|memory://tests/rename/resources|,
- generatedSources=|memory://tests/rename/generated-sources|);
-
-// Test renaming given the location of a module and rename parameters
-list[DocumentEdit] getEdits(loc singleModule, int cursorAtOldNameOccurrence, str oldName, str newName, PathConfig pcfg = testPathConfig) {
- void checkNoErrors(list[ModuleMessages] msgs) {
- if (errors := {p | p:program(_, pmsgs) <- msgs, m <- pmsgs, m is error}, errors != {})
- throw errors;
- }
-
- loc f = resolveLocation(singleModule);
-
- checkNoErrors(checkAll(f, getRascalCoreCompilerConfig(pcfg)));
-
- return getEdits(parseModuleWithSpaces(f), cursorAtOldNameOccurrence, oldName, newName, pcfg=pcfg);
-}
-
-// Test renaming given a module Tree and rename parameters
-list[DocumentEdit] getEdits(start[Module] m, int cursorAtOldNameOccurrence, str oldName, str newName, PathConfig pcfg = testPathConfig) {
- Tree cursor = [n | /Name n := m.top, "" == oldName][cursorAtOldNameOccurrence];
- return renameRascalSymbol(m, cursor, {}, pcfg, newName);
-}
-
-tuple[list[DocumentEdit], loc] getEditsAndModule(str stmtsStr, int cursorAtOldNameOccurrence, str oldName, str newName, str decls = "", str imports = "") {
- str moduleName = "TestModule";
- str moduleStr =
- "module
- '
- 'void main() {
- '
- '}";
-
- // Write the file to disk (and clean up later) to easily emulate typical editor behaviour
- loc moduleFileName = |memory://tests/rename/src/.rsc|;
- writeFile(moduleFileName, moduleStr);
- list[DocumentEdit] edits = getEdits(moduleFileName, cursorAtOldNameOccurrence, oldName, newName);
-
- return ;
-}
-
-list[DocumentEdit] getEdits(str stmtsStr, int cursorAtOldNameOccurrence, str oldName, str newName, str decls = "") {
- = getEditsAndModule(stmtsStr, cursorAtOldNameOccurrence, oldName, newName, decls=decls);
- remove(l);
- return edits;
-}
-
-set[int] testRenameOccurrences(str stmtsStr, int cursorAtOldNameOccurrence = 0, str oldName = "foo", str newName = "bar", str decls = "") {
- = getEditsAndModule(stmtsStr, cursorAtOldNameOccurrence, oldName, newName, decls=decls);
- start[Module] m = parseModuleWithSpaces(moduleFileName);
- occs = extractRenameOccurrences(m, edits, oldName);
- remove(moduleFileName);
- return occs;
-}
-
-// Test renames that are expected to throw an exception
-bool testRename(str stmtsStr, int cursorAtOldNameOccurrence = 0, str oldName = "foo", str newName = "bar", str decls = "") {
- edits = getEdits(stmtsStr, cursorAtOldNameOccurrence, oldName, newName decls=decls);
-
- print("UNEXPECTED EDITS: ");
- iprintln(edits);
-
- return false;
-}
-
-set[int] extractRenameOccurrences(start[Module] m, list[DocumentEdit] edits, str name) {
- list[loc] oldNameOccurrences = [];
- for (/Name n := m, "" == name) {
- oldNameOccurrences += n.src;
- }
-
- if ([changed(_, replaces)] := edits) {
- idx = {};
- for (replace(l, _) <- replaces, i := indexOf(oldNameOccurrences, l)) {
- if (i == -1) throw "Cannot find in ";
- idx += i;
- }
-
- return idx;
- } else {
- throw "Unexpected changes: ";
- }
-}
-
-list[DocumentEdit] multiModuleTest() {
- PathConfig pcfg = pathConfig(
- bin=|memory://tests/rename/bin|,
- libs=[|lib://rascal|
- , resolveLocation(|project://rascal-vscode-extension/test-workspace/test-lib/src/main/rascal/|)],
- srcs=[resolveLocation(|project://rascal-vscode-extension/test-workspace/test-project/src/main/rascal/|)
- , resolveLocation(|project://rascal-vscode-extension/test-workspace/test-lib/src/main/rascal/|)]
- );
-
- return getEdits(resolveLocation(|project://rascal-vscode-extension/test-workspace/test-project/src/main/rascal/LibCall.rsc|)
- , 0, "fib", "fibonacci", pcfg=pcfg);
-}
diff --git a/rascal-lsp/src/main/rascal/lang/rascal/tests/rename/Annotations.rsc b/rascal-lsp/src/main/rascal/lang/rascal/tests/rename/Annotations.rsc
new file mode 100644
index 000000000..e64f9a32f
--- /dev/null
+++ b/rascal-lsp/src/main/rascal/lang/rascal/tests/rename/Annotations.rsc
@@ -0,0 +1,45 @@
+@license{
+Copyright (c) 2018-2023, 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.
+}
+module lang::rascal::tests::rename::Annotations
+
+import lang::rascal::tests::rename::TestUtils;
+import lang::rascal::lsp::refactor::Exception;
+
+test bool userDefinedAnno() = testRenameOccurrences({0, 1, 2}, "
+ 'x = d();
+ 'x@foo = 8;
+ 'int i = x@foo;
+ ", decls = "
+ 'data D = d();
+ 'anno int D@foo;
+");
+
+@expected{illegalRename}
+test bool builtinAnno() = testRename("
+ 'Tree t = char(8);
+ 'x = t@\\loc;
+", oldName = "\\loc", imports = "import ParseTree;");
diff --git a/rascal-lsp/src/main/rascal/lang/rascal/tests/rename/Fields.rsc b/rascal-lsp/src/main/rascal/lang/rascal/tests/rename/Fields.rsc
new file mode 100644
index 000000000..0b3b6aaca
--- /dev/null
+++ b/rascal-lsp/src/main/rascal/lang/rascal/tests/rename/Fields.rsc
@@ -0,0 +1,235 @@
+@license{
+Copyright (c) 2018-2023, 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.
+}
+module lang::rascal::tests::rename::Fields
+
+import lang::rascal::tests::rename::TestUtils;
+import lang::rascal::lsp::refactor::Exception;
+
+test bool constructorField() = testRenameOccurrences({0, 1}, "
+ 'D oneTwo = d(1, 2);
+ 'x = oneTwo.foo;
+ ", decls = "data D = d(int foo, int baz);"
+);
+
+test bool constructorKeywordField() = testRenameOccurrences({0, 1, 2}, "
+ 'D dd = d(foo=1, baz=2);
+ 'x = dd.foo;
+ ", decls="data D = d(int foo = 0, int baz = 0);"
+);
+
+test bool commonKeywordField() = testRenameOccurrences({0, 1, 2}, "
+ 'D oneTwo = d(foo=1, baz=2);
+ 'x = oneTwo.foo;
+ ", decls = "data D(int foo = 0, int baz = 0) = d();"
+);
+
+// Flaky. Fix for non-determinism in typepal, upcoming in future release of Rascal (Core)
+// https://github.com/usethesource/typepal/commit/55456edcc52653e42d7f534a5412147b01b68c29
+test bool multipleConstructorField() = testRenameOccurrences({0, 1, 2}, "
+ 'x = d(1, 2);
+ 'y = x.foo;
+ ", decls = "data D = d(int foo) | d(int foo, int baz);"
+);
+
+@expected{illegalRename}
+test bool duplicateConstructorField() = testRename("", decls =
+ "data D = d(int foo, int bar);"
+);
+
+@expected{illegalRename}
+test bool differentTypeAcrossConstructorField() = testRename("", decls =
+ "data D = d0(int foo) | d1(str bar);"
+);
+
+test bool sameTypeFields() = testRenameOccurrences({0, 1},
+ "x = d({}, {});
+ 'xx = x.foo;
+ 'xy = x.baz;
+ ",
+ decls = "data D = d(set[loc] foo, set[loc] baz);"
+);
+
+test bool commonKeywordFieldsSameType() = testRenameOccurrences({0, 1},
+ "x = d();
+ 'xx = x.foo;
+ 'xy = x.baz;
+ ",
+ decls = "data D (set[loc] foo = {}, set[loc] baz = {})= d();"
+);
+
+test bool complexDataType() = testRenameOccurrences({0, 1},
+ "WorkspaceInfo ws = workspaceInfo(
+ ' ProjectFiles() { return {}; },
+ ' ProjectFiles() { return {}; },
+ ' set[TModel](ProjectFiles projectFiles) { return { tmodel() }; }
+ ');
+ 'ws.projects += {};
+ 'ws.sourceFiles += {};
+ ",
+ decls = "
+ 'data TModel = tmodel();
+ 'data Define;
+ 'data AType;
+ 'alias ProjectFiles = rel[loc projectFolder, loc file];
+ 'data WorkspaceInfo (
+ ' // Instance fields
+ ' // Read-only
+ ' rel[loc use, loc def] useDef = {},
+ ' set[Define] defines = {},
+ ' set[loc] sourceFiles = {},
+ ' map[loc, Define] definitions = (),
+ ' map[loc, AType] facts = (),
+ ' set[loc] projects = {}
+ ') = workspaceInfo(
+ ' ProjectFiles() preloadFiles,
+ ' ProjectFiles() allFiles,
+ ' set[TModel](ProjectFiles) tmodelsForLocs
+ ');"
+, oldName = "sourceFiles", newName = "sources");
+
+test bool crossModuleConstructorField() = testRenameOccurrences({
+ byText("Foo", "data D = a(int foo) | b(int baz);", {0}),
+ byText("Main", "
+ 'import Foo;
+ 'void main() {
+ ' f = a(8);
+ ' g = a.foo;
+ '}
+ ", {0})
+});
+
+test bool extendedConstructorField() = testRenameOccurrences({
+ byText("Scratch1", "
+ 'data Foo = f(int foo);
+ ", {0}),
+ byText("Scratch2", "
+ 'extend Scratch1;
+ 'data Foo = g(int foo);
+ ", {0})
+});
+
+test bool dataTypeReusedName() = testRenameOccurrences({
+ byText("Scratch1", "
+ 'data Foo = f();
+ ", {0}),
+ byText("Scratch2", "
+ 'data Foo = g();
+ ", {})
+}, oldName = "Foo", newName = "Bar");
+
+test bool dataFieldReusedName() = testRenameOccurrences({
+ byText("Scratch1", "
+ 'data Foo = f(int foo);
+ ", {0}),
+ byText("Scratch2", "
+ 'data Foo = f(int foo);
+ ", {})
+});
+
+test bool dataKeywordFieldReusedName() = testRenameOccurrences({
+ byText("Scratch1", "
+ 'data Foo = f(int foo = 0);
+ ", {0}),
+ byText("Scratch2", "
+ 'data Foo = f(int foo = 0);
+ ", {})
+});
+
+test bool dataCommonKeywordFieldReusedName() = testRenameOccurrences({
+ byText("Scratch1", "
+ 'data Foo(int foo = 0) = f();
+ ", {0}),
+ byText("Scratch2", "
+ 'data Foo(int foo = 0) = g();
+ ", {})
+});
+
+test bool relField() = testRenameOccurrences({0, 1}, "
+ 'rel[str foo, str baz] r = {};
+ 'f = r.foo;
+");
+
+test bool lrelField() = testRenameOccurrences({0, 1}, "
+ 'lrel[str foo, str baz] r = [];
+ 'f = r.foo;
+");
+
+test bool relSubscript() = testRenameOccurrences({0, 1}, "
+ 'rel[str foo, str baz] r = {};
+ 'x = r\;
+");
+
+test bool relSubscriptWithVar() = testRenameOccurrences({0, 2}, "
+ 'rel[str foo, str baz] r = {};
+ 'str foo = \"foo\";
+ 'x = r\;
+");
+
+test bool tupleFieldSubscriptUpdate() = testRenameOccurrences({0, 1, 2}, "
+ 'tuple[str foo, int baz] t = \<\"one\", 1\>;
+ 'u = t[foo = \"two\"];
+ 'v = u.foo;
+");
+
+test bool tupleFieldAccessUpdate() = testRenameOccurrences({0, 1}, "
+ 'tuple[str foo, int baz] t = \<\"one\", 1\>;
+ 't.foo = \"two\";
+");
+
+test bool similarCollectionTypes() = testRenameOccurrences({0, 1, 2, 3, 4}, "
+ 'rel[str foo, int baz] r = {};
+ 'lrel[str foo, int baz] lr = [];
+ 'set[tuple[str foo, int baz]] st = {};
+ 'list[tuple[str foo, int baz]] lt = [];
+ 'tuple[str foo, int baz] t = \<\"\", 0\>;
+");
+
+test bool differentRelWithSameField() = testRenameOccurrences({0, 1}, "
+ 'rel[str foo, int baz] r1 = {};
+ 'foos1 = r1.foo;
+ 'rel[int n, str foo] r2 = {};
+ 'foos2 = r2.foo;
+");
+
+test bool tupleField() = testRenameOccurrences({0, 1}, "
+ 'tuple[int foo] t = \<8\>;
+ 'y = t.foo;
+");
+
+// We would prefer an illegalRename exception here
+@expected{illegalRename}
+test bool builtinFieldSimpleType() = testRename("
+ 'loc l = |unknown:///|;
+ 'f = l.top;
+", oldName = "top", newName = "summit");
+// We would prefer an illegalRename exception here
+
+@expected{illegalRename}
+test bool builtinFieldCollectionType() = testRename("
+ 'loc l = |unknown:///|;
+ 'f = l.ls;
+", oldName = "ls", newName = "contents");
diff --git a/rascal-lsp/src/main/rascal/lang/rascal/tests/rename/FormalParameters.rsc b/rascal-lsp/src/main/rascal/lang/rascal/tests/rename/FormalParameters.rsc
new file mode 100644
index 000000000..f6daf01d7
--- /dev/null
+++ b/rascal-lsp/src/main/rascal/lang/rascal/tests/rename/FormalParameters.rsc
@@ -0,0 +1,146 @@
+@license{
+Copyright (c) 2018-2023, 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.
+}
+module lang::rascal::tests::rename::FormalParameters
+
+import lang::rascal::tests::rename::TestUtils;
+import lang::rascal::lsp::refactor::Exception;
+
+test bool outerNestedFunctionParameter() = testRenameOccurrences({0, 3}, "
+ 'int f(int foo) {
+ ' int g(int foo) {
+ ' return foo;
+ ' }
+ ' return f(foo);
+ '}
+");
+
+test bool innerNestedFunctionParameter() = testRenameOccurrences({1, 2}, "
+ 'int f(int foo) {
+ ' int g(int foo) {
+ ' return foo;
+ ' }
+ ' return f(foo);
+ '}
+");
+
+test bool publicFunctionParameter() = testRenameOccurrences({0, 1}, "", decls = "
+ 'public int f(int foo) {
+ ' return foo;
+ '}
+");
+
+test bool defaultFunctionParameter() = testRenameOccurrences({0, 1}, "", decls = "
+ 'int f(int foo) {
+ ' return foo;
+ '}
+");
+
+test bool privateFunctionParameter() = testRenameOccurrences({0, 1}, "", decls = "
+ 'private int f(int foo) {
+ ' return foo;
+ '}
+");
+
+test bool nestedKeywordParameter() = testRenameOccurrences({0, 1, 2}, "
+ 'int f(int foo = 8) = foo;
+ 'int x = f(foo = 10);
+", skipCursors = {2});
+
+test bool keywordParameter() = testRenameOccurrences({0, 1, 2},
+ "int x = f(foo = 10);"
+ , decls="int f(int foo = 8) = foo;"
+ , skipCursors = {2}
+);
+
+@expected{illegalRename} test bool doubleParameterDeclaration1() = testRename("int f(int foo, int bar) = 1;");
+@expected{illegalRename} test bool doubleParameterDeclaration2() = testRename("int f(int bar, int foo) = 1;");
+
+@expected{illegalRename} test bool doubleNormalAndKeywordParameterDeclaration1() = testRename("int f(int foo, int bar = 9) = 1;");
+@expected{illegalRename} test bool doubleNormalAndKeywordParameterDeclaration2() = testRename("int f(int bar, int foo = 8) = 1;");
+
+@expected{illegalRename} test bool doubleKeywordParameterDeclaration1() = testRename("int f(int foo = 8, int bar = 9) = 1;");
+@expected{illegalRename} test bool doubleKeywordParameterDeclaration2() = testRename("int f(int bar = 9, int foo = 8) = 1;");
+
+test bool renameParamToConstructorName() = testRenameOccurrences({0, 1},
+ "int f(int foo) = foo;",
+ decls = "data Bar = bar();"
+);
+
+@expected{illegalRename}
+test bool renameParamToUsedConstructorName() = testRename(
+ "Bar f(int foo) = bar(foo);",
+ decls = "data Bar = bar(int x);"
+);
+
+test bool paremeterShadowsParameter1() = testRenameOccurrences({0, 3}, "
+ 'int f1(int foo) {
+ ' int f2(int foo) {
+ ' int baz = 9;
+ ' return foo + baz;
+ ' }
+ ' return f2(foo);
+ '}
+");
+
+test bool paremeterShadowsParameter2() = testRenameOccurrences({1, 2}, "
+ 'int f1(int foo) {
+ ' int f2(int foo) {
+ ' int baz = 9;
+ ' return foo + baz;
+ ' }
+ ' return f2(foo);
+ '}
+");
+
+@expected{illegalRename}
+test bool paremeterShadowsParameter3() = testRename("
+ 'int f(int bar) {
+ ' int g(int baz) {
+ ' int h(int foo) {
+ ' return bar;
+ ' }
+ ' return h(baz);
+ ' }
+ ' return g(bar);
+ '}
+");
+
+@expected{illegalRename}
+test bool captureFunctionParameter() = testRename("
+ 'int f(int foo) {
+ ' int bar = 9;
+ ' return foo + bar;
+ '}
+");
+
+@expected{illegalRename}
+test bool doubleVariableAndParameterDeclaration() = testRename("
+ 'int f(int foo) {
+ ' int bar = 9;
+ ' return foo + bar;
+ '}
+");
diff --git a/rascal-lsp/src/main/rascal/lang/rascal/tests/rename/Functions.rsc b/rascal-lsp/src/main/rascal/lang/rascal/tests/rename/Functions.rsc
new file mode 100644
index 000000000..591fa5995
--- /dev/null
+++ b/rascal-lsp/src/main/rascal/lang/rascal/tests/rename/Functions.rsc
@@ -0,0 +1,249 @@
+@license{
+Copyright (c) 2018-2023, 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.
+}
+module lang::rascal::tests::rename::Functions
+
+import lang::rascal::tests::rename::TestUtils;
+
+test bool nestedFunctionParameter() = testRenameOccurrences({0, 1}, "
+ 'int f(int foo, int baz) {
+ ' return foo;
+ '}
+");
+
+test bool nestedRecursiveFunctionName() = testRenameOccurrences({0, 1, 2, 3}, "
+ 'int fib(int n) {
+ ' switch (n) {
+ ' case 0: {
+ ' return 1;
+ ' }
+ ' case 1: {
+ ' return 1;
+ ' }
+ ' default: {
+ ' return fib(n - 1) + fib(n - 2);
+ ' }
+ ' }
+ '}
+ '
+ 'fib(7);
+", oldName = "fib", newName = "fibonacci");
+
+test bool recursiveFunctionName() = testRenameOccurrences({0, 1, 2, 3}, "fib(7);", decls = "
+ 'int fib(int n) {
+ ' switch (n) {
+ ' case 0: {
+ ' return 1;
+ ' }
+ ' case 1: {
+ ' return 1;
+ ' }
+ ' default: {
+ ' return fib(n - 1) + fib(n - 2);
+ ' }
+ ' }
+ '}
+", oldName = "fib", newName = "fibonacci");
+
+test bool nestedPublicFunction() = testRenameOccurrences({0, 1}, "
+ 'public int foo(int f) {
+ ' return f;
+ '}
+ 'foo(1);
+");
+
+test bool nestedDefaultFunction() = testRenameOccurrences({0, 1}, "
+ 'int foo(int f) {
+ ' return f;
+ '}
+ 'foo(1);
+");
+
+test bool nestedPrivateFunction() = testRenameOccurrences({0, 1}, "
+ 'private int foo(int f) {
+ ' return f;
+ '}
+ 'foo(1);
+");
+
+test bool publicFunction() = testRenameOccurrences({0, 1}, "foo(1);", decls = "
+ 'public int foo(int f) {
+ ' return f;
+ '}
+");
+
+test bool defaultFunction() = testRenameOccurrences({0, 1}, "foo(1);", decls = "
+ 'int foo(int f) {
+ ' return f;
+ '}
+");
+
+test bool privateFunction() = testRenameOccurrences({0, 1}, "foo(1);", decls = "
+ 'private int foo(int f) {
+ ' return f;
+ '}
+");
+
+test bool backtrackOverload() = testRenameOccurrences({0, 1, 2}, "x = foo(3);", decls = "
+ 'int foo(int x) = x when x \< 2;
+ 'default int foo(int x) = x;
+");
+
+test bool patternOverload() = testRenameOccurrences({0, 1, 2, 3}, "x = size([1, 2]);", decls = "
+ 'int size(list[&T] _: []) = 0;
+ 'int size(list[&T] _: [_, *l]) = 1 + size(l);
+", oldName = "size", newName = "sizeof");
+
+test bool typeOverload() = testRenameOccurrences({0, 1, 2, 3, 4, 5, 6}, "x = size([1, 2]);", decls = "
+ 'int size(list[&T] _: []) = 0;
+ 'int size(list[&T] _: [_, *l]) = 1 + size(l);
+ '
+ 'int size(set[&T] _: {}) = 0;
+ 'int size(set[&T] _: {_, *s}) = 1 + size(s);
+", oldName = "size", newName = "sizeof");
+
+test bool arityOverload() = testRenameOccurrences({0, 1, 2, 3, 4, 5}, "x = concat(\"foo\", \"bar\");", decls = "
+ 'str concat(str s) = s;
+ 'str concat(str s1, str s2) = s1 + concat(s2);
+ 'str concat(str s1, str s2, str s3) = s1 + concat(s2, s3);
+", oldName = "concat", newName = "foo");
+
+test bool overloadClash() = testRenameOccurrences({0}, "", decls = "
+ 'int foo = 0;
+ 'int foo(int x) = x when x \< 2;
+");
+
+test bool crossModuleOverload() = testRenameOccurrences({
+ byText("Str", "
+ 'str concat(str s) = s;
+ 'str concat(str s1, str s2) = s1 + concat(s2);
+ ", {0, 1, 2})
+ , byText("Main", "
+ 'extend Str;
+ 'str concat(str s1, str s2, str s3) = s1 + concat(s2, s3);
+ ", {0, 1})
+}, oldName = "concat", newName = "conc");
+
+test bool simpleTypeParams() = testRenameOccurrences({0, 1}, "
+ '&T foo(&T l) = l;
+ '&T bar(&T x, int y) = x;
+", oldName = "T", newName = "U");
+
+test bool typeParams() = testRenameOccurrences({0, 1, 2}, "
+ '&T foo(&T l) {
+ ' &T m = l;
+ ' return m;
+ '}
+", oldName = "T", newName = "U");
+
+test bool keywordTypeParamFromReturn() = testRenameOccurrences({0, 1, 2}, "
+ '&T foo(&T \<: int l, &T \<: int kw = 1) = l + kw;
+", oldName = "T", newName = "U");
+
+@expected{illegalRename}
+test bool typeParamsClash() = testRename("
+ '&T foo(&T l, &U m) = l;
+", oldName = "T", newName = "U", cursorAtOldNameOccurrence = 1);
+
+test bool nestedTypeParams() = testRenameOccurrences({0, 1, 2, 3}, "
+ '&T f(&T t) {
+ ' &T g(&T t) = t;
+ ' return g(t);
+ '}
+", oldName = "T", newName = "U");
+
+@expected{illegalRename}
+test bool nestedTypeParamClash() = testRename("
+ 'void f(&S s, &T t) {
+ ' &S g(&S s) = s;
+ ' &T g(&T t) = t;
+ }
+", oldName = "S", newName = "T", cursorAtOldNameOccurrence = 1);
+
+test bool adjacentTypeParams() = testRenameOccurrences({0, 1}, "
+ '&S f(&S s) = s;
+ '&T f(&T t) = t;
+", oldName = "S", newName = "T");
+
+test bool typeParamsListReturn() = testRenameOccurrences({0, 1, 2}, "
+ 'list[&T] foo(&T l) {
+ ' list[&T] m = [l, l];
+ ' return m;
+ '}
+", oldName = "T", newName = "U");
+
+test bool localOverloadedFunction() = testRenameOccurrences({0, 1, 2, 3}, "
+ 'bool foo(g()) = true;
+ 'bool foo(h()) = foo(g());
+ '
+ 'foo(h());
+", decls = "data D = g() | h();");
+
+test bool functionsInVModuleStructure() = testRenameOccurrences({
+ byText("Left", "str foo(str s) = s when s == \"x\";", {0}), byText("Right", "str foo(str s) = s when s == \"y\";", {0})
+ , byText("Merger",
+ "import Left;
+ 'import Right;
+ 'void main() { x = foo(\"foo\"); }
+ ", {0})
+});
+
+test bool functionsInYModuleStructure() = testRenameOccurrences({
+ byText("Left", "str foo(str s) = s when s == \"x\";", {0}), byText("Right", "str foo(str s) = s when s == \"x\";", {0})
+ , byText("Merger",
+ "extend Left;
+ 'extend Right;
+ ", {})
+ , byText("User",
+ "import Merger;
+ 'void main() { foo(\"foo\"); }
+ ", {0})
+});
+
+test bool functionsInInvertedVModuleStructure() = testRenameOccurrences({
+ byText("Definer", "str foo(str s) = s when s == \"x\";
+ 'str foo(str s) = s when s == \"y\";", {0, 1}),
+ byText("Left", "import Definer;
+ 'void main() { foo(\"foo\"); }", {0}), byText("Right", "import Definer;
+ 'void main() { foo(\"fu\"); }", {0})
+});
+
+test bool functionsInDiamondModuleStructure() = testRenameOccurrences({
+ byText("Definer", "str foo(str s) = s when s == \"x\";
+ 'str foo(str s) = s when s == \"y\";", {0, 1}),
+ byText("Left", "extend Definer;", {}), byText("Right", "extend Definer;", {}),
+ byText("User", "import Left;
+ 'import Right;
+ 'void main() { foo(\"foo\"); }", {0})
+});
+
+test bool functionsInIIModuleStructure() = testRenameOccurrences({
+ byText("LeftDefiner", "str foo(str s) = s when s == \"x\";", {0}), byText("RightDefiner", "str foo(str s) = s when s == \"y\";", {}),
+ byText("LeftExtender", "extend LeftDefiner;", {}), byText("RightExtender", "extend RightDefiner;", {}),
+ byText("LeftUser", "import LeftExtender;
+ 'void main() { foo(\"foo\"); }", {0}), byText("RightUser", "import RightExtender;
+ 'void main() { foo(\"fu\"); }", {})
+});
diff --git a/rascal-lsp/src/main/rascal/lang/rascal/tests/rename/Modules.rsc b/rascal-lsp/src/main/rascal/lang/rascal/tests/rename/Modules.rsc
new file mode 100644
index 000000000..9b0299864
--- /dev/null
+++ b/rascal-lsp/src/main/rascal/lang/rascal/tests/rename/Modules.rsc
@@ -0,0 +1,82 @@
+@license{
+Copyright (c) 2018-2023, 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.
+}
+module lang::rascal::tests::rename::Modules
+
+import lang::rascal::tests::rename::TestUtils;
+
+test bool deepModule() = testRenameOccurrences({
+ byText("some::path::to::Foo", "
+ 'data Bool = t() | f();
+ 'Bool and(Bool l, Bool r) = r is t ? l : f;
+ ", {0}, newName = "some::path::to::Bar"),
+ byText("Main", "
+ 'import some::path::to::Foo;
+ 'void main() {
+ ' some::path::to::Foo::Bool b = and(t(), f());
+ '}
+ ", {0, 1})
+}, oldName = "Foo", newName = "Bar");
+
+test bool shadowedModuleWithVar() = testRenameOccurrences({
+ byText("Foo", "
+ 'data Bool = t() | f();
+ 'Bool and(Bool l, Bool r) = r is t ? l : f;
+ ", {0}, newName = "Bar"),
+ byText("shadow::Foo", "", {}),
+ byText("Main", "
+ 'import Foo;
+ 'void main() {
+ ' Foo::Bool b = and(t(), f());
+ '}
+ ", {0, 1})
+}, oldName = "Foo", newName = "Bar");
+
+test bool shadowedModuleWithFunc() = testRenameOccurrences({
+ byText("Foo", "
+ 'void f() { fail; }
+ ", {0}, newName = "Bar"),
+ byText("shadow::Foo", "", {}),
+ byText("Main", "
+ 'import Foo;
+ 'void main() {
+ ' Foo::f();
+ '}
+ ", {0, 1})
+}, oldName = "Foo", newName = "Bar");
+
+test bool singleModule() = testRenameOccurrences({
+ byText("util::Foo", "
+ 'data Bool = t() | f();
+ 'Bool and(Bool l, Bool r) = r is t ? l : f;
+ ", {0}, newName = "util::Bar"),
+ byText("Main", "
+ 'import util::Foo;
+ 'void main() {
+ ' util::Foo::Bool b = and(t(), f());
+ '}
+ ", {0, 1})
+}, oldName = "Foo", newName = "Bar");
diff --git a/rascal-lsp/src/main/rascal/lang/rascal/tests/rename/Performance.rsc b/rascal-lsp/src/main/rascal/lang/rascal/tests/rename/Performance.rsc
new file mode 100644
index 000000000..832234c39
--- /dev/null
+++ b/rascal-lsp/src/main/rascal/lang/rascal/tests/rename/Performance.rsc
@@ -0,0 +1,43 @@
+@license{
+Copyright (c) 2018-2023, 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.
+}
+module lang::rascal::tests::rename::Performance
+
+import lang::rascal::tests::rename::TestUtils;
+
+import List;
+
+int LARGE_TEST_SIZE = 200;
+test bool largeTest() = testRenameOccurrences(({0} | it + {foos + 3, foos + 4, foos + 5} | i <- [0..LARGE_TEST_SIZE], foos := 5 * i), (
+ "int foo = 8;"
+ | "
+ 'int f(int foo) = foo;
+ 'foo = foo + foo;"
+ | i <- [0..LARGE_TEST_SIZE])
+, skipCursors = toSet([1..LARGE_TEST_SIZE * 5]));
+
+@expected{unsupportedRename}
+test bool failOnError() = testRename("int foo = x + y;");
diff --git a/rascal-lsp/src/main/rascal/lang/rascal/tests/rename/TestUtils.rsc b/rascal-lsp/src/main/rascal/lang/rascal/tests/rename/TestUtils.rsc
new file mode 100644
index 000000000..d28a5e5d6
--- /dev/null
+++ b/rascal-lsp/src/main/rascal/lang/rascal/tests/rename/TestUtils.rsc
@@ -0,0 +1,316 @@
+@license{
+Copyright (c) 2018-2023, 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.
+}
+@bootstrapParser
+module lang::rascal::tests::rename::TestUtils
+
+import lang::rascal::lsp::refactor::Rename; // Module under test
+
+import lang::rascal::lsp::refactor::Util;
+
+import IO;
+import List;
+import Location;
+import Set;
+import String;
+
+import lang::rascal::\syntax::Rascal; // `Name`
+
+import lang::rascalcore::check::Checker;
+import lang::rascalcore::check::BasicRascalConfig;
+import lang::rascalcore::check::RascalConfig;
+import lang::rascalcore::compile::util::Names;
+
+import analysis::diff::edits::ExecuteTextEdits;
+
+import util::FileSystem;
+import util::Math;
+import util::Reflective;
+
+
+//// Fixtures and utility functions
+data TestModule = byText(str name, str body, set[int] nameOccs, str newName = name, set[int] skipCursors = {})
+ | byLoc(loc file, set[int] nameOccs, str newName = name, set[int] skipCursors = {});
+
+private list[DocumentEdit] sortEdits(list[DocumentEdit] edits) = [sortChanges(e) | e <- edits];
+
+private DocumentEdit sortChanges(changed(loc l, list[TextEdit] edits)) = changed(l, sort(edits, bool(TextEdit e1, TextEdit e2) {
+ return e1.range.offset < e2.range.offset;
+}));
+private default DocumentEdit sortChanges(DocumentEdit e) = e;
+
+private void verifyTypeCorrectRenaming(loc root, list[DocumentEdit] edits, PathConfig pcfg) {
+ executeDocumentEdits(sortEdits(edits));
+ remove(pcfg.resources);
+ RascalCompilerConfig ccfg = rascalCompilerConfig(pcfg)[forceCompilationTopModule = true][verbose = false][logPathConfig = false];
+ throwAnyErrors(checkAll(root, ccfg));
+}
+
+bool expectEq(&T expected, &T actual, str epilogue = "") {
+ if (expected != actual) {
+ if (epilogue != "") println(epilogue);
+
+ print("EXPECTED: ");
+ iprintln(expected);
+ println();
+
+ print("ACTUAL: ");
+ iprintln(actual);
+ println();
+ return false;
+ }
+ return true;
+}
+
+bool testRenameOccurrences(set[TestModule] modules, str oldName = "foo", str newName = "bar") {
+ bool success = true;
+ for (mm <- modules, cursorOcc <- (mm.nameOccs - mm.skipCursors)) {
+ str testName = "Test";
+ loc testDir = |memory://tests/rename/|;
+
+ if(any(m <- modules, m is byLoc)) {
+ testDir = cover([m.file | m <- modules, m is byLoc]);
+ } else {
+ // If none of the modules refers to an existing file, clear the test directory before writing files.
+ remove(testDir);
+ }
+
+ pcfg = getTestPathConfig(testDir);
+ modulesByLocation = {mByLoc | m <- modules, mByLoc := (m is byLoc ? m : byLoc(storeTestModule(testDir, m.name, m.body), m.nameOccs, newName = m.newName, skipCursors = m.skipCursors))};
+ cursorT = findCursor([m.file | m <- modulesByLocation, getModuleName(m.file, pcfg) == mm.name][0], oldName, cursorOcc);
+
+ println("Renaming \'\' from ");
+ edits = rascalRenameSymbol(cursorT, toSet(pcfg.srcs), newName, PathConfig(loc _) { return pcfg; });
+
+ renamesPerModule = (
+ beforeRename: afterRename
+ | renamed(oldLoc, newLoc) <- edits
+ , beforeRename := getModuleName(oldLoc, pcfg)
+ , afterRename := getModuleName(newLoc, pcfg)
+ );
+
+ replacesPerModule = (
+ name: occs
+ | changed(file, changes) <- edits
+ , name := getModuleName(file, pcfg)
+ , locs := {l | replace(l, _) <- changes}
+ , occs := locsToOccs(parseModuleWithSpaces(file), oldName, locs)
+ );
+
+ editsPerModule = (
+ name :
+ | srcDir <- pcfg.srcs
+ , file <- find(srcDir, "rsc")
+ , name := getModuleName(file, pcfg)
+ , occs := replacesPerModule[name] ? {}
+ , nameAfterRename := renamesPerModule[name] ? name
+ );
+
+ expectedEditsPerModule = (name: | m <- modulesByLocation, name := getModuleName(m.file, pcfg));
+
+ if (!expectEq(expectedEditsPerModule, editsPerModule, epilogue = "Rename from cursor failed:")) {
+ success = false;
+ }
+
+ for (src <- pcfg.srcs) {
+ verifyTypeCorrectRenaming(src, edits, pcfg);
+ }
+
+ remove(testDir);
+ }
+
+ return success;
+}
+
+bool testRenameOccurrences(set[int] oldNameOccurrences, str stmtsStr, str oldName = "foo", str newName = "bar", str decls = "", str imports = "", set[int] skipCursors = {}) {
+ bool success = true;
+ map[int, set[int]] results = ();
+ for (cursor <- oldNameOccurrences - skipCursors) {
+ <_, renamedOccs> = getEditsAndModule(stmtsStr, cursor, oldName, newName, decls, imports);
+ results[cursor] = renamedOccs;
+ if (renamedOccs != oldNameOccurrences) success = false;
+ }
+
+ if (!success) {
+ println("Test returned unexpected renames for some possible cursors (expected: ):");
+ iprintln(results);
+ }
+
+ return success;
+}
+
+// Test renames that are expected to throw an exception
+bool testRename(str stmtsStr, int cursorAtOldNameOccurrence = 0, str oldName = "foo", str newName = "bar", str decls = "", str imports = "") {
+ edits = getEdits(stmtsStr, cursorAtOldNameOccurrence, oldName, newName, decls, imports);
+
+ print("UNEXPECTED EDITS: ");
+ iprintln(edits);
+
+ return false;
+}
+
+private PathConfig getTestPathConfig(loc testDir) {
+ return pathConfig(
+ bin=testDir + "bin",
+ libs=[|lib://rascal|],
+ srcs=[testDir + "rascal"],
+ resources=testDir + "bin",
+ generatedSources=testDir + "generated-sources"
+ );
+}
+
+PathConfig getRascalCorePathConfig(loc rascalCoreProject, loc typepalProject) {
+ return pathConfig(
+ srcs = [
+ |std:///|,
+ rascalCoreProject + "src/org/rascalmpl/core/library",
+ typepalProject + "src"
+ ],
+ bin = rascalCoreProject + "target/test-classes",
+ generatedSources = rascalCoreProject + "target/generated-test-sources",
+ resources = rascalCoreProject + "target/generated-test-resources",
+ libs = []
+ );
+}
+
+PathConfig resolveLocations(PathConfig pcfg) {
+ return visit(pcfg) {
+ case loc l => resolveLocation(l)
+ };
+}
+
+PathConfig getPathConfig(loc project) {
+ println("Getting path config for ()");
+
+ if (project.file == "rascal-core") {
+ pcfg = getRascalCorePathConfig();
+ return resolveLocations(pcfg);
+ }
+
+ pcfg = getProjectPathConfig(project);
+ return resolveLocations(pcfg);
+}
+
+list[DocumentEdit] testRascalCore(loc rascalCoreDir, loc typepalDir) {
+ registerLocations("project", "", (
+ |project://rascal-core/target/test-classes|: rascalCoreDir + "target/test-classes",
+ |project://rascal-core/target/generated-test-sources|: rascalCoreDir + "target/generated-test-sources",
+ |project://rascal-core/target/generated-test-resources|: rascalCoreDir + "target/generated-test-resources",
+ |project://rascal-core/src/org/rascalmpl/core/library|: rascalCoreDir + "src/org/rascalmpl/core/library",
+ |project://typepal/src|: typepalDir + "src"));
+
+ return getEdits(rascalCoreDir + "src/org/rascalmpl/core/library/lang/rascalcore/check/ATypeBase.rsc", {resolveLocation(rascalCoreDir), resolveLocation(typepalDir)}, 0, "arat", "arational", getPathConfig);
+}
+
+list[DocumentEdit] getEdits(loc singleModule, set[loc] projectDirs, int cursorAtOldNameOccurrence, str oldName, str newName, PathConfig(loc) getPathConfig) {
+ loc f = resolveLocation(singleModule);
+ m = parseModuleWithSpaces(f);
+
+ Tree cursor = [n | /Name n := m.top, "" == oldName][cursorAtOldNameOccurrence];
+ return rascalRenameSymbol(cursor, projectDirs, newName, getPathConfig);
+}
+
+tuple[list[DocumentEdit], set[int]] getEditsAndOccurrences(loc singleModule, loc projectDir, int cursorAtOldNameOccurrence, str oldName, str newName, PathConfig pcfg = getTestPathConfig(projectDir)) {
+ edits = getEdits(singleModule, {projectDir}, cursorAtOldNameOccurrence, oldName, newName, PathConfig(loc _) { return pcfg; });
+ occs = extractRenameOccurrences(singleModule, edits, oldName);
+
+ for (src <- pcfg.srcs) {
+ verifyTypeCorrectRenaming(src, edits, pcfg);
+ }
+
+ return ;
+}
+
+list[DocumentEdit] getEdits(str stmtsStr, int cursorAtOldNameOccurrence, str oldName, str newName, str decls, str imports) {
+ = getEditsAndModule(stmtsStr, cursorAtOldNameOccurrence, oldName, newName, decls, imports);
+ return edits;
+}
+
+private tuple[list[DocumentEdit], set[int]] getEditsAndModule(str stmtsStr, int cursorAtOldNameOccurrence, str oldName, str newName, str decls, str imports, str moduleName = "TestModule") {
+ str moduleStr =
+ "module
+ '
+ '
+ 'void main() {
+ '
+ '}";
+
+ // Write the file to disk (and clean up later) to easily emulate typical editor behaviour
+ loc testDir = |memory://tests/rename/|;
+ loc moduleFileName = testDir + "rascal" + ".rsc";
+ writeFile(moduleFileName, moduleStr);
+
+ = getEditsAndOccurrences(moduleFileName, testDir, cursorAtOldNameOccurrence, oldName, newName);
+ return ;
+}
+
+private set[int] extractRenameOccurrences(loc moduleFileName, list[DocumentEdit] edits, str name) {
+ start[Module] m = parseModuleWithSpaces(moduleFileName);
+ list[loc] oldNameOccurrences = [];
+ for (/Name n := m, "" == name) {
+ oldNameOccurrences += n.src;
+ }
+
+ // print("All locations of \'\': ");
+ // iprintln(sort(oldNameOccurrences, byOffset));
+
+ if ([changed(_, replaces)] := edits) {
+ set[int] idx = {};
+ for (replace(replaceAt, replaceWith) <- replaces) {
+ if (oldText := readFile(replaceAt), "" != name) throw "Unexpected change for \'\' at ";
+ idx += indexOf(oldNameOccurrences, replaceAt);
+ }
+ return idx;
+ } else {
+ print("Unexpected changes: ");
+ iprintln(edits);
+ throw "Unexpected changes: ";
+ }
+}
+
+private str moduleNameToPath(str name) = replaceAll(name, "::", "/");
+private str modulePathToName(str path) = replaceAll(path, "/", "::");
+
+private Tree findCursor(loc f, str id, int occ) {
+ m = parseModuleWithSpaces(f);
+ return [n | /Name n := m.top, "" == id][occ];
+}
+
+private loc storeTestModule(loc dir, str name, str body) {
+ str moduleStr = "
+ 'module
+ '
+ ";
+
+ loc moduleFile = dir + "rascal" + (moduleNameToPath(name) + ".rsc");
+ writeFile(moduleFile, moduleStr);
+
+ return moduleFile;
+}
+
+private set[Tree] occsToTrees(start[Module] m, str name, set[int] occs) = {n | i <- occs, n := [n | /Name n := m.top, "" == name][i]};
+private set[loc] occsToLocs(start[Module] m, str name, set[int] occs) = {t.src | t <- occsToTrees(m, name, occs)};
+private set[int] locsToOccs(start[Module] m, str name, set[loc] occs) = {indexOf(names, occ) | names := [n.src | /Name n := m.top, "" == name], occ <- occs};
diff --git a/rascal-lsp/src/main/rascal/lang/rascal/tests/rename/Types.rsc b/rascal-lsp/src/main/rascal/lang/rascal/tests/rename/Types.rsc
new file mode 100644
index 000000000..d98f64a5e
--- /dev/null
+++ b/rascal-lsp/src/main/rascal/lang/rascal/tests/rename/Types.rsc
@@ -0,0 +1,177 @@
+@license{
+Copyright (c) 2018-2023, 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.
+}
+module lang::rascal::tests::rename::Types
+
+import lang::rascal::tests::rename::TestUtils;
+
+test bool globalAlias() = testRenameOccurrences({0, 1, 2, 3}, "
+ 'Foo f(Foo x) = x;
+ 'Foo foo = 8;
+ 'y = f(foo);
+", decls = "alias Foo = int;", oldName = "Foo", newName = "Bar");
+
+test bool multiModuleAlias() = testRenameOccurrences({
+ byText("alg::Fib", "alias Foo = int;
+ 'Foo fib(int n) {
+ ' if (n \< 2) {
+ ' return 1;
+ ' }
+ ' return fib(n - 1) + fib(n -2);
+ '}"
+ , {0, 1})
+ , byText("Main", "import alg::Fib;
+ '
+ 'int main() {
+ ' Foo result = fib(8);
+ ' return 0;
+ '}"
+ , {0})
+ }, oldName = "Foo", newName = "Bar");
+
+test bool globalData() = testRenameOccurrences({0, 1, 2, 3}, "
+ 'Foo f(Foo x) = x;
+ 'Foo x = foo();
+ 'y = f(x);
+", decls = "data Foo = foo();", oldName = "Foo", newName = "Bar");
+
+test bool multiModuleData() = testRenameOccurrences({
+ byText("values::Bool", "
+ 'data Bool = t() | f();
+ '
+ 'Bool and(Bool l, Bool r) = r is t ? l : f;
+ '"
+ , {1, 2, 3, 4})
+ , byText("Main", "import values::Bool;
+ '
+ 'void main() {
+ ' Bool b = and(t(), f());
+ '}"
+ , {1})
+ }, oldName = "Bool", newName = "Boolean");
+
+test bool dataTypesInVModuleStructure() = testRenameOccurrences({
+ byText("Left", "data Foo = f();", {0}), byText("Right", "data Foo = g();", {0})
+ , byText("Merger",
+ "import Left;
+ 'import Right;
+ 'bool f(Foo foo) = (foo == f() || foo == g());
+ ", {0})
+}, oldName = "Foo", newName = "Bar");
+
+@synopsis{
+ (defs)
+ / \
+ A B C
+ \/ \/
+ D E
+ / \
+ (uses)
+}
+test bool dataTypesInWModuleStructureWithoutMerge() = testRenameOccurrences({
+ byText("A", "data Foo = f();", {0}), byText("B", "", {}), byText("C", "data Foo = h();", {})
+ , byText("D",
+ "import A;
+ 'import B;
+ 'bool func(Foo foo) = foo == f();
+ ", {0}), byText("E",
+ "import B;
+ 'import C;
+ 'bool func(Foo foo) = foo == h();", {})
+}, oldName = "Foo", newName = "Bar");
+
+@synopsis{
+ (defs)
+ / | \
+ v v v
+ A B C
+ \/ \/
+ D E
+ ^ ^
+ / \
+ (uses)
+}
+test bool dataTypesInWModuleStructureWithMerge() = testRenameOccurrences({
+ byText("A", "data Foo = f();", {0}), byText("B", "data Foo = g();", {0}), byText("C", "data Foo = h();", {0})
+ , byText("D",
+ "import A;
+ 'import B;
+ 'bool func(Foo foo) = foo == f();
+ ", {0}), byText("E",
+ "import B;
+ 'import C;
+ 'bool func(Foo foo) = foo == h();", {0})
+}, oldName = "Foo", newName = "Bar");
+
+test bool dataTypesInYModuleStructure() = testRenameOccurrences({
+ byText("Left", "data Foo = f();", {0}), byText("Right", "data Foo = g();", {0})
+ , byText("Merger",
+ "extend Left;
+ 'extend Right;
+ ", {})
+ , byText("User",
+ "import Merger;
+ 'bool f(Foo foo) = (foo == f() || foo == g());
+ ", {0})
+}, oldName = "Foo", newName = "Bar");
+
+test bool dataTypesInInvertedVModuleStructure() = testRenameOccurrences({
+ byText("Definer", "data Foo = f() | g();", {0}),
+ byText("Left", "import Definer;
+ 'bool isF(Foo foo) = foo == f();", {0}), byText("Right", "import Definer;
+ 'bool isG(Foo foo) = foo == g();", {0})
+}, oldName = "Foo", newName = "Bar");
+
+test bool dataTypesInDiamondModuleStructure() = testRenameOccurrences({
+ byText("Definer", "data Foo = f() | g();", {0}),
+ byText("Left", "extend Definer;", {}), byText("Right", "extend Definer;", {}),
+ byText("User", "import Left;
+ 'import Right;
+ 'bool isF(Foo foo) = foo == f();
+ 'bool isG(Foo foo) = foo == g();", {0, 1})
+}, oldName = "Foo", newName = "Bar");
+
+@synopsis{
+ Two disjunct module trees. Both trees define `data Foo`. Since the trees are disjunct,
+ we expect a renaming triggered from the left side leaves the right side untouched.
+}
+test bool dataTypesInIIModuleStructure() = testRenameOccurrences({
+ byText("LeftDefiner", "data Foo = f();", {0}), byText("RightDefiner", "data Foo = g();", {}),
+ byText("LeftExtender", "extend LeftDefiner;", {}), byText("RightExtender", "extend RightDefiner;", {}),
+ byText("LeftUser", "import LeftExtender;
+ 'bool func(Foo foo) = foo == f();", {0}), byText("RightUser", "import RightExtender;
+ 'bool func(Foo foo) = foo == g();", {})
+}, oldName = "Foo", newName = "Bar");
+
+test bool sameIdRoleOnly() = testRenameOccurrences({
+ byText("A", "data foo = f();", {})
+ , byText("B", "extend A;
+ 'data foo = g();", {})
+ , byText("C", "extend B;
+ 'int foo = 8;",{0})
+ , byText("D", "import C;
+ 'int baz = C::foo + 1;", {0})
+});
diff --git a/rascal-lsp/src/main/rascal/lang/rascal/tests/rename/ValidNames.rsc b/rascal-lsp/src/main/rascal/lang/rascal/tests/rename/ValidNames.rsc
new file mode 100644
index 000000000..b342ce294
--- /dev/null
+++ b/rascal-lsp/src/main/rascal/lang/rascal/tests/rename/ValidNames.rsc
@@ -0,0 +1,58 @@
+@license{
+Copyright (c) 2018-2023, 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.
+}
+module lang::rascal::tests::rename::ValidNames
+
+import lang::rascal::tests::rename::TestUtils;
+import lang::rascal::lsp::refactor::Exception;
+
+import analysis::diff::edits::TextEdits;
+
+test bool renameToReservedName() {
+ edits = getEdits("int foo = 8;", 0, "foo", "int", "", "");
+ newNames = {name | e <- edits, changed(_, replaces) := e
+ , r <- replaces, replace(_, name) := r};
+
+ return newNames == {"\\int"};
+}
+
+@expected{illegalRename}
+test bool renameToUsedReservedName() = testRename("
+ 'int \\int = 0;
+ 'int foo = 8;
+", newName = "int");
+
+@expected{illegalRename}
+test bool newNameIsNonAlphaNumeric() = testRename("int foo = 8;", newName = "b@r");
+
+@expected{illegalRename}
+test bool newNameIsNumber() = testRename("int foo = 8;", newName = "8");
+
+@expected{illegalRename}
+test bool newNameHasNumericPrefix() = testRename("int foo = 8;", newName = "8abc");
+
+@expected{illegalRename}
+test bool newNameIsEscapedInvalid() = testRename("int foo = 8;", newName = "\\8int");
diff --git a/rascal-lsp/src/main/rascal/lang/rascal/tests/rename/Variables.rsc b/rascal-lsp/src/main/rascal/lang/rascal/tests/rename/Variables.rsc
new file mode 100644
index 000000000..ed1408305
--- /dev/null
+++ b/rascal-lsp/src/main/rascal/lang/rascal/tests/rename/Variables.rsc
@@ -0,0 +1,206 @@
+@license{
+Copyright (c) 2018-2023, 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.
+}
+module lang::rascal::tests::rename::Variables
+
+import lang::rascal::tests::rename::TestUtils;
+import lang::rascal::lsp::refactor::Exception;
+
+
+//// Local
+
+test bool freshName() = testRenameOccurrences({0}, "
+ 'int foo = 8;
+ 'int qux = 10;
+");
+
+test bool shadowVariableInInnerScope() = testRenameOccurrences({0}, "
+ 'int foo = 8;
+ '{
+ ' int bar = 9;
+ '}
+");
+
+test bool parameterShadowsVariable() = testRenameOccurrences({0}, "
+ 'int foo = 8;
+ 'int f(int bar) {
+ ' return bar;
+ '}
+");
+
+@expected{illegalRename}
+test bool implicitVariableDeclarationInSameScopeBecomesUse() = testRename("
+ 'int foo = 8;
+ 'bar = 9;
+");
+
+@expected{illegalRename}
+test bool implicitVariableDeclarationInInnerScopeBecomesUse() = testRename("
+ 'int foo = 8;
+ '{
+ ' bar = 9;
+ '}
+");
+
+@expected{illegalRename}
+test bool doubleVariableDeclaration() = testRename("
+ 'int foo = 8;
+ 'int bar = 9;
+");
+
+test bool adjacentScopes() = testRenameOccurrences({0}, "
+ '{
+ ' int foo = 8;
+ '}
+ '{
+ ' int bar = 9;
+ '}
+");
+
+@expected{illegalRename}
+test bool implicitPatterVariableInSameScopeBecomesUse() = testRename("
+ 'int foo = 8;
+ 'bar := 9;
+");
+
+@expected{illegalRename}
+test bool implicitNestedPatterVariableInSameScopeBecomesUse() = testRename("
+ 'int foo = 8;
+ '\ := \<9, 99\>;
+");
+
+@expected{illegalRename}
+test bool implicitPatterVariableInInnerScopeBecomesUse() = testRename("
+ 'int foo = 8;
+ 'if (bar := 9) {
+ ' temp = 2 * bar;
+ '}
+");
+
+test bool explicitPatternVariableInInnerScope() = testRenameOccurrences({0}, "
+ 'int foo = 8;
+ 'if (int bar := 9) {
+ ' bar = 2 * bar;
+ '}
+");
+
+test bool becomesPatternInInnerScope() = testRenameOccurrences({0}, "
+ 'int foo = 8;
+ 'if (bar : int _ := 9) {
+ ' bar = 2 * bar;
+ '}
+");
+
+@expected{illegalRename}
+test bool implicitPatternVariableBecomesInInnerScope() = testRename("
+ 'int foo = 8;
+ 'if (bar : _ := 9) {
+ ' bar = 2 * foo;
+ '}
+");
+
+@expected{illegalRename}
+test bool explicitPatternVariableBecomesInInnerScope() = testRename("
+ 'int foo = 8;
+ 'if (bar : int _ := 9) {
+ ' bar = 2 * foo;
+ '}
+");
+
+@expected{illegalRename}
+test bool shadowDeclaration() = testRename("
+ 'int foo = 8;
+ 'if (int bar := 9) {
+ ' foo = 2 * bar;
+ '}
+");
+
+// Although this is fine statically, it will cause runtime errors when `bar` is called
+// > A value of type int is not something you can call like a function, a constructor or a closure.
+@expected{illegalRename}
+test bool doubleVariableAndFunctionDeclaration() = testRename("
+ 'int foo = 8;
+ 'void bar() {}
+");
+
+// Although this is fine statically, it will cause runtime errors when `bar` is called
+// > A value of type int is not something you can call like a function, a constructor or a closure.
+@expected{illegalRename}
+test bool doubleFunctionAndVariableDeclaration() = testRename("
+ 'void bar() {}
+ 'foo = 8;
+");
+
+@expected{illegalRename}
+test bool doubleFunctionAndNestedVariableDeclaration() = testRename("
+ 'bool bar() = true;
+ 'void f() {
+ ' int foo = 0;
+ '}
+");
+
+test bool tupleVariable() = testRenameOccurrences({0}, "\ = \<0, 1\>;");
+
+test bool tuplePatternVariable() = testRenameOccurrences({0, 1}, "
+ 'if (\ := \<0, 1\>)
+ ' qux = foo;
+");
+
+
+//// Global
+
+test bool globalVar() = testRenameOccurrences({0, 3}, "
+ 'int f(int foo) = foo;
+ 'foo = 16;
+", decls = "
+ 'int foo = 8;
+");
+
+test bool multiModuleVar() = testRenameOccurrences({
+ byText("alg::Fib", "int foo = 8;
+ '
+ 'int fib(int n) {
+ ' if (n \< 2) {
+ ' return 1;
+ ' }
+ ' return fib(n - 1) + fib(n -2);
+ '}"
+ , {0})
+ , byText("Main", "import alg::Fib;
+ '
+ 'int main() {
+ ' fib(alg::Fib::foo);
+ ' return 0;
+ '}"
+ , {0})
+ });
+
+test bool unrelatedVar() = testRenameOccurrences({
+ byText("Module1", "int foo = 8;", {0})
+ , byText("Module2", "import Module1;
+ 'int foo = 2;
+ 'int baz = foo;", {})
+});
diff --git a/rascal-lsp/src/test/java/engineering/swat/rascal/lsp/util/LineColumnOffsetMapTests.java b/rascal-lsp/src/test/java/engineering/swat/rascal/lsp/util/LineColumnOffsetMapTests.java
index 1c507942b..a5f2bcfff 100644
--- a/rascal-lsp/src/test/java/engineering/swat/rascal/lsp/util/LineColumnOffsetMapTests.java
+++ b/rascal-lsp/src/test/java/engineering/swat/rascal/lsp/util/LineColumnOffsetMapTests.java
@@ -28,19 +28,19 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
-import org.junit.jupiter.api.Test;
+import org.junit.Test;
import org.rascalmpl.vscode.lsp.util.locations.LineColumnOffsetMap;
import org.rascalmpl.vscode.lsp.util.locations.impl.ArrayLineOffsetMap;
public class LineColumnOffsetMapTests {
@Test
- void noUnicodeChars() {
+ public void noUnicodeChars() {
LineColumnOffsetMap map = ArrayLineOffsetMap.build("1234\n1234");
assertEquals(2, map.translateColumn(0, 2, false));
}
@Test
- void singleWideChar() {
+ public void singleWideChar() {
LineColumnOffsetMap map = ArrayLineOffsetMap.build("12🎉45\n1234🎉");
assertEquals(3, map.translateColumn(0, 3, false));
assertEquals(4, map.translateColumn(0, 3, true));
@@ -49,7 +49,7 @@ void singleWideChar() {
@Test
- void doubleChars() {
+ public void doubleChars() {
LineColumnOffsetMap map = ArrayLineOffsetMap.build("12🎉4🎉6\n1234");
assertEquals(6, map.translateColumn(0, 5, false));
assertEquals(7, map.translateColumn(0, 5, true));
@@ -57,13 +57,13 @@ void doubleChars() {
}
@Test
- void noUnicodeCharsInverse() {
+ public void noUnicodeCharsInverse() {
LineColumnOffsetMap map = ArrayLineOffsetMap.build("1234\n1234");
assertEquals(2, map.translateInverseColumn(0, 2, false));
}
@Test
- void singleWideCharInverse() {
+ public void singleWideCharInverse() {
LineColumnOffsetMap map = ArrayLineOffsetMap.build("12🎉45\n1234🎉");
assertEquals(3, map.translateInverseColumn(0, 3, false));
assertEquals(3, map.translateInverseColumn(0, 4, false));
@@ -72,7 +72,7 @@ void singleWideCharInverse() {
@Test
- void doubleCharsInverse() {
+ public void doubleCharsInverse() {
LineColumnOffsetMap map = ArrayLineOffsetMap.build("12🎉4🎉6\n1234");
assertEquals(5, map.translateInverseColumn(0, 6, false));
assertEquals(5, map.translateInverseColumn(0, 7, true));
diff --git a/rascal-lsp/src/test/java/engineering/swat/rascal/lsp/util/LookupTests.java b/rascal-lsp/src/test/java/engineering/swat/rascal/lsp/util/LookupTests.java
index ffd6c0a0a..4ed1cc998 100644
--- a/rascal-lsp/src/test/java/engineering/swat/rascal/lsp/util/LookupTests.java
+++ b/rascal-lsp/src/test/java/engineering/swat/rascal/lsp/util/LookupTests.java
@@ -26,13 +26,14 @@
*/
package engineering.swat.rascal.lsp.util;
-import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.Assert.assertSame;
+
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import org.eclipse.lsp4j.Position;
import org.eclipse.lsp4j.Range;
-import org.junit.jupiter.api.Test;
+import org.junit.Test;
import org.rascalmpl.vscode.lsp.util.locations.impl.TreeMapLookup;
public class LookupTests {
@@ -157,7 +158,7 @@ public void randomRanges() {
ranges.forEach(target::put);
for (var e: ranges.entrySet()) {
var found = target.lookup(e.getKey());
- assertSame(e.getValue(), found, "Entry " + e + "should be found");
+ assertSame("Entry " + e + "should be found", e.getValue(), found);
}
}
diff --git a/rascal-lsp/src/test/java/engineering/swat/rascal/lsp/util/RenameTests.java b/rascal-lsp/src/test/java/engineering/swat/rascal/lsp/util/RenameTests.java
index ec5211cb1..a96ea4ec8 100644
--- a/rascal-lsp/src/test/java/engineering/swat/rascal/lsp/util/RenameTests.java
+++ b/rascal-lsp/src/test/java/engineering/swat/rascal/lsp/util/RenameTests.java
@@ -31,5 +31,5 @@
import org.rascalmpl.test.infrastructure.RascalJUnitTestRunner;
@RunWith(RascalJUnitTestRunner.class)
-@RascalJUnitTestPrefix("lang::rascal::tests")
+@RascalJUnitTestPrefix("lang::rascal::tests::rename")
public class RenameTests {}