Auto merge of #153838 - oli-obk:use-tree-span, r=davidtwco

Use fine grained component-wise span tracking in use trees

This often produces nicer spans and even doesn't need a Span field anymore (not that I expect the unused field to affect any perf, but still neat).
This commit is contained in:
bors
2026-04-08 20:09:27 +00:00
21 changed files with 115 additions and 75 deletions
+24 -2
View File
@@ -3338,7 +3338,7 @@ pub enum UseTreeKind {
/// ```
Nested { items: ThinVec<(UseTree, NodeId)>, span: Span },
/// `use prefix::*`
Glob,
Glob(Span),
}
/// A tree of paths sharing common prefixes.
@@ -3347,10 +3347,11 @@ pub enum UseTreeKind {
pub struct UseTree {
pub prefix: Path,
pub kind: UseTreeKind,
pub span: Span,
}
impl UseTree {
/// If the `UseTree` is just an identifier, return that.
/// Panics if it's a glob (`*`) or a nested use tree.
pub fn ident(&self) -> Ident {
match self.kind {
UseTreeKind::Simple(Some(rename)) => rename,
@@ -3360,6 +3361,27 @@ pub fn ident(&self) -> Ident {
_ => panic!("`UseTree::ident` can only be used on a simple import"),
}
}
/// Returns the full span from the start of the path to the
/// closing `}` or nested spans, `*` of glob spans or the end of the
/// identifier of simple spans.
pub fn span(&self) -> Span {
self.prefix.span.to(self.hi_span())
}
/// Returns the trailing element's span. So for a nested
/// span you get the entire `{}`-block, for a glob you
/// get the span of the `*` itself, and for simple use trees
/// you get the identifier to rename the import to or the full
/// path if no rename is specified.
pub fn hi_span(&self) -> Span {
match self.kind {
UseTreeKind::Simple(None) => self.prefix.span,
UseTreeKind::Simple(Some(name)) => name.span,
UseTreeKind::Nested { span, .. } => span,
UseTreeKind::Glob(span) => span,
}
}
}
/// Distinguishes between `Attribute`s that decorate items and Attributes that
+8 -4
View File
@@ -153,7 +153,7 @@ pub(super) fn lower_mod(
self.lower_item_id_use_tree(nested, vec);
}
}
UseTreeKind::Simple(..) | UseTreeKind::Glob => {}
UseTreeKind::Simple(..) | UseTreeKind::Glob(_) => {}
}
}
@@ -287,7 +287,11 @@ fn lower_item_kind(
}
ItemKind::Use(use_tree) => {
// Start with an empty prefix.
let prefix = Path { segments: ThinVec::new(), span: use_tree.span, tokens: None };
let prefix = Path {
segments: ThinVec::new(),
span: use_tree.prefix.span.shrink_to_lo(),
tokens: None,
};
self.lower_use_tree(use_tree, &prefix, id, vis_span, attrs)
}
@@ -659,7 +663,7 @@ fn lower_use_tree(
let ident = self.lower_ident(ident);
hir::ItemKind::Use(path, hir::UseKind::Single(ident))
}
UseTreeKind::Glob => {
UseTreeKind::Glob(_) => {
let res = self.expect_full_res(id);
let res = self.lower_res(res);
// Put the result in the appropriate namespace.
@@ -731,7 +735,7 @@ fn lower_use_tree(
owner_id,
kind,
vis_span,
span: this.lower_span(use_tree.span),
span: this.lower_span(use_tree.span()),
has_delayed_lints: !this.delayed_lints.is_empty(),
eii: find_attr!(attrs, EiiImpls(..) | EiiDeclaration(..)),
};
@@ -867,7 +867,7 @@ fn print_use_tree(&mut self, tree: &ast::UseTree) {
self.print_ident(rename);
}
}
ast::UseTreeKind::Glob => {
ast::UseTreeKind::Glob(_) => {
if !tree.prefix.segments.is_empty() {
self.print_path(&tree.prefix, false, 0);
self.word("::");
@@ -102,7 +102,6 @@ fn build_initial_imports(&self) -> Stmt {
UseTree {
prefix: this.cx.path(this.span, vec![Ident::with_dummy_span(sym)]),
kind: UseTreeKind::Simple(None),
span: this.span,
},
DUMMY_NODE_ID,
)
@@ -121,7 +120,6 @@ fn build_initial_imports(&self) -> Stmt {
],
span: self.span,
},
span: self.span,
}),
),
)
@@ -68,8 +68,7 @@ pub fn inject(
thin_vec![cx.attr_word(sym::prelude_import, span)],
ast::ItemKind::Use(ast::UseTree {
prefix: cx.path(span, import_path),
kind: ast::UseTreeKind::Glob,
span,
kind: ast::UseTreeKind::Glob(span),
}),
);
+1 -1
View File
@@ -1442,7 +1442,7 @@ fn declared_idents(&self) -> Vec<Ident> {
if let ItemKind::Use(ut) = &self.kind {
fn collect_use_tree_leaves(ut: &ast::UseTree, idents: &mut Vec<Ident>) {
match &ut.kind {
ast::UseTreeKind::Glob => {}
ast::UseTreeKind::Glob(_) => {}
ast::UseTreeKind::Simple(_) => idents.push(ut.ident()),
ast::UseTreeKind::Nested { items, .. } => {
for (ut, _) in items {
+1 -1
View File
@@ -1178,7 +1178,7 @@ fn check_use_tree(&self, cx: &EarlyContext<'_>, use_tree: &ast::UseTree, item: &
}
rename.unwrap_or(orig_ident).name
}
ast::UseTreeKind::Glob => sym::asterisk,
ast::UseTreeKind::Glob(_) => sym::asterisk,
ast::UseTreeKind::Nested { .. } => return,
};
+3 -3
View File
@@ -433,7 +433,7 @@ fn parse_use_item(&mut self) -> PResult<'a, ItemKind> {
let tree = self.parse_use_tree()?;
if let Err(mut e) = self.expect_semi() {
match tree.kind {
UseTreeKind::Glob => {
UseTreeKind::Glob(_) => {
e.note("the wildcard token must be last on the path");
}
UseTreeKind::Nested { .. } => {
@@ -1328,13 +1328,13 @@ fn parse_use_tree(&mut self) -> PResult<'a, UseTree> {
}
};
Ok(UseTree { prefix, kind, span: lo.to(self.prev_token.span) })
Ok(UseTree { prefix, kind })
}
/// Parses `*` or `{...}`.
fn parse_use_tree_glob_or_nested(&mut self) -> PResult<'a, UseTreeKind> {
Ok(if self.eat(exp!(Star)) {
UseTreeKind::Glob
UseTreeKind::Glob(self.prev_token.span)
} else {
let lo = self.token.span;
UseTreeKind::Nested {
@@ -603,12 +603,15 @@ fn build_reduced_graph_for_use_tree(
// so prefixes are prepended with crate root segment if necessary.
// The root is prepended lazily, when the first non-empty prefix or terminating glob
// appears, so imports in braced groups can have roots prepended independently.
let is_glob = matches!(use_tree.kind, ast::UseTreeKind::Glob);
let crate_root = match prefix_iter.peek() {
Some(seg) if !seg.ident.is_path_segment_keyword() && seg.ident.span.is_rust_2015() => {
Some(seg.ident.span.ctxt())
}
None if is_glob && use_tree.span.is_rust_2015() => Some(use_tree.span.ctxt()),
None if let ast::UseTreeKind::Glob(span) = use_tree.kind
&& span.is_rust_2015() =>
{
Some(span.ctxt())
}
_ => None,
}
.map(|ctxt| {
@@ -696,7 +699,7 @@ fn build_reduced_graph_for_use_tree(
}
// Deny `use ::{self};` after edition 2015
kw::PathRoot if !self.r.path_root_is_crate_root(source.ident) => {
self.r.dcx().span_err(use_tree.span, "extern prelude cannot be imported");
self.r.dcx().span_err(use_tree.span(), "extern prelude cannot be imported");
return;
}
_ => {}
@@ -727,12 +730,12 @@ fn build_reduced_graph_for_use_tree(
id,
};
self.add_import(module_path, kind, use_tree.span, item, root_span, item.id, vis);
self.add_import(module_path, kind, use_tree.span(), item, root_span, item.id, vis);
}
ast::UseTreeKind::Glob => {
ast::UseTreeKind::Glob(_) => {
if !ast::attr::contains_name(&item.attrs, sym::prelude_import) {
let kind = ImportKind::Glob { max_vis: CmCell::new(None), id };
self.add_import(prefix, kind, use_tree.span, item, root_span, item.id, vis);
self.add_import(prefix, kind, use_tree.span(), item, root_span, item.id, vis);
} else {
// Resolve the prelude import early.
let path_res =
@@ -740,7 +743,7 @@ fn build_reduced_graph_for_use_tree(
if let PathResult::Module(ModuleOrUniformRoot::Module(module)) = path_res {
self.r.prelude = Some(module);
} else {
self.r.dcx().span_err(use_tree.span, "cannot resolve a prelude import");
self.r.dcx().span_err(use_tree.span(), "cannot resolve a prelude import");
}
}
}
@@ -764,7 +767,6 @@ fn build_reduced_graph_for_use_tree(
let tree = ast::UseTree {
prefix: ast::Path::from_ident(Ident::new(kw::SelfLower, new_span)),
kind: ast::UseTreeKind::Simple(Some(Ident::new(kw::Underscore, new_span))),
span: use_tree.span,
};
self.build_reduced_graph_for_use_tree(
// This particular use tree
@@ -835,7 +837,7 @@ fn build_reduced_graph_for_item(&mut self, item: &'a Item) {
// The whole `use` item
item,
vis,
use_tree.span,
use_tree.span(),
);
}
+24 -9
View File
@@ -294,22 +294,25 @@ fn calc_unused_spans(
) -> UnusedSpanResult {
// The full span is the whole item's span if this current tree is not nested inside another
// This tells rustfix to remove the whole item if all the imports are unused
let full_span = if unused_import.use_tree.span == use_tree.span {
let full_span = if unused_import.use_tree.span() == use_tree.span() {
unused_import.item_span
} else {
use_tree.span
use_tree.span()
};
match use_tree.kind {
ast::UseTreeKind::Simple(..) | ast::UseTreeKind::Glob => {
ast::UseTreeKind::Simple(..) | ast::UseTreeKind::Glob(_) => {
if unused_import.unused.contains(&use_tree_id) {
UnusedSpanResult::Unused { spans: vec![use_tree.span], remove: full_span }
UnusedSpanResult::Unused { spans: vec![use_tree.span()], remove: full_span }
} else {
UnusedSpanResult::Used
}
}
ast::UseTreeKind::Nested { items: ref nested, span: tree_span } => {
if nested.is_empty() {
return UnusedSpanResult::Unused { spans: vec![use_tree.span], remove: full_span };
return UnusedSpanResult::Unused {
spans: vec![use_tree.span()],
remove: full_span,
};
}
let mut unused_spans = Vec::new();
@@ -340,10 +343,10 @@ fn calc_unused_spans(
} else if pos == nested.len() - 1 || used_children > 0 {
// Delete everything from the end of the last import, to delete the
// previous comma
nested[pos - 1].0.span.shrink_to_hi().to(use_tree.span)
nested[pos - 1].0.hi_span().shrink_to_hi().to(use_tree.hi_span())
} else {
// Delete everything until the next import, to delete the trailing commas
use_tree.span.to(nested[pos + 1].0.span.shrink_to_lo())
use_tree.prefix.span.to(nested[pos + 1].0.prefix.span.shrink_to_lo())
};
// Try to collapse adjacent spans into a single one. This prevents all cases of
@@ -379,11 +382,23 @@ fn calc_unused_spans(
if used_children == 1 && !contains_self {
// Left brace, from the start of the nested group to the first item.
to_remove.push(
tree_span.shrink_to_lo().to(nested.first().unwrap().0.span.shrink_to_lo()),
tree_span.shrink_to_lo().to(nested
.first()
.unwrap()
.0
.prefix
.span
.shrink_to_lo()),
);
// Right brace, from the end of the last item to the end of the nested group.
to_remove.push(
nested.last().unwrap().0.span.shrink_to_hi().to(tree_span.shrink_to_hi()),
nested
.last()
.unwrap()
.0
.hi_span()
.shrink_to_hi()
.to(tree_span.shrink_to_hi()),
);
}
+3 -3
View File
@@ -169,8 +169,8 @@ fn visit_item(&mut self, i: &'a Item) {
DefKind::Macro(macro_kinds)
}
ItemKind::GlobalAsm(..) => DefKind::GlobalAsm,
ItemKind::Use(use_tree) => {
self.create_def(i.id, None, DefKind::Use, use_tree.span);
ItemKind::Use(_) => {
self.create_def(i.id, None, DefKind::Use, i.span);
return visit::walk_item(self, i);
}
ItemKind::MacCall(..) | ItemKind::DelegationMac(..) => {
@@ -261,7 +261,7 @@ fn visit_fn(&mut self, fn_kind: FnKind<'a>, _: &AttrVec, span: Span, _: NodeId)
}
fn visit_nested_use_tree(&mut self, use_tree: &'a UseTree, id: NodeId) {
self.create_def(id, None, DefKind::Use, use_tree.span);
self.create_def(id, None, DefKind::Use, use_tree.span());
visit::walk_use_tree(self, use_tree);
}
+1 -1
View File
@@ -2970,7 +2970,7 @@ fn resolve_item(&mut self, item: &'ast Item) {
ItemKind::Use(use_tree) => {
let maybe_exported = match use_tree.kind {
UseTreeKind::Simple(_) | UseTreeKind::Glob => MaybeExported::Ok(item.id),
UseTreeKind::Simple(_) | UseTreeKind::Glob(_) => MaybeExported::Ok(item.id),
UseTreeKind::Nested { .. } => MaybeExported::NestedUse(&item.vis),
};
self.resolve_doc_links(&item.attrs, maybe_exported);
@@ -210,7 +210,7 @@ fn track_uses(
if !macros.contains(&name) {
single_use_usages.push(SingleUse {
name,
span: tree.0.span,
span: tree.0.span(),
item_id: item.id,
can_suggest: false,
});
@@ -48,7 +48,7 @@ fn check_use_tree(use_tree: &UseTree, cx: &EarlyContext<'_>, span: Span) {
.ident;
unsafe_to_safe_check(old_name, new_name, cx, span);
},
UseTreeKind::Simple(None) | UseTreeKind::Glob => {},
UseTreeKind::Simple(None) | UseTreeKind::Glob(_) => {},
UseTreeKind::Nested { ref items, .. } => {
for (use_tree, _) in items {
check_use_tree(use_tree, cx, span);
@@ -812,7 +812,7 @@ pub fn eq_const_item_rhs(l: &ConstItemRhsKind, r: &ConstItemRhsKind) -> bool {
pub fn eq_use_tree_kind(l: &UseTreeKind, r: &UseTreeKind) -> bool {
use UseTreeKind::*;
match (l, r) {
(Glob, Glob) => true,
(Glob(_), Glob(_)) => true,
(Simple(l), Simple(r)) => both(l.as_ref(), r.as_ref(), |l, r| eq_id(*l, *r)),
(Nested { items: l, .. }, Nested { items: r, .. }) => over(l, r, |(l, _), (r, _)| eq_use_tree(l, r)),
_ => false,
+7 -7
View File
@@ -429,9 +429,9 @@ fn from_ast(
attrs: Option<ast::AttrVec>,
) -> UseTree {
let span = if let Some(lo) = opt_lo {
mk_sp(lo, a.span.hi())
mk_sp(lo, a.hi_span().hi())
} else {
a.span
a.span()
};
let mut result = UseTree {
path: vec![],
@@ -456,7 +456,7 @@ fn from_ast(
let style_edition = context.config.style_edition();
match a.kind {
UseTreeKind::Glob => {
UseTreeKind::Glob(_) => {
// in case of a global path and the glob starts at the root, e.g., "::*"
if a.prefix.segments.len() == 1 && leading_modsep {
let kind = UseSegmentKind::Ident("".to_owned(), None);
@@ -480,11 +480,11 @@ fn from_ast(
list.iter().map(|(tree, _)| tree),
"}",
",",
|tree| tree.span.lo(),
|tree| tree.span.hi(),
|tree| tree.prefix.span.lo(),
|tree| tree.hi_span().hi(),
|_| Ok("".to_owned()), // We only need comments for now.
context.snippet_provider.span_after(a.span, "{"),
a.span.hi(),
context.snippet_provider.span_after(a.span(), "{"),
a.hi_span().hi(),
false,
);
+1 -1
View File
@@ -2,7 +2,7 @@ error[E0432]: unresolved import `foo`
--> $DIR/issue-28388-1.rs:4:5
|
LL | use foo::{};
| ^^^^^^^ no `foo` in the root
| ^^^ no `foo` in the root
error: aborting due to 1 previous error
+8 -8
View File
@@ -46,8 +46,8 @@ macro_rules! test {() => {
// Failed resolutions of `proc_macro_item`
const _: () = {
// token_site_span::proc_macro_item
invoke_with_crate!{input proc_macro_item} //~ ERROR unresolved import `$crate`
invoke_with_ident!{input proc_macro_item} //~ ERROR unresolved import `$crate`
invoke_with_crate!{input proc_macro_item} //~ ERROR unresolved import `$crate::proc_macro_item`
invoke_with_ident!{input proc_macro_item} //~ ERROR unresolved import `$crate::proc_macro_item`
invoke_with_crate!{call proc_macro_item} //~ ERROR unresolved import `$crate`
invoke_with_ident!{call proc_macro_item} //~ ERROR unresolved import `$crate`
invoke_with_ident!{hello call proc_macro_item} //~ ERROR unresolved import `$crate`
@@ -59,8 +59,8 @@ macro_rules! test {() => {
macro_rules! test {() => {
// crate::proc_macro_item
invoke_with_ident!{$crate input proc_macro_item} //~ ERROR unresolved import `$crate`
with_crate!{$crate input proc_macro_item} //~ ERROR unresolved import `$crate`
invoke_with_ident!{$crate input proc_macro_item} //~ ERROR unresolved import `$crate::proc_macro_item`
with_crate!{$crate input proc_macro_item} //~ ERROR unresolved import `$crate::proc_macro_item`
with_crate!{$crate call proc_macro_item} //~ ERROR unresolved import `$crate`
// token_site_span::proc_macro_item
@@ -98,8 +98,8 @@ macro_rules! test {() => {
macro_rules! test {() => {
// crate::TokenItem
invoke_with_ident!{$crate input TokenItem} //~ ERROR unresolved import `$crate`
with_crate!{$crate input TokenItem} //~ ERROR unresolved import `$crate`
invoke_with_ident!{$crate input TokenItem} //~ ERROR unresolved import `$crate::TokenItem`
with_crate!{$crate input TokenItem} //~ ERROR unresolved import `$crate::TokenItem`
with_crate!{$crate call TokenItem} //~ ERROR unresolved import `$crate`
// mixed_site_span::TokenItem
@@ -128,8 +128,8 @@ macro_rules! test {() => {
// Failed resolutions of `ItemUse`
const _: () = {
// token_site_span::ItemUse
invoke_with_crate!{input ItemUse} //~ ERROR unresolved import `$crate`
invoke_with_ident!{input ItemUse} //~ ERROR unresolved import `$crate`
invoke_with_crate!{input ItemUse} //~ ERROR unresolved import `$crate::ItemUse`
invoke_with_ident!{input ItemUse} //~ ERROR unresolved import `$crate::ItemUse`
// mixed_site_span::ItemUse
invoke_with_crate!{mixed ItemUse} //~ ERROR unresolved import `$crate`
+16 -16
View File
@@ -1,4 +1,4 @@
error[E0432]: unresolved import `$crate`
error[E0432]: unresolved import `$crate::proc_macro_item`
--> $DIR/mixed-site-span.rs:49:5
|
LL | invoke_with_crate!{input proc_macro_item}
@@ -6,7 +6,7 @@ LL | invoke_with_crate!{input proc_macro_item}
|
= note: this error originates in the macro `invoke_with_crate` (in Nightly builds, run with -Z macro-backtrace for more info)
error[E0432]: unresolved import `$crate`
error[E0432]: unresolved import `$crate::proc_macro_item`
--> $DIR/mixed-site-span.rs:50:5
|
LL | invoke_with_ident!{input proc_macro_item}
@@ -77,11 +77,11 @@ LL - with_crate!{krate call proc_macro_item}
LL + with_crate!{krate call proc_macro_rules}
|
error[E0432]: unresolved import `$crate`
error[E0432]: unresolved import `$crate::proc_macro_item`
--> $DIR/mixed-site-span.rs:62:28
|
LL | invoke_with_ident!{$crate input proc_macro_item}
| ^^^^^^ no `proc_macro_item` in the root
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ no `proc_macro_item` in the root
...
LL | test!();
| ------- in this macro invocation
@@ -93,11 +93,11 @@ LL - invoke_with_ident!{$crate input proc_macro_item}
LL + invoke_with_ident!{$crate input proc_macro_rules}
|
error[E0432]: unresolved import `$crate`
error[E0432]: unresolved import `$crate::proc_macro_item`
--> $DIR/mixed-site-span.rs:63:21
|
LL | with_crate!{$crate input proc_macro_item}
| ^^^^^^ no `proc_macro_item` in the root
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ no `proc_macro_item` in the root
...
LL | test!();
| ------- in this macro invocation
@@ -230,11 +230,11 @@ LL - with_crate!{krate mixed TokenItem}
LL + token_site_span::TokenItem as _
|
error[E0432]: unresolved import `$crate`
error[E0432]: unresolved import `$crate::TokenItem`
--> $DIR/mixed-site-span.rs:101:28
|
LL | invoke_with_ident!{$crate input TokenItem}
| ^^^^^^ no `TokenItem` in the root
| ^^^^^^^^^^^^^^^^^^^^^^ no `TokenItem` in the root
...
LL | test!();
| ------- in this macro invocation
@@ -243,14 +243,14 @@ LL | test!();
help: consider importing this struct instead
|
LL - invoke_with_ident!{$crate input TokenItem}
LL + invoke_with_ident!{token_site_span::TokenItem as _ input TokenItem}
LL + invoke_with_ident!{token_site_span::TokenItem as _}
|
error[E0432]: unresolved import `$crate`
error[E0432]: unresolved import `$crate::TokenItem`
--> $DIR/mixed-site-span.rs:102:21
|
LL | with_crate!{$crate input TokenItem}
| ^^^^^^ no `TokenItem` in the root
| ^^^^^^^^^^^^^^^^^^^^^^ no `TokenItem` in the root
...
LL | test!();
| ------- in this macro invocation
@@ -259,7 +259,7 @@ LL | test!();
help: consider importing this struct instead
|
LL - with_crate!{$crate input TokenItem}
LL + with_crate!{token_site_span::TokenItem as _ input TokenItem}
LL + with_crate!{token_site_span::TokenItem as _}
|
error[E0432]: unresolved import `$crate`
@@ -311,7 +311,7 @@ LL - with_crate!{$crate mixed TokenItem}
LL + token_site_span::TokenItem as _
|
error[E0432]: unresolved import `$crate`
error[E0432]: unresolved import `$crate::ItemUse`
--> $DIR/mixed-site-span.rs:131:5
|
LL | invoke_with_crate!{input ItemUse}
@@ -322,10 +322,10 @@ help: consider importing this struct instead
--> $DIR/auxiliary/token-site-span.rs:14:42
|
LL - ($s:ident $i:ident) => { with_crate!{$crate $s $i} };
LL + ($s:ident $i:ident) => { with_crate!{ItemUse as _ $s $i} };
LL + ($s:ident $i:ident) => { with_crate!{ItemUse as _} };
|
error[E0432]: unresolved import `$crate`
error[E0432]: unresolved import `$crate::ItemUse`
--> $DIR/mixed-site-span.rs:132:5
|
LL | invoke_with_ident!{input ItemUse}
@@ -336,7 +336,7 @@ help: consider importing this struct instead
--> $DIR/auxiliary/token-site-span.rs:19:42
|
LL - ($s:ident $i:ident) => { with_crate!{krate $s $i} };
LL + ($s:ident $i:ident) => { with_crate!{ItemUse as _ $s $i} };
LL + ($s:ident $i:ident) => { with_crate!{ItemUse as _} };
|
error[E0432]: unresolved import `$crate`
@@ -2,7 +2,7 @@ error[E0432]: unresolved import `Nonexistent`
--> $DIR/resolve-bad-import-prefix.rs:13:5
|
LL | use Nonexistent::{};
| ^^^^^^^^^^^^^^^ no `Nonexistent` in the root
| ^^^^^^^^^^^ no `Nonexistent` in the root
error: aborting due to 1 previous error
@@ -2,7 +2,7 @@ error[E0658]: use of unstable library feature `unstable_test_feature`
--> $DIR/issue-28388-3.rs:7:5
|
LL | use lint_stability::UnstableEnum::{};
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= help: add `#![feature(unstable_test_feature)]` to the crate attributes to enable
= note: this compiler was built on YYYY-MM-DD; consider upgrading it if it is out of date