Skip to content

Commit f87a7b8

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 3c2aca1 commit f87a7b8

File tree

12 files changed

+504
-58
lines changed

12 files changed

+504
-58
lines changed

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.
@@ -211,11 +211,15 @@ impl Path {
211211
mod_path.segments()[..mod_path.segments().len() - 1].iter().cloned(),
212212
));
213213
let qualifier_generic_args = &generic_args[..generic_args.len() - 1];
214-
Some(Path::Normal(NormalPath::new(
215-
type_anchor,
216-
qualifier_mod_path,
217-
qualifier_generic_args.iter().cloned(),
218-
)))
214+
if type_anchor.is_none() && qualifier_generic_args.iter().all(|it| it.is_none()) {
215+
Some(Path::BarePath(qualifier_mod_path))
216+
} else {
217+
Some(Path::Normal(NormalPath::new(
218+
type_anchor,
219+
qualifier_mod_path,
220+
qualifier_generic_args.iter().cloned(),
221+
)))
222+
}
219223
}
220224
Path::LangItem(..) => None,
221225
}

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::{
@@ -342,15 +343,7 @@ impl Resolver {
342343
}
343344

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

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

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

798918
impl Resolver {

crates/hir/src/semantics.rs

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

1313
use either::Either;
1414
use hir_def::{
15-
hir::{Expr, ExprOrPatId},
15+
expr_store::Body,
16+
hir::{BindingId, Expr, ExprId, ExprOrPatId, Pat},
1617
lower::LowerCtx,
1718
nameres::{MacroSubNs, ModuleOrigin},
1819
path::ModPath,
@@ -631,6 +632,31 @@ impl<'db> SemanticsImpl<'db> {
631632
)
632633
}
633634

635+
/// Checks if renaming `renamed` to `new_name` may introduce conflicts with other locals,
636+
/// and returns the conflicting locals.
637+
pub fn rename_conflicts(&self, renamed: &Local, new_name: &str) -> Vec<Local> {
638+
let body = self.db.body(renamed.parent);
639+
let resolver = renamed.parent.resolver(self.db.upcast());
640+
let starting_expr =
641+
body.binding_owners.get(&renamed.binding_id).copied().unwrap_or(body.body_expr);
642+
let mut visitor = RenameConflictsVisitor {
643+
body: &body,
644+
conflicts: FxHashSet::default(),
645+
db: self.db,
646+
new_name: Symbol::intern(new_name),
647+
old_name: renamed.name(self.db).symbol().clone(),
648+
owner: renamed.parent,
649+
renamed: renamed.binding_id,
650+
resolver,
651+
};
652+
visitor.rename_conflicts(starting_expr);
653+
visitor
654+
.conflicts
655+
.into_iter()
656+
.map(|binding_id| Local { parent: renamed.parent, binding_id })
657+
.collect()
658+
}
659+
634660
/// Retrieves all the formatting parts of the format_args! (or `asm!`) template string.
635661
pub fn as_format_args_parts(
636662
&self,
@@ -2153,3 +2179,69 @@ impl ops::Deref for VisibleTraits {
21532179
&self.0
21542180
}
21552181
}
2182+
2183+
struct RenameConflictsVisitor<'a> {
2184+
db: &'a dyn HirDatabase,
2185+
owner: DefWithBodyId,
2186+
resolver: Resolver,
2187+
body: &'a Body,
2188+
renamed: BindingId,
2189+
new_name: Symbol,
2190+
old_name: Symbol,
2191+
conflicts: FxHashSet<BindingId>,
2192+
}
2193+
2194+
impl RenameConflictsVisitor<'_> {
2195+
fn resolve_path(&mut self, node: ExprOrPatId, path: &Path) {
2196+
if let Path::BarePath(path) = path {
2197+
if let Some(name) = path.as_ident() {
2198+
if *name.symbol() == self.new_name {
2199+
if let Some(conflicting) = self.resolver.rename_will_conflict_to_renamed(
2200+
self.db.upcast(),
2201+
name,
2202+
path,
2203+
self.body.expr_or_pat_path_hygiene(node),
2204+
self.renamed,
2205+
) {
2206+
self.conflicts.insert(conflicting);
2207+
}
2208+
} else if *name.symbol() == self.old_name {
2209+
if let Some(conflicting) =
2210+
self.resolver.rename_will_conflict_to_another_variable(
2211+
self.db.upcast(),
2212+
name,
2213+
path,
2214+
self.body.expr_or_pat_path_hygiene(node),
2215+
&self.new_name,
2216+
self.renamed,
2217+
)
2218+
{
2219+
self.conflicts.insert(conflicting);
2220+
}
2221+
}
2222+
}
2223+
}
2224+
}
2225+
2226+
fn rename_conflicts(&mut self, expr: ExprId) {
2227+
match &self.body[expr] {
2228+
Expr::Path(path) => {
2229+
let guard = self.resolver.update_to_inner_scope(self.db.upcast(), self.owner, expr);
2230+
self.resolve_path(expr.into(), path);
2231+
self.resolver.reset_to_guard(guard);
2232+
}
2233+
&Expr::Assignment { target, .. } => {
2234+
let guard = self.resolver.update_to_inner_scope(self.db.upcast(), self.owner, expr);
2235+
self.body.walk_pats(target, &mut |pat| {
2236+
if let Pat::Path(path) = &self.body[pat] {
2237+
self.resolve_path(pat.into(), path);
2238+
}
2239+
});
2240+
self.resolver.reset_to_guard(guard);
2241+
}
2242+
_ => {}
2243+
}
2244+
2245+
self.body.walk_child_exprs(expr, |expr| self.rename_conflicts(expr));
2246+
}
2247+
}

0 commit comments

Comments
 (0)