Skip to content

Commit

Permalink
RenameSymbol request allows collisions (#11892)
Browse files Browse the repository at this point in the history
close #11658

Changelog:
- add: `DefinitionAlreadyExists` error when the definition with the provided name already exists

# Important Notes
In the GUI it looks like this


https://github.com/user-attachments/assets/824e7881-81bf-4547-a74a-7532753db614
  • Loading branch information
4e6 authored Dec 17, 2024
1 parent 30075d2 commit 909afff
Show file tree
Hide file tree
Showing 7 changed files with 372 additions and 7 deletions.
29 changes: 28 additions & 1 deletion docs/language-server/protocol-language-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,8 @@ transport formats, please look [here](./protocol-architecture).
- [`ExpressionNotFoundError`](#expressionnotfounderror)
- [`FailedToApplyEdits`](#failedtoapplyedits)
- [`RefactoringNotSupported`](#refactoringnotsupported)
- [`ProjectRenameFailed`](#projectrenamefailed)
- [`DefinitionAlreadyExists`](#definitionalreadyexists)

<!-- /MarkdownTOC -->

Expand Down Expand Up @@ -3151,7 +3153,8 @@ type RefactoringRenameProjectResult = null;

#### Errors

None
- [`ProjectRenameFailed`](#projectrenamefailed) to signal that the project
rename operation has failed.

### `refactoring/renameSymbol`

Expand Down Expand Up @@ -3231,6 +3234,8 @@ interface RefactoringRenameSymbolResult {
operation was not able to apply generated edits.
- [`RefactoringNotSupported`](#refactoringnotsupported) to signal that the
refactoring of the given expression is not supported.
- [`DefinitionAlreadyExists`](#definitionalreadyexists) to signal that the
definition with the provided name already exists in scope.

### `refactoring/projectRenamed`

Expand Down Expand Up @@ -5911,6 +5916,28 @@ Signals that the refactoring of the given expression is not supported.
}
```

### `ProjectRenameFailed`

Signals that the project rename failed.

```typescript
"error" : {
"code" : 9004,
"message" : "Project rename failed [<oldName>, <newName>]"
}
```

### `DefinitionAlreadyExists`

Signals that the definition with the provided name already exists in the scope.

```typescript
"error" : {
"code" : 9005,
"message" : "Definition [<name>] already exists"
}
```

### `AiHttpError`

Signals about an error during the processing of AI http respnse.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,4 +77,7 @@ object RefactoringApi {
case class ProjectRenameFailed(oldName: String, newName: String)
extends Error(9004, s"Project rename failed [$oldName, $newName]")

case class DefinitionAlreadyExists(name: String)
extends Error(9005, s"Definition [$name] already exists")

}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ object RenameFailureMapper {
case error: Api.SymbolRenameFailed.ExpressionNotFound =>
RefactoringApi.ExpressionNotFoundError(error.expressionId)

case error: Api.SymbolRenameFailed.DefinitionAlreadyExists =>
RefactoringApi.DefinitionAlreadyExists(error.name)

case error: Api.SymbolRenameFailed.FailedToApplyEdits =>
RefactoringApi.FailedToApplyEdits(error.module)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1330,11 +1330,18 @@ object Runtime {
*
* @param expressionId the id of expression
*/

@named("symbolRenameFailedExpressionNotFound")
final case class ExpressionNotFound(expressionId: ExpressionId)
extends SymbolRenameFailed.Error

/** Signals that the definition with the provided name already exists in the scope.
*
* @param name the definition name
*/
@named("symbolRenameFailedDefinitionAlreadyExists")
final case class DefinitionAlreadyExists(name: String)
extends SymbolRenameFailed.Error

/** Signals that it was unable to apply edits to the current module contents.
*
* @param module the module name
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ package org.enso.compiler.refactoring

import org.enso.compiler.core.Implicits.AsMetadata
import org.enso.compiler.core.{ExternalID, IR, Identifier}
import org.enso.compiler.core.ir.Name
import org.enso.compiler.core.ir.{Expression, Name}
import org.enso.compiler.core.ir.expression.Application
import org.enso.compiler.core.ir.module.scope.definition.Method
import org.enso.compiler.data.BindingsMap
import org.enso.compiler.pass.analyse.DataflowAnalysis
import org.enso.compiler.pass.resolve.MethodCalls
Expand Down Expand Up @@ -31,6 +32,69 @@ trait IRUtils {
None
}

/** Find definitions with the provided name.
*
* @param ir the IR where to search the definition
* @param name the definition name to look for
* @return the list of definitions with the provided name
*/
def findModuleDefinitions(ir: IR, name: String): Set[IR] = {
val builder = Set.newBuilder[IR]
IR.preorder(
ir,
{
case methodExplicit: Method.Explicit
if methodExplicit.methodName.name == name =>
builder.addOne(methodExplicit)
case _ =>
}
)
builder.result()
}

/** Find definitions with the provided name.
*
* @param scope the IR where to search the definition
* @param name the definition name to look for
* @return the list of definitions with the provided name
*/
def findLocalDefinitions(scope: IR, name: String): Set[IR] = {
val builder = Set.newBuilder[IR]
IR.preorder(
scope,
{
case expressionBinding: Expression.Binding
if expressionBinding.name.name == name =>
builder.addOne(expressionBinding)
case _ =>
}
)
builder.result()
}

/** Get the [[Expression.Block]] containing the provided expression.
*
* @param scope the scope where to look
* @param expression the expression to look for
* @return the block containing the provided expression
*/
def getExpressionBlock(
scope: IR,
expression: IR
): Option[Expression.Block] = {
val blocksBuilder = Set.newBuilder[Expression.Block]
IR.preorder(
scope,
{
case block: Expression.Block => blocksBuilder.addOne(block)
case _ =>
}
)
val blocks = blocksBuilder.result()

blocks.find(block => findById(block, expression.getId).isDefined)
}

/** Find usages of a local defined in the body block.
*
* @param ir the syntax tree
Expand Down Expand Up @@ -63,7 +127,7 @@ trait IRUtils {
node: Name
): Option[Set[Name.Literal]] =
for {
usages <- findDynamicUsages(ir, node)
usages <- findDynamicUsages(ir, node.name)
} yield {
usages.collect {
case Application.Prefix(function: Name.Literal, args, _, _, _)
Expand Down Expand Up @@ -117,16 +181,16 @@ trait IRUtils {
/** Find usages of a dynamic dependency in the [[DataflowAnalysis]] metadata.
*
* @param ir the syntax tree
* @param node the name to look for
* @param name the name to look for
* @return the list of usages of the given name in the `ir`
*/
private def findDynamicUsages(
ir: IR,
node: Name
name: String
): Option[Set[IR]] = {
for {
metadata <- ir.getMetadata(DataflowAnalysis)
key = DataflowAnalysis.DependencyInfo.Type.Dynamic(node.name, None)
key = DataflowAnalysis.DependencyInfo.Type.Dynamic(name, None)
dependents <- metadata.dependents.get(key)
} yield {
dependents
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,13 @@ final class RefactoringRenameJob(
)
)
Seq()
case ex: RefactoringRenameJob.DefinitionAlreadyExists =>
reply(
Api.SymbolRenameFailed(
Api.SymbolRenameFailed.DefinitionAlreadyExists(ex.name)
)
)
Seq()
case ex: RefactoringRenameJob.FailedToApplyEdits =>
reply(
Api.SymbolRenameFailed(
Expand Down Expand Up @@ -95,6 +102,26 @@ final class RefactoringRenameJob(
throw new RefactoringRenameJob.OperationNotSupported(expressionId)
)

// check if global definition exists
methodDefinition.foreach { _ =>
val moduleDefs =
IRUtils.findModuleDefinitions(module.getIr, newSymbolName)
if (moduleDefs.nonEmpty) {
throw new RefactoringRenameJob.DefinitionAlreadyExists(newSymbolName)
}
}

// check if local definition exists
local.foreach { symbol =>
val scopeOpt = IRUtils.getExpressionBlock(module.getIr, symbol)
scopeOpt.foreach { scope =>
val localDefs = IRUtils.findLocalDefinitions(scope, newSymbolName)
if (localDefs.nonEmpty) {
throw new RefactoringRenameJob.DefinitionAlreadyExists(newSymbolName)
}
}
}

def localUsages = local.flatMap(IRUtils.findLocalUsages(module.getIr, _))
def methodDefinitionUsages = methodDefinition.flatMap(
IRUtils.findModuleMethodUsages(module.getName, module.getIr, _)
Expand Down Expand Up @@ -179,6 +206,9 @@ object RefactoringRenameJob {
final private class ExpressionNotFound(val expressionId: UUID @ExternalID)
extends Exception(s"Expression was not found by id [$expressionId].")

final private class DefinitionAlreadyExists(val name: String)
extends Exception(s"Definition [$name] already exists in scope")

final private class FailedToApplyEdits(val module: String)
extends Exception(s"Failed to apply edits to module [$module]")

Expand Down
Loading

0 comments on commit 909afff

Please sign in to comment.