rust-analyzer/crates/ide-db/src/traits.rs
Chayim Refael Friedman 9d3368f2c2 Properly account for editions in names
This PR touches a lot of parts. But the main changes are changing
`hir_expand::Name` to be raw edition-dependently and only when necessary
(unrelated to how the user originally wrote the identifier),
and changing `is_keyword()` and `is_raw_identifier()` to be edition-aware
(this was done in #17896, but the FIXMEs were fixed here).

It is possible that I missed some cases, but most IDE parts should properly
escape (or not escape) identifiers now.

The rules of thumb are:

 - If we show the identifier to the user, its rawness should be determined
   by the edition of the edited crate. This is nice for IDE features,
   but really important for changes we insert to the source code.
 - For tests, I chose `Edition::CURRENT` (so we only have to (maybe) update
   tests when an edition becomes stable, to avoid churn).
 - For debugging tools (helper methods and logs), I used `Edition::LATEST`.
2024-08-16 16:46:24 +03:00

277 lines
7.8 KiB
Rust

//! Functionality for obtaining data related to traits from the DB.
use crate::{defs::Definition, RootDatabase};
use hir::{db::HirDatabase, AsAssocItem, Semantics};
use rustc_hash::FxHashSet;
use syntax::{ast, AstNode};
/// Given the `impl` block, attempts to find the trait this `impl` corresponds to.
pub fn resolve_target_trait(
sema: &Semantics<'_, RootDatabase>,
impl_def: &ast::Impl,
) -> Option<hir::Trait> {
let ast_path =
impl_def.trait_().map(|it| it.syntax().clone()).and_then(ast::PathType::cast)?.path()?;
match sema.resolve_path(&ast_path) {
Some(hir::PathResolution::Def(hir::ModuleDef::Trait(def))) => Some(def),
_ => None,
}
}
/// Given the `impl` block, returns the list of associated items (e.g. functions or types) that are
/// missing in this `impl` block.
pub fn get_missing_assoc_items(
sema: &Semantics<'_, RootDatabase>,
impl_def: &ast::Impl,
) -> Vec<hir::AssocItem> {
let imp = match sema.to_def(impl_def) {
Some(it) => it,
None => return vec![],
};
// Names must be unique between constants and functions. However, type aliases
// may share the same name as a function or constant.
let mut impl_fns_consts = FxHashSet::default();
let mut impl_type = FxHashSet::default();
let edition = imp.module(sema.db).krate().edition(sema.db);
for item in imp.items(sema.db) {
match item {
hir::AssocItem::Function(it) => {
impl_fns_consts.insert(it.name(sema.db).display(sema.db, edition).to_string());
}
hir::AssocItem::Const(it) => {
if let Some(name) = it.name(sema.db) {
impl_fns_consts.insert(name.display(sema.db, edition).to_string());
}
}
hir::AssocItem::TypeAlias(it) => {
impl_type.insert(it.name(sema.db).display(sema.db, edition).to_string());
}
}
}
resolve_target_trait(sema, impl_def).map_or(vec![], |target_trait| {
target_trait
.items(sema.db)
.into_iter()
.filter(|i| match i {
hir::AssocItem::Function(f) => !impl_fns_consts
.contains(&f.name(sema.db).display(sema.db, edition).to_string()),
hir::AssocItem::TypeAlias(t) => {
!impl_type.contains(&t.name(sema.db).display(sema.db, edition).to_string())
}
hir::AssocItem::Const(c) => c
.name(sema.db)
.map(|n| !impl_fns_consts.contains(&n.display(sema.db, edition).to_string()))
.unwrap_or_default(),
})
.collect()
})
}
/// Converts associated trait impl items to their trait definition counterpart
pub(crate) fn convert_to_def_in_trait(db: &dyn HirDatabase, def: Definition) -> Definition {
(|| {
let assoc = def.as_assoc_item(db)?;
let trait_ = assoc.implemented_trait(db)?;
assoc_item_of_trait(db, assoc, trait_)
})()
.unwrap_or(def)
}
/// If this is an trait (impl) assoc item, returns the assoc item of the corresponding trait definition.
pub(crate) fn as_trait_assoc_def(db: &dyn HirDatabase, def: Definition) -> Option<Definition> {
let assoc = def.as_assoc_item(db)?;
let trait_ = match assoc.container(db) {
hir::AssocItemContainer::Trait(_) => return Some(def),
hir::AssocItemContainer::Impl(i) => i.trait_(db),
}?;
assoc_item_of_trait(db, assoc, trait_)
}
fn assoc_item_of_trait(
db: &dyn HirDatabase,
assoc: hir::AssocItem,
trait_: hir::Trait,
) -> Option<Definition> {
use hir::AssocItem::*;
let name = match assoc {
Function(it) => it.name(db),
Const(it) => it.name(db)?,
TypeAlias(it) => it.name(db),
};
let item = trait_.items(db).into_iter().find(|it| match (it, assoc) {
(Function(trait_func), Function(_)) => trait_func.name(db) == name,
(Const(trait_konst), Const(_)) => trait_konst.name(db).map_or(false, |it| it == name),
(TypeAlias(trait_type_alias), TypeAlias(_)) => trait_type_alias.name(db) == name,
_ => false,
})?;
Some(Definition::from(item))
}
#[cfg(test)]
mod tests {
use expect_test::{expect, Expect};
use hir::FilePosition;
use hir::Semantics;
use span::Edition;
use syntax::ast::{self, AstNode};
use test_fixture::ChangeFixture;
use crate::RootDatabase;
/// Creates analysis from a multi-file fixture, returns positions marked with $0.
pub(crate) fn position(ra_fixture: &str) -> (RootDatabase, FilePosition) {
let change_fixture = ChangeFixture::parse(ra_fixture);
let mut database = RootDatabase::default();
database.apply_change(change_fixture.change);
let (file_id, range_or_offset) =
change_fixture.file_position.expect("expected a marker ($0)");
let offset = range_or_offset.expect_offset();
(database, FilePosition { file_id, offset })
}
fn check_trait(ra_fixture: &str, expect: Expect) {
let (db, position) = position(ra_fixture);
let sema = Semantics::new(&db);
let file = sema.parse(position.file_id);
let impl_block: ast::Impl =
sema.find_node_at_offset_with_descend(file.syntax(), position.offset).unwrap();
let trait_ = crate::traits::resolve_target_trait(&sema, &impl_block);
let actual = match trait_ {
Some(trait_) => trait_.name(&db).display(&db, Edition::CURRENT).to_string(),
None => String::new(),
};
expect.assert_eq(&actual);
}
fn check_missing_assoc(ra_fixture: &str, expect: Expect) {
let (db, position) = position(ra_fixture);
let sema = Semantics::new(&db);
let file = sema.parse(position.file_id);
let impl_block: ast::Impl =
sema.find_node_at_offset_with_descend(file.syntax(), position.offset).unwrap();
let items = crate::traits::get_missing_assoc_items(&sema, &impl_block);
let actual = items
.into_iter()
.map(|item| item.name(&db).unwrap().display(&db, Edition::CURRENT).to_string())
.collect::<Vec<_>>()
.join("\n");
expect.assert_eq(&actual);
}
#[test]
fn resolve_trait() {
check_trait(
r#"
pub trait Foo {
fn bar();
}
impl Foo for u8 {
$0
}
"#,
expect![["Foo"]],
);
check_trait(
r#"
pub trait Foo {
fn bar();
}
impl Foo for u8 {
fn bar() {
fn baz() {
$0
}
baz();
}
}
"#,
expect![["Foo"]],
);
check_trait(
r#"
pub trait Foo {
fn bar();
}
pub struct Bar;
impl Bar {
$0
}
"#,
expect![[""]],
);
}
#[test]
fn missing_assoc_items() {
check_missing_assoc(
r#"
pub trait Foo {
const FOO: u8;
fn bar();
}
impl Foo for u8 {
$0
}"#,
expect![[r#"
FOO
bar"#]],
);
check_missing_assoc(
r#"
pub trait Foo {
const FOO: u8;
fn bar();
}
impl Foo for u8 {
const FOO: u8 = 10;
$0
}"#,
expect![[r#"
bar"#]],
);
check_missing_assoc(
r#"
pub trait Foo {
const FOO: u8;
fn bar();
}
impl Foo for u8 {
const FOO: u8 = 10;
fn bar() {$0}
}"#,
expect![[r#""#]],
);
check_missing_assoc(
r#"
pub struct Foo;
impl Foo {
fn bar() {$0}
}"#,
expect![[r#""#]],
);
check_missing_assoc(
r#"
trait Tr {
fn required();
}
macro_rules! m {
() => { fn required() {} };
}
impl Tr for () {
m!();
$0
}
"#,
expect![[r#""#]],
);
}
}