Skip to content

Commit 0bd3229

Browse files
Warn the user when a rename will change the meaning of the program
Specifically, when a rename of a local will change some code that refers it to refer another local, or some code that refer another local to refer to it. We do it by introducing a dummy edit with an annotation. I'm not a fond of this approach, but I don't think LSP has a better way.
1 parent 6a3ede1 commit 0bd3229

File tree

12 files changed

+509
-59
lines changed

12 files changed

+509
-59
lines changed

Diff for: src/tools/rust-analyzer/crates/hir-def/src/path.rs

+10-6
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ pub enum Path {
5757
/// or type anchor, it is `Path::Normal` with the generics filled with `None` even if there are none (practically
5858
/// this is not a problem since many more paths have generics than a type anchor).
5959
BarePath(Interned<ModPath>),
60-
/// `Path::Normal` may have empty generics and type anchor (but generic args will be filled with `None`).
60+
/// `Path::Normal` will always have either generics or type anchor.
6161
Normal(NormalPath),
6262
/// A link to a lang item. It is used in desugaring of things like `it?`. We can show these
6363
/// links via a normal path since they might be private and not accessible in the usage place.
@@ -208,11 +208,15 @@ impl Path {
208208
mod_path.segments()[..mod_path.segments().len() - 1].iter().cloned(),
209209
));
210210
let qualifier_generic_args = &generic_args[..generic_args.len() - 1];
211-
Some(Path::Normal(NormalPath::new(
212-
type_anchor,
213-
qualifier_mod_path,
214-
qualifier_generic_args.iter().cloned(),
215-
)))
211+
if type_anchor.is_none() && qualifier_generic_args.iter().all(|it| it.is_none()) {
212+
Some(Path::BarePath(qualifier_mod_path))
213+
} else {
214+
Some(Path::Normal(NormalPath::new(
215+
type_anchor,
216+
qualifier_mod_path,
217+
qualifier_generic_args.iter().cloned(),
218+
)))
219+
}
216220
}
217221
Path::LangItem(..) => None,
218222
}

Diff for: src/tools/rust-analyzer/crates/hir-def/src/resolver.rs

+143-23
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@ use std::{fmt, iter, mem};
33

44
use base_db::CrateId;
55
use hir_expand::{name::Name, MacroDefId};
6-
use intern::sym;
6+
use intern::{sym, Symbol};
77
use itertools::Itertools as _;
88
use rustc_hash::FxHashSet;
99
use smallvec::{smallvec, SmallVec};
10+
use span::SyntaxContextId;
1011
use triomphe::Arc;
1112

1213
use crate::{
@@ -343,15 +344,7 @@ impl Resolver {
343344
}
344345

345346
if n_segments <= 1 {
346-
let mut hygiene_info = if !hygiene_id.is_root() {
347-
let ctx = hygiene_id.lookup(db);
348-
ctx.outer_expn.map(|expansion| {
349-
let expansion = db.lookup_intern_macro_call(expansion);
350-
(ctx.parent, expansion.def)
351-
})
352-
} else {
353-
None
354-
};
347+
let mut hygiene_info = hygiene_info(db, hygiene_id);
355348
for scope in self.scopes() {
356349
match scope {
357350
Scope::ExprScope(scope) => {
@@ -371,19 +364,7 @@ impl Resolver {
371364
}
372365
}
373366
Scope::MacroDefScope(macro_id) => {
374-
if let Some((parent_ctx, label_macro_id)) = hygiene_info {
375-
if label_macro_id == **macro_id {
376-
// A macro is allowed to refer to variables from before its declaration.
377-
// Therefore, if we got to the rib of its declaration, give up its hygiene
378-
// and use its parent expansion.
379-
let parent_ctx = db.lookup_intern_syntax_context(parent_ctx);
380-
hygiene_id = HygieneId::new(parent_ctx.opaque_and_semitransparent);
381-
hygiene_info = parent_ctx.outer_expn.map(|expansion| {
382-
let expansion = db.lookup_intern_macro_call(expansion);
383-
(parent_ctx.parent, expansion.def)
384-
});
385-
}
386-
}
367+
handle_macro_def_scope(db, &mut hygiene_id, &mut hygiene_info, macro_id)
387368
}
388369
Scope::GenericParams { params, def } => {
389370
if let Some(id) = params.find_const_by_name(first_name, *def) {
@@ -730,6 +711,107 @@ impl Resolver {
730711
})
731712
}
732713

714+
/// Checks if we rename `renamed` (currently named `current_name`) to `new_name`, will the meaning of this reference
715+
/// (that contains `current_name` path) change from `renamed` to some another variable (returned as `Some`).
716+
pub fn rename_will_conflict_with_another_variable(
717+
&self,
718+
db: &dyn DefDatabase,
719+
current_name: &Name,
720+
current_name_as_path: &ModPath,
721+
mut hygiene_id: HygieneId,
722+
new_name: &Symbol,
723+
to_be_renamed: BindingId,
724+
) -> Option<BindingId> {
725+
let mut hygiene_info = hygiene_info(db, hygiene_id);
726+
let mut will_be_resolved_to = None;
727+
for scope in self.scopes() {
728+
match scope {
729+
Scope::ExprScope(scope) => {
730+
for entry in scope.expr_scopes.entries(scope.scope_id) {
731+
if entry.hygiene() == hygiene_id {
732+
if entry.binding() == to_be_renamed {
733+
// This currently resolves to our renamed variable, now `will_be_resolved_to`
734+
// contains `Some` if the meaning will change or `None` if not.
735+
return will_be_resolved_to;
736+
} else if entry.name().symbol() == new_name {
737+
will_be_resolved_to = Some(entry.binding());
738+
}
739+
}
740+
}
741+
}
742+
Scope::MacroDefScope(macro_id) => {
743+
handle_macro_def_scope(db, &mut hygiene_id, &mut hygiene_info, macro_id)
744+
}
745+
Scope::GenericParams { params, def } => {
746+
if params.find_const_by_name(current_name, *def).is_some() {
747+
// It does not resolve to our renamed variable.
748+
return None;
749+
}
750+
}
751+
Scope::AdtScope(_) | Scope::ImplDefScope(_) => continue,
752+
Scope::BlockScope(m) => {
753+
if m.resolve_path_in_value_ns(db, current_name_as_path).is_some() {
754+
// It does not resolve to our renamed variable.
755+
return None;
756+
}
757+
}
758+
}
759+
}
760+
// It does not resolve to our renamed variable.
761+
None
762+
}
763+
764+
/// Checks if we rename `renamed` to `name`, will the meaning of this reference (that contains `name` path) change
765+
/// from some other variable (returned as `Some`) to `renamed`.
766+
pub fn rename_will_conflict_with_renamed(
767+
&self,
768+
db: &dyn DefDatabase,
769+
name: &Name,
770+
name_as_path: &ModPath,
771+
mut hygiene_id: HygieneId,
772+
to_be_renamed: BindingId,
773+
) -> Option<BindingId> {
774+
let mut hygiene_info = hygiene_info(db, hygiene_id);
775+
let mut will_resolve_to_renamed = false;
776+
for scope in self.scopes() {
777+
match scope {
778+
Scope::ExprScope(scope) => {
779+
for entry in scope.expr_scopes.entries(scope.scope_id) {
780+
if entry.binding() == to_be_renamed {
781+
will_resolve_to_renamed = true;
782+
} else if entry.hygiene() == hygiene_id && entry.name() == name {
783+
if will_resolve_to_renamed {
784+
// This will resolve to the renamed variable before it resolves to the original variable.
785+
return Some(entry.binding());
786+
} else {
787+
// This will resolve to the original variable.
788+
return None;
789+
}
790+
}
791+
}
792+
}
793+
Scope::MacroDefScope(macro_id) => {
794+
handle_macro_def_scope(db, &mut hygiene_id, &mut hygiene_info, macro_id)
795+
}
796+
Scope::GenericParams { params, def } => {
797+
if params.find_const_by_name(name, *def).is_some() {
798+
// Here and below, it might actually resolve to our renamed variable - in which case it'll
799+
// hide the generic parameter or some other thing (not a variable). We don't check for that
800+
// because due to naming conventions, it is rare that variable will shadow a non-variable.
801+
return None;
802+
}
803+
}
804+
Scope::AdtScope(_) | Scope::ImplDefScope(_) => continue,
805+
Scope::BlockScope(m) => {
806+
if m.resolve_path_in_value_ns(db, name_as_path).is_some() {
807+
return None;
808+
}
809+
}
810+
}
811+
}
812+
None
813+
}
814+
733815
/// `expr_id` is required to be an expression id that comes after the top level expression scope in the given resolver
734816
#[must_use]
735817
pub fn update_to_inner_scope(
@@ -795,6 +877,44 @@ impl Resolver {
795877
}
796878
}
797879

880+
#[inline]
881+
fn handle_macro_def_scope(
882+
db: &dyn DefDatabase,
883+
hygiene_id: &mut HygieneId,
884+
hygiene_info: &mut Option<(SyntaxContextId, MacroDefId)>,
885+
macro_id: &MacroDefId,
886+
) {
887+
if let Some((parent_ctx, label_macro_id)) = hygiene_info {
888+
if label_macro_id == macro_id {
889+
// A macro is allowed to refer to variables from before its declaration.
890+
// Therefore, if we got to the rib of its declaration, give up its hygiene
891+
// and use its parent expansion.
892+
let parent_ctx = db.lookup_intern_syntax_context(*parent_ctx);
893+
*hygiene_id = HygieneId::new(parent_ctx.opaque_and_semitransparent);
894+
*hygiene_info = parent_ctx.outer_expn.map(|expansion| {
895+
let expansion = db.lookup_intern_macro_call(expansion);
896+
(parent_ctx.parent, expansion.def)
897+
});
898+
}
899+
}
900+
}
901+
902+
#[inline]
903+
fn hygiene_info(
904+
db: &dyn DefDatabase,
905+
hygiene_id: HygieneId,
906+
) -> Option<(SyntaxContextId, MacroDefId)> {
907+
if !hygiene_id.is_root() {
908+
let ctx = hygiene_id.lookup(db);
909+
ctx.outer_expn.map(|expansion| {
910+
let expansion = db.lookup_intern_macro_call(expansion);
911+
(ctx.parent, expansion.def)
912+
})
913+
} else {
914+
None
915+
}
916+
}
917+
798918
pub struct UpdateGuard(usize);
799919

800920
impl Resolver {

Diff for: src/tools/rust-analyzer/crates/hir/src/semantics.rs

+93-2
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ use std::{
1212

1313
use either::Either;
1414
use hir_def::{
15-
expr_store::ExprOrPatSource,
16-
hir::{Expr, ExprOrPatId},
15+
expr_store::{Body, ExprOrPatSource},
16+
hir::{BindingId, Expr, ExprId, ExprOrPatId, Pat},
1717
lower::LowerCtx,
1818
nameres::{MacroSubNs, ModuleOrigin},
1919
path::ModPath,
@@ -629,6 +629,31 @@ impl<'db> SemanticsImpl<'db> {
629629
)
630630
}
631631

632+
/// Checks if renaming `renamed` to `new_name` may introduce conflicts with other locals,
633+
/// and returns the conflicting locals.
634+
pub fn rename_conflicts(&self, to_be_renamed: &Local, new_name: &str) -> Vec<Local> {
635+
let body = self.db.body(to_be_renamed.parent);
636+
let resolver = to_be_renamed.parent.resolver(self.db.upcast());
637+
let starting_expr =
638+
body.binding_owners.get(&to_be_renamed.binding_id).copied().unwrap_or(body.body_expr);
639+
let mut visitor = RenameConflictsVisitor {
640+
body: &body,
641+
conflicts: FxHashSet::default(),
642+
db: self.db,
643+
new_name: Symbol::intern(new_name),
644+
old_name: to_be_renamed.name(self.db).symbol().clone(),
645+
owner: to_be_renamed.parent,
646+
to_be_renamed: to_be_renamed.binding_id,
647+
resolver,
648+
};
649+
visitor.rename_conflicts(starting_expr);
650+
visitor
651+
.conflicts
652+
.into_iter()
653+
.map(|binding_id| Local { parent: to_be_renamed.parent, binding_id })
654+
.collect()
655+
}
656+
632657
/// Retrieves all the formatting parts of the format_args! (or `asm!`) template string.
633658
pub fn as_format_args_parts(
634659
&self,
@@ -2094,3 +2119,69 @@ impl ops::Deref for VisibleTraits {
20942119
&self.0
20952120
}
20962121
}
2122+
2123+
struct RenameConflictsVisitor<'a> {
2124+
db: &'a dyn HirDatabase,
2125+
owner: DefWithBodyId,
2126+
resolver: Resolver,
2127+
body: &'a Body,
2128+
to_be_renamed: BindingId,
2129+
new_name: Symbol,
2130+
old_name: Symbol,
2131+
conflicts: FxHashSet<BindingId>,
2132+
}
2133+
2134+
impl RenameConflictsVisitor<'_> {
2135+
fn resolve_path(&mut self, node: ExprOrPatId, path: &Path) {
2136+
if let Path::BarePath(path) = path {
2137+
if let Some(name) = path.as_ident() {
2138+
if *name.symbol() == self.new_name {
2139+
if let Some(conflicting) = self.resolver.rename_will_conflict_with_renamed(
2140+
self.db.upcast(),
2141+
name,
2142+
path,
2143+
self.body.expr_or_pat_path_hygiene(node),
2144+
self.to_be_renamed,
2145+
) {
2146+
self.conflicts.insert(conflicting);
2147+
}
2148+
} else if *name.symbol() == self.old_name {
2149+
if let Some(conflicting) =
2150+
self.resolver.rename_will_conflict_with_another_variable(
2151+
self.db.upcast(),
2152+
name,
2153+
path,
2154+
self.body.expr_or_pat_path_hygiene(node),
2155+
&self.new_name,
2156+
self.to_be_renamed,
2157+
)
2158+
{
2159+
self.conflicts.insert(conflicting);
2160+
}
2161+
}
2162+
}
2163+
}
2164+
}
2165+
2166+
fn rename_conflicts(&mut self, expr: ExprId) {
2167+
match &self.body[expr] {
2168+
Expr::Path(path) => {
2169+
let guard = self.resolver.update_to_inner_scope(self.db.upcast(), self.owner, expr);
2170+
self.resolve_path(expr.into(), path);
2171+
self.resolver.reset_to_guard(guard);
2172+
}
2173+
&Expr::Assignment { target, .. } => {
2174+
let guard = self.resolver.update_to_inner_scope(self.db.upcast(), self.owner, expr);
2175+
self.body.walk_pats(target, &mut |pat| {
2176+
if let Pat::Path(path) = &self.body[pat] {
2177+
self.resolve_path(pat.into(), path);
2178+
}
2179+
});
2180+
self.resolver.reset_to_guard(guard);
2181+
}
2182+
_ => {}
2183+
}
2184+
2185+
self.body.walk_child_exprs(expr, |expr| self.rename_conflicts(expr));
2186+
}
2187+
}

0 commit comments

Comments
 (0)