5858: Draft: Show reason for failed rename refactoring r=rherrmann a=rherrmann

Return an error with a meaningful message for requests to
`textDocument/rename` if the operation cannot be performed.
Pass errors, raised by rename handling code to the LSP runtime.

As a consequence, the VS Code client shows and logs the request
as if a server-side programming error occured.
Screenshot of a rename error showing in VS Code
![invalid-rename-ui](https://user-images.githubusercontent.com/607182/91059560-2c08a380-e62a-11ea-9297-f092db935a3b.png)

I would kindly ask to get feedback from the maintainers if they can spare the time:
* Is the general direction of the proposed changes acceptable?
* I'm new to Rust. The code feels clumsy and redundant, please suggest improvements
if you find the time for. E.g. is there a simple replacement for `RenameError`?
* Should presenting the error with proper severity (i.e. not as a programming error) be part of this change or in a followup change?

See https://github.com/rust-analyzer/rust-analyzer/issues/3981

Co-authored-by: Rüdiger Herrmann <ruediger.herrmann@gmx.de>
This commit is contained in:
bors[bot] 2020-10-10 11:59:25 +00:00 committed by GitHub
commit b7596d2483
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 138 additions and 61 deletions

View file

@ -77,7 +77,9 @@ pub use crate::{
hover::{HoverAction, HoverConfig, HoverGotoTypeData, HoverResult}, hover::{HoverAction, HoverConfig, HoverGotoTypeData, HoverResult},
inlay_hints::{InlayHint, InlayHintsConfig, InlayKind}, inlay_hints::{InlayHint, InlayHintsConfig, InlayKind},
markup::Markup, markup::Markup,
references::{Declaration, Reference, ReferenceAccess, ReferenceKind, ReferenceSearchResult}, references::{
Declaration, Reference, ReferenceAccess, ReferenceKind, ReferenceSearchResult, RenameError,
},
runnables::{Runnable, RunnableKind, TestId}, runnables::{Runnable, RunnableKind, TestId},
syntax_highlighting::{ syntax_highlighting::{
Highlight, HighlightModifier, HighlightModifiers, HighlightTag, HighlightedRange, Highlight, HighlightModifier, HighlightModifiers, HighlightTag, HighlightedRange,
@ -490,7 +492,7 @@ impl Analysis {
&self, &self,
position: FilePosition, position: FilePosition,
new_name: &str, new_name: &str,
) -> Cancelable<Option<RangeInfo<SourceChange>>> { ) -> Cancelable<Result<RangeInfo<SourceChange>, RenameError>> {
self.with_db(|db| references::rename(db, position, new_name)) self.with_db(|db| references::rename(db, position, new_name))
} }

View file

@ -26,6 +26,7 @@ use syntax::{
use crate::{display::TryToNav, FilePosition, FileRange, NavigationTarget, RangeInfo}; use crate::{display::TryToNav, FilePosition, FileRange, NavigationTarget, RangeInfo};
pub(crate) use self::rename::rename; pub(crate) use self::rename::rename;
pub use self::rename::RenameError;
pub use ide_db::search::{Reference, ReferenceAccess, ReferenceKind}; pub use ide_db::search::{Reference, ReferenceAccess, ReferenceKind};

View file

@ -6,11 +6,16 @@ use ide_db::{
defs::{classify_name, classify_name_ref, Definition, NameClass, NameRefClass}, defs::{classify_name, classify_name_ref, Definition, NameClass, NameRefClass},
RootDatabase, RootDatabase,
}; };
use std::convert::TryInto;
use std::{
convert::TryInto,
error::Error,
fmt::{self, Display},
};
use syntax::{ use syntax::{
algo::find_node_at_offset, algo::find_node_at_offset,
ast::{self, NameOwner}, ast::{self, NameOwner},
lex_single_valid_syntax_kind, match_ast, AstNode, SyntaxKind, SyntaxNode, SyntaxToken, lex_single_syntax_kind, match_ast, AstNode, SyntaxKind, SyntaxNode, SyntaxToken,
}; };
use test_utils::mark; use test_utils::mark;
use text_edit::TextEdit; use text_edit::TextEdit;
@ -20,17 +25,37 @@ use crate::{
SourceChange, SourceFileEdit, TextRange, TextSize, SourceChange, SourceFileEdit, TextRange, TextSize,
}; };
#[derive(Debug)]
pub struct RenameError(pub(crate) String);
impl fmt::Display for RenameError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Display::fmt(&self.0, f)
}
}
impl Error for RenameError {}
pub(crate) fn rename( pub(crate) fn rename(
db: &RootDatabase, db: &RootDatabase,
position: FilePosition, position: FilePosition,
new_name: &str, new_name: &str,
) -> Option<RangeInfo<SourceChange>> { ) -> Result<RangeInfo<SourceChange>, RenameError> {
let sema = Semantics::new(db); let sema = Semantics::new(db);
match lex_single_valid_syntax_kind(new_name)? { match lex_single_syntax_kind(new_name) {
SyntaxKind::IDENT | SyntaxKind::UNDERSCORE => (), Some(res) => match res {
SyntaxKind::SELF_KW => return rename_to_self(&sema, position), (SyntaxKind::IDENT, _) => (),
_ => return None, (SyntaxKind::UNDERSCORE, _) => (),
(SyntaxKind::SELF_KW, _) => return rename_to_self(&sema, position),
(_, Some(syntax_error)) => {
return Err(RenameError(format!("Invalid name `{}`: {}", new_name, syntax_error)))
}
(_, None) => {
return Err(RenameError(format!("Invalid name `{}`: not an identifier", new_name)))
}
},
None => return Err(RenameError(format!("Invalid name `{}`: not an identifier", new_name))),
} }
let source_file = sema.parse(position.file_id); let source_file = sema.parse(position.file_id);
@ -103,7 +128,7 @@ fn rename_mod(
position: FilePosition, position: FilePosition,
module: Module, module: Module,
new_name: &str, new_name: &str,
) -> Option<RangeInfo<SourceChange>> { ) -> Result<RangeInfo<SourceChange>, RenameError> {
let mut source_file_edits = Vec::new(); let mut source_file_edits = Vec::new();
let mut file_system_edits = Vec::new(); let mut file_system_edits = Vec::new();
@ -125,7 +150,7 @@ fn rename_mod(
if let Some(src) = module.declaration_source(sema.db) { if let Some(src) = module.declaration_source(sema.db) {
let file_id = src.file_id.original_file(sema.db); let file_id = src.file_id.original_file(sema.db);
let name = src.value.name()?; let name = src.value.name().unwrap();
let edit = SourceFileEdit { let edit = SourceFileEdit {
file_id, file_id,
edit: TextEdit::replace(name.syntax().text_range(), new_name.into()), edit: TextEdit::replace(name.syntax().text_range(), new_name.into()),
@ -133,35 +158,40 @@ fn rename_mod(
source_file_edits.push(edit); source_file_edits.push(edit);
} }
let RangeInfo { range, info: refs } = find_all_refs(sema, position, None)?; let RangeInfo { range, info: refs } = find_all_refs(sema, position, None)
.ok_or_else(|| RenameError("No references found at position".to_string()))?;
let ref_edits = refs let ref_edits = refs
.references .references
.into_iter() .into_iter()
.map(|reference| source_edit_from_reference(reference, new_name)); .map(|reference| source_edit_from_reference(reference, new_name));
source_file_edits.extend(ref_edits); source_file_edits.extend(ref_edits);
Some(RangeInfo::new(range, SourceChange::from_edits(source_file_edits, file_system_edits))) Ok(RangeInfo::new(range, SourceChange::from_edits(source_file_edits, file_system_edits)))
} }
fn rename_to_self( fn rename_to_self(
sema: &Semantics<RootDatabase>, sema: &Semantics<RootDatabase>,
position: FilePosition, position: FilePosition,
) -> Option<RangeInfo<SourceChange>> { ) -> Result<RangeInfo<SourceChange>, RenameError> {
let source_file = sema.parse(position.file_id); let source_file = sema.parse(position.file_id);
let syn = source_file.syntax(); let syn = source_file.syntax();
let fn_def = find_node_at_offset::<ast::Fn>(syn, position.offset)?; let fn_def = find_node_at_offset::<ast::Fn>(syn, position.offset)
let params = fn_def.param_list()?; .ok_or_else(|| RenameError("No surrounding method declaration found".to_string()))?;
let params =
fn_def.param_list().ok_or_else(|| RenameError("Method has no parameters".to_string()))?;
if params.self_param().is_some() { if params.self_param().is_some() {
return None; // method already has self param return Err(RenameError("Method already has a self parameter".to_string()));
} }
let first_param = params.params().next()?; let first_param =
params.params().next().ok_or_else(|| RenameError("Method has no parameters".into()))?;
let mutable = match first_param.ty() { let mutable = match first_param.ty() {
Some(ast::Type::RefType(rt)) => rt.mut_token().is_some(), Some(ast::Type::RefType(rt)) => rt.mut_token().is_some(),
_ => return None, // not renaming other types _ => return Err(RenameError("Not renaming other types".to_string())),
}; };
let RangeInfo { range, info: refs } = find_all_refs(sema, position, None)?; let RangeInfo { range, info: refs } = find_all_refs(sema, position, None)
.ok_or_else(|| RenameError("No reference found at position".to_string()))?;
let param_range = first_param.syntax().text_range(); let param_range = first_param.syntax().text_range();
let (param_ref, usages): (Vec<Reference>, Vec<Reference>) = refs let (param_ref, usages): (Vec<Reference>, Vec<Reference>) = refs
@ -169,7 +199,7 @@ fn rename_to_self(
.partition(|reference| param_range.intersect(reference.file_range.range).is_some()); .partition(|reference| param_range.intersect(reference.file_range.range).is_some());
if param_ref.is_empty() { if param_ref.is_empty() {
return None; return Err(RenameError("Parameter to rename not found".to_string()));
} }
let mut edits = usages let mut edits = usages
@ -185,7 +215,7 @@ fn rename_to_self(
), ),
}); });
Some(RangeInfo::new(range, SourceChange::from(edits))) Ok(RangeInfo::new(range, SourceChange::from(edits)))
} }
fn text_edit_from_self_param( fn text_edit_from_self_param(
@ -216,12 +246,13 @@ fn rename_self_to_param(
position: FilePosition, position: FilePosition,
self_token: SyntaxToken, self_token: SyntaxToken,
new_name: &str, new_name: &str,
) -> Option<RangeInfo<SourceChange>> { ) -> Result<RangeInfo<SourceChange>, RenameError> {
let source_file = sema.parse(position.file_id); let source_file = sema.parse(position.file_id);
let syn = source_file.syntax(); let syn = source_file.syntax();
let text = sema.db.file_text(position.file_id); let text = sema.db.file_text(position.file_id);
let fn_def = find_node_at_offset::<ast::Fn>(syn, position.offset)?; let fn_def = find_node_at_offset::<ast::Fn>(syn, position.offset)
.ok_or_else(|| RenameError("No surrounding method declaration found".to_string()))?;
let search_range = fn_def.syntax().text_range(); let search_range = fn_def.syntax().text_range();
let mut edits: Vec<SourceFileEdit> = vec![]; let mut edits: Vec<SourceFileEdit> = vec![];
@ -235,7 +266,8 @@ fn rename_self_to_param(
syn.token_at_offset(offset).find(|t| t.kind() == SyntaxKind::SELF_KW) syn.token_at_offset(offset).find(|t| t.kind() == SyntaxKind::SELF_KW)
{ {
let edit = if let Some(ref self_param) = ast::SelfParam::cast(usage.parent()) { let edit = if let Some(ref self_param) = ast::SelfParam::cast(usage.parent()) {
text_edit_from_self_param(syn, self_param, new_name)? text_edit_from_self_param(syn, self_param, new_name)
.ok_or_else(|| RenameError("No target type found".to_string()))?
} else { } else {
TextEdit::replace(usage.text_range(), String::from(new_name)) TextEdit::replace(usage.text_range(), String::from(new_name))
}; };
@ -246,15 +278,18 @@ fn rename_self_to_param(
let range = ast::SelfParam::cast(self_token.parent()) let range = ast::SelfParam::cast(self_token.parent())
.map_or(self_token.text_range(), |p| p.syntax().text_range()); .map_or(self_token.text_range(), |p| p.syntax().text_range());
Some(RangeInfo::new(range, SourceChange::from(edits))) Ok(RangeInfo::new(range, SourceChange::from(edits)))
} }
fn rename_reference( fn rename_reference(
sema: &Semantics<RootDatabase>, sema: &Semantics<RootDatabase>,
position: FilePosition, position: FilePosition,
new_name: &str, new_name: &str,
) -> Option<RangeInfo<SourceChange>> { ) -> Result<RangeInfo<SourceChange>, RenameError> {
let RangeInfo { range, info: refs } = find_all_refs(sema, position, None)?; let RangeInfo { range, info: refs } = match find_all_refs(sema, position, None) {
Some(range_info) => range_info,
None => return Err(RenameError("No references found at position".to_string())),
};
let edit = refs let edit = refs
.into_iter() .into_iter()
@ -262,10 +297,10 @@ fn rename_reference(
.collect::<Vec<_>>(); .collect::<Vec<_>>();
if edit.is_empty() { if edit.is_empty() {
return None; return Err(RenameError("No references found at position".to_string()));
} }
Some(RangeInfo::new(range, SourceChange::from(edit))) Ok(RangeInfo::new(range, SourceChange::from(edit)))
} }
#[cfg(test)] #[cfg(test)]
@ -280,25 +315,45 @@ mod tests {
fn check(new_name: &str, ra_fixture_before: &str, ra_fixture_after: &str) { fn check(new_name: &str, ra_fixture_before: &str, ra_fixture_after: &str) {
let ra_fixture_after = &trim_indent(ra_fixture_after); let ra_fixture_after = &trim_indent(ra_fixture_after);
let (analysis, position) = fixture::position(ra_fixture_before); let (analysis, position) = fixture::position(ra_fixture_before);
let source_change = analysis.rename(position, new_name).unwrap(); let rename_result = analysis
.rename(position, new_name)
.unwrap_or_else(|err| panic!("Rename to '{}' was cancelled: {}", new_name, err));
match rename_result {
Ok(source_change) => {
let mut text_edit_builder = TextEdit::builder(); let mut text_edit_builder = TextEdit::builder();
let mut file_id: Option<FileId> = None; let mut file_id: Option<FileId> = None;
if let Some(change) = source_change { for edit in source_change.info.source_file_edits {
for edit in change.info.source_file_edits {
file_id = Some(edit.file_id); file_id = Some(edit.file_id);
for indel in edit.edit.into_iter() { for indel in edit.edit.into_iter() {
text_edit_builder.replace(indel.delete, indel.insert); text_edit_builder.replace(indel.delete, indel.insert);
} }
} }
}
let mut result = analysis.file_text(file_id.unwrap()).unwrap().to_string(); let mut result = analysis.file_text(file_id.unwrap()).unwrap().to_string();
text_edit_builder.finish().apply(&mut result); text_edit_builder.finish().apply(&mut result);
assert_eq_text!(ra_fixture_after, &*result); assert_eq_text!(ra_fixture_after, &*result);
} }
Err(err) => {
if ra_fixture_after.starts_with("error:") {
let error_message = ra_fixture_after
.chars()
.into_iter()
.skip("error:".len())
.collect::<String>();
assert_eq!(error_message.trim(), err.to_string());
return;
} else {
panic!("Rename to '{}' failed unexpectedly: {}", new_name, err)
}
}
};
}
fn check_expect(new_name: &str, ra_fixture: &str, expect: Expect) { fn check_expect(new_name: &str, ra_fixture: &str, expect: Expect) {
let (analysis, position) = fixture::position(ra_fixture); let (analysis, position) = fixture::position(ra_fixture);
let source_change = analysis.rename(position, new_name).unwrap().unwrap(); let source_change = analysis
.rename(position, new_name)
.unwrap()
.expect("Expect returned RangeInfo to be Some, but was None");
expect.assert_debug_eq(&source_change) expect.assert_debug_eq(&source_change)
} }
@ -313,11 +368,30 @@ mod tests {
} }
#[test] #[test]
fn test_rename_to_invalid_identifier() { fn test_rename_to_invalid_identifier1() {
let (analysis, position) = fixture::position(r#"fn main() { let i<|> = 1; }"#); check(
let new_name = "invalid!"; "invalid!",
let source_change = analysis.rename(position, new_name).unwrap(); r#"fn main() { let i<|> = 1; }"#,
assert!(source_change.is_none()); "error: Invalid name `invalid!`: not an identifier",
);
}
#[test]
fn test_rename_to_invalid_identifier2() {
check(
"multiple tokens",
r#"fn main() { let i<|> = 1; }"#,
"error: Invalid name `multiple tokens`: not an identifier",
);
}
#[test]
fn test_rename_to_invalid_identifier3() {
check(
"let",
r#"fn main() { let i<|> = 1; }"#,
"error: Invalid name `let`: not an identifier",
);
} }
#[test] #[test]
@ -349,6 +423,15 @@ fn main() {
); );
} }
#[test]
fn test_rename_unresolved_reference() {
check(
"new_name",
r#"fn main() { let _ = unresolved_ref<|>; }"#,
"error: No references found at position",
);
}
#[test] #[test]
fn test_rename_for_macro_args() { fn test_rename_for_macro_args() {
check( check(

View file

@ -646,14 +646,9 @@ pub(crate) fn handle_prepare_rename(
let _p = profile::span("handle_prepare_rename"); let _p = profile::span("handle_prepare_rename");
let position = from_proto::file_position(&snap, params)?; let position = from_proto::file_position(&snap, params)?;
let optional_change = snap.analysis.rename(position, "dummy")?; let change = snap.analysis.rename(position, "dummy")??;
let range = match optional_change {
None => return Ok(None),
Some(it) => it.range,
};
let line_index = snap.analysis.file_line_index(position.file_id)?; let line_index = snap.analysis.file_line_index(position.file_id)?;
let range = to_proto::range(&line_index, range); let range = to_proto::range(&line_index, change.range);
Ok(Some(PrepareRenameResponse::Range(range))) Ok(Some(PrepareRenameResponse::Range(range)))
} }
@ -672,12 +667,8 @@ pub(crate) fn handle_rename(
.into()); .into());
} }
let optional_change = snap.analysis.rename(position, &*params.new_name)?; let change = snap.analysis.rename(position, &*params.new_name)??;
let source_change = match optional_change { let workspace_edit = to_proto::workspace_edit(&snap, change.info)?;
None => return Ok(None),
Some(it) => it.info,
};
let workspace_edit = to_proto::workspace_edit(&snap, source_change)?;
Ok(Some(workspace_edit)) Ok(Some(workspace_edit))
} }