Skip to content

feat: implement attribute completions for diagnostics module #19908

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 54 additions & 23 deletions crates/ide-completion/src/completions/attribute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ use crate::{

mod cfg;
mod derive;
mod diagnostic;
mod lint;
mod macro_use;
mod repr;
Expand All @@ -40,23 +41,22 @@ pub(crate) fn complete_known_attribute_input(
extern_crate: Option<&ast::ExternCrate>,
) -> Option<()> {
let attribute = fake_attribute_under_caret;
let name_ref = match attribute.path() {
Some(p) => Some(p.as_single_name_ref()?),
None => None,
};
let (path, tt) = name_ref.zip(attribute.token_tree())?;
tt.l_paren_token()?;
let path = attribute.path()?;
let segments = path.segments().map(|s| s.name_ref()).collect::<Option<Vec<_>>>()?;
let segments = segments.iter().map(|n| n.text()).collect::<Vec<_>>();
let segments = segments.iter().map(|t| t.as_str()).collect::<Vec<_>>();
let tt = attribute.token_tree()?;

match path.text().as_str() {
"repr" => repr::complete_repr(acc, ctx, tt),
"feature" => lint::complete_lint(
match segments.as_slice() {
["repr"] => repr::complete_repr(acc, ctx, tt),
["feature"] => lint::complete_lint(
acc,
ctx,
colon_prefix,
&parse_tt_as_comma_sep_paths(tt, ctx.edition)?,
FEATURES,
),
"allow" | "expect" | "deny" | "forbid" | "warn" => {
["allow"] | ["expect"] | ["deny"] | ["forbid"] | ["warn"] => {
let existing_lints = parse_tt_as_comma_sep_paths(tt, ctx.edition)?;

let lints: Vec<Lint> = CLIPPY_LINT_GROUPS
Expand All @@ -70,13 +70,14 @@ pub(crate) fn complete_known_attribute_input(

lint::complete_lint(acc, ctx, colon_prefix, &existing_lints, &lints);
}
"cfg" => cfg::complete_cfg(acc, ctx),
"macro_use" => macro_use::complete_macro_use(
["cfg"] => cfg::complete_cfg(acc, ctx),
["macro_use"] => macro_use::complete_macro_use(
acc,
ctx,
extern_crate,
&parse_tt_as_comma_sep_paths(tt, ctx.edition)?,
),
["diagnostic", "on_unimplemented"] => diagnostic::complete_on_unimplemented(acc, ctx, tt),
_ => (),
}
Some(())
Expand Down Expand Up @@ -139,6 +140,8 @@ pub(crate) fn complete_attribute_path(
}
Qualified::TypeAnchor { .. } | Qualified::With { .. } => {}
}
let qualifier_path =
if let Qualified::With { path, .. } = qualified { Some(path) } else { None };

let attributes = annotated_item_kind.and_then(|kind| {
if ast::Expr::can_cast(kind) {
Expand All @@ -149,18 +152,33 @@ pub(crate) fn complete_attribute_path(
});

let add_completion = |attr_completion: &AttrCompletion| {
let mut item = CompletionItem::new(
SymbolKind::Attribute,
ctx.source_range(),
attr_completion.label,
ctx.edition,
);
// if we don't already have the qualifiers of the completion, then
// add the missing parts to the label and snippet
let mut label = attr_completion.label.to_owned();
let mut snippet = attr_completion.snippet.map(|s| s.to_owned());
let segments = qualifier_path.iter().flat_map(|q| q.segments()).collect::<Vec<_>>();
let qualifiers = attr_completion.qualifiers;
let matching_qualifiers = segments
.iter()
.zip(qualifiers)
.take_while(|(s, q)| s.name_ref().is_some_and(|t| t.text() == **q))
.count();
if matching_qualifiers != qualifiers.len() {
let prefix = qualifiers[matching_qualifiers..].join("::");
label = format!("{prefix}::{label}");
if let Some(s) = snippet.as_mut() {
*s = format!("{prefix}::{s}");
}
}

let mut item =
CompletionItem::new(SymbolKind::Attribute, ctx.source_range(), label, ctx.edition);

if let Some(lookup) = attr_completion.lookup {
item.lookup_by(lookup);
}

if let Some((snippet, cap)) = attr_completion.snippet.zip(ctx.config.snippet_cap) {
if let Some((snippet, cap)) = snippet.zip(ctx.config.snippet_cap) {
item.insert_snippet(cap, snippet);
}

Expand All @@ -184,6 +202,7 @@ struct AttrCompletion {
label: &'static str,
lookup: Option<&'static str>,
snippet: Option<&'static str>,
qualifiers: &'static [&'static str],
prefer_inner: bool,
}

Expand All @@ -192,6 +211,10 @@ impl AttrCompletion {
self.lookup.unwrap_or(self.label)
}

const fn qualifiers(self, qualifiers: &'static [&'static str]) -> AttrCompletion {
AttrCompletion { qualifiers, ..self }
}

const fn prefer_inner(self) -> AttrCompletion {
AttrCompletion { prefer_inner: true, ..self }
}
Expand All @@ -202,7 +225,7 @@ const fn attr(
lookup: Option<&'static str>,
snippet: Option<&'static str>,
) -> AttrCompletion {
AttrCompletion { label, lookup, snippet, prefer_inner: false }
AttrCompletion { label, lookup, snippet, qualifiers: &[], prefer_inner: false }
}

macro_rules! attrs {
Expand Down Expand Up @@ -264,14 +287,14 @@ static KIND_TO_ATTRIBUTES: LazyLock<FxHashMap<SyntaxKind, &[&str]>> = LazyLock::
FN,
attrs!(
item, linkable,
"cold", "ignore", "inline", "must_use", "panic_handler", "proc_macro",
"cold", "ignore", "inline", "panic_handler", "proc_macro",
"proc_macro_derive", "proc_macro_attribute", "should_panic", "target_feature",
"test", "track_caller"
),
),
(STATIC, attrs!(item, linkable, "global_allocator", "used")),
(TRAIT, attrs!(item, "must_use")),
(IMPL, attrs!(item, "automatically_derived")),
(TRAIT, attrs!(item, "diagnostic::on_unimplemented")),
(IMPL, attrs!(item, "automatically_derived", "diagnostic::do_not_recommend")),
(ASSOC_ITEM_LIST, attrs!(item)),
(EXTERN_BLOCK, attrs!(item, "link")),
(EXTERN_ITEM_LIST, attrs!(item, "link")),
Expand Down Expand Up @@ -311,6 +334,14 @@ const ATTRIBUTES: &[AttrCompletion] = &[
attr("deny(…)", Some("deny"), Some("deny(${0:lint})")),
attr(r#"deprecated"#, Some("deprecated"), Some(r#"deprecated"#)),
attr("derive(…)", Some("derive"), Some(r#"derive(${0:Debug})"#)),
attr("do_not_recommend", Some("diagnostic::do_not_recommend"), None)
.qualifiers(&["diagnostic"]),
attr(
"on_unimplemented",
Some("diagnostic::on_unimplemented"),
Some(r#"on_unimplemented(${0:keys})"#),
)
.qualifiers(&["diagnostic"]),
attr(r#"doc = "…""#, Some("doc"), Some(r#"doc = "${0:docs}""#)),
attr(r#"doc(alias = "…")"#, Some("docalias"), Some(r#"doc(alias = "${0:docs}")"#)),
attr(r#"doc(hidden)"#, Some("dochidden"), Some(r#"doc(hidden)"#)),
Expand Down
60 changes: 60 additions & 0 deletions crates/ide-completion/src/completions/attribute/diagnostic.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
//! Completion for diagnostic attributes.

use ide_db::SymbolKind;
use syntax::ast;

use crate::{CompletionItem, Completions, context::CompletionContext};

use super::AttrCompletion;

pub(super) fn complete_on_unimplemented(
acc: &mut Completions,
ctx: &CompletionContext<'_>,
input: ast::TokenTree,
) {
if let Some(existing_keys) = super::parse_comma_sep_expr(input) {
for attr in ATTRIBUTE_ARGS {
let already_annotated = existing_keys
.iter()
.filter_map(|expr| match expr {
ast::Expr::PathExpr(path) => path.path()?.as_single_name_ref(),
ast::Expr::BinExpr(bin)
if bin.op_kind() == Some(ast::BinaryOp::Assignment { op: None }) =>
{
match bin.lhs()? {
ast::Expr::PathExpr(path) => path.path()?.as_single_name_ref(),
_ => None,
}
}
_ => None,
})
.any(|it| {
let text = it.text();
attr.key() == text && text != "note"
});
if already_annotated {
continue;
}

let mut item = CompletionItem::new(
SymbolKind::BuiltinAttr,
ctx.source_range(),
attr.label,
ctx.edition,
);
if let Some(lookup) = attr.lookup {
item.lookup_by(lookup);
}
if let Some((snippet, cap)) = attr.snippet.zip(ctx.config.snippet_cap) {
item.insert_snippet(cap, snippet);
}
item.add_to(acc, ctx.db);
}
}
}

const ATTRIBUTE_ARGS: &[AttrCompletion] = &[
super::attr(r#"label = "…""#, Some("label"), Some(r#"label = "${0:label}""#)),
super::attr(r#"message = "…""#, Some("message"), Some(r#"message = "${0:message}""#)),
super::attr(r#"note = "…""#, Some("note"), Some(r#"note = "${0:note}""#)),
];
78 changes: 76 additions & 2 deletions crates/ide-completion/src/tests/attribute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ pub struct Foo(#[m$0] i32);
at deprecated
at derive macro derive
at derive(…)
at diagnostic::do_not_recommend
at diagnostic::on_unimplemented
at doc = "…"
at doc(alias = "…")
at doc(hidden)
Expand Down Expand Up @@ -472,13 +474,13 @@ fn attr_on_trait() {
at cfg_attr(…)
at deny(…)
at deprecated
at diagnostic::on_unimplemented
at doc = "…"
at doc(alias = "…")
at doc(hidden)
at expect(…)
at forbid(…)
at must_use
at must_use
at no_mangle
at warn(…)
kw crate::
Expand All @@ -498,6 +500,7 @@ fn attr_on_impl() {
at cfg_attr(…)
at deny(…)
at deprecated
at diagnostic::do_not_recommend
at doc = "…"
at doc(alias = "…")
at doc(hidden)
Expand Down Expand Up @@ -532,6 +535,76 @@ fn attr_on_impl() {
);
}

#[test]
fn attr_with_qualifier() {
check(
r#"#[diagnostic::$0] impl () {}"#,
expect![[r#"
at allow(…)
at automatically_derived
at cfg(…)
at cfg_attr(…)
at deny(…)
at deprecated
at do_not_recommend
at doc = "…"
at doc(alias = "…")
at doc(hidden)
at expect(…)
at forbid(…)
at must_use
at no_mangle
at warn(…)
"#]],
);
check(
r#"#[diagnostic::$0] trait Foo {}"#,
expect![[r#"
at allow(…)
at cfg(…)
at cfg_attr(…)
at deny(…)
at deprecated
at doc = "…"
at doc(alias = "…")
at doc(hidden)
at expect(…)
at forbid(…)
at must_use
at no_mangle
at on_unimplemented
at warn(…)
"#]],
);
}

#[test]
fn attr_diagnostic_on_unimplemented() {
check(
r#"#[diagnostic::on_unimplemented($0)] trait Foo {}"#,
expect![[r#"
ba label = "…"
ba message = "…"
ba note = "…"
"#]],
);
check(
r#"#[diagnostic::on_unimplemented(message = "foo", $0)] trait Foo {}"#,
expect![[r#"
ba label = "…"
ba note = "…"
"#]],
);
check(
r#"#[diagnostic::on_unimplemented(note = "foo", $0)] trait Foo {}"#,
expect![[r#"
ba label = "…"
ba message = "…"
ba note = "…"
"#]],
);
}

#[test]
fn attr_on_extern_block() {
check(
Expand Down Expand Up @@ -619,7 +692,6 @@ fn attr_on_fn() {
at link_name = "…"
at link_section = "…"
at must_use
at must_use
at no_mangle
at panic_handler
at proc_macro
Expand Down Expand Up @@ -649,6 +721,8 @@ fn attr_in_source_file_end() {
at deny(…)
at deprecated
at derive(…)
at diagnostic::do_not_recommend
at diagnostic::on_unimplemented
at doc = "…"
at doc(alias = "…")
at doc(hidden)
Expand Down