Skip to content

How-to rename & rename API doc fixes #15

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

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
20 changes: 10 additions & 10 deletions doc/TypePal/Collector/Collector.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,23 @@ A `Collector` collects constraints from source code and produces an initial `TMo

#### Description

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:
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:

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

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


##### Nested Info

Expand Down
246 changes: 246 additions & 0 deletions doc/TypePal/RenameRefactoring/RenameRefactoring.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
---
title: Rename refactoring
---

#### Synopsis

TypePal offers a framework for rename refactoring. A `Renamer` collects document edits and diagnostics.

#### Description

The 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.

::: Prerequisite
The language uses a TypePal-based type-checker.
:::

##### Basic usage

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).

The first step is to configure the renaming, by at least providing parse and type-check functions.
```rascal
extend analysis::typepal::refactor::Rename;
import examples::pico::Syntax;
import examples::pico::Checker;

RenameConfig picoRenameConfig = rconfig(
Tree(loc l) { return parse(#start[Program], l); }
, collectAndSolve
);
```

We can re-use this config for any renaming for `pico`.

::: Caution
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.
:::

Using the configuration, define a rename function.

```rascal
import Exception;

tuple[list[DocumentEdit] edits, set[Message] msgs] renamePico(list[Tree] cursor, str newName) {
if (!isValidName(newName)) return <[], {error("\'<newName>\' is not a valid name here.", cursor[0].src)}>;
return rename(cursor, newName, picoRenameConfig);
}

bool isValidName(str name) {
try {
parse(#Id, name);
return true;
} catch ParseError(_): {
return false;
}
}
```

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.
The IDE will then apply these text edits.

##### Advanced usage

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.

* Resolving the definition(s) of the name under the cursor
* Finding all uses of that declaration
* Checking that no definitions with the new name already exist

Choose a reason for hiding this comment

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

exist -> exists

Copy link
Contributor Author

Choose a reason for hiding this comment

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

'definitions' plural, so 'exist' should be correct?

* Finding where the name is in the definition tree
* Producing the edits to fulfil the actual renaming

###### Advanced configuration

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).

Additionally, one can extend the configuration with keyword arguments, for example to retain some state for a single rename. Example:

```rascal
extend analysis::typepal::refactor::Rename;
import examples::pico::Syntax;
import examples::pico::Checker;

data RenameConfig(set[loc] workspaceFolders = {});

tuple[list[DocumentEdit] edits, set[Message] msgs] renamePico(list[Tree] cursor, str newName, set[loc] workspaceFolders)
= rename(cursor
, newName
, rconfig(
Tree(loc l) { return parse(#start[Program], l); }
, collectAndSolve
, wokspaceFolders = workspaceFolders
)
);
```

###### Resolving definitions

Resolve the name under the cursor to definition(s).

```rascal
set[Define] getCursorDefinitions(Focus cursor, Tree(loc) getTree, TModel(Tree) getModel, Renamer r);
```

The default implementation only looks for definitions in the file where the cursor is.

###### Find relevant files

Find files that might contain one of the following occurrences. This should be a fast over-approximation.

* Definitions of the name under the cursor.
* References to/uses of aforementioned definitions.
* Definitions or uses of the new name.

```rascal
tuple[set[loc] defFiles, set[loc] useFiles, set[loc] newNameFiles] findOccurrenceFiles(set[Define] cursorDefs, Focus cursor, str newName, Tree(loc) getTree, Renamer r);
```

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.

1. The amount of work done per file should be reasonable.
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.

###### Find additional definitions

For each files in `defFiles` from [`findOccurrenceFiles`](#find-relevant-files), find additional definitions to rename.

```rascal
set[Define] findAdditionalDefinitions(set[Define] cursorDefs, Tree tr, TModel tm, Renamer r);
```

The default implementation returns the empty set. The following example overrides the default to find overloaded definitions.

```rascal
extend analysis::typepal::refactor::Rename;

set[Define] findAdditionalDefinitions(set[Define] defs, Tree _, TModel tm, Renamer _) =
{
tm.definitions[d]
| loc d <- (tm.defines<idRole, id, defined>)[defs.idRole, defs.id] - defs.defined
, tm.config.mayOverload(defs.defined + d, tm.definitions)
};
```

###### Validate occurrences of new name

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.

```rascal
void validateNewNameOccurrences(set[Define] cursorDefs, str newName, Tree tr, Renamer r);
```

The default implementation raises an error when a occurrence of `newName` exists here.

Example (simplified from the renaming implementaion for Rascal itself) that checks for shadowing, overloading and double declarations introduced by the rename.
```rascal
void validateNewNameOccurrences(set[Define] cursorDefs, str newName, Tree tr, Renamer r) {
tm = r.getConfig().tmodelForLoc(tr.src.top);

defUse = invert(tm.useDef);
reachable = rascalGetReflexiveModulePaths(tm).to;
newNameDefs = {nD | Define nD:<_, newName, _, _, _, _> <- tm.defines};
curAndNewDefinitions = (d.defined: d | d <- currentDefs + newNameDefs); // temporary map for overloading checks

for (<Define c, Define n> <- currentDefs * newNameDefs) {
set[loc] curUses = defUse[c.defined];
set[loc] newUses = defUse[n.defined];

// Will this rename hide a used definition of `oldName` behind an existing definition of `newName` (shadowing)?
for (loc cU <- curUses
, isContainedInScope(cU, n.scope, tm)
, isContainedInScope(n.scope, c.scope, tm)) {
r.error(cU, "Renaming this to \'<newName>\' would change the program semantics; its original definition would be shadowed by <n.defined>.");
}

// Will this rename hide a used definition of `newName` behind a definition of `oldName` (shadowing)?
for (isContainedInScope(c.scope, n.scope, tm)
, loc nU <- newUses
, isContainedInScope(nU, c.scope, tm)) {
r.error(c.defined, "Renaming this to \'<newName>\' would change the program semantics; it would shadow the declaration of <nU>.");
}

// Is `newName` already resolvable from a scope where `oldName` is currently declared?
if (tm.config.mayOverload({c.defined, n.defined}, curAndNewDefinitions)) {
// Overloading
if (c.scope in reachable || isContainedInScope(c.defined, n.scope, tm) || isContainedInScope(n.defined, c.scope, tm)) {
r.error(c.defined, "Renaming this to \'<newName>\' would overload an existing definition at <n.defined>.");
}
} else if (isContainedInScope(c.defined, n.scope, tm)) {
// Double declaration
r.error(c.defined, "Renaming this to \'<newName>\' would cause a double declaration (with <n.defined>).");
}
}
}
```

###### Find name location

Finds the location of the name in a definitions parse tree.

```rascal
loc nameLocation(Tree t, Define d);
```

The default implementation returns the location of the first (left-most) sub-tree 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.

###### Rename definition

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`.

```rascal
void renameDefinition(Define d, loc nameLoc, str newName, TModel tm, Renamer r);
```

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.


The following example override registers an edit for renaming a file when renaming a module.
```rascal
import Location;

str makeFileName(str name) = ...;

data RenamConfig(set[loc] srcDirs = {|file:///source1|, |file:///source2|});

void renameDefinition(Define d:<_, currentName, _, moduleId(), _, _>, loc nameLoc, str newName, TModel _, Renamer r) {
loc moduleFile = d.defined.top;
if (loc srcDir <- r.getConfig().srcDirs, loc relModulePath := relativize(srcDir, moduleFile), relModulePath != moduleFile) {
// Change the module header
r.textEdit(replace(nameLoc, newName));
// Rename the file
r.documentEdit(renamed(moduleFile, srcDir + makeFileName(newName)));
} else {
r.error(moduleFile, "Cannot rename <currentName>, since it is not defined in this project.");
}
}
```

###### Rename uses

In a single file, rename all uses of the definitions. This is called for all `useFiles` from the [selected files](#find-relevant-files).

```rascal
void renameUses(set[Define] defs, str newName, TModel tm, Renamer r);
```

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.
1 change: 1 addition & 0 deletions doc/TypePal/TypePal.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ details:
- Solver
- Configuration
- Utilities
- RenameRefactoring
- Examples
---

Expand Down
19 changes: 15 additions & 4 deletions src/analysis/typepal/refactor/Rename.rsc
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,15 @@ alias Focus = list[Tree];
private int WORKSPACE_WORK = 10;
private int FILE_WORK = 5;

@synopsis{Tracks state of renaming and provides halper functions.}
@description{
Tracks the state of the renaming, as an argument to every function of the rename framework.

* `msg` registers a ((FailMessage)). Registration of an ((error)) triggers premature termination of the renaming at the soonest possibility (typically before the next rename phase).
* `documentEdit` registers a ((DocumentEdit)), which represents a change required for the renaming.
* `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.
* `getConfig` retrieves the ((analysis::typepal::refactor::Rename::RenameConfig)).
}
data Renamer
= renamer(
void(FailMessage) msg
Expand Down Expand Up @@ -321,6 +330,7 @@ RenameResult rename(
}

// TODO If performance bottleneck, rewrite to binary search
@synopsis{Compute locations of names of `defs` in `tr`.}
private map[Define, loc] defNameLocations(Tree tr, set[Define] defs, Renamer _r) {
map[loc, Define] definitions = (d.defined: d | d <- defs);
set[loc] defsToDo = defs.defined;
Expand All @@ -346,8 +356,8 @@ private map[Define, loc] defNameLocations(Tree tr, set[Define] defs, Renamer _r)
return defNames;
}

@synopsis{Computes ((Define))(s) for the name under the cursor.}
default set[Define] getCursorDefinitions(Focus cursor, Tree(loc) _r, TModel(Tree) getModel, Renamer r) {
@synopsis{Computes ((Define))(s) for the name under `cursor`.}
default set[Define] getCursorDefinitions(Focus cursor, Tree(loc) _getTree, TModel(Tree) getModel, Renamer r) {
loc cursorLoc = cursor[0].src;
TModel tm = getModel(cursor[-1]);
for (Tree c <- cursor) {
Expand All @@ -368,6 +378,7 @@ default set[Define] getCursorDefinitions(Focus cursor, Tree(loc) _r, TModel(Tree
}

@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.}
@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.}
default tuple[set[loc] defFiles, set[loc] useFiles, set[loc] newNameFiles] findOccurrenceFiles(set[Define] cursorDefs, Focus cursor, str newName, Tree(loc) _getTree, Renamer r) {
loc f = cursor[0].src.top;
if (any(d <- cursorDefs, f != d.defined.top)) {
Expand All @@ -389,12 +400,12 @@ default void validateNewNameOccurrences(set[Define] cursorDefs, str newName, Tre
}
}

@synopsis{Renames a single ((Define)) with its name at `nameLoc`, by producing a corresponding ((DocumentEdit)).}
@synopsis{Renames a single ((Define)) `_d `with its name at `nameLoc`, defined in ((TModel)) `_tm`, to `newName`, by producing corresponding ((DocumentEdit))s.}
default void renameDefinition(Define _d, loc nameLoc, str newName, TModel _tm, Renamer r) {
r.textEdit(replace(nameLoc, newName));
}

@synopsis{{Renames all uses of `defs` in a single file/((TModel)), by producing corresponding ((DocumentEdit))s.}}
@synopsis{{Renames all uses of `defs` in a single file/((TModel)) `tm`, by producing corresponding ((DocumentEdit))s.}}
default void renameUses(set[Define] defs, str newName, TModel tm, Renamer r) {
for (loc u <- invert(tm.useDef)[defs.defined] - defs.defined) {
r.textEdit(replace(u, newName));
Expand Down
17 changes: 5 additions & 12 deletions src/examples/modfun/Rename.rsc
Original file line number Diff line number Diff line change
Expand Up @@ -133,18 +133,11 @@ bool tryParse(type[&T <: Tree] tp, str s) {
bool isValidName(moduleId(), str name) = tryParse(#ModId, name);
bool isValidName(variableId(), str name) = tryParse(#Id, name);

set[Define] findAdditionalDefinitions(set[Define] cursorDefs, Tree _, TModel tm, Renamer _) {
set[Define] overloads = {};
for (d <- tm.defines
&& d.idRole in cursorDefs.idRole
&& d.id in cursorDefs.id
&& d.defined notin cursorDefs.defined) {
if (tm.config.mayOverload(cursorDefs.defined + d.defined, tm.definitions)) {
overloads += d;
}
}
return overloads;
}
set[Define] findAdditionalDefinitions(set[Define] cursorDefs, Tree _, TModel tm, Renamer _) =
{ tm.definitions[d]
| loc d <- (tm.defines<idRole, id, defined>)[cursorDefs.idRole, cursorDefs.id] - cursorDefs.defined
, tm.config.mayOverload(cursorDefs.defined + d, tm.definitions)
};

void renameUses(set[Define] defs, str newName, TModel tm, Renamer r) {
// Somehow, tm.useDef is empty, so we need to use tm.uses
Expand Down
6 changes: 0 additions & 6 deletions src/examples/pico/Rename.rsc
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,9 @@ module examples::pico::Rename
import examples::pico::Syntax;
import examples::pico::Checker;

import analysis::typepal::TModel;

extend analysis::typepal::refactor::Rename;
import analysis::diff::edits::TextEdits;

import Exception;
import IO;
import Relation;
import util::FileSystem;

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