feat: introduce crate_attrs field in rust-project.json

Since the commit 50384460c68f
("Rewrite method resolution to follow rustc more closely"), the method
resolution logic has changed: rust-analyzer only looks up inherent
methods for primitive types in sysroot crates.

Unfortunately, this change broke at least one project that relies on
`rust-project.json`: Rust-for-Linux. Its auto-generated
`rust-project.json` directly embeds `core`, `alloc`, and `std` in the
`crates` list without defining `sysroot_src`. Consequently,
rust-analyzer fails to identify them as sysroot crates, breaking IDE
support for primitive methods (e.g., `0_i32.rotate_left(0)`).

However, specifying `sysroot_src` creates a new issue: it implicitly
adds `std` as a dependency to all kernel module crates, which are
actually compiled with `-Zcrate-attr=no_std`. Since rust-analyzer cannot
see compiler flags passed outside of the project definition, we need a
method to explicitly specify `#![no_std]` or, more generally,
crate-level attributes through the project configuration.

To resolve this, extend the `rust-project.json` format with a new
`crate_attrs` field. This allows users to specify crate-level attributes
such as `#![no_std]` directly into the configuration, enabling
rust-analyzer to respect them when analyzing crates.

References:
- The original Zulip discussion:
  https://rust-lang.zulipchat.com/#narrow/channel/185405-t-compiler.2Frust-analyzer/topic/Primitive.20type.20inherent.20method.20lookup.20fails/with/562983853
This commit is contained in:
Jesung Yang 2025-12-16 05:49:37 +00:00
parent efdb3de4f2
commit 7f6858fd09
26 changed files with 473 additions and 41 deletions

View File

@ -351,6 +351,8 @@ pub struct CrateData<Id> {
/// declared in source via `extern crate test`.
pub dependencies: Vec<Dependency<Id>>,
pub origin: CrateOrigin,
/// Extra crate-level attributes, including the surrounding `#![]`.
pub crate_attrs: Box<[Box<str>]>,
pub is_proc_macro: bool,
/// The working directory to run proc-macros in invoked in the context of this crate.
/// This is the workspace root of the cargo workspace for workspace members, the crate manifest
@ -530,6 +532,7 @@ impl CrateGraphBuilder {
mut potential_cfg_options: Option<CfgOptions>,
mut env: Env,
origin: CrateOrigin,
crate_attrs: Vec<String>,
is_proc_macro: bool,
proc_macro_cwd: Arc<AbsPathBuf>,
ws_data: Arc<CrateWorkspaceData>,
@ -539,12 +542,17 @@ impl CrateGraphBuilder {
if let Some(potential_cfg_options) = &mut potential_cfg_options {
potential_cfg_options.shrink_to_fit();
}
let crate_attrs: Vec<_> = crate_attrs
.into_iter()
.map(|raw_attr| format!("#![{raw_attr}]").into_boxed_str())
.collect();
self.arena.alloc(CrateBuilder {
basic: CrateData {
root_file_id,
edition,
dependencies: Vec::new(),
origin,
crate_attrs: crate_attrs.into_boxed_slice(),
is_proc_macro,
proc_macro_cwd,
},
@ -648,6 +656,7 @@ impl CrateGraphBuilder {
edition: krate.basic.edition,
is_proc_macro: krate.basic.is_proc_macro,
origin: krate.basic.origin.clone(),
crate_attrs: krate.basic.crate_attrs.clone(),
root_file_id: krate.basic.root_file_id,
proc_macro_cwd: krate.basic.proc_macro_cwd.clone(),
};
@ -975,6 +984,7 @@ mod tests {
Default::default(),
Env::default(),
CrateOrigin::Local { repo: None, name: None },
Vec::new(),
false,
Arc::new(AbsPathBuf::assert_utf8(std::env::current_dir().unwrap())),
empty_ws_data(),
@ -988,6 +998,7 @@ mod tests {
Default::default(),
Env::default(),
CrateOrigin::Local { repo: None, name: None },
Vec::new(),
false,
Arc::new(AbsPathBuf::assert_utf8(std::env::current_dir().unwrap())),
empty_ws_data(),
@ -1001,6 +1012,7 @@ mod tests {
Default::default(),
Env::default(),
CrateOrigin::Local { repo: None, name: None },
Vec::new(),
false,
Arc::new(AbsPathBuf::assert_utf8(std::env::current_dir().unwrap())),
empty_ws_data(),
@ -1034,6 +1046,7 @@ mod tests {
Default::default(),
Env::default(),
CrateOrigin::Local { repo: None, name: None },
Vec::new(),
false,
Arc::new(AbsPathBuf::assert_utf8(std::env::current_dir().unwrap())),
empty_ws_data(),
@ -1047,6 +1060,7 @@ mod tests {
Default::default(),
Env::default(),
CrateOrigin::Local { repo: None, name: None },
Vec::new(),
false,
Arc::new(AbsPathBuf::assert_utf8(std::env::current_dir().unwrap())),
empty_ws_data(),
@ -1075,6 +1089,7 @@ mod tests {
Default::default(),
Env::default(),
CrateOrigin::Local { repo: None, name: None },
Vec::new(),
false,
Arc::new(AbsPathBuf::assert_utf8(std::env::current_dir().unwrap())),
empty_ws_data(),
@ -1088,6 +1103,7 @@ mod tests {
Default::default(),
Env::default(),
CrateOrigin::Local { repo: None, name: None },
Vec::new(),
false,
Arc::new(AbsPathBuf::assert_utf8(std::env::current_dir().unwrap())),
empty_ws_data(),
@ -1101,6 +1117,7 @@ mod tests {
Default::default(),
Env::default(),
CrateOrigin::Local { repo: None, name: None },
Vec::new(),
false,
Arc::new(AbsPathBuf::assert_utf8(std::env::current_dir().unwrap())),
empty_ws_data(),
@ -1129,6 +1146,7 @@ mod tests {
Default::default(),
Env::default(),
CrateOrigin::Local { repo: None, name: None },
Vec::new(),
false,
Arc::new(AbsPathBuf::assert_utf8(std::env::current_dir().unwrap())),
empty_ws_data(),
@ -1142,6 +1160,7 @@ mod tests {
Default::default(),
Env::default(),
CrateOrigin::Local { repo: None, name: None },
Vec::new(),
false,
Arc::new(AbsPathBuf::assert_utf8(std::env::current_dir().unwrap())),
empty_ws_data(),

View File

@ -39,7 +39,7 @@ use rustc_abi::ReprOptions;
use rustc_hash::FxHashSet;
use smallvec::SmallVec;
use syntax::{
AstNode, AstToken, NodeOrToken, SmolStr, SyntaxNode, SyntaxToken, T,
AstNode, AstToken, NodeOrToken, SmolStr, SourceFile, SyntaxNode, SyntaxToken, T,
ast::{self, AttrDocCommentIter, HasAttrs, IsString, TokenTreeChildren},
};
use tt::{TextRange, TextSize};
@ -292,35 +292,69 @@ bitflags::bitflags! {
}
}
pub fn parse_extra_crate_attrs(db: &dyn DefDatabase, krate: Crate) -> Option<SourceFile> {
let crate_data = krate.data(db);
let crate_attrs = &crate_data.crate_attrs;
if crate_attrs.is_empty() {
return None;
}
// All attributes are already enclosed in `#![]`.
let combined = crate_attrs.concat();
let p = SourceFile::parse(&combined, crate_data.edition);
let errs = p.errors();
if !errs.is_empty() {
let base_msg = "Failed to parse extra crate-level attribute";
let crate_name =
krate.extra_data(db).display_name.as_ref().map_or("{unknown}", |name| name.as_str());
let mut errs = errs.iter().peekable();
let mut offset = TextSize::from(0);
for raw_attr in crate_attrs {
let attr_end = offset + TextSize::of(&**raw_attr);
if errs.peeking_take_while(|e| e.range().start() < attr_end).count() > 0 {
tracing::error!("{base_msg} {raw_attr} for crate {crate_name}");
}
offset = attr_end
}
return None;
}
Some(p.tree())
}
fn attrs_source(
db: &dyn DefDatabase,
owner: AttrDefId,
) -> (InFile<ast::AnyHasAttrs>, Option<InFile<ast::Module>>, Crate) {
) -> (InFile<ast::AnyHasAttrs>, Option<InFile<ast::Module>>, Option<SourceFile>, Crate) {
let (owner, krate) = match owner {
AttrDefId::ModuleId(id) => {
let def_map = id.def_map(db);
let (definition, declaration) = match def_map[id].origin {
let krate = def_map.krate();
let (definition, declaration, extra_crate_attrs) = match def_map[id].origin {
ModuleOrigin::CrateRoot { definition } => {
let file = db.parse(definition).tree();
(InFile::new(definition.into(), ast::AnyHasAttrs::from(file)), None)
let definition_source = db.parse(definition).tree();
let definition = InFile::new(definition.into(), definition_source.into());
let extra_crate_attrs = parse_extra_crate_attrs(db, krate);
(definition, None, extra_crate_attrs)
}
ModuleOrigin::File { declaration, declaration_tree_id, definition, .. } => {
let definition_source = db.parse(definition).tree();
let definition = InFile::new(definition.into(), definition_source.into());
let declaration = InFile::new(declaration_tree_id.file_id(), declaration);
let declaration = declaration.with_value(declaration.to_node(db));
let definition_source = db.parse(definition).tree();
(InFile::new(definition.into(), definition_source.into()), Some(declaration))
(definition, Some(declaration), None)
}
ModuleOrigin::Inline { definition_tree_id, definition } => {
let definition = InFile::new(definition_tree_id.file_id(), definition);
let definition = definition.with_value(definition.to_node(db).into());
(definition, None)
(definition, None, None)
}
ModuleOrigin::BlockExpr { block, .. } => {
let definition = block.to_node(db);
(block.with_value(definition.into()), None)
(block.with_value(definition.into()), None, None)
}
};
return (definition, declaration, def_map.krate());
return (definition, declaration, extra_crate_attrs, krate);
}
AttrDefId::AdtId(AdtId::StructId(it)) => attrs_from_ast_id_loc(db, it),
AttrDefId::AdtId(AdtId::UnionId(it)) => attrs_from_ast_id_loc(db, it),
@ -339,7 +373,7 @@ fn attrs_source(
AttrDefId::ExternCrateId(it) => attrs_from_ast_id_loc(db, it),
AttrDefId::UseId(it) => attrs_from_ast_id_loc(db, it),
};
(owner, None, krate)
(owner, None, None, krate)
}
fn collect_attrs<BreakValue>(
@ -347,14 +381,15 @@ fn collect_attrs<BreakValue>(
owner: AttrDefId,
mut callback: impl FnMut(Meta) -> ControlFlow<BreakValue>,
) -> Option<BreakValue> {
let (source, outer_mod_decl, krate) = attrs_source(db, owner);
let (source, outer_mod_decl, extra_crate_attrs, krate) = attrs_source(db, owner);
let extra_attrs = extra_crate_attrs
.into_iter()
.flat_map(|src| src.attrs())
.chain(outer_mod_decl.into_iter().flat_map(|it| it.value.attrs()));
let mut cfg_options = None;
expand_cfg_attr(
outer_mod_decl
.into_iter()
.flat_map(|it| it.value.attrs())
.chain(ast::attrs_including_inner(&source.value)),
extra_attrs.chain(ast::attrs_including_inner(&source.value)),
|| cfg_options.get_or_insert_with(|| krate.cfg_options(db)),
move |meta, _, _, _| callback(meta),
)
@ -1013,10 +1048,12 @@ impl AttrFlags {
pub fn doc_html_root_url(db: &dyn DefDatabase, krate: Crate) -> Option<SmolStr> {
let root_file_id = krate.root_file_id(db);
let syntax = db.parse(root_file_id).tree();
let extra_crate_attrs =
parse_extra_crate_attrs(db, krate).into_iter().flat_map(|src| src.attrs());
let mut cfg_options = None;
expand_cfg_attr(
syntax.attrs(),
extra_crate_attrs.chain(syntax.attrs()),
|| cfg_options.get_or_insert(krate.cfg_options(db)),
|attr, _, _, _| {
if let Meta::TokenTree { path, tt } = attr
@ -1231,8 +1268,11 @@ impl AttrFlags {
// We LRU this query because it is only used by IDE.
#[salsa::tracked(returns(ref), lru = 250)]
pub fn docs(db: &dyn DefDatabase, owner: AttrDefId) -> Option<Box<Docs>> {
let (source, outer_mod_decl, krate) = attrs_source(db, owner);
let (source, outer_mod_decl, _extra_crate_attrs, krate) = attrs_source(db, owner);
let inner_attrs_node = source.value.inner_attributes_node();
// Note: we don't have to pass down `_extra_crate_attrs` here, since `extract_docs`
// does not handle crate-level attributes related to docs.
// See: https://doc.rust-lang.org/rustdoc/write-documentation/the-doc-attribute.html#at-the-crate-level
extract_docs(&|| krate.cfg_options(db), source, outer_mod_decl, inner_attrs_node)
}
@ -1480,8 +1520,9 @@ mod tests {
use test_fixture::WithFixture;
use tt::{TextRange, TextSize};
use crate::attrs::IsInnerDoc;
use crate::{attrs::Docs, test_db::TestDB};
use crate::AttrDefId;
use crate::attrs::{AttrFlags, Docs, IsInnerDoc};
use crate::test_db::TestDB;
#[test]
fn docs() {
@ -1617,4 +1658,15 @@ mod tests {
Some((in_file(range(263, 265)), IsInnerDoc::Yes))
);
}
#[test]
fn crate_attrs() {
let fixture = r#"
//- /lib.rs crate:foo crate-attr:no_std crate-attr:cfg(target_arch="x86")
"#;
let (db, file_id) = TestDB::with_single_file(fixture);
let module = db.module_for_file(file_id.file_id(&db));
let attrs = AttrFlags::query(&db, AttrDefId::ModuleId(module));
assert!(attrs.contains(AttrFlags::IS_NO_STD | AttrFlags::HAS_CFG));
}
}

View File

@ -44,6 +44,7 @@ use std::{
};
use ast::{AstNode, StructKind};
use cfg::CfgOptions;
use hir_expand::{
ExpandTo, HirFileId,
mod_path::{ModPath, PathKind},
@ -52,13 +53,17 @@ use hir_expand::{
use intern::Interned;
use la_arena::{Idx, RawIdx};
use rustc_hash::FxHashMap;
use span::{AstIdNode, Edition, FileAstId, SyntaxContext};
use span::{
AstIdNode, Edition, FileAstId, NO_DOWNMAP_ERASED_FILE_AST_ID_MARKER, Span, SpanAnchor,
SyntaxContext,
};
use stdx::never;
use syntax::{SyntaxKind, ast, match_ast};
use syntax::{SourceFile, SyntaxKind, ast, match_ast};
use thin_vec::ThinVec;
use triomphe::Arc;
use tt::TextRange;
use crate::{BlockId, Lookup, db::DefDatabase};
use crate::{BlockId, Lookup, attrs::parse_extra_crate_attrs, db::DefDatabase};
pub(crate) use crate::item_tree::{
attrs::*,
@ -88,6 +93,33 @@ impl fmt::Debug for RawVisibilityId {
}
}
fn lower_extra_crate_attrs<'a>(
db: &dyn DefDatabase,
crate_attrs_as_src: SourceFile,
file_id: span::EditionedFileId,
cfg_options: &dyn Fn() -> &'a CfgOptions,
) -> AttrsOrCfg {
#[derive(Copy, Clone)]
struct FakeSpanMap {
file_id: span::EditionedFileId,
}
impl syntax_bridge::SpanMapper<Span> for FakeSpanMap {
fn span_for(&self, range: TextRange) -> Span {
Span {
range,
anchor: SpanAnchor {
file_id: self.file_id,
ast_id: NO_DOWNMAP_ERASED_FILE_AST_ID_MARKER,
},
ctx: SyntaxContext::root(self.file_id.edition()),
}
}
}
let span_map = FakeSpanMap { file_id };
AttrsOrCfg::lower(db, &crate_attrs_as_src, cfg_options, span_map)
}
#[salsa_macros::tracked(returns(deref))]
pub(crate) fn file_item_tree_query(db: &dyn DefDatabase, file_id: HirFileId) -> Arc<ItemTree> {
let _p = tracing::info_span!("file_item_tree_query", ?file_id).entered();
@ -98,7 +130,19 @@ pub(crate) fn file_item_tree_query(db: &dyn DefDatabase, file_id: HirFileId) ->
let mut item_tree = match_ast! {
match syntax {
ast::SourceFile(file) => {
let top_attrs = ctx.lower_attrs(&file);
let krate = file_id.krate(db);
let root_file_id = krate.root_file_id(db);
let extra_top_attrs = (file_id == root_file_id).then(|| {
parse_extra_crate_attrs(db, krate).map(|crate_attrs| {
let file_id = root_file_id.editioned_file_id(db);
lower_extra_crate_attrs(db, crate_attrs, file_id, &|| ctx.cfg_options())
})
}).flatten();
let top_attrs = match extra_top_attrs {
Some(attrs @ AttrsOrCfg::Enabled { .. }) => attrs.merge(ctx.lower_attrs(&file)),
Some(attrs @ AttrsOrCfg::CfgDisabled(_)) => attrs,
None => ctx.lower_attrs(&file)
};
let mut item_tree = ctx.lower_module_items(&file);
item_tree.top_attrs = top_attrs;
item_tree

View File

@ -16,9 +16,9 @@ use hir_expand::{
attrs::{Attr, AttrId, AttrInput, Meta, collect_item_tree_attrs},
mod_path::ModPath,
name::Name,
span_map::SpanMapRef,
};
use intern::{Interned, Symbol, sym};
use span::Span;
use syntax::{AstNode, T, ast};
use syntax_bridge::DocCommentDesugarMode;
use tt::token_to_literal;
@ -42,12 +42,15 @@ impl Default for AttrsOrCfg {
}
impl AttrsOrCfg {
pub(crate) fn lower<'a>(
pub(crate) fn lower<'a, S>(
db: &dyn DefDatabase,
owner: &dyn ast::HasAttrs,
cfg_options: &dyn Fn() -> &'a CfgOptions,
span_map: SpanMapRef<'_>,
) -> AttrsOrCfg {
span_map: S,
) -> AttrsOrCfg
where
S: syntax_bridge::SpanMapper<Span> + Copy,
{
let mut attrs = Vec::new();
let result =
collect_item_tree_attrs::<Infallible>(owner, cfg_options, |meta, container, _, _| {
@ -55,17 +58,17 @@ impl AttrsOrCfg {
// tracking.
let (span, path_range, input) = match meta {
Meta::NamedKeyValue { path_range, name: _, value } => {
let span = span_map.span_for_range(path_range);
let span = span_map.span_for(path_range);
let input = value.map(|value| {
Box::new(AttrInput::Literal(token_to_literal(
value.text(),
span_map.span_for_range(value.text_range()),
span_map.span_for(value.text_range()),
)))
});
(span, path_range, input)
}
Meta::TokenTree { path, tt } => {
let span = span_map.span_for_range(path.range);
let span = span_map.span_for(path.range);
let tt = syntax_bridge::syntax_node_to_token_tree(
tt.syntax(),
span_map,
@ -76,7 +79,7 @@ impl AttrsOrCfg {
(span, path.range, input)
}
Meta::Path { path } => {
let span = span_map.span_for_range(path.range);
let span = span_map.span_for(path.range);
(span, path.range, None)
}
};
@ -90,7 +93,7 @@ impl AttrsOrCfg {
.filter(|it| it.kind().is_any_identifier());
ModPath::from_tokens(
db,
&mut |range| span_map.span_for_range(range).ctx,
&mut |range| span_map.span_for(range).ctx,
is_abs,
segments,
)
@ -107,6 +110,44 @@ impl AttrsOrCfg {
None => AttrsOrCfg::Enabled { attrs },
}
}
// Merges two `AttrsOrCfg`s, assuming `self` is placed before `other` in the source code.
// The operation follows these rules:
//
// - If `self` and `other` are both `AttrsOrCfg::Enabled`, the result is a new
// `AttrsOrCfg::Enabled`. It contains the concatenation of `self`'s attributes followed by
// `other`'s.
// - If `self` is `AttrsOrCfg::Enabled` but `other` is `AttrsOrCfg::CfgDisabled`, the result
// is a new `AttrsOrCfg::CfgDisabled`. It contains the concatenation of `self`'s attributes
// followed by `other`'s.
// - If `self` is `AttrsOrCfg::CfgDisabled`, return `self` as-is.
//
// The rationale is that attribute collection is sequential and order-sensitive. This operation
// preserves those semantics when combining attributes from two different sources.
// `AttrsOrCfg::CfgDisabled` marks a point where collection stops due to a false `#![cfg(...)]`
// condition. It acts as a "breakpoint": attributes beyond it are not collected. Therefore,
// when merging, an `AttrsOrCfg::CfgDisabled` on the left-hand side short-circuits the
// operation, while an `AttrsOrCfg::CfgDisabled` on the right-hand side preserves all
// attributes collected up to that point.
//
// Note that this operation is neither commutative nor associative.
pub(crate) fn merge(self, other: AttrsOrCfg) -> AttrsOrCfg {
match (self, other) {
(AttrsOrCfg::Enabled { attrs }, AttrsOrCfg::Enabled { attrs: other_attrs }) => {
let mut v = attrs.0.into_vec();
v.extend(other_attrs.0);
AttrsOrCfg::Enabled { attrs: AttrsOwned(v.into_boxed_slice()) }
}
(AttrsOrCfg::Enabled { attrs }, AttrsOrCfg::CfgDisabled(mut other)) => {
let other_attrs = &mut other.1;
let mut v = attrs.0.into_vec();
v.extend(std::mem::take(&mut other_attrs.0));
other_attrs.0 = v.into_boxed_slice();
AttrsOrCfg::CfgDisabled(other)
}
(this @ AttrsOrCfg::CfgDisabled(_), _) => this,
}
}
}
#[derive(Debug, PartialEq, Eq)]

View File

@ -244,3 +244,45 @@ pub(self) struct S;
"#]],
)
}
#[test]
fn crate_attrs_should_preserve_order() {
check(
r#"
//- /main.rs crate:foo crate-attr:no_std crate-attr:features(f16) crate-attr:crate_type="bin"
"#,
expect![[r##"
#![no_std]
#![features(f16)]
#![crate_type = "bin"]
"##]],
);
}
#[test]
fn crate_attrs_with_disabled_cfg_injected() {
check(
r#"
//- /main.rs crate:foo crate-attr:no_std crate-attr:cfg(false) crate-attr:features(f16,f128) crate-attr:crate_type="bin"
"#,
expect![[r#"
#![no_std]
#![cfg(false)]
"#]],
);
}
#[test]
fn crate_attrs_with_disabled_cfg_in_source() {
check(
r#"
//- /lib.rs crate:foo crate-attr:no_std
#![cfg(false)]
#![no_core]
"#,
expect![[r#"
#![no_std]
#![cfg(false)]
"#]],
);
}

View File

@ -2608,4 +2608,17 @@ foo!(KABOOM);
"#,
);
}
#[test]
fn crate_attrs() {
let fixture = r#"
//- /lib.rs crate:foo crate-attr:recursion_limit="4" crate-attr:no_core crate-attr:no_std crate-attr:feature(register_tool)
"#;
let (db, file_id) = TestDB::with_single_file(fixture);
let def_map = crate_def_map(&db, file_id.krate(&db));
assert_eq!(def_map.recursion_limit(), 4);
assert!(def_map.is_no_core());
assert!(def_map.is_no_std());
assert!(def_map.is_unstable_feature_enabled(&sym::register_tool));
}
}

View File

@ -76,6 +76,7 @@ pub const BAZ: u32 = 0;
None,
Env::default(),
CrateOrigin::Local { repo: None, name: Some(Symbol::intern(crate_name)) },
Vec::new(),
false,
Arc::new(
// FIXME: This is less than ideal
@ -117,6 +118,7 @@ pub const BAZ: u32 = 0;
expect![[r#"
[
"crate_local_def_map",
"file_item_tree_query",
"crate_local_def_map",
]
"#]],

View File

@ -14,6 +14,7 @@ use base_db::FxIndexSet;
use either::Either;
use hir_def::{
DefWithBodyId, FunctionId, MacroId, StructId, TraitId, VariantId,
attrs::parse_extra_crate_attrs,
expr_store::{Body, ExprOrPatSource, HygieneId, path::Path},
hir::{BindingId, Expr, ExprId, ExprOrPatId, Pat},
nameres::{ModuleOrigin, crate_def_map},
@ -266,14 +267,27 @@ impl<DB: HirDatabase + ?Sized> Semantics<'_, DB> {
pub fn lint_attrs(
&self,
file_id: FileId,
krate: Crate,
item: ast::AnyHasAttrs,
) -> impl DoubleEndedIterator<Item = (LintAttr, SmolStr)> {
let mut cfg_options = None;
let cfg_options = || *cfg_options.get_or_insert_with(|| krate.id.cfg_options(self.db));
let is_crate_root = file_id == krate.root_file(self.imp.db);
let is_source_file = ast::SourceFile::can_cast(item.syntax().kind());
let extra_crate_attrs = (is_crate_root && is_source_file)
.then(|| {
parse_extra_crate_attrs(self.imp.db, krate.id)
.into_iter()
.flat_map(|src| src.attrs())
})
.into_iter()
.flatten();
let mut result = Vec::new();
hir_expand::attrs::expand_cfg_attr::<Infallible>(
ast::attrs_including_inner(&item),
extra_crate_attrs.chain(ast::attrs_including_inner(&item)),
cfg_options,
|attr, _, _, _| {
let hir_expand::attrs::Meta::TokenTree { path, tt } = attr else {

View File

@ -387,6 +387,46 @@ struct S { field : u32 }
fn f(S { field }: error) {
// ^^^^^ 💡 warn: unused variable
}
"#,
);
}
#[test]
fn crate_attrs_lint_smoke_test() {
check_diagnostics(
r#"
//- /lib.rs crate:foo crate-attr:deny(unused_variables)
fn main() {
let x = 2;
//^ 💡 error: unused variable
}
"#,
);
}
#[test]
fn crate_attrs_should_not_override_lints_in_source() {
check_diagnostics(
r#"
//- /lib.rs crate:foo crate-attr:allow(unused_variables)
#![deny(unused_variables)]
fn main() {
let x = 2;
//^ 💡 error: unused variable
}
"#,
);
}
#[test]
fn crate_attrs_should_preserve_lint_order() {
check_diagnostics(
r#"
//- /lib.rs crate:foo crate-attr:allow(unused_variables) crate-attr:warn(unused_variables)
fn main() {
let x = 2;
//^ 💡 warn: unused variable
}
"#,
);
}

View File

@ -485,7 +485,7 @@ pub fn semantic_diagnostics(
// The edition isn't accurate (each diagnostics may have its own edition due to macros),
// but it's okay as it's only being used for error recovery.
handle_lints(&ctx.sema, krate, &mut lints, editioned_file_id.edition(db));
handle_lints(&ctx.sema, file_id, krate, &mut lints, editioned_file_id.edition(db));
res.retain(|d| d.severity != Severity::Allow);
@ -593,6 +593,7 @@ fn build_lints_map(
fn handle_lints(
sema: &Semantics<'_, RootDatabase>,
file_id: FileId,
krate: hir::Crate,
diagnostics: &mut [(InFile<SyntaxNode>, &mut Diagnostic)],
edition: Edition,
@ -609,10 +610,10 @@ fn handle_lints(
}
let mut diag_severity =
lint_severity_at(sema, krate, node, &lint_groups(&diag.code, edition));
lint_severity_at(sema, file_id, krate, node, &lint_groups(&diag.code, edition));
if let outline_diag_severity @ Some(_) =
find_outline_mod_lint_severity(sema, krate, node, diag, edition)
find_outline_mod_lint_severity(sema, file_id, krate, node, diag, edition)
{
diag_severity = outline_diag_severity;
}
@ -635,6 +636,7 @@ fn default_lint_severity(lint: &Lint, edition: Edition) -> Severity {
fn find_outline_mod_lint_severity(
sema: &Semantics<'_, RootDatabase>,
file_id: FileId,
krate: hir::Crate,
node: &InFile<SyntaxNode>,
diag: &Diagnostic,
@ -651,6 +653,7 @@ fn find_outline_mod_lint_severity(
let lint_groups = lint_groups(&diag.code, edition);
lint_attrs(
sema,
file_id,
krate,
ast::AnyHasAttrs::cast(module_source_file.value).expect("SourceFile always has attrs"),
)
@ -659,6 +662,7 @@ fn find_outline_mod_lint_severity(
fn lint_severity_at(
sema: &Semantics<'_, RootDatabase>,
file_id: FileId,
krate: hir::Crate,
node: &InFile<SyntaxNode>,
lint_groups: &LintGroups,
@ -667,21 +671,28 @@ fn lint_severity_at(
.ancestors()
.filter_map(ast::AnyHasAttrs::cast)
.find_map(|ancestor| {
lint_attrs(sema, krate, ancestor)
lint_attrs(sema, file_id, krate, ancestor)
.find_map(|(lint, severity)| lint_groups.contains(&lint).then_some(severity))
})
.or_else(|| {
lint_severity_at(sema, krate, &sema.find_parent_file(node.file_id)?, lint_groups)
lint_severity_at(
sema,
file_id,
krate,
&sema.find_parent_file(node.file_id)?,
lint_groups,
)
})
}
// FIXME: Switch this to analysis' `expand_cfg_attr`.
fn lint_attrs(
sema: &Semantics<'_, RootDatabase>,
file_id: FileId,
krate: hir::Crate,
ancestor: ast::AnyHasAttrs,
) -> impl Iterator<Item = (SmolStr, Severity)> {
sema.lint_attrs(krate, ancestor).rev().map(|(lint_attr, lint)| {
sema.lint_attrs(file_id, krate, ancestor).rev().map(|(lint_attr, lint)| {
let severity = match lint_attr {
hir::LintAttr::Allow | hir::LintAttr::Expect => Severity::Allow,
hir::LintAttr::Warn => Severity::Warning,

View File

@ -658,6 +658,21 @@ pub struct B$0ar
);
}
#[test]
fn rewrite_html_root_url_using_crate_attr() {
check_rewrite(
r#"
//- /main.rs crate:foo crate-attr:doc(arbitrary_attribute="test",html_root_url="https:/example.com",arbitrary_attribute2)
pub mod foo {
pub struct Foo;
}
/// [Foo](foo::Foo)
pub struct B$0ar
"#,
expect![[r#"[Foo](https://example.com/foo/foo/struct.Foo.html)"#]],
);
}
#[test]
fn rewrite_on_field() {
check_rewrite(

View File

@ -254,6 +254,7 @@ impl Analysis {
TryFrom::try_from(&*std::env::current_dir().unwrap().as_path().to_string_lossy())
.unwrap(),
);
let crate_attrs = Vec::new();
cfg_options.insert_atom(sym::test);
crate_graph.add_crate_root(
file_id,
@ -264,6 +265,7 @@ impl Analysis {
None,
Env::default(),
CrateOrigin::Local { repo: None, name: None },
crate_attrs,
false,
proc_macro_cwd,
Arc::new(CrateWorkspaceData {

View File

@ -40,6 +40,7 @@ pub(crate) fn status(db: &RootDatabase, file_id: Option<FileId>) -> String {
edition,
dependencies,
origin,
crate_attrs,
is_proc_macro,
proc_macro_cwd,
} = crate_id.data(db);
@ -62,6 +63,7 @@ pub(crate) fn status(db: &RootDatabase, file_id: Option<FileId>) -> String {
format_to!(buf, " Potential cfgs: {:?}\n", potential_cfg_options);
format_to!(buf, " Env: {:?}\n", env);
format_to!(buf, " Origin: {:?}\n", origin);
format_to!(buf, " Extra crate-level attrs: {:?}\n", crate_attrs);
format_to!(buf, " Is a proc macro crate: {}\n", is_proc_macro);
format_to!(buf, " Proc macro cwd: {:?}\n", proc_macro_cwd);
let deps = dependencies

View File

@ -163,6 +163,7 @@ impl ProjectJson {
cfg,
target: crate_data.target,
env: crate_data.env,
crate_attrs: crate_data.crate_attrs,
proc_macro_dylib_path: crate_data
.proc_macro_dylib_path
.map(absolutize_on_base),
@ -244,6 +245,8 @@ pub struct Crate {
pub(crate) cfg: Vec<CfgAtom>,
pub(crate) target: Option<String>,
pub(crate) env: FxHashMap<String, String>,
// Extra crate-level attributes, without the surrounding `#![]`.
pub(crate) crate_attrs: Vec<String>,
pub(crate) proc_macro_dylib_path: Option<AbsPathBuf>,
pub(crate) is_workspace_member: bool,
pub(crate) include: Vec<AbsPathBuf>,
@ -365,6 +368,8 @@ struct CrateData {
target: Option<String>,
#[serde(default)]
env: FxHashMap<String, String>,
#[serde(default)]
crate_attrs: Vec<String>,
proc_macro_dylib_path: Option<Utf8PathBuf>,
is_workspace_member: Option<bool>,
source: Option<CrateSource>,

View File

@ -198,6 +198,15 @@ fn rust_project_cfg_groups() {
check_crate_graph(crate_graph, expect_file!["../test_data/output/rust_project_cfg_groups.txt"]);
}
#[test]
fn rust_project_crate_attrs() {
let (crate_graph, _proc_macros) = load_rust_project("crate-attrs.json");
check_crate_graph(
crate_graph,
expect_file!["../test_data/output/rust_project_crate_attrs.txt"],
);
}
#[test]
fn crate_graph_dedup_identical() {
let (mut crate_graph, proc_macros) = load_cargo("regex-metadata.json");

View File

@ -1093,6 +1093,7 @@ fn project_json_to_crate_graph(
cfg,
target,
env,
crate_attrs,
proc_macro_dylib_path,
is_proc_macro,
repository,
@ -1163,6 +1164,7 @@ fn project_json_to_crate_graph(
} else {
CrateOrigin::Local { repo: None, name: None }
},
crate_attrs.clone(),
*is_proc_macro,
match proc_macro_cwd {
Some(path) => Arc::new(path.clone()),
@ -1467,6 +1469,7 @@ fn detached_file_to_crate_graph(
repo: None,
name: display_name.map(|n| n.canonical_name().to_owned()),
},
Vec::new(),
false,
Arc::new(detached_file.parent().to_path_buf()),
crate_ws_data,
@ -1647,6 +1650,7 @@ fn add_target_crate_root(
potential_cfg_options,
env,
origin,
Vec::new(),
matches!(kind, TargetKind::Lib { is_proc_macro: true }),
proc_macro_cwd,
crate_ws_data,
@ -1830,6 +1834,7 @@ fn sysroot_to_crate_graph(
None,
Env::default(),
CrateOrigin::Lang(LangCrateOrigin::from(&*stitched[krate].name)),
Vec::new(),
false,
Arc::new(stitched[krate].root.parent().to_path_buf()),
crate_ws_data.clone(),

View File

@ -0,0 +1,13 @@
{
"sysroot_src": null,
"crates": [
{
"display_name": "foo",
"root_module": "$ROOT$src/lib.rs",
"edition": "2024",
"deps": [],
"crate_attrs": ["no_std", "feature(f16,f128)", "crate_type = \"lib\""],
"is_workspace_member": true
}
]
}

View File

@ -21,6 +21,7 @@
"hello-world",
),
},
crate_attrs: [],
is_proc_macro: false,
proc_macro_cwd: AbsPathBuf(
"$ROOT$hello-world",
@ -106,6 +107,7 @@
"hello-world",
),
},
crate_attrs: [],
is_proc_macro: false,
proc_macro_cwd: AbsPathBuf(
"$ROOT$hello-world",
@ -191,6 +193,7 @@
"hello-world",
),
},
crate_attrs: [],
is_proc_macro: false,
proc_macro_cwd: AbsPathBuf(
"$ROOT$hello-world",
@ -276,6 +279,7 @@
"hello-world",
),
},
crate_attrs: [],
is_proc_macro: false,
proc_macro_cwd: AbsPathBuf(
"$ROOT$hello-world",
@ -344,6 +348,7 @@
),
name: "libc",
},
crate_attrs: [],
is_proc_macro: false,
proc_macro_cwd: AbsPathBuf(
"$ROOT$.cargo/registry/src/github.com-1ecc6299db9ec823/libc-0.2.98",

View File

@ -21,6 +21,7 @@
"hello-world",
),
},
crate_attrs: [],
is_proc_macro: false,
proc_macro_cwd: AbsPathBuf(
"$ROOT$hello-world",
@ -106,6 +107,7 @@
"hello-world",
),
},
crate_attrs: [],
is_proc_macro: false,
proc_macro_cwd: AbsPathBuf(
"$ROOT$hello-world",
@ -191,6 +193,7 @@
"hello-world",
),
},
crate_attrs: [],
is_proc_macro: false,
proc_macro_cwd: AbsPathBuf(
"$ROOT$hello-world",
@ -276,6 +279,7 @@
"hello-world",
),
},
crate_attrs: [],
is_proc_macro: false,
proc_macro_cwd: AbsPathBuf(
"$ROOT$hello-world",
@ -344,6 +348,7 @@
),
name: "libc",
},
crate_attrs: [],
is_proc_macro: false,
proc_macro_cwd: AbsPathBuf(
"$ROOT$.cargo/registry/src/github.com-1ecc6299db9ec823/libc-0.2.98",

View File

@ -21,6 +21,7 @@
"hello-world",
),
},
crate_attrs: [],
is_proc_macro: false,
proc_macro_cwd: AbsPathBuf(
"$ROOT$hello-world",
@ -105,6 +106,7 @@
"hello-world",
),
},
crate_attrs: [],
is_proc_macro: false,
proc_macro_cwd: AbsPathBuf(
"$ROOT$hello-world",
@ -189,6 +191,7 @@
"hello-world",
),
},
crate_attrs: [],
is_proc_macro: false,
proc_macro_cwd: AbsPathBuf(
"$ROOT$hello-world",
@ -273,6 +276,7 @@
"hello-world",
),
},
crate_attrs: [],
is_proc_macro: false,
proc_macro_cwd: AbsPathBuf(
"$ROOT$hello-world",
@ -340,6 +344,7 @@
),
name: "libc",
},
crate_attrs: [],
is_proc_macro: false,
proc_macro_cwd: AbsPathBuf(
"$ROOT$.cargo/registry/src/github.com-1ecc6299db9ec823/libc-0.2.98",

View File

@ -12,6 +12,7 @@
"hello_world",
),
},
crate_attrs: [],
is_proc_macro: false,
proc_macro_cwd: AbsPathBuf(
"$ROOT$",
@ -62,6 +63,7 @@
"other_crate",
),
},
crate_attrs: [],
is_proc_macro: false,
proc_macro_cwd: AbsPathBuf(
"$ROOT$",

View File

@ -0,0 +1,54 @@
{
0: CrateBuilder {
basic: CrateData {
root_file_id: FileId(
1,
),
edition: Edition2024,
dependencies: [],
origin: Local {
repo: None,
name: Some(
"foo",
),
},
crate_attrs: [
"#![no_std]",
"#![feature(f16,f128)]",
"#![crate_type = \"lib\"]",
],
is_proc_macro: false,
proc_macro_cwd: AbsPathBuf(
"$ROOT$",
),
},
extra: ExtraCrateData {
version: None,
display_name: Some(
CrateDisplayName {
crate_name: CrateName(
"foo",
),
canonical_name: "foo",
},
),
potential_cfg_options: None,
},
cfg_options: CfgOptions(
[
"rust_analyzer",
"test",
"true",
],
),
env: Env {
entries: {},
},
ws_data: CrateWorkspaceData {
target: Err(
"test has no target data",
),
toolchain: None,
},
},
}

View File

@ -12,6 +12,7 @@
"hello_world",
),
},
crate_attrs: [],
is_proc_macro: false,
proc_macro_cwd: AbsPathBuf(
"$ROOT$",

View File

@ -239,6 +239,7 @@ impl ChangeFixture {
Some(meta.cfg),
meta.env,
origin,
meta.crate_attrs,
false,
proc_macro_cwd.clone(),
crate_ws_data.clone(),
@ -292,6 +293,7 @@ impl ChangeFixture {
String::from("__ra_is_test_fixture"),
)]),
CrateOrigin::Lang(LangCrateOrigin::Core),
Vec::new(),
false,
proc_macro_cwd.clone(),
crate_ws_data.clone(),
@ -322,6 +324,7 @@ impl ChangeFixture {
Some(default_cfg),
default_env,
CrateOrigin::Local { repo: None, name: None },
Vec::new(),
false,
proc_macro_cwd.clone(),
crate_ws_data.clone(),
@ -385,6 +388,7 @@ impl ChangeFixture {
String::from("__ra_is_test_fixture"),
)]),
CrateOrigin::Local { repo: None, name: None },
Vec::new(),
true,
proc_macro_cwd,
crate_ws_data,
@ -635,6 +639,7 @@ struct FileMeta {
cfg: CfgOptions,
edition: Edition,
env: Env,
crate_attrs: Vec<String>,
introduce_new_source_root: Option<SourceRootKind>,
}
@ -666,6 +671,7 @@ impl FileMeta {
cfg,
edition: f.edition.map_or(Edition::CURRENT, |v| Edition::from_str(&v).unwrap()),
env: f.env.into_iter().collect(),
crate_attrs: f.crate_attrs,
introduce_new_source_root,
}
}

View File

@ -107,6 +107,11 @@ pub struct Fixture {
///
/// Syntax: `env:PATH=/bin,RUST_LOG=debug`
pub env: FxHashMap<String, String>,
/// Specifies extra crate-level attributes injected at the top of the crate root file.
/// This must be used with `crate` meta.
///
/// Syntax: `crate-attr:no_std crate-attr:features(f16,f128) crate-attr:cfg(target_arch="x86")`
pub crate_attrs: Vec<String>,
/// Introduces a new source root. This file **and the following
/// files** will belong the new source root. This must be used
/// with `crate` meta.
@ -275,6 +280,7 @@ impl FixtureWithProjectMeta {
let mut krate = None;
let mut deps = Vec::new();
let mut crate_attrs = Vec::new();
let mut extern_prelude = None;
let mut edition = None;
let mut cfgs = Vec::new();
@ -292,6 +298,7 @@ impl FixtureWithProjectMeta {
match key {
"crate" => krate = Some(value.to_owned()),
"deps" => deps = value.split(',').map(|it| it.to_owned()).collect(),
"crate-attr" => crate_attrs.push(value.to_owned()),
"extern-prelude" => {
if value.is_empty() {
extern_prelude = Some(Vec::new());
@ -334,6 +341,7 @@ impl FixtureWithProjectMeta {
line,
krate,
deps,
crate_attrs,
extern_prelude,
cfgs,
edition,
@ -548,7 +556,7 @@ fn parse_fixture_gets_full_meta() {
//- toolchain: nightly
//- proc_macros: identity
//- minicore: coerce_unsized
//- /lib.rs crate:foo deps:bar,baz cfg:foo=a,bar=b,atom env:OUTDIR=path/to,OTHER=foo
//- /lib.rs crate:foo deps:bar,baz crate-attr:no_std crate-attr:features(f16,f128) crate-attr:cfg(target_arch="x86") cfg:foo=a,bar=b,atom env:OUTDIR=path/to,OTHER=foo
mod m;
"#,
);
@ -561,6 +569,14 @@ mod m;
assert_eq!("mod m;\n", meta.text);
assert_eq!("foo", meta.krate.as_ref().unwrap());
assert_eq!(
vec![
"no_std".to_owned(),
"features(f16,f128)".to_owned(),
"cfg(target_arch=\"x86\")".to_owned()
],
meta.crate_attrs
);
assert_eq!("/lib.rs", meta.path);
assert_eq!(2, meta.env.len());
}

View File

@ -144,6 +144,15 @@ interface Crate {
/// Environment variables, used for
/// the `env!` macro
env: { [key: string]: string; };
/// Extra crate-level attributes applied to this crate.
///
/// rust-analyzer will behave as if these attributes
/// were present before the first source line of the
/// crate root.
///
/// Each string should contain the contents of a `#![...]`
/// crate-level attribute, without the surrounding `#![]`.
crate_attrs?: string[];
/// Whether the crate is a proc-macro crate.
is_proc_macro: boolean;