Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rename refactoring: keyword fields #444

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
ed62e20
Extend/improve data type tests.
toinehartman Aug 16, 2024
f852237
Basic implementation of constructor field renaming.
toinehartman Aug 19, 2024
2fc4ec7
Implement basic constructor keyword fields.
toinehartman Aug 21, 2024
9ffbaff
Implement common keyword fields.
toinehartman Aug 22, 2024
fa965c1
Find nested (field) definitions as well.
toinehartman Aug 26, 2024
d2d8a7a
Fix cursor at ADT field def.
toinehartman Aug 26, 2024
98809e5
Simplify cursor calculation/preloading.
toinehartman Aug 26, 2024
f709bf0
Resolve reachable definitions in two directions.
toinehartman Aug 26, 2024
949c609
Re-use more generic field machinery.
toinehartman Aug 27, 2024
df99b82
Only rename definitions with the same role.
toinehartman Aug 28, 2024
5be976b
Fix iteration in presence of multiple definitions.
toinehartman Aug 29, 2024
0f28d3f
Support skipping cursors in multi-module tests.
toinehartman Aug 29, 2024
2a6108d
Resolve fields in data definitions.
toinehartman Aug 29, 2024
2d1bbf9
Fix existing field detection.
toinehartman Aug 29, 2024
18a18bf
Consider overloaded/reachable ADT defs only.
toinehartman Aug 30, 2024
216acc1
Find fields to rename in reachable scopes only.
toinehartman Aug 30, 2024
86a359a
Optimize repeated code and clean up.
toinehartman Aug 30, 2024
72a3202
Fix type errors.
toinehartman Sep 2, 2024
7dd59d0
Accept renaming from collection field projection.
toinehartman Sep 2, 2024
0a2ac13
Update documentation.
toinehartman Sep 2, 2024
d2ce994
Merge branch 'feature/rename-refactoring/cross-module' into feature/r…
toinehartman Sep 2, 2024
6b6c336
Clean up some more debugging artefacts.
toinehartman Sep 2, 2024
16d39b6
Merge branch 'feature/rename-refactoring/cross-module' into feature/r…
toinehartman Sep 5, 2024
fac01e3
Small fixes based on review by @PieterOlivier
toinehartman Sep 5, 2024
9c8c98f
Move path config function to workspace info field
toinehartman Sep 5, 2024
54d1a41
Split up logic to determine cursor kind.
toinehartman Sep 5, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions rascal-lsp/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,12 @@
<dependency>
<groupId>org.rascalmpl</groupId>
<artifactId>rascal-core</artifactId>
<version>0.12.3</version>
<version>0.12.4</version>
toinehartman marked this conversation as resolved.
Show resolved Hide resolved
</dependency>
<dependency>
<groupId>org.rascalmpl</groupId>
<artifactId>typepal</artifactId>
<version>0.13.4</version>
<version>0.14.0</version>
</dependency>
<!-- Rascal tests require JUnit 4 -->
<dependency>
Expand Down
212 changes: 143 additions & 69 deletions rascal-lsp/src/main/rascal/lang/rascal/lsp/refactor/Rename.rsc

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion rascal-lsp/src/main/rascal/lang/rascal/lsp/refactor/Util.rsc
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,9 @@ rel[&K, &V] groupBy(set[&V] s, &K(&V) pred) =
@synopsis{
Predicate to sort locations by length.
}
bool byLength(loc l1, loc l2) = l1.length < l2.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.
Expand Down
279 changes: 234 additions & 45 deletions rascal-lsp/src/main/rascal/lang/rascal/lsp/refactor/WorkspaceInfo.rsc

Large diffs are not rendered by default.

149 changes: 124 additions & 25 deletions rascal-lsp/src/main/rascal/lang/rascal/tests/rename/Fields.rsc
Original file line number Diff line number Diff line change
Expand Up @@ -29,26 +29,90 @@ module lang::rascal::tests::rename::Fields
import lang::rascal::tests::rename::TestUtils;
import lang::rascal::lsp::refactor::Exception;

test bool dataField() = testRenameOccurrences({0, 1}, "
test bool constructorField() = testRenameOccurrences({0, 1}, "
'D oneTwo = d(1, 2);
'x = d.foo;
'x = oneTwo.foo;
", decls = "data D = d(int foo, int baz);"
);

// TODO Implement data types properly
// test bool multipleConstructorDataField() = testRenameOccurrences({0, 1, 2}, "
// 'x = d(1, 2);
// 'y = x.foo;
// ", decls = "data D = d(int foo) | d(int foo, int baz);"
// , cursorAtOldNameOccurrence = 1);
test bool constructorKeywordField() = testRenameOccurrences({0, 1, 2}, "

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand I am too late to the party, but what is the rationale not to use tests that show the source code before and after renaming?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test framework only considers occurrences of oldName (by default, that's 'foo'). For every returned document edit, it checks if it matches one of the expected occurrences. Occurrences are simply indices. If the renaming return an edit that does not refer to a location where oldName occurs, or when it renames an occurrences of oldName that is not in the expected list of indices, the test fails.

Additionally, the test framework applies the returned edits to the test workspace (thus obtaining the actual renamed files) and runs the type checker on them.

We could have written the source code as an expectation. This is probably more readable but a lot more to write. Additionally, we still would need to specify from cursor(s) to rename, which, as @rodinaarssen noted, is the same set of indices as the expected renamed occurrences. All in all, this notation is a lot more concise and requires less modifications when extending existing tests.

'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 duplicateDataField() = testRename("", decls =
test bool duplicateConstructorField() = testRename("", decls =
"data D = d(int foo, int bar);"
);

test bool crossModuleDataField() = testRenameOccurrences({
byText("Foo", "data D = a(int foo) | b(int bar);", {0}),
@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() {
Expand All @@ -58,16 +122,51 @@ test bool crossModuleDataField() = testRenameOccurrences({
", {0})
});

// TODO Implement data types properly
// test bool extendedDataField() = testRenameOccurrences({
// byText("Scratch1", "
// 'data Foo = f(int foo);
// ", {0}),
// byText("Scratch2", "
// 'extend Scratch1;
// 'data Foo = g(int 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 = {};
Expand All @@ -82,13 +181,13 @@ test bool lrelField() = testRenameOccurrences({0, 1}, "
test bool relSubscript() = testRenameOccurrences({0, 1}, "
'rel[str foo, str baz] r = {};
'x = r\<foo\>;
", skipCursors = {1});
");

test bool relSubscriptWithVar() = testRenameOccurrences({0, 2}, "
'rel[str foo, str baz] r = {};
'str foo = \"foo\";
'x = r\<foo\>;
", skipCursors = {2});
");

test bool tupleFieldSubscriptUpdate() = testRenameOccurrences({0, 1, 2}, "
'tuple[str foo, int baz] t = \<\"one\", 1\>;
Expand Down Expand Up @@ -122,14 +221,14 @@ test bool tupleField() = testRenameOccurrences({0, 1}, "
");

// We would prefer an illegalRename exception here
@expected{unsupportedRename}
@expected{illegalRename}
test bool builtinFieldSimpleType() = testRename("
'loc l = |unknown:///|;
'f = l.top;
", oldName = "top", newName = "summit");
// We would prefer an illegalRename exception here

@expected{unsupportedRename}
@expected{illegalRename}
test bool builtinFieldCollectionType() = testRename("
'loc l = |unknown:///|;
'f = l.ls;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ 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;
Expand All @@ -50,8 +52,8 @@ import util::Reflective;


//// Fixtures and utility functions
data TestModule = byText(str name, str body, set[int] nameOccs, str newName = name)
| byLoc(loc file, set[int] nameOccs, str newName = name);
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];

Expand Down Expand Up @@ -85,7 +87,7 @@ bool expectEq(&T expected, &T actual, str epilogue = "") {

bool testRenameOccurrences(set[TestModule] modules, str oldName = "foo", str newName = "bar") {
bool success = true;
for (mm <- modules, cursorOcc <- mm.nameOccs) {
for (mm <- modules, cursorOcc <- (mm.nameOccs - mm.skipCursors)) {
str testName = "Test<abs(arbInt())>";
loc testDir = |memory://tests/rename/<testName>|;

Expand All @@ -97,7 +99,7 @@ bool testRenameOccurrences(set[TestModule] modules, str oldName = "foo", str new
}

pcfg = getTestPathConfig(testDir);
modulesByLocation = {mByLoc | m <- modules, mByLoc := (m is byLoc ? m : byLoc(storeTestModule(testDir, m.name, m.body), m.nameOccs, newName = m.newName))};
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 \'<oldName>\' from <cursorT.src>");
Expand Down Expand Up @@ -131,8 +133,6 @@ bool testRenameOccurrences(set[TestModule] modules, str oldName = "foo", str new

if (!expectEq(expectedEditsPerModule, editsPerModule, epilogue = "Rename from cursor <cursorT.src> failed:")) {
success = false;
println("Unexpected edits: ");
iprintln(edits);
}

for (src <- pcfg.srcs) {
Expand Down Expand Up @@ -274,6 +274,9 @@ private set[int] extractRenameOccurrences(loc moduleFileName, list[DocumentEdit]
oldNameOccurrences += n.src;
}

// print("All locations of \'<name>\': ");
// iprintln(sort(oldNameOccurrences, byOffset));

if ([changed(_, replaces)] := edits) {
set[int] idx = {};
for (replace(replaceAt, replaceWith) <- replaces) {
Expand Down
10 changes: 10 additions & 0 deletions rascal-lsp/src/main/rascal/lang/rascal/tests/rename/Types.rsc
Original file line number Diff line number Diff line change
Expand Up @@ -165,3 +165,13 @@ test bool dataTypesInIIModuleStructure() = testRenameOccurrences({
'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({

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain how these tests work? What exactly is being renamed and how is the result being tested?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I answered this while giving context here. Please let me know if anything is still unclear!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In addition, these are multi-module. They specifiy the contents and to-be-renamed per module. Otherwise, this uses the same machinery as the single-module tests.

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})
});
Original file line number Diff line number Diff line change
Expand Up @@ -197,3 +197,10 @@ test bool multiModuleVar() = testRenameOccurrences({
'}"
, {0})
});

test bool unrelatedVar() = testRenameOccurrences({
byText("Module1", "int foo = 8;", {0})
, byText("Module2", "import Module1;
'int foo = 2;
'int baz = foo;", {})
});
Loading