Auto merge of #12966 - OleStrohm:master, r=Veykril

feat: Display the value of enum variant on hover

fixes #12955

This PR adds const eval support for enums, as well as showing their value on hover, just as consts currently have.

I developed these two things at the same time, but I've realized now that they are separate. However since the hover is just a 10 line change (not including tests), I figured I may as well put them in the same PR. Though if you want them split up into "enum const eval support"  and "show enum variant value on hover", I think that's reasonable too.

Since this adds const eval support for enums this also allows consts that reference enums to have their values computed now too.

The const evaluation itself is quite rudimentary, it doesn't keep track of the actual type of the enum, but it turns out that Rust doesn't actually either, and `E::A as u8` is valid regardless of the `repr` on `E`.

It also doesn't really care about what expression the enum variant contains, it could for example be a string, despite that not being allowed, but I guess it's up to the `cargo check` diagnostics to inform of such issues anyway?
This commit is contained in:
bors 2022-09-20 14:01:16 +00:00
commit 817a6a8609
16 changed files with 366 additions and 16 deletions

View File

@ -27,7 +27,7 @@
macro_id_to_def_id,
nameres::DefMap,
path::{ModPath, Path},
src::HasSource,
src::{HasChildSource, HasSource},
AsMacroCall, BlockId, DefWithBodyId, HasModule, LocalModuleId, Lookup, MacroId, ModuleId,
UnresolvedMacro,
};
@ -324,6 +324,12 @@ pub(crate) fn body_with_source_map_query(
let src = s.source(db);
(src.file_id, s.module(db), src.value.body())
}
DefWithBodyId::VariantId(v) => {
let e = v.parent.lookup(db);
let src = v.parent.child_source(db);
let variant = &src.value[v.local_id];
(src.file_id, e.container, variant.expr())
}
};
let expander = Expander::new(db, file_id, module);
let (mut body, source_map) = Body::new(db, expander, params, body);

View File

@ -2,6 +2,8 @@
use std::fmt::{self, Write};
use syntax::ast::HasName;
use crate::{
expr::{Array, BindingAnnotation, Literal, Statement},
pretty::{print_generic_args, print_path, print_type_ref},
@ -32,6 +34,16 @@ pub(super) fn print_body_hir(db: &dyn DefDatabase, body: &Body, owner: DefWithBo
};
format!("const {} = ", name)
}
DefWithBodyId::VariantId(it) => {
needs_semi = false;
let src = it.parent.child_source(db);
let variant = &src.value[it.local_id];
let name = match &variant.name() {
Some(name) => name.to_string(),
None => "_".to_string(),
};
format!("{}", name)
}
};
let mut p = Printer { body, buf: header, indent_level: 0, needs_indent: false };

View File

@ -474,16 +474,25 @@ pub enum DefWithBodyId {
FunctionId(FunctionId),
StaticId(StaticId),
ConstId(ConstId),
VariantId(EnumVariantId),
}
impl_from!(FunctionId, ConstId, StaticId for DefWithBodyId);
// FIXME: Rename EnumVariantId to VariantId so that the macro above can be used
impl From<EnumVariantId> for DefWithBodyId {
fn from(id: EnumVariantId) -> Self {
DefWithBodyId::VariantId(id)
}
}
impl DefWithBodyId {
pub fn as_generic_def_id(self) -> Option<GenericDefId> {
match self {
DefWithBodyId::FunctionId(f) => Some(f.into()),
DefWithBodyId::StaticId(_) => None,
DefWithBodyId::ConstId(c) => Some(c.into()),
DefWithBodyId::VariantId(c) => Some(c.into()),
}
}
}
@ -681,6 +690,7 @@ fn module(&self, db: &dyn db::DefDatabase) -> ModuleId {
DefWithBodyId::FunctionId(it) => it.lookup(db).module(db),
DefWithBodyId::StaticId(it) => it.lookup(db).module(db),
DefWithBodyId::ConstId(it) => it.lookup(db).module(db),
DefWithBodyId::VariantId(it) => it.parent.lookup(db).container,
}
}
}
@ -691,6 +701,7 @@ pub fn as_mod_item(self, db: &dyn db::DefDatabase) -> ModItem {
DefWithBodyId::FunctionId(it) => it.lookup(db).id.value.into(),
DefWithBodyId::StaticId(it) => it.lookup(db).id.value.into(),
DefWithBodyId::ConstId(it) => it.lookup(db).id.value.into(),
DefWithBodyId::VariantId(it) => it.parent.lookup(db).id.value.into(),
}
}
}

View File

@ -839,6 +839,7 @@ fn resolver(self, db: &dyn DefDatabase) -> Resolver {
DefWithBodyId::ConstId(c) => c.resolver(db),
DefWithBodyId::FunctionId(f) => f.resolver(db),
DefWithBodyId::StaticId(s) => s.resolver(db),
DefWithBodyId::VariantId(v) => v.parent.resolver(db),
}
}
}

View File

@ -7,14 +7,17 @@
use chalk_ir::{BoundVar, DebruijnIndex, GenericArgData, IntTy, Scalar};
use hir_def::{
builtin_type::BuiltinInt,
expr::{ArithOp, BinaryOp, Expr, ExprId, Literal, Pat, PatId},
path::ModPath,
resolver::{resolver_for_expr, ResolveValueResult, Resolver, ValueNs},
src::HasChildSource,
type_ref::ConstScalar,
ConstId, DefWithBodyId,
ConstId, DefWithBodyId, EnumVariantId, Lookup,
};
use la_arena::{Arena, Idx};
use la_arena::{Arena, Idx, RawIdx};
use stdx::never;
use syntax::ast::HasName;
use crate::{
db::HirDatabase, infer::InferenceContext, lower::ParamLoweringMode, to_placeholder_idx,
@ -77,6 +80,7 @@ pub enum ConstEvalError {
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ComputedExpr {
Literal(Literal),
Enum(String, EnumVariantId, Literal),
Tuple(Box<[ComputedExpr]>),
}
@ -104,6 +108,7 @@ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
Literal::String(x) => std::fmt::Debug::fmt(x, f),
Literal::ByteString(x) => std::fmt::Debug::fmt(x, f),
},
ComputedExpr::Enum(name, _, _) => name.fmt(f),
ComputedExpr::Tuple(t) => {
f.write_char('(')?;
for x in &**t {
@ -148,13 +153,47 @@ fn is_valid(scalar: &Scalar, value: i128) -> bool {
}
}
fn get_name(variant: EnumVariantId, ctx: &mut ConstEvalCtx<'_>) -> String {
let loc = variant.parent.lookup(ctx.db.upcast());
let children = variant.parent.child_source(ctx.db.upcast());
let item_tree = loc.id.item_tree(ctx.db.upcast());
let variant_name = children.value[variant.local_id].name();
let enum_name = item_tree[loc.id.value].name.to_string();
enum_name + "::" + &variant_name.unwrap().to_string()
}
pub fn eval_const(
expr_id: ExprId,
ctx: &mut ConstEvalCtx<'_>,
) -> Result<ComputedExpr, ConstEvalError> {
let expr = &ctx.exprs[expr_id];
match expr {
Expr::Missing => Err(ConstEvalError::IncompleteExpr),
Expr::Missing => match ctx.owner {
DefWithBodyId::VariantId(variant) => {
let prev_idx: u32 = variant.local_id.into_raw().into();
let prev_idx = prev_idx.checked_sub(1).map(|idx| Idx::from_raw(RawIdx::from(idx)));
let value = match prev_idx {
Some(prev) => {
let prev_variant = EnumVariantId { local_id: prev, ..variant };
1 + match ctx.db.const_eval_variant(prev_variant)? {
ComputedExpr::Literal(Literal::Int(v, _)) => v,
ComputedExpr::Literal(Literal::Uint(v, _)) => v
.try_into()
.map_err(|_| ConstEvalError::NotSupported("too big u128"))?,
_ => {
return Err(ConstEvalError::NotSupported(
"Enum can't contain this kind of value",
))
}
}
}
_ => 0,
};
Ok(ComputedExpr::Literal(Literal::Int(value, Some(BuiltinInt::I128))))
}
_ => Err(ConstEvalError::IncompleteExpr),
},
Expr::Literal(l) => Ok(ComputedExpr::Literal(l.clone())),
&Expr::UnaryOp { expr, op } => {
let ty = &ctx.expr_ty(expr);
@ -339,9 +378,21 @@ pub fn eval_const(
ValueNs::GenericParam(_) => {
Err(ConstEvalError::NotSupported("const generic without substitution"))
}
ValueNs::EnumVariantId(id) => match ctx.db.const_eval_variant(id)? {
ComputedExpr::Literal(lit) => {
Ok(ComputedExpr::Enum(get_name(id, ctx), id, lit))
}
_ => Err(ConstEvalError::NotSupported(
"Enums can't evalute to anything but numbers",
)),
},
_ => Err(ConstEvalError::NotSupported("path that are not const or local")),
}
}
&Expr::Cast { expr, .. } => match eval_const(expr, ctx)? {
ComputedExpr::Enum(_, _, lit) => Ok(ComputedExpr::Literal(lit)),
_ => Err(ConstEvalError::NotSupported("Can't cast these types")),
},
_ => Err(ConstEvalError::NotSupported("This kind of expression")),
}
}
@ -412,6 +463,14 @@ pub(crate) fn const_eval_recover(
Err(ConstEvalError::Loop)
}
pub(crate) fn const_eval_recover_variant(
_: &dyn HirDatabase,
_: &[String],
_: &EnumVariantId,
) -> Result<ComputedExpr, ConstEvalError> {
Err(ConstEvalError::Loop)
}
pub(crate) fn const_eval_query(
db: &dyn HirDatabase,
const_id: ConstId,
@ -433,6 +492,26 @@ pub(crate) fn const_eval_query(
result
}
pub(crate) fn const_eval_query_variant(
db: &dyn HirDatabase,
variant_id: EnumVariantId,
) -> Result<ComputedExpr, ConstEvalError> {
let def = variant_id.into();
let body = db.body(def);
let infer = &db.infer(def);
eval_const(
body.body_expr,
&mut ConstEvalCtx {
db,
owner: def,
exprs: &body.exprs,
pats: &body.pats,
local_data: HashMap::default(),
infer,
},
)
}
pub(crate) fn eval_to_const<'a>(
expr: Idx<Expr>,
mode: ParamLoweringMode,

View File

@ -87,6 +87,49 @@ fn consts() {
);
}
#[test]
fn enums() {
check_number(
r#"
enum E {
F1 = 1,
F2 = 2 * E::F1 as u8,
F3 = 3 * E::F2 as u8,
}
const GOAL: i32 = E::F3 as u8;
"#,
6,
);
check_number(
r#"
enum E { F1 = 1, F2, }
const GOAL: i32 = E::F2 as u8;
"#,
2,
);
check_number(
r#"
enum E { F1, }
const GOAL: i32 = E::F1 as u8;
"#,
0,
);
let r = eval_goal(
r#"
enum E { A = 1, }
const GOAL: E = E::A;
"#,
)
.unwrap();
match r {
ComputedExpr::Enum(name, _, Literal::Uint(val, _)) => {
assert_eq!(name, "E::A");
assert_eq!(val, 1);
}
x => panic!("Expected enum but found {:?}", x),
}
}
#[test]
fn const_loop() {
check_fail(

View File

@ -6,8 +6,9 @@
use arrayvec::ArrayVec;
use base_db::{impl_intern_key, salsa, CrateId, Upcast};
use hir_def::{
db::DefDatabase, expr::ExprId, BlockId, ConstId, ConstParamId, DefWithBodyId, FunctionId,
GenericDefId, ImplId, LifetimeParamId, LocalFieldId, TypeOrConstParamId, VariantId,
db::DefDatabase, expr::ExprId, BlockId, ConstId, ConstParamId, DefWithBodyId, EnumVariantId,
FunctionId, GenericDefId, ImplId, LifetimeParamId, LocalFieldId, Lookup, TypeOrConstParamId,
VariantId,
};
use la_arena::ArenaMap;
@ -47,6 +48,10 @@ pub trait HirDatabase: DefDatabase + Upcast<dyn DefDatabase> {
#[salsa::cycle(crate::consteval::const_eval_recover)]
fn const_eval(&self, def: ConstId) -> Result<ComputedExpr, ConstEvalError>;
#[salsa::invoke(crate::consteval::const_eval_query_variant)]
#[salsa::cycle(crate::consteval::const_eval_recover_variant)]
fn const_eval_variant(&self, def: EnumVariantId) -> Result<ComputedExpr, ConstEvalError>;
#[salsa::invoke(crate::lower::impl_trait_query)]
fn impl_trait(&self, def: ImplId) -> Option<Binders<TraitRef>>;
@ -188,6 +193,13 @@ fn infer_wait(db: &dyn HirDatabase, def: DefWithBodyId) -> Arc<InferenceResult>
DefWithBodyId::ConstId(it) => {
db.const_data(it).name.clone().unwrap_or_else(Name::missing).to_string()
}
DefWithBodyId::VariantId(it) => {
let up_db: &dyn DefDatabase = db.upcast();
let loc = it.parent.lookup(up_db);
let item_tree = loc.id.item_tree(up_db);
let konst = &item_tree[loc.id.value];
konst.name.to_string()
}
});
db.infer_query(def)
}

View File

@ -18,7 +18,9 @@ pub fn missing_unsafe(db: &dyn HirDatabase, def: DefWithBodyId) -> Vec<ExprId> {
let is_unsafe = match def {
DefWithBodyId::FunctionId(it) => db.function_data(it).has_unsafe_kw(),
DefWithBodyId::StaticId(_) | DefWithBodyId::ConstId(_) => false,
DefWithBodyId::StaticId(_) | DefWithBodyId::ConstId(_) | DefWithBodyId::VariantId(_) => {
false
}
};
if is_unsafe {
return res;

View File

@ -67,6 +67,10 @@ pub(crate) fn infer_query(db: &dyn HirDatabase, def: DefWithBodyId) -> Arc<Infer
DefWithBodyId::ConstId(c) => ctx.collect_const(&db.const_data(c)),
DefWithBodyId::FunctionId(f) => ctx.collect_fn(f),
DefWithBodyId::StaticId(s) => ctx.collect_static(&db.static_data(s)),
DefWithBodyId::VariantId(v) => {
// FIXME: This should return the `repr(...)` type of the enum
ctx.return_ty = TyBuilder::def_ty(db, v.parent.into()).fill_with_unknown().build()
}
}
ctx.infer_body();

View File

@ -16,7 +16,7 @@
use expect_test::Expect;
use hir_def::{
body::{Body, BodySourceMap, SyntheticSyntax},
db::DefDatabase,
db::{DefDatabase, InternDatabase},
expr::{ExprId, PatId},
item_scope::ItemScope,
nameres::DefMap,
@ -135,6 +135,10 @@ fn check_impl(ra_fixture: &str, allow_none: bool, only_types: bool, display_sour
let loc = it.lookup(&db);
loc.source(&db).value.syntax().text_range().start()
}
DefWithBodyId::VariantId(it) => {
let loc = db.lookup_intern_enum(it.parent);
loc.source(&db).value.syntax().text_range().start()
}
});
let mut unexpected_type_mismatches = String::new();
for def in defs {
@ -388,6 +392,10 @@ fn infer_with_mismatches(content: &str, include_mismatches: bool) -> String {
let loc = it.lookup(&db);
loc.source(&db).value.syntax().text_range().start()
}
DefWithBodyId::VariantId(it) => {
let loc = db.lookup_intern_enum(it.parent);
loc.source(&db).value.syntax().text_range().start()
}
});
for def in defs {
let (_body, source_map) = db.body_with_source_map(def);

View File

@ -140,6 +140,7 @@ fn from(def: DefWithBody) -> Self {
DefWithBody::Function(it) => DefWithBodyId::FunctionId(it.id),
DefWithBody::Static(it) => DefWithBodyId::StaticId(it.id),
DefWithBody::Const(it) => DefWithBodyId::ConstId(it.id),
DefWithBody::Variant(it) => DefWithBodyId::VariantId(it.into()),
}
}
}
@ -150,6 +151,7 @@ fn from(def: DefWithBodyId) -> Self {
DefWithBodyId::FunctionId(it) => DefWithBody::Function(it.into()),
DefWithBodyId::StaticId(it) => DefWithBody::Static(it.into()),
DefWithBodyId::ConstId(it) => DefWithBody::Const(it.into()),
DefWithBodyId::VariantId(it) => DefWithBody::Variant(it.into()),
}
}
}
@ -172,9 +174,7 @@ fn from(def: GenericDef) -> Self {
GenericDef::Trait(it) => GenericDefId::TraitId(it.id),
GenericDef::TypeAlias(it) => GenericDefId::TypeAliasId(it.id),
GenericDef::Impl(it) => GenericDefId::ImplId(it.id),
GenericDef::Variant(it) => {
GenericDefId::EnumVariantId(EnumVariantId { parent: it.parent.id, local_id: it.id })
}
GenericDef::Variant(it) => GenericDefId::EnumVariantId(it.into()),
GenericDef::Const(it) => GenericDefId::ConstId(it.id),
}
}
@ -188,9 +188,7 @@ fn from(def: GenericDefId) -> Self {
GenericDefId::TraitId(it) => GenericDef::Trait(it.into()),
GenericDefId::TypeAliasId(it) => GenericDef::TypeAlias(it.into()),
GenericDefId::ImplId(it) => GenericDef::Impl(it.into()),
GenericDefId::EnumVariantId(it) => {
GenericDef::Variant(Variant { parent: it.parent.into(), id: it.local_id })
}
GenericDefId::EnumVariantId(it) => GenericDef::Variant(it.into()),
GenericDefId::ConstId(it) => GenericDef::Const(it.into()),
}
}

View File

@ -73,7 +73,7 @@
use rustc_hash::FxHashSet;
use stdx::{impl_from, never};
use syntax::{
ast::{self, HasAttrs as _, HasDocComments, HasName},
ast::{self, Expr, HasAttrs as _, HasDocComments, HasName},
AstNode, AstPtr, SmolStr, SyntaxNodePtr, TextRange, T,
};
@ -952,6 +952,10 @@ pub fn variants(self, db: &dyn HirDatabase) -> Vec<Variant> {
pub fn ty(self, db: &dyn HirDatabase) -> Type {
Type::from_def(db, self.id)
}
pub fn is_data_carrying(self, db: &dyn HirDatabase) -> bool {
self.variants(db).iter().any(|v| !matches!(v.kind(db), StructKind::Unit))
}
}
impl HasVisibility for Enum {
@ -960,6 +964,12 @@ fn visibility(&self, db: &dyn HirDatabase) -> Visibility {
}
}
impl From<&Variant> for DefWithBodyId {
fn from(&v: &Variant) -> Self {
DefWithBodyId::VariantId(v.into())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Variant {
pub(crate) parent: Enum,
@ -994,6 +1004,14 @@ pub fn kind(self, db: &dyn HirDatabase) -> StructKind {
pub(crate) fn variant_data(self, db: &dyn HirDatabase) -> Arc<VariantData> {
db.enum_data(self.parent.id).variants[self.id].variant_data.clone()
}
pub fn value(self, db: &dyn HirDatabase) -> Option<Expr> {
self.source(db)?.value.expr()
}
pub fn eval(self, db: &dyn HirDatabase) -> Result<ComputedExpr, ConstEvalError> {
db.const_eval_variant(self.into())
}
}
/// Variants inherit visibility from the parent enum.
@ -1129,6 +1147,7 @@ pub enum DefWithBody {
Function(Function),
Static(Static),
Const(Const),
Variant(Variant),
}
impl_from!(Function, Const, Static for DefWithBody);
@ -1138,6 +1157,7 @@ pub fn module(self, db: &dyn HirDatabase) -> Module {
DefWithBody::Const(c) => c.module(db),
DefWithBody::Function(f) => f.module(db),
DefWithBody::Static(s) => s.module(db),
DefWithBody::Variant(v) => v.module(db),
}
}
@ -1146,6 +1166,7 @@ pub fn name(self, db: &dyn HirDatabase) -> Option<Name> {
DefWithBody::Function(f) => Some(f.name(db)),
DefWithBody::Static(s) => Some(s.name(db)),
DefWithBody::Const(c) => c.name(db),
DefWithBody::Variant(v) => Some(v.name(db)),
}
}
@ -1155,6 +1176,7 @@ pub fn body_type(self, db: &dyn HirDatabase) -> Type {
DefWithBody::Function(it) => it.ret_type(db),
DefWithBody::Static(it) => it.ty(db),
DefWithBody::Const(it) => it.ty(db),
DefWithBody::Variant(it) => it.parent.ty(db),
}
}
@ -1163,6 +1185,7 @@ fn id(&self) -> DefWithBodyId {
DefWithBody::Function(it) => it.id.into(),
DefWithBody::Static(it) => it.id.into(),
DefWithBody::Const(it) => it.id.into(),
DefWithBody::Variant(it) => it.into(),
}
}
@ -1379,6 +1402,7 @@ pub fn diagnostics(self, db: &dyn HirDatabase, acc: &mut Vec<AnyDiagnostic>) {
DefWithBody::Function(it) => it.into(),
DefWithBody::Static(it) => it.into(),
DefWithBody::Const(it) => it.into(),
DefWithBody::Variant(it) => it.into(),
};
for diag in hir_ty::diagnostics::incorrect_case(db, krate, def.into()) {
acc.push(diag.into())

View File

@ -244,6 +244,10 @@ fn def_with_body_id_name(&self, body_id: DefWithBodyId) -> Option<SmolStr> {
DefWithBodyId::ConstId(id) => Some(
id.lookup(self.db.upcast()).source(self.db.upcast()).value.name()?.text().into(),
),
DefWithBodyId::VariantId(id) => Some({
let db = self.db.upcast();
id.parent.lookup(db).source(db).value.name()?.text().into()
}),
}
}

View File

@ -239,6 +239,7 @@ fn search_scope(&self, db: &RootDatabase) -> SearchScope {
DefWithBody::Function(f) => f.source(db).map(|src| src.syntax().cloned()),
DefWithBody::Const(c) => c.source(db).map(|src| src.syntax().cloned()),
DefWithBody::Static(s) => s.source(db).map(|src| src.syntax().cloned()),
DefWithBody::Variant(v) => v.source(db).map(|src| src.syntax().cloned()),
};
return match def {
Some(def) => SearchScope::file_range(def.as_ref().original_file_range(db)),

View File

@ -346,7 +346,16 @@ pub(super) fn definition(
Definition::Module(it) => label_and_docs(db, it),
Definition::Function(it) => label_and_docs(db, it),
Definition::Adt(it) => label_and_docs(db, it),
Definition::Variant(it) => label_and_docs(db, it),
Definition::Variant(it) => label_value_and_docs(db, it, |&it| {
if !it.parent_enum(db).is_data_carrying(db) {
match it.eval(db) {
Ok(x) => Some(format!("{}", x)),
Err(_) => it.value(db).map(|x| format!("{:?}", x)),
}
} else {
None
}
}),
Definition::Const(it) => label_value_and_docs(db, it, |it| {
let body = it.eval(db);
match body {

View File

@ -698,6 +698,7 @@ fn hover_enum_variant() {
check(
r#"
enum Option<T> {
Some(T)
/// The None variant
Non$0e
}
@ -3527,6 +3528,112 @@ impl<const LEN: usize> Foo<LEN$0> {}
);
}
#[test]
fn hover_const_eval_variant() {
// show hex for <10
check(
r#"
#[repr(u8)]
enum E {
/// This is a doc
A$0 = 1 << 3,
}
"#,
expect![[r#"
*A*
```rust
test::E
```
```rust
A = 8
```
---
This is a doc
"#]],
);
// show hex for >10
check(
r#"
#[repr(u8)]
enum E {
/// This is a doc
A$0 = (1 << 3) + (1 << 2),
}
"#,
expect![[r#"
*A*
```rust
test::E
```
```rust
A = 12 (0xC)
```
---
This is a doc
"#]],
);
// enums in const eval
check(
r#"
#[repr(u8)]
enum E {
A = 1,
/// This is a doc
B$0 = E::A as u8 + 1,
}
"#,
expect![[r#"
*B*
```rust
test::E
```
```rust
B = 2
```
---
This is a doc
"#]],
);
// unspecified variant should increment by one
check(
r#"
#[repr(u8)]
enum E {
A = 4,
/// This is a doc
B$0,
}
"#,
expect![[r#"
*B*
```rust
test::E
```
```rust
B = 5
```
---
This is a doc
"#]],
);
}
#[test]
fn hover_const_eval() {
// show hex for <10
@ -3820,6 +3927,35 @@ fn foo() {
---
This is a doc
"#]],
);
check(
r#"
enum E {
/// This is a doc
A = 3,
}
fn foo(e: E) {
match e {
E::A$0 => (),
_ => ()
}
}
"#,
expect![[r#"
*A*
```rust
test::E
```
```rust
A = 3
```
---
This is a doc
"#]],
);