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) = { | 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 {}