Skip to content

Commit 6ef0e6c

Browse files
authored
Merge pull request #15 from usethesource/chore/document-rename-framework
How-to rename & rename API doc fixes
2 parents a57d6ab + c3380a8 commit 6ef0e6c

6 files changed

Lines changed: 277 additions & 32 deletions

File tree

doc/TypePal/Collector/Collector.md

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,23 @@ A `Collector` collects constraints from source code and produces an initial `TMo
88

99
#### Description
1010

11-
A `Collector` is a statefull object that provides all the functions described below to access and change its internal state. The global services provided by a `Collector` are:
11+
A `Collector` is a stateful object that provides all the functions described below to access and change its internal state. The global services provided by a `Collector` are:
1212

1313
* Register facts, calculators, and requirements as collected from the source program.
1414
* Maintain a global (key,value) store to store global information relevant for the collection process. Typical examples are:
15-
** Configuration information.
16-
** The files that have been imported.
15+
- Configuration information.
16+
- The files that have been imported.
1717
* Manage scopes.
1818
* Maintain a single value per scope. This enables decoupling the collection of information from separate but related language constructs.
1919
Typical examples are:
20-
** While collecting information from a function declaration:
20+
- While collecting information from a function declaration:
2121
create a new function scope and associate the required return type with it so that return statements in the function body can check that
22-
(a) they occur inside a function;
23-
(b) that the type of their returned value is compatible with the required return type.
24-
** While collecting information from an optionally labelled loop statement:
22+
1. they occur inside a function;
23+
2. that the type of their returned value is compatible with the required return type.
24+
- While collecting information from an optionally labelled loop statement:
2525
create a new loop scope and associate the label with it so that break/continue statements can check that:
26-
(a) they occur inside a loop statement;
27-
(b) which loop statement they should (dis)continue.
26+
1. they occur inside a loop statement;
27+
2. which loop statement they should (dis)continue.
2828
* Reporting.
2929

3030
The functions provided by a `Collector` are summarized below:
@@ -191,7 +191,7 @@ void collect(current:(Statement) `break <Target target>;`, Collector c){
191191
<1> Introduces a data type to represent loop information.
192192
<2> When handling a while statement, the current scope is marked as `loopScope` and `loopInfo` is associated with it.
193193
<3> When handling a `break` statement, we get all available ScopeInfo for loopScopes (innermost first) and check the associated loopInfo.
194-
194+
195195

196196
##### Nested Info
197197

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
---
2+
title: Rename refactoring
3+
---
4+
5+
#### Synopsis
6+
7+
TypePal offers a framework for rename refactoring. A `Renamer` collects document edits and diagnostics.
8+
9+
#### Description
10+
11+
A rename refactoring is one of the most commonly used refactorings; it renames all corresponding definitions and references to a new name. TypePal includes a framework that enables efficient implementation of rename refactoring for a language by removing boilerplate and generic default behaviour.
12+
13+
::: Prerequisite
14+
The language uses a TypePal-based typechecker.
15+
:::
16+
17+
##### Basic usage
18+
19+
This is an example of a very basic renaming for the `pico` [example](https://github.com/usethesource/typepal/tree/main/src/examples/pico/Rename.rsc).
20+
21+
The first step is to configure the renaming, by at least providing parse and type-check functions.
22+
```rascal
23+
extend analysis::typepal::refactor::Rename;
24+
import examples::pico::Syntax;
25+
import examples::pico::Checker;
26+
27+
RenameConfig picoRenameConfig = rconfig(
28+
Tree(loc l) { return parse(#start[Program], l); }
29+
, collectAndSolve
30+
);
31+
```
32+
33+
We can re-use this config for any renaming for `pico`.
34+
35+
::: Caution
36+
To access the functions in the configuration during the various steps of the renaming, use `Renamer::getConfig` to retrieve the config instead of the above declaration. This will ensure the use of internal caches.
37+
:::
38+
39+
Using the configuration, define a rename function.
40+
41+
```rascal
42+
import Exception;
43+
44+
tuple[list[DocumentEdit] edits, set[Message] msgs] renamePico(list[Tree] cursor, str newName) {
45+
if (!isValidName(newName)) return <[], {error("\'<newName>\' is not a valid name here.", cursor[0].src)}>;
46+
return rename(cursor, newName, picoRenameConfig);
47+
}
48+
49+
bool isValidName(str name) {
50+
try {
51+
parse(#Id, name);
52+
return true;
53+
} catch ParseError(_): {
54+
return false;
55+
}
56+
}
57+
```
58+
59+
This is enough to get a simple rename refactoring for a language like `pico`. The framework will take care of finding the locations to substitute with the new name automatically.
60+
The IDE will then apply these text edits.
61+
62+
##### Advanced usage
63+
64+
The framework goes through multiple stages, analysing files and looking for occurrences of the name under the cursor. It takes care of all the bookkeeping. For any stage, there is the possibility of overriding the default behaviour. Overriding works just like for TypePal's type-check functionality, by `extend`ing.
65+
66+
* Resolving the definition(s) of the name under the cursor
67+
* Finding all uses of that declaration
68+
* Checking that no definitions with the new name already exist
69+
* Finding where the name is in the definition tree
70+
* Producing the edits to fulfil the actual renaming
71+
72+
###### Advanced configuration
73+
74+
The `RenameConfig` exposes some additional properties through optional keyword arguments. For reference see [here](https://www.rascal-mpl.org/docs/Packages/Typepal/API/analysis/typepal/refactor/Rename/#analysis-typepal-refactor-Rename-RenameConfig).
75+
76+
Additionally, one can extend the configuration with keyword arguments, for example to retain some state for a single rename. Example:
77+
78+
```rascal
79+
extend analysis::typepal::refactor::Rename;
80+
import examples::pico::Syntax;
81+
import examples::pico::Checker;
82+
83+
data RenameConfig(set[loc] workspaceFolders = {});
84+
85+
tuple[list[DocumentEdit] edits, set[Message] msgs] renamePico(list[Tree] cursor, str newName, set[loc] workspaceFolders)
86+
= rename(cursor
87+
, newName
88+
, rconfig(
89+
Tree(loc l) { return parse(#start[Program], l); }
90+
, collectAndSolve
91+
, wokspaceFolders = workspaceFolders
92+
)
93+
);
94+
```
95+
96+
###### Resolving definitions
97+
98+
Resolve the name under the cursor to definition(s).
99+
100+
```rascal
101+
set[Define] getCursorDefinitions(Focus cursor, Tree(loc) getTree, TModel(Tree) getModel, Renamer r);
102+
```
103+
104+
The default implementation only looks for definitions in the file where the cursor is.
105+
106+
###### Find relevant files
107+
108+
Find files that might contain one of the following occurrences. This should be a fast over-approximation.
109+
110+
* Definitions of the name under the cursor.
111+
* References to/uses of aforementioned definitions.
112+
* Definitions or uses of the new name.
113+
114+
```rascal
115+
tuple[set[loc] defFiles, set[loc] useFiles, set[loc] newNameFiles] findOccurrenceFiles(set[Define] cursorDefs, Focus cursor, str newName, Tree(loc) getTree, Renamer r);
116+
```
117+
118+
The default implementation only looks at the file where the cursor is. For multi-file projects, this step should probably consider more files. If the number of files in projects can be large, it is wise to consider performance when overriding this function.
119+
120+
1. The amount of work done per file should be reasonable.
121+
2. The files returned here will be the inputs to the next steps. Most of those steps will trigger the type-checker on the file first. If type-checking is expensive, try not to over-approximate too liberally here.
122+
123+
###### Find additional definitions
124+
125+
For each files in `defFiles` from [`findOccurrenceFiles`](#find-relevant-files), find additional definitions to rename.
126+
127+
```rascal
128+
set[Define] findAdditionalDefinitions(set[Define] cursorDefs, Tree tr, TModel tm, Renamer r);
129+
```
130+
131+
The default implementation returns the empty set. The following example overrides the default to find overloaded definitions.
132+
133+
```rascal
134+
extend analysis::typepal::refactor::Rename;
135+
136+
set[Define] findAdditionalDefinitions(set[Define] defs, Tree _, TModel tm, Renamer _) =
137+
{
138+
tm.definitions[d]
139+
| loc d <- (tm.defines<idRole, id, defined>)[defs.idRole, defs.id] - defs.defined
140+
, tm.config.mayOverload(defs.defined + d, tm.definitions)
141+
};
142+
```
143+
144+
###### Validate occurrences of new name
145+
146+
For all `newFiles` from the [selected files](#find-relevant-files), check if renaming `cursorDefs` will cause problems with the existing occurrences of `newName` in the file.
147+
148+
```rascal
149+
void validateNewNameOccurrences(set[Define] cursorDefs, str newName, Tree tr, Renamer r);
150+
```
151+
152+
The default implementation raises an error when a occurrence of `newName` exists here.
153+
154+
Example (simplified from the renaming implementation for Rascal itself) that checks for shadowing, overloading and double declarations introduced by the rename.
155+
```rascal
156+
void validateNewNameOccurrences(set[Define] cursorDefs, str newName, Tree tr, Renamer r) {
157+
tm = r.getConfig().tmodelForLoc(tr.src.top);
158+
159+
defUse = invert(tm.useDef);
160+
reachable = rascalGetReflexiveModulePaths(tm).to;
161+
newNameDefs = {nD | Define nD:<_, newName, _, _, _, _> <- tm.defines};
162+
curAndNewDefinitions = (d.defined: d | d <- currentDefs + newNameDefs); // temporary map for overloading checks
163+
164+
for (<Define c, Define n> <- currentDefs * newNameDefs) {
165+
set[loc] curUses = defUse[c.defined];
166+
set[loc] newUses = defUse[n.defined];
167+
168+
// Will this rename hide a used definition of `oldName` behind an existing definition of `newName` (shadowing)?
169+
for (loc cU <- curUses
170+
, isContainedInScope(cU, n.scope, tm)
171+
, isContainedInScope(n.scope, c.scope, tm)) {
172+
r.error(cU, "Renaming this to \'<newName>\' would change the program semantics; its original definition would be shadowed by <n.defined>.");
173+
}
174+
175+
// Will this rename hide a used definition of `newName` behind a definition of `oldName` (shadowing)?
176+
for (isContainedInScope(c.scope, n.scope, tm)
177+
, loc nU <- newUses
178+
, isContainedInScope(nU, c.scope, tm)) {
179+
r.error(c.defined, "Renaming this to \'<newName>\' would change the program semantics; it would shadow the declaration of <nU>.");
180+
}
181+
182+
// Is `newName` already resolvable from a scope where `oldName` is currently declared?
183+
if (tm.config.mayOverload({c.defined, n.defined}, curAndNewDefinitions)) {
184+
// Overloading
185+
if (c.scope in reachable || isContainedInScope(c.defined, n.scope, tm) || isContainedInScope(n.defined, c.scope, tm)) {
186+
r.error(c.defined, "Renaming this to \'<newName>\' would overload an existing definition at <n.defined>.");
187+
}
188+
} else if (isContainedInScope(c.defined, n.scope, tm)) {
189+
// Double declaration
190+
r.error(c.defined, "Renaming this to \'<newName>\' would cause a double declaration (with <n.defined>).");
191+
}
192+
}
193+
}
194+
```
195+
196+
###### Find name location
197+
198+
Finds the location of the name in a definitions parse tree.
199+
200+
```rascal
201+
loc nameLocation(Tree t, Define d);
202+
```
203+
204+
The default implementation returns the location of the first (left-most) subtree of which the un-parsed representation matches the name of the definition. If no match is found, it returns the location of the parse tree.
205+
206+
###### Rename definition
207+
208+
Rename a single definition, with its name at `nameLoc` (determined by [`nameLocation`](#find-name-location)) to `newName`. This is called for each definition collected by `getCursorDefinitions` and `findAdditionalDefinitions`.
209+
210+
```rascal
211+
void renameDefinition(Define d, loc nameLoc, str newName, TModel tm, Renamer r);
212+
```
213+
214+
The default implementation registers an edit to replace the text at `nameLoc` with `newName`. Overriding this can be useful, e.g. if extra checks are required to confirm the rename is valid, or if the renaming requires additional edits, like moving a file.
215+
216+
217+
The following example override registers an edit for renaming a file when renaming a module.
218+
```rascal
219+
import Location;
220+
221+
str makeFileName(str name) = ...;
222+
223+
data RenamConfig(set[loc] srcDirs = {|file:///source1|, |file:///source2|});
224+
225+
void renameDefinition(Define d:<_, currentName, _, moduleId(), _, _>, loc nameLoc, str newName, TModel _, Renamer r) {
226+
loc moduleFile = d.defined.top;
227+
if (loc srcDir <- r.getConfig().srcDirs, loc relModulePath := relativize(srcDir, moduleFile), relModulePath != moduleFile) {
228+
// Change the module header
229+
r.textEdit(replace(nameLoc, newName));
230+
// Rename the file
231+
r.documentEdit(renamed(moduleFile, srcDir + makeFileName(newName)));
232+
} else {
233+
r.error(moduleFile, "Cannot rename <currentName>, since it is not defined in this project.");
234+
}
235+
}
236+
```
237+
238+
###### Rename uses
239+
240+
In a single file, rename all uses of the definitions. This is called for all `useFiles` from the [selected files](#find-relevant-files).
241+
242+
```rascal
243+
void renameUses(set[Define] defs, str newName, TModel tm, Renamer r);
244+
```
245+
246+
The default implementation registers edits to replace the text at any use with `newName`. Overriding this can be useful, e.g. if extra checks are required to confirm the rename is valid, or if additional edits are necessary.

doc/TypePal/TypePal.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ details:
1010
- Solver
1111
- Configuration
1212
- Utilities
13+
- RenameRefactoring
1314
- Examples
1415
---
1516

src/analysis/typepal/refactor/Rename.rsc

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,15 @@ alias Focus = list[Tree];
5050
private int WORKSPACE_WORK = 10;
5151
private int FILE_WORK = 5;
5252

53+
@synopsis{Tracks state of renaming and provides helper functions.}
54+
@description{
55+
Tracks the state of the renaming, as an argument to every function of the rename framework.
56+
57+
* `msg` registers a ((FailMessage)). Registration of an ((analysis::typepal::FailMessage-error)) triggers premature termination of the renaming at the soonest possibility (typically before the next rename phase).
58+
* `documentEdit` registers a ((DocumentEdit)), which represents a change required for the renaming.
59+
* `textEdit` registers a ((TextEdit)), which represents a change required for the renaming. It is a convenience function that converts to a ((DocumentEdit)) internally, grouping ((TextEdit))s to the same file where possible.
60+
* `getConfig` retrieves the ((analysis::typepal::refactor::Rename::RenameConfig)).
61+
}
5362
data Renamer
5463
= renamer(
5564
void(FailMessage) msg
@@ -321,6 +330,7 @@ RenameResult rename(
321330
}
322331

323332
// TODO If performance bottleneck, rewrite to binary search
333+
@synopsis{Compute locations of names of `defs` in `tr`.}
324334
private map[Define, loc] defNameLocations(Tree tr, set[Define] defs, Renamer _r) {
325335
map[loc, Define] definitions = (d.defined: d | d <- defs);
326336
set[loc] defsToDo = defs.defined;
@@ -346,8 +356,8 @@ private map[Define, loc] defNameLocations(Tree tr, set[Define] defs, Renamer _r)
346356
return defNames;
347357
}
348358
349-
@synopsis{Computes ((Define))(s) for the name under the cursor.}
350-
default set[Define] getCursorDefinitions(Focus cursor, Tree(loc) _r, TModel(Tree) getModel, Renamer r) {
359+
@synopsis{Computes ((Define))(s) for the name under `cursor`.}
360+
default set[Define] getCursorDefinitions(Focus cursor, Tree(loc) _getTree, TModel(Tree) getModel, Renamer r) {
351361
loc cursorLoc = cursor[0].src;
352362
TModel tm = getModel(cursor[-1]);
353363
for (Tree c <- cursor) {
@@ -368,6 +378,7 @@ default set[Define] getCursorDefinitions(Focus cursor, Tree(loc) _r, TModel(Tree
368378
}
369379
370380
@synopsis{Computes in which files occurrences of `cursorDefs` and `newName` *might* occur (over-approximation). This is not supposed to call the type-checker on any file for performance reasons.}
381+
@pitfalls{For any file in `defFiles + useFiles`, the framework calls `RenameConfig::tmodelForLoc`. If type-cehcking is expensive and this function over-approximates by a large margin, the performance of the renaming might degrade.}
371382
default tuple[set[loc] defFiles, set[loc] useFiles, set[loc] newNameFiles] findOccurrenceFiles(set[Define] cursorDefs, Focus cursor, str newName, Tree(loc) _getTree, Renamer r) {
372383
loc f = cursor[0].src.top;
373384
if (any(d <- cursorDefs, f != d.defined.top)) {
@@ -389,12 +400,12 @@ default void validateNewNameOccurrences(set[Define] cursorDefs, str newName, Tre
389400
}
390401
}
391402
392-
@synopsis{Renames a single ((Define)) with its name at `nameLoc`, by producing a corresponding ((DocumentEdit)).}
403+
@synopsis{Renames a single ((Define)) `_d `with its name at `nameLoc`, defined in ((TModel)) `_tm`, to `newName`, by producing corresponding ((DocumentEdit))s.}
393404
default void renameDefinition(Define _d, loc nameLoc, str newName, TModel _tm, Renamer r) {
394405
r.textEdit(replace(nameLoc, newName));
395406
}
396407
397-
@synopsis{{Renames all uses of `defs` in a single file/((TModel)), by producing corresponding ((DocumentEdit))s.}}
408+
@synopsis{{Renames all uses of `defs` in a single file/((TModel)) `tm`, by producing corresponding ((DocumentEdit))s.}}
398409
default void renameUses(set[Define] defs, str newName, TModel tm, Renamer r) {
399410
for (loc u <- invert(tm.useDef)[defs.defined] - defs.defined) {
400411
r.textEdit(replace(u, newName));

src/examples/modfun/Rename.rsc

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -133,18 +133,11 @@ bool tryParse(type[&T <: Tree] tp, str s) {
133133
bool isValidName(moduleId(), str name) = tryParse(#ModId, name);
134134
bool isValidName(variableId(), str name) = tryParse(#Id, name);
135135
136-
set[Define] findAdditionalDefinitions(set[Define] cursorDefs, Tree _, TModel tm, Renamer _) {
137-
set[Define] overloads = {};
138-
for (d <- tm.defines
139-
&& d.idRole in cursorDefs.idRole
140-
&& d.id in cursorDefs.id
141-
&& d.defined notin cursorDefs.defined) {
142-
if (tm.config.mayOverload(cursorDefs.defined + d.defined, tm.definitions)) {
143-
overloads += d;
144-
}
145-
}
146-
return overloads;
147-
}
136+
set[Define] findAdditionalDefinitions(set[Define] cursorDefs, Tree _, TModel tm, Renamer _) =
137+
{ tm.definitions[d]
138+
| loc d <- (tm.defines<idRole, id, defined>)[cursorDefs.idRole, cursorDefs.id] - cursorDefs.defined
139+
, tm.config.mayOverload(cursorDefs.defined + d, tm.definitions)
140+
};
148141
149142
void renameUses(set[Define] defs, str newName, TModel tm, Renamer r) {
150143
// Somehow, tm.useDef is empty, so we need to use tm.uses

src/examples/pico/Rename.rsc

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,9 @@ module examples::pico::Rename
2929
import examples::pico::Syntax;
3030
import examples::pico::Checker;
3131

32-
import analysis::typepal::TModel;
33-
3432
extend analysis::typepal::refactor::Rename;
35-
import analysis::diff::edits::TextEdits;
3633

3734
import Exception;
38-
import IO;
39-
import Relation;
40-
import util::FileSystem;
4135

4236
public tuple[list[DocumentEdit] edits, set[Message] msgs] renamePico(list[Tree] cursor, str newName) {
4337
if (!isValidName(newName)) {

0 commit comments

Comments
 (0)