Skip to content

Commit e90931a

Browse files
committed
Show reason for failed rename refactoring
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. Resolves #3981
1 parent 05261f5 commit e90931a

File tree

4 files changed

+138
-61
lines changed

4 files changed

+138
-61
lines changed

crates/ide/src/lib.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,9 @@ pub use crate::{
7777
hover::{HoverAction, HoverConfig, HoverGotoTypeData, HoverResult},
7878
inlay_hints::{InlayHint, InlayHintsConfig, InlayKind},
7979
markup::Markup,
80-
references::{Declaration, Reference, ReferenceAccess, ReferenceKind, ReferenceSearchResult},
80+
references::{
81+
Declaration, Reference, ReferenceAccess, ReferenceKind, ReferenceSearchResult, RenameError,
82+
},
8183
runnables::{Runnable, RunnableKind, TestId},
8284
syntax_highlighting::{
8385
Highlight, HighlightModifier, HighlightModifiers, HighlightTag, HighlightedRange,
@@ -490,7 +492,7 @@ impl Analysis {
490492
&self,
491493
position: FilePosition,
492494
new_name: &str,
493-
) -> Cancelable<Option<RangeInfo<SourceChange>>> {
495+
) -> Cancelable<Result<RangeInfo<SourceChange>, RenameError>> {
494496
self.with_db(|db| references::rename(db, position, new_name))
495497
}
496498

crates/ide/src/references.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ use syntax::{
2626
use crate::{display::TryToNav, FilePosition, FileRange, NavigationTarget, RangeInfo};
2727

2828
pub(crate) use self::rename::rename;
29+
pub use self::rename::RenameError;
2930

3031
pub use ide_db::search::{Reference, ReferenceAccess, ReferenceKind};
3132

crates/ide/src/references/rename.rs

Lines changed: 129 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,16 @@ use ide_db::{
66
defs::{classify_name, classify_name_ref, Definition, NameClass, NameRefClass},
77
RootDatabase,
88
};
9-
use std::convert::TryInto;
9+
10+
use std::{
11+
convert::TryInto,
12+
error::Error,
13+
fmt::{self, Display},
14+
};
1015
use syntax::{
1116
algo::find_node_at_offset,
1217
ast::{self, NameOwner},
13-
lex_single_valid_syntax_kind, match_ast, AstNode, SyntaxKind, SyntaxNode, SyntaxToken,
18+
lex_single_syntax_kind, match_ast, AstNode, SyntaxKind, SyntaxNode, SyntaxToken,
1419
};
1520
use test_utils::mark;
1621
use text_edit::TextEdit;
@@ -20,17 +25,37 @@ use crate::{
2025
SourceChange, SourceFileEdit, TextRange, TextSize,
2126
};
2227

28+
#[derive(Debug)]
29+
pub struct RenameError(pub(crate) String);
30+
31+
impl fmt::Display for RenameError {
32+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33+
Display::fmt(&self.0, f)
34+
}
35+
}
36+
37+
impl Error for RenameError {}
38+
2339
pub(crate) fn rename(
2440
db: &RootDatabase,
2541
position: FilePosition,
2642
new_name: &str,
27-
) -> Option<RangeInfo<SourceChange>> {
43+
) -> Result<RangeInfo<SourceChange>, RenameError> {
2844
let sema = Semantics::new(db);
2945

30-
match lex_single_valid_syntax_kind(new_name)? {
31-
SyntaxKind::IDENT | SyntaxKind::UNDERSCORE => (),
32-
SyntaxKind::SELF_KW => return rename_to_self(&sema, position),
33-
_ => return None,
46+
match lex_single_syntax_kind(new_name) {
47+
Some(res) => match res {
48+
(SyntaxKind::IDENT, _) => (),
49+
(SyntaxKind::UNDERSCORE, _) => (),
50+
(SyntaxKind::SELF_KW, _) => return rename_to_self(&sema, position),
51+
(_, Some(syntax_error)) => {
52+
return Err(RenameError(format!("Invalid name `{}`: {}", new_name, syntax_error)))
53+
}
54+
(_, None) => {
55+
return Err(RenameError(format!("Invalid name `{}`: not an identifier", new_name)))
56+
}
57+
},
58+
None => return Err(RenameError(format!("Invalid name `{}`: not an identifier", new_name))),
3459
}
3560

3661
let source_file = sema.parse(position.file_id);
@@ -103,7 +128,7 @@ fn rename_mod(
103128
position: FilePosition,
104129
module: Module,
105130
new_name: &str,
106-
) -> Option<RangeInfo<SourceChange>> {
131+
) -> Result<RangeInfo<SourceChange>, RenameError> {
107132
let mut source_file_edits = Vec::new();
108133
let mut file_system_edits = Vec::new();
109134

@@ -125,51 +150,56 @@ fn rename_mod(
125150

126151
if let Some(src) = module.declaration_source(sema.db) {
127152
let file_id = src.file_id.original_file(sema.db);
128-
let name = src.value.name()?;
153+
let name = src.value.name().unwrap();
129154
let edit = SourceFileEdit {
130155
file_id,
131156
edit: TextEdit::replace(name.syntax().text_range(), new_name.into()),
132157
};
133158
source_file_edits.push(edit);
134159
}
135160

136-
let RangeInfo { range, info: refs } = find_all_refs(sema, position, None)?;
161+
let RangeInfo { range, info: refs } = find_all_refs(sema, position, None)
162+
.ok_or_else(|| RenameError("No references found at position".to_string()))?;
137163
let ref_edits = refs
138164
.references
139165
.into_iter()
140166
.map(|reference| source_edit_from_reference(reference, new_name));
141167
source_file_edits.extend(ref_edits);
142168

143-
Some(RangeInfo::new(range, SourceChange::from_edits(source_file_edits, file_system_edits)))
169+
Ok(RangeInfo::new(range, SourceChange::from_edits(source_file_edits, file_system_edits)))
144170
}
145171

146172
fn rename_to_self(
147173
sema: &Semantics<RootDatabase>,
148174
position: FilePosition,
149-
) -> Option<RangeInfo<SourceChange>> {
175+
) -> Result<RangeInfo<SourceChange>, RenameError> {
150176
let source_file = sema.parse(position.file_id);
151177
let syn = source_file.syntax();
152178

153-
let fn_def = find_node_at_offset::<ast::Fn>(syn, position.offset)?;
154-
let params = fn_def.param_list()?;
179+
let fn_def = find_node_at_offset::<ast::Fn>(syn, position.offset)
180+
.ok_or_else(|| RenameError("No surrounding method declaration found".to_string()))?;
181+
let params =
182+
fn_def.param_list().ok_or_else(|| RenameError("Method has no parameters".to_string()))?;
155183
if params.self_param().is_some() {
156-
return None; // method already has self param
184+
return Err(RenameError("Method already has a self parameter".to_string()));
157185
}
158-
let first_param = params.params().next()?;
186+
let first_param =
187+
params.params().next().ok_or_else(|| RenameError("Method has no parameters".into()))?;
159188
let mutable = match first_param.ty() {
160189
Some(ast::Type::RefType(rt)) => rt.mut_token().is_some(),
161-
_ => return None, // not renaming other types
190+
_ => return Err(RenameError("Not renaming other types".to_string())),
162191
};
163192

164-
let RangeInfo { range, info: refs } = find_all_refs(sema, position, None)?;
193+
let RangeInfo { range, info: refs } = find_all_refs(sema, position, None)
194+
.ok_or_else(|| RenameError("No reference found at position".to_string()))?;
165195

166196
let param_range = first_param.syntax().text_range();
167197
let (param_ref, usages): (Vec<Reference>, Vec<Reference>) = refs
168198
.into_iter()
169199
.partition(|reference| param_range.intersect(reference.file_range.range).is_some());
170200

171201
if param_ref.is_empty() {
172-
return None;
202+
return Err(RenameError("Parameter to rename not found".to_string()));
173203
}
174204

175205
let mut edits = usages
@@ -185,7 +215,7 @@ fn rename_to_self(
185215
),
186216
});
187217

188-
Some(RangeInfo::new(range, SourceChange::from(edits)))
218+
Ok(RangeInfo::new(range, SourceChange::from(edits)))
189219
}
190220

191221
fn text_edit_from_self_param(
@@ -216,12 +246,13 @@ fn rename_self_to_param(
216246
position: FilePosition,
217247
self_token: SyntaxToken,
218248
new_name: &str,
219-
) -> Option<RangeInfo<SourceChange>> {
249+
) -> Result<RangeInfo<SourceChange>, RenameError> {
220250
let source_file = sema.parse(position.file_id);
221251
let syn = source_file.syntax();
222252

223253
let text = sema.db.file_text(position.file_id);
224-
let fn_def = find_node_at_offset::<ast::Fn>(syn, position.offset)?;
254+
let fn_def = find_node_at_offset::<ast::Fn>(syn, position.offset)
255+
.ok_or_else(|| RenameError("No surrounding method declaration found".to_string()))?;
225256
let search_range = fn_def.syntax().text_range();
226257

227258
let mut edits: Vec<SourceFileEdit> = vec![];
@@ -235,7 +266,8 @@ fn rename_self_to_param(
235266
syn.token_at_offset(offset).find(|t| t.kind() == SyntaxKind::SELF_KW)
236267
{
237268
let edit = if let Some(ref self_param) = ast::SelfParam::cast(usage.parent()) {
238-
text_edit_from_self_param(syn, self_param, new_name)?
269+
text_edit_from_self_param(syn, self_param, new_name)
270+
.ok_or_else(|| RenameError("No target type found".to_string()))?
239271
} else {
240272
TextEdit::replace(usage.text_range(), String::from(new_name))
241273
};
@@ -246,26 +278,29 @@ fn rename_self_to_param(
246278
let range = ast::SelfParam::cast(self_token.parent())
247279
.map_or(self_token.text_range(), |p| p.syntax().text_range());
248280

249-
Some(RangeInfo::new(range, SourceChange::from(edits)))
281+
Ok(RangeInfo::new(range, SourceChange::from(edits)))
250282
}
251283

252284
fn rename_reference(
253285
sema: &Semantics<RootDatabase>,
254286
position: FilePosition,
255287
new_name: &str,
256-
) -> Option<RangeInfo<SourceChange>> {
257-
let RangeInfo { range, info: refs } = find_all_refs(sema, position, None)?;
288+
) -> Result<RangeInfo<SourceChange>, RenameError> {
289+
let RangeInfo { range, info: refs } = match find_all_refs(sema, position, None) {
290+
Some(range_info) => range_info,
291+
None => return Err(RenameError("No references found at position".to_string())),
292+
};
258293

259294
let edit = refs
260295
.into_iter()
261296
.map(|reference| source_edit_from_reference(reference, new_name))
262297
.collect::<Vec<_>>();
263298

264299
if edit.is_empty() {
265-
return None;
300+
return Err(RenameError("No references found at position".to_string()));
266301
}
267302

268-
Some(RangeInfo::new(range, SourceChange::from(edit)))
303+
Ok(RangeInfo::new(range, SourceChange::from(edit)))
269304
}
270305

271306
#[cfg(test)]
@@ -280,25 +315,45 @@ mod tests {
280315
fn check(new_name: &str, ra_fixture_before: &str, ra_fixture_after: &str) {
281316
let ra_fixture_after = &trim_indent(ra_fixture_after);
282317
let (analysis, position) = fixture::position(ra_fixture_before);
283-
let source_change = analysis.rename(position, new_name).unwrap();
284-
let mut text_edit_builder = TextEdit::builder();
285-
let mut file_id: Option<FileId> = None;
286-
if let Some(change) = source_change {
287-
for edit in change.info.source_file_edits {
288-
file_id = Some(edit.file_id);
289-
for indel in edit.edit.into_iter() {
290-
text_edit_builder.replace(indel.delete, indel.insert);
318+
let rename_result = analysis
319+
.rename(position, new_name)
320+
.unwrap_or_else(|err| panic!("Rename to '{}' was cancelled: {}", new_name, err));
321+
match rename_result {
322+
Ok(source_change) => {
323+
let mut text_edit_builder = TextEdit::builder();
324+
let mut file_id: Option<FileId> = None;
325+
for edit in source_change.info.source_file_edits {
326+
file_id = Some(edit.file_id);
327+
for indel in edit.edit.into_iter() {
328+
text_edit_builder.replace(indel.delete, indel.insert);
329+
}
291330
}
331+
let mut result = analysis.file_text(file_id.unwrap()).unwrap().to_string();
332+
text_edit_builder.finish().apply(&mut result);
333+
assert_eq_text!(ra_fixture_after, &*result);
292334
}
293-
}
294-
let mut result = analysis.file_text(file_id.unwrap()).unwrap().to_string();
295-
text_edit_builder.finish().apply(&mut result);
296-
assert_eq_text!(ra_fixture_after, &*result);
335+
Err(err) => {
336+
if ra_fixture_after.starts_with("error:") {
337+
let error_message = ra_fixture_after
338+
.chars()
339+
.into_iter()
340+
.skip("error:".len())
341+
.collect::<String>();
342+
assert_eq!(error_message.trim(), err.to_string());
343+
return;
344+
} else {
345+
panic!("Rename to '{}' failed unexpectedly: {}", new_name, err)
346+
}
347+
}
348+
};
297349
}
298350

299351
fn check_expect(new_name: &str, ra_fixture: &str, expect: Expect) {
300352
let (analysis, position) = fixture::position(ra_fixture);
301-
let source_change = analysis.rename(position, new_name).unwrap().unwrap();
353+
let source_change = analysis
354+
.rename(position, new_name)
355+
.unwrap()
356+
.expect("Expect returned RangeInfo to be Some, but was None");
302357
expect.assert_debug_eq(&source_change)
303358
}
304359

@@ -313,11 +368,30 @@ mod tests {
313368
}
314369

315370
#[test]
316-
fn test_rename_to_invalid_identifier() {
317-
let (analysis, position) = fixture::position(r#"fn main() { let i<|> = 1; }"#);
318-
let new_name = "invalid!";
319-
let source_change = analysis.rename(position, new_name).unwrap();
320-
assert!(source_change.is_none());
371+
fn test_rename_to_invalid_identifier1() {
372+
check(
373+
"invalid!",
374+
r#"fn main() { let i<|> = 1; }"#,
375+
"error: Invalid name `invalid!`: not an identifier",
376+
);
377+
}
378+
379+
#[test]
380+
fn test_rename_to_invalid_identifier2() {
381+
check(
382+
"multiple tokens",
383+
r#"fn main() { let i<|> = 1; }"#,
384+
"error: Invalid name `multiple tokens`: not an identifier",
385+
);
386+
}
387+
388+
#[test]
389+
fn test_rename_to_invalid_identifier3() {
390+
check(
391+
"let",
392+
r#"fn main() { let i<|> = 1; }"#,
393+
"error: Invalid name `let`: not an identifier",
394+
);
321395
}
322396

323397
#[test]
@@ -349,6 +423,15 @@ fn main() {
349423
);
350424
}
351425

426+
#[test]
427+
fn test_rename_unresolved_reference() {
428+
check(
429+
"new_name",
430+
r#"fn main() { let _ = unresolved_ref<|>; }"#,
431+
"error: No references found at position",
432+
);
433+
}
434+
352435
#[test]
353436
fn test_rename_for_macro_args() {
354437
check(

crates/rust-analyzer/src/handlers.rs

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -646,14 +646,9 @@ pub(crate) fn handle_prepare_rename(
646646
let _p = profile::span("handle_prepare_rename");
647647
let position = from_proto::file_position(&snap, params)?;
648648

649-
let optional_change = snap.analysis.rename(position, "dummy")?;
650-
let range = match optional_change {
651-
None => return Ok(None),
652-
Some(it) => it.range,
653-
};
654-
649+
let change = snap.analysis.rename(position, "dummy")??;
655650
let line_index = snap.analysis.file_line_index(position.file_id)?;
656-
let range = to_proto::range(&line_index, range);
651+
let range = to_proto::range(&line_index, change.range);
657652
Ok(Some(PrepareRenameResponse::Range(range)))
658653
}
659654

@@ -672,12 +667,8 @@ pub(crate) fn handle_rename(
672667
.into());
673668
}
674669

675-
let optional_change = snap.analysis.rename(position, &*params.new_name)?;
676-
let source_change = match optional_change {
677-
None => return Ok(None),
678-
Some(it) => it.info,
679-
};
680-
let workspace_edit = to_proto::workspace_edit(&snap, source_change)?;
670+
let change = snap.analysis.rename(position, &*params.new_name)??;
671+
let workspace_edit = to_proto::workspace_edit(&snap, change.info)?;
681672
Ok(Some(workspace_edit))
682673
}
683674

0 commit comments

Comments
 (0)