Lifetime reference search

This commit is contained in:
Lukas Wirth 2020-12-16 21:35:15 +01:00
parent 067067a6c1
commit 55faa2daa3
12 changed files with 373 additions and 41 deletions

@ -9,7 +9,7 @@ use ide_db::{defs::Definition, RootDatabase};
use syntax::{
ast::{self, NameOwner},
match_ast, AstNode, SmolStr,
SyntaxKind::{self, IDENT_PAT, TYPE_PARAM},
SyntaxKind::{self, IDENT_PAT, LIFETIME_PARAM, TYPE_PARAM},
TextRange,
};
@ -182,6 +182,7 @@ impl TryToNav for Definition {
Definition::SelfType(it) => Some(it.to_nav(db)),
Definition::Local(it) => Some(it.to_nav(db)),
Definition::TypeParam(it) => Some(it.to_nav(db)),
Definition::LifetimeParam(it) => Some(it.to_nav(db)),
}
}
}
@ -376,6 +377,23 @@ impl ToNav for hir::TypeParam {
}
}
impl ToNav for hir::LifetimeParam {
fn to_nav(&self, db: &RootDatabase) -> NavigationTarget {
let src = self.source(db);
let full_range = src.value.syntax().text_range();
NavigationTarget {
file_id: src.file_id.original_file(db),
name: self.name(db).to_string().into(),
kind: LIFETIME_PARAM,
full_range,
focus_range: Some(full_range),
container_name: None,
description: None,
docs: None,
}
}
}
pub(crate) fn docs_from_symbol(db: &RootDatabase, symbol: &FileSymbol) -> Option<Documentation> {
let parse = db.parse(symbol.file_id);
let node = symbol.ptr.to_node(parse.tree().syntax());

@ -190,7 +190,10 @@ fn rewrite_intra_doc_link(
},
Definition::Macro(it) => it.resolve_doc_path(db, link, ns),
Definition::Field(it) => it.resolve_doc_path(db, link, ns),
Definition::SelfType(_) | Definition::Local(_) | Definition::TypeParam(_) => return None,
Definition::SelfType(_)
| Definition::Local(_)
| Definition::TypeParam(_)
| Definition::LifetimeParam(_) => return None,
}?;
let krate = resolved.module(db)?.krate();
let canonical_path = resolved.canonical_path(db)?;

@ -1,3 +1,4 @@
use either::Either;
use hir::Semantics;
use ide_db::{
base_db::FileId,
@ -33,7 +34,7 @@ pub(crate) fn goto_definition(
let nav_targets = match_ast! {
match parent {
ast::NameRef(name_ref) => {
reference_definition(&sema, &name_ref).to_vec()
reference_definition(&sema, Either::Right(&name_ref)).to_vec()
},
ast::Name(name) => {
let def = NameClass::classify(&sema, &name)?.referenced_or_defined(sema.db);
@ -53,6 +54,13 @@ pub(crate) fn goto_definition(
let self_param = func.param_list()?.self_param()?;
vec![self_to_nav_target(self_param, position.file_id)?]
},
ast::Lifetime(lt) => if let Some(name_class) = NameClass::classify_lifetime(&sema, &lt) {
let def = name_class.referenced_or_defined(sema.db);
let nav = def.try_to_nav(sema.db)?;
vec![nav]
} else {
reference_definition(&sema, Either::Left(&lt)).to_vec()
},
_ => return None,
}
};
@ -64,7 +72,7 @@ fn pick_best(tokens: TokenAtOffset<SyntaxToken>) -> Option<SyntaxToken> {
return tokens.max_by_key(priority);
fn priority(n: &SyntaxToken) -> usize {
match n.kind() {
IDENT | INT_NUMBER | T![self] => 2,
IDENT | INT_NUMBER | LIFETIME_IDENT | T![self] => 2,
kind if kind.is_trivia() => 0,
_ => 1,
}
@ -102,9 +110,12 @@ impl ReferenceResult {
pub(crate) fn reference_definition(
sema: &Semantics<RootDatabase>,
name_ref: &ast::NameRef,
name_ref: Either<&ast::Lifetime, &ast::NameRef>,
) -> ReferenceResult {
let name_kind = NameRefClass::classify(sema, name_ref);
let name_kind = name_ref.either(
|lifetime| NameRefClass::classify_lifetime(sema, lifetime),
|name_ref| NameRefClass::classify(sema, name_ref),
);
if let Some(def) = name_kind {
let def = def.referenced(sema.db);
return match def.try_to_nav(sema.db) {
@ -114,10 +125,9 @@ pub(crate) fn reference_definition(
}
// Fallback index based approach:
let navs = symbol_index::index_resolve(sema.db, name_ref)
.into_iter()
.map(|s| s.to_nav(sema.db))
.collect();
let name = name_ref.either(ast::Lifetime::text, ast::NameRef::text);
let navs =
symbol_index::index_resolve(sema.db, name).into_iter().map(|s| s.to_nav(sema.db)).collect();
ReferenceResult::Approximate(navs)
}
@ -1033,6 +1043,37 @@ impl Foo {
fn bar(&self<|>) {
//^^^^
}
}"#,
)
}
#[test]
fn goto_lifetime_param_on_decl() {
check(
r#"
fn foo<'foobar<|>>(_: &'foobar ()) {
//^^^^^^^
}"#,
)
}
#[test]
fn goto_lifetime_param_decl() {
check(
r#"
fn foo<'foobar>(_: &'foobar<|> ()) {
//^^^^^^^
}"#,
)
}
#[test]
fn goto_lifetime_param_decl_nested() {
check(
r#"
fn foo<'foobar>(_: &'foobar ()) {
fn foo<'foobar>(_: &'foobar<|> ()) {}
//^^^^^^^
}"#,
)
}

@ -364,7 +364,7 @@ fn hover_for_definition(db: &RootDatabase, def: Definition) -> Option<Markup> {
Adt::Enum(it) => from_def_source(db, it, mod_path),
})
}
Definition::TypeParam(_) => {
Definition::TypeParam(_) | Definition::LifetimeParam(_) => {
// FIXME: Hover for generic param
None
}

@ -528,6 +528,13 @@ impl Analysis {
self.with_db(|db| references::rename::rename(db, position, new_name))
}
pub fn prepare_rename(
&self,
position: FilePosition,
) -> Cancelable<Result<RangeInfo<()>, RenameError>> {
self.with_db(|db| references::rename::prepare_rename(db, position))
}
pub fn structural_search_replace(
&self,
query: &str,

@ -130,6 +130,8 @@ pub(crate) fn find_all_refs(
kind = ReferenceKind::FieldShorthandForLocal;
}
}
} else if let Definition::LifetimeParam(_) = def {
kind = ReferenceKind::Lifetime;
};
let declaration = Declaration { nav, kind, access: decl_access(&def, &syntax, decl_range) };
@ -148,11 +150,29 @@ fn find_name(
let range = name.syntax().text_range();
return Some(RangeInfo::new(range, def));
}
let name_ref =
sema.find_node_at_offset_with_descend::<ast::NameRef>(&syntax, position.offset)?;
let def = NameRefClass::classify(sema, &name_ref)?.referenced(sema.db);
let range = name_ref.syntax().text_range();
Some(RangeInfo::new(range, def))
let (text_range, def) = if let Some(lifetime) =
sema.find_node_at_offset_with_descend::<ast::Lifetime>(&syntax, position.offset)
{
if let Some(def) = NameRefClass::classify_lifetime(sema, &lifetime)
.map(|class| NameRefClass::referenced(class, sema.db))
{
(lifetime.syntax().text_range(), def)
} else {
(
lifetime.syntax().text_range(),
NameClass::classify_lifetime(sema, &lifetime)?.referenced_or_defined(sema.db),
)
}
} else {
let name_ref =
sema.find_node_at_offset_with_descend::<ast::NameRef>(&syntax, position.offset)?;
(
name_ref.syntax().text_range(),
NameRefClass::classify(sema, &name_ref)?.referenced(sema.db),
)
};
Some(RangeInfo::new(text_range, def))
}
fn decl_access(def: &Definition, syntax: &SyntaxNode, range: TextRange) -> Option<ReferenceAccess> {
@ -1005,4 +1025,65 @@ impl Foo {
}
expect.assert_eq(&actual)
}
#[test]
fn test_find_lifetimes_function() {
check(
r#"
trait Foo<'a> {}
impl<'a> Foo<'a> for &'a () {}
fn foo<'a, 'b: 'a>(x: &'a<|> ()) -> &'a () where &'a (): Foo<'a> {
fn bar<'a>(_: &'a ()) {}
x
}
"#,
expect![[r#"
'a LIFETIME_PARAM FileId(0) 55..57 55..57 Lifetime
FileId(0) 63..65 Lifetime
FileId(0) 71..73 Lifetime
FileId(0) 82..84 Lifetime
FileId(0) 95..97 Lifetime
FileId(0) 106..108 Lifetime
"#]],
);
}
#[test]
fn test_find_lifetimes_type_alias() {
check(
r#"
type Foo<'a, T> where T: 'a<|> = &'a T;
"#,
expect![[r#"
'a LIFETIME_PARAM FileId(0) 9..11 9..11 Lifetime
FileId(0) 25..27 Lifetime
FileId(0) 31..33 Lifetime
"#]],
);
}
#[test]
fn test_find_lifetimes_trait_impl() {
check(
r#"
trait Foo<'a> {
fn foo() -> &'a ();
}
impl<'a> Foo<'a> for &'a () {
fn foo() -> &'a<|> () {
unimplemented!()
}
}
"#,
expect![[r#"
'a LIFETIME_PARAM FileId(0) 47..49 47..49 Lifetime
FileId(0) 55..57 Lifetime
FileId(0) 64..66 Lifetime
FileId(0) 89..91 Lifetime
"#]],
);
}
}

@ -35,6 +35,29 @@ impl fmt::Display for RenameError {
impl Error for RenameError {}
pub(crate) fn prepare_rename(
db: &RootDatabase,
position: FilePosition,
) -> Result<RangeInfo<()>, RenameError> {
let sema = Semantics::new(db);
let source_file = sema.parse(position.file_id);
let syntax = source_file.syntax();
if let Some(module) = find_module_at_offset(&sema, position, syntax) {
rename_mod(&sema, position, module, "dummy")
} else if let Some(self_token) =
syntax.token_at_offset(position.offset).find(|t| t.kind() == SyntaxKind::SELF_KW)
{
rename_self_to_param(&sema, position, self_token, "dummy")
} else {
let range = match find_all_refs(&sema, position, None) {
Some(RangeInfo { range, .. }) => range,
None => return Err(RenameError("No references found at position".to_string())),
};
Ok(RangeInfo::new(range, SourceChange::from(vec![])))
}
.map(|info| RangeInfo::new(info.range, ()))
}
pub(crate) fn rename(
db: &RootDatabase,
position: FilePosition,
@ -49,11 +72,18 @@ pub(crate) fn rename_with_semantics(
position: FilePosition,
new_name: &str,
) -> Result<RangeInfo<SourceChange>, RenameError> {
match lex_single_syntax_kind(new_name) {
let is_lifetime_name = match lex_single_syntax_kind(new_name) {
Some(res) => match res {
(SyntaxKind::IDENT, _) => (),
(SyntaxKind::UNDERSCORE, _) => (),
(SyntaxKind::IDENT, _) => false,
(SyntaxKind::UNDERSCORE, _) => false,
(SyntaxKind::SELF_KW, _) => return rename_to_self(&sema, position),
(SyntaxKind::LIFETIME_IDENT, _) if new_name != "'static" && new_name != "'_" => true,
(SyntaxKind::LIFETIME_IDENT, _) => {
return Err(RenameError(format!(
"Invalid name `{0}`: Cannot rename lifetime to {0}",
new_name
)))
}
(_, Some(syntax_error)) => {
return Err(RenameError(format!("Invalid name `{}`: {}", new_name, syntax_error)))
}
@ -62,18 +92,21 @@ pub(crate) fn rename_with_semantics(
}
},
None => return Err(RenameError(format!("Invalid name `{}`: not an identifier", new_name))),
}
};
let source_file = sema.parse(position.file_id);
let syntax = source_file.syntax();
if let Some(module) = find_module_at_offset(&sema, position, syntax) {
// this is here to prevent lifetime renames from happening on modules and self
if is_lifetime_name {
rename_reference(&sema, position, new_name, is_lifetime_name)
} else if let Some(module) = find_module_at_offset(&sema, position, syntax) {
rename_mod(&sema, position, module, new_name)
} else if let Some(self_token) =
syntax.token_at_offset(position.offset).find(|t| t.kind() == SyntaxKind::SELF_KW)
{
rename_self_to_param(&sema, position, self_token, new_name)
} else {
rename_reference(&sema, position, new_name)
rename_reference(&sema, position, new_name, is_lifetime_name)
}
}
@ -355,12 +388,26 @@ fn rename_reference(
sema: &Semantics<RootDatabase>,
position: FilePosition,
new_name: &str,
is_lifetime_name: bool,
) -> Result<RangeInfo<SourceChange>, RenameError> {
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())),
};
match (refs.declaration.kind == ReferenceKind::Lifetime, is_lifetime_name) {
(true, false) => {
return Err(RenameError(format!(
"Invalid name `{}`: not a lifetime identifier",
new_name
)))
}
(false, true) => {
return Err(RenameError(format!("Invalid name `{}`: not an identifier", new_name)))
}
_ => (),
}
let edit = refs
.into_iter()
.map(|reference| source_edit_from_reference(sema, reference, new_name))
@ -464,6 +511,24 @@ mod tests {
);
}
#[test]
fn test_rename_to_invalid_identifier_lifetime() {
check(
"'foo",
r#"fn main() { let i<|> = 1; }"#,
"error: Invalid name `'foo`: not an identifier",
);
}
#[test]
fn test_rename_to_invalid_identifier_lifetime2() {
check(
"foo",
r#"fn main<'a>(_: &'a<|> ()) {}"#,
"error: Invalid name `foo`: not a lifetime identifier",
);
}
#[test]
fn test_rename_for_local() {
check(
@ -1393,6 +1458,33 @@ struct Foo {
fn foo(Foo { i: bar }: foo) -> i32 {
bar
}
"#,
)
}
#[test]
fn test_rename_lifetimes() {
check(
"'yeeee",
r#"
trait Foo<'a> {
fn foo() -> &'a ();
}
impl<'a> Foo<'a> for &'a () {
fn foo() -> &'a<|> () {
unimplemented!()
}
}
"#,
r#"
trait Foo<'a> {
fn foo() -> &'a ();
}
impl<'yeeee> Foo<'yeeee> for &'yeeee () {
fn foo() -> &'yeeee () {
unimplemented!()
}
}
"#,
)
}

@ -806,6 +806,7 @@ fn highlight_def(db: &RootDatabase, def: Definition) -> Highlight {
}
return h;
}
Definition::LifetimeParam(_) => HighlightTag::Lifetime,
}
.into()
}

@ -6,12 +6,12 @@
// FIXME: this badly needs rename/rewrite (matklad, 2020-02-06).
use hir::{
db::HirDatabase, Crate, Field, HasVisibility, ImplDef, Local, MacroDef, Module, ModuleDef,
Name, PathResolution, Semantics, TypeParam, Visibility,
db::HirDatabase, Crate, Field, HasVisibility, ImplDef, LifetimeParam, Local, MacroDef, Module,
ModuleDef, Name, PathResolution, Semantics, TypeParam, Visibility,
};
use syntax::{
ast::{self, AstNode},
match_ast, SyntaxNode,
match_ast, SyntaxKind, SyntaxNode,
};
use crate::RootDatabase;
@ -25,6 +25,8 @@ pub enum Definition {
SelfType(ImplDef),
Local(Local),
TypeParam(TypeParam),
LifetimeParam(LifetimeParam),
// FIXME: Label
}
impl Definition {
@ -36,6 +38,7 @@ impl Definition {
Definition::SelfType(it) => Some(it.module(db)),
Definition::Local(it) => Some(it.module(db)),
Definition::TypeParam(it) => Some(it.module(db)),
Definition::LifetimeParam(it) => Some(it.module(db)),
}
}
@ -47,6 +50,7 @@ impl Definition {
Definition::SelfType(_) => None,
Definition::Local(_) => None,
Definition::TypeParam(_) => None,
Definition::LifetimeParam(_) => None,
}
}
@ -72,6 +76,7 @@ impl Definition {
Definition::SelfType(_) => return None,
Definition::Local(it) => it.name(db)?,
Definition::TypeParam(it) => it.name(db),
Definition::LifetimeParam(it) => it.name(db),
};
Some(name)
}
@ -229,6 +234,25 @@ impl NameClass {
}
}
}
pub fn classify_lifetime(
sema: &Semantics<RootDatabase>,
lifetime: &ast::Lifetime,
) -> Option<NameClass> {
let _p = profile::span("classify_lifetime").detail(|| lifetime.to_string());
let parent = lifetime.syntax().parent()?;
match_ast! {
match parent {
ast::LifetimeParam(it) => {
let def = sema.to_def(&it)?;
Some(NameClass::Definition(Definition::LifetimeParam(def)))
},
ast::Label(_it) => None,
_ => None,
}
}
}
}
#[derive(Debug)]
@ -338,6 +362,35 @@ impl NameRefClass {
let resolved = sema.resolve_extern_crate(&extern_crate)?;
Some(NameRefClass::ExternCrate(resolved))
}
pub fn classify_lifetime(
sema: &Semantics<RootDatabase>,
lifetime: &ast::Lifetime,
) -> Option<NameRefClass> {
let _p = profile::span("classify_lifetime_ref").detail(|| lifetime.to_string());
let parent = lifetime.syntax().parent()?;
match parent.kind() {
SyntaxKind::LIFETIME_ARG
| SyntaxKind::SELF_PARAM
| SyntaxKind::TYPE_BOUND
| SyntaxKind::WHERE_PRED
| SyntaxKind::REF_TYPE => sema
.resolve_lifetime_param(lifetime)
.map(Definition::LifetimeParam)
.map(NameRefClass::Definition),
// lifetime bounds, as in the 'b in 'a: 'b aren't wrapped in TypeBound nodes so we gotta check
// if our lifetime is in a LifetimeParam without being the constrained lifetime
_ if ast::LifetimeParam::cast(parent).and_then(|param| param.lifetime()).as_ref()
!= Some(lifetime) =>
{
sema.resolve_lifetime_param(lifetime)
.map(Definition::LifetimeParam)
.map(NameRefClass::Definition)
}
SyntaxKind::BREAK_EXPR | SyntaxKind::CONTINUE_EXPR => None,
_ => None,
}
}
}
impl From<PathResolution> for Definition {

@ -33,6 +33,7 @@ pub enum ReferenceKind {
RecordFieldExprOrPat,
SelfKw,
EnumLiteral,
Lifetime,
Other,
}
@ -129,6 +130,25 @@ impl Definition {
return SearchScope::new(res);
}
if let Definition::LifetimeParam(param) = self {
let range = match param.parent(db) {
hir::GenericDef::Function(it) => it.source(db).value.syntax().text_range(),
hir::GenericDef::Adt(it) => match it {
hir::Adt::Struct(it) => it.source(db).value.syntax().text_range(),
hir::Adt::Union(it) => it.source(db).value.syntax().text_range(),
hir::Adt::Enum(it) => it.source(db).value.syntax().text_range(),
},
hir::GenericDef::Trait(it) => it.source(db).value.syntax().text_range(),
hir::GenericDef::TypeAlias(it) => it.source(db).value.syntax().text_range(),
hir::GenericDef::ImplDef(it) => it.source(db).value.syntax().text_range(),
hir::GenericDef::EnumVariant(it) => it.source(db).value.syntax().text_range(),
hir::GenericDef::Const(it) => it.source(db).value.syntax().text_range(),
};
let mut res = FxHashMap::default();
res.insert(file_id, Some(range));
return SearchScope::new(res);
}
let vis = self.visibility(db);
if let Some(Visibility::Module(module)) = vis.and_then(|it| it.into()) {
@ -255,25 +275,42 @@ impl<'a> FindUsages<'a> {
continue;
}
match sema.find_node_at_offset_with_descend(&tree, offset) {
Some(name_ref) => {
if self.found_name_ref(&name_ref, sink) {
return;
}
if let Some(name_ref) = sema.find_node_at_offset_with_descend(&tree, offset) {
if self.found_name_ref(&name_ref, sink) {
return;
}
} else if let Some(name) = sema.find_node_at_offset_with_descend(&tree, offset) {
if self.found_name(&name, sink) {
return;
}
} else if let Some(lifetime) = sema.find_node_at_offset_with_descend(&tree, offset)
{
if self.found_lifetime(&lifetime, sink) {
return;
}
None => match sema.find_node_at_offset_with_descend(&tree, offset) {
Some(name) => {
if self.found_name(&name, sink) {
return;
}
}
None => {}
},
}
}
}
}
fn found_lifetime(
&self,
lifetime: &ast::Lifetime,
sink: &mut dyn FnMut(Reference) -> bool,
) -> bool {
match NameRefClass::classify_lifetime(self.sema, lifetime) {
Some(NameRefClass::Definition(def)) if &def == self.def => {
let reference = Reference {
file_range: self.sema.original_range(lifetime.syntax()),
kind: ReferenceKind::Lifetime,
access: None,
};
sink(reference)
}
_ => false, // not a usage
}
}
fn found_name_ref(
&self,
name_ref: &ast::NameRef,

@ -209,8 +209,7 @@ pub fn crate_symbols(db: &RootDatabase, krate: CrateId, query: Query) -> Vec<Fil
query.search(&buf)
}
pub fn index_resolve(db: &RootDatabase, name_ref: &ast::NameRef) -> Vec<FileSymbol> {
let name = name_ref.text();
pub fn index_resolve(db: &RootDatabase, name: &SmolStr) -> Vec<FileSymbol> {
let mut query = Query::new(name.to_string());
query.exact();
query.limit(4);

@ -733,7 +733,7 @@ pub(crate) fn handle_prepare_rename(
let _p = profile::span("handle_prepare_rename");
let position = from_proto::file_position(&snap, params)?;
let change = snap.analysis.rename(position, "dummy")??;
let change = snap.analysis.prepare_rename(position)??;
let line_index = snap.analysis.file_line_index(position.file_id)?;
let range = to_proto::range(&line_index, change.range);
Ok(Some(PrepareRenameResponse::Range(range)))