From 7f6858fd09dccfefcffff3e042555ffa75409ab8 Mon Sep 17 00:00:00 2001 From: Jesung Yang Date: Tue, 16 Dec 2025 05:49:37 +0000 Subject: [PATCH] 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 --- crates/base-db/src/input.rs | 19 ++++ crates/hir-def/src/attrs.rs | 92 +++++++++++++++---- crates/hir-def/src/item_tree.rs | 52 ++++++++++- crates/hir-def/src/item_tree/attrs.rs | 59 ++++++++++-- crates/hir-def/src/item_tree/tests.rs | 42 +++++++++ crates/hir-def/src/nameres/collector.rs | 13 +++ .../hir-def/src/nameres/tests/incremental.rs | 2 + crates/hir/src/semantics.rs | 16 +++- .../src/handlers/unused_variables.rs | 40 ++++++++ crates/ide-diagnostics/src/lib.rs | 23 +++-- crates/ide/src/doc_links/tests.rs | 15 +++ crates/ide/src/lib.rs | 2 + crates/ide/src/status.rs | 2 + crates/project-model/src/project_json.rs | 5 + crates/project-model/src/tests.rs | 9 ++ crates/project-model/src/workspace.rs | 5 + .../project-model/test_data/crate-attrs.json | 13 +++ .../cargo_hello_world_project_model.txt | 5 + ...project_model_with_selective_overrides.txt | 5 + ..._project_model_with_wildcard_overrides.txt | 5 + .../output/rust_project_cfg_groups.txt | 2 + .../output/rust_project_crate_attrs.txt | 54 +++++++++++ ...rust_project_hello_world_project_model.txt | 1 + crates/test-fixture/src/lib.rs | 6 ++ crates/test-utils/src/fixture.rs | 18 +++- docs/book/src/non_cargo_based_projects.md | 9 ++ 26 files changed, 473 insertions(+), 41 deletions(-) create mode 100644 crates/project-model/test_data/crate-attrs.json create mode 100644 crates/project-model/test_data/output/rust_project_crate_attrs.txt diff --git a/crates/base-db/src/input.rs b/crates/base-db/src/input.rs index 14649dde648f..240f1264917a 100644 --- a/crates/base-db/src/input.rs +++ b/crates/base-db/src/input.rs @@ -351,6 +351,8 @@ pub struct CrateData { /// declared in source via `extern crate test`. pub dependencies: Vec>, pub origin: CrateOrigin, + /// Extra crate-level attributes, including the surrounding `#![]`. + pub crate_attrs: Box<[Box]>, 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, mut env: Env, origin: CrateOrigin, + crate_attrs: Vec, is_proc_macro: bool, proc_macro_cwd: Arc, ws_data: Arc, @@ -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(), diff --git a/crates/hir-def/src/attrs.rs b/crates/hir-def/src/attrs.rs index febc794b5a05..34a9230794d1 100644 --- a/crates/hir-def/src/attrs.rs +++ b/crates/hir-def/src/attrs.rs @@ -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 { + 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, Option>, Crate) { +) -> (InFile, Option>, Option, 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( @@ -347,14 +381,15 @@ fn collect_attrs( owner: AttrDefId, mut callback: impl FnMut(Meta) -> ControlFlow, ) -> Option { - 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 { 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> { - 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)); + } } diff --git a/crates/hir-def/src/item_tree.rs b/crates/hir-def/src/item_tree.rs index 2a104fff2b92..6eab8888d92d 100644 --- a/crates/hir-def/src/item_tree.rs +++ b/crates/hir-def/src/item_tree.rs @@ -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 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 { 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 diff --git a/crates/hir-def/src/item_tree/attrs.rs b/crates/hir-def/src/item_tree/attrs.rs index 5c635a4b3831..81a9b28b628e 100644 --- a/crates/hir-def/src/item_tree/attrs.rs +++ b/crates/hir-def/src/item_tree/attrs.rs @@ -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 + Copy, + { let mut attrs = Vec::new(); let result = collect_item_tree_attrs::(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)] diff --git a/crates/hir-def/src/item_tree/tests.rs b/crates/hir-def/src/item_tree/tests.rs index a57432f33c3d..1926ed74e869 100644 --- a/crates/hir-def/src/item_tree/tests.rs +++ b/crates/hir-def/src/item_tree/tests.rs @@ -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)] + "#]], + ); +} diff --git a/crates/hir-def/src/nameres/collector.rs b/crates/hir-def/src/nameres/collector.rs index 08edf41c5694..10581378bef9 100644 --- a/crates/hir-def/src/nameres/collector.rs +++ b/crates/hir-def/src/nameres/collector.rs @@ -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)); + } } diff --git a/crates/hir-def/src/nameres/tests/incremental.rs b/crates/hir-def/src/nameres/tests/incremental.rs index 57243346019f..225ba958634e 100644 --- a/crates/hir-def/src/nameres/tests/incremental.rs +++ b/crates/hir-def/src/nameres/tests/incremental.rs @@ -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", ] "#]], diff --git a/crates/hir/src/semantics.rs b/crates/hir/src/semantics.rs index 80fac3512ece..b65a24d61c6e 100644 --- a/crates/hir/src/semantics.rs +++ b/crates/hir/src/semantics.rs @@ -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 Semantics<'_, DB> { pub fn lint_attrs( &self, + file_id: FileId, krate: Crate, item: ast::AnyHasAttrs, ) -> impl DoubleEndedIterator { 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::( - 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 { diff --git a/crates/ide-diagnostics/src/handlers/unused_variables.rs b/crates/ide-diagnostics/src/handlers/unused_variables.rs index b7ec8fa53fa7..52a2f44fd0f8 100644 --- a/crates/ide-diagnostics/src/handlers/unused_variables.rs +++ b/crates/ide-diagnostics/src/handlers/unused_variables.rs @@ -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 +} "#, ); } diff --git a/crates/ide-diagnostics/src/lib.rs b/crates/ide-diagnostics/src/lib.rs index 2b8474c3163b..0b321442497b 100644 --- a/crates/ide-diagnostics/src/lib.rs +++ b/crates/ide-diagnostics/src/lib.rs @@ -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, &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, 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, 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 { - 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, diff --git a/crates/ide/src/doc_links/tests.rs b/crates/ide/src/doc_links/tests.rs index 8594a0a22475..a61a6c677f65 100644 --- a/crates/ide/src/doc_links/tests.rs +++ b/crates/ide/src/doc_links/tests.rs @@ -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( diff --git a/crates/ide/src/lib.rs b/crates/ide/src/lib.rs index 94362649043d..0066ceed21ed 100644 --- a/crates/ide/src/lib.rs +++ b/crates/ide/src/lib.rs @@ -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 { diff --git a/crates/ide/src/status.rs b/crates/ide/src/status.rs index cfcd76d2aa3b..7f377e416b32 100644 --- a/crates/ide/src/status.rs +++ b/crates/ide/src/status.rs @@ -40,6 +40,7 @@ pub(crate) fn status(db: &RootDatabase, file_id: Option) -> 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) -> 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 diff --git a/crates/project-model/src/project_json.rs b/crates/project-model/src/project_json.rs index 041b9accf41d..b3478d2cfe03 100644 --- a/crates/project-model/src/project_json.rs +++ b/crates/project-model/src/project_json.rs @@ -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, pub(crate) target: Option, pub(crate) env: FxHashMap, + // Extra crate-level attributes, without the surrounding `#![]`. + pub(crate) crate_attrs: Vec, pub(crate) proc_macro_dylib_path: Option, pub(crate) is_workspace_member: bool, pub(crate) include: Vec, @@ -365,6 +368,8 @@ struct CrateData { target: Option, #[serde(default)] env: FxHashMap, + #[serde(default)] + crate_attrs: Vec, proc_macro_dylib_path: Option, is_workspace_member: Option, source: Option, diff --git a/crates/project-model/src/tests.rs b/crates/project-model/src/tests.rs index 1908fc02904a..a03ed562e1be 100644 --- a/crates/project-model/src/tests.rs +++ b/crates/project-model/src/tests.rs @@ -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"); diff --git a/crates/project-model/src/workspace.rs b/crates/project-model/src/workspace.rs index 747aa68f082d..fa3a79e041e0 100644 --- a/crates/project-model/src/workspace.rs +++ b/crates/project-model/src/workspace.rs @@ -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(), diff --git a/crates/project-model/test_data/crate-attrs.json b/crates/project-model/test_data/crate-attrs.json new file mode 100644 index 000000000000..b2a7e3715017 --- /dev/null +++ b/crates/project-model/test_data/crate-attrs.json @@ -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 + } + ] +} diff --git a/crates/project-model/test_data/output/cargo_hello_world_project_model.txt b/crates/project-model/test_data/output/cargo_hello_world_project_model.txt index 4f6ce4dc9537..a895ef53afa0 100644 --- a/crates/project-model/test_data/output/cargo_hello_world_project_model.txt +++ b/crates/project-model/test_data/output/cargo_hello_world_project_model.txt @@ -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", diff --git a/crates/project-model/test_data/output/cargo_hello_world_project_model_with_selective_overrides.txt b/crates/project-model/test_data/output/cargo_hello_world_project_model_with_selective_overrides.txt index 4f6ce4dc9537..a895ef53afa0 100644 --- a/crates/project-model/test_data/output/cargo_hello_world_project_model_with_selective_overrides.txt +++ b/crates/project-model/test_data/output/cargo_hello_world_project_model_with_selective_overrides.txt @@ -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", diff --git a/crates/project-model/test_data/output/cargo_hello_world_project_model_with_wildcard_overrides.txt b/crates/project-model/test_data/output/cargo_hello_world_project_model_with_wildcard_overrides.txt index 6862918e09ae..9eb47947b6fd 100644 --- a/crates/project-model/test_data/output/cargo_hello_world_project_model_with_wildcard_overrides.txt +++ b/crates/project-model/test_data/output/cargo_hello_world_project_model_with_wildcard_overrides.txt @@ -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", diff --git a/crates/project-model/test_data/output/rust_project_cfg_groups.txt b/crates/project-model/test_data/output/rust_project_cfg_groups.txt index 28ad3236ae81..32f9206a3e16 100644 --- a/crates/project-model/test_data/output/rust_project_cfg_groups.txt +++ b/crates/project-model/test_data/output/rust_project_cfg_groups.txt @@ -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$", diff --git a/crates/project-model/test_data/output/rust_project_crate_attrs.txt b/crates/project-model/test_data/output/rust_project_crate_attrs.txt new file mode 100644 index 000000000000..21b484bc0c42 --- /dev/null +++ b/crates/project-model/test_data/output/rust_project_crate_attrs.txt @@ -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, + }, + }, +} \ No newline at end of file diff --git a/crates/project-model/test_data/output/rust_project_hello_world_project_model.txt b/crates/project-model/test_data/output/rust_project_hello_world_project_model.txt index dabb3aa67441..de793115b9d5 100644 --- a/crates/project-model/test_data/output/rust_project_hello_world_project_model.txt +++ b/crates/project-model/test_data/output/rust_project_hello_world_project_model.txt @@ -12,6 +12,7 @@ "hello_world", ), }, + crate_attrs: [], is_proc_macro: false, proc_macro_cwd: AbsPathBuf( "$ROOT$", diff --git a/crates/test-fixture/src/lib.rs b/crates/test-fixture/src/lib.rs index 5e8b250c24a0..01e4215cfb86 100644 --- a/crates/test-fixture/src/lib.rs +++ b/crates/test-fixture/src/lib.rs @@ -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, introduce_new_source_root: Option, } @@ -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, } } diff --git a/crates/test-utils/src/fixture.rs b/crates/test-utils/src/fixture.rs index 831d2b30c1b6..1f6262c897c7 100644 --- a/crates/test-utils/src/fixture.rs +++ b/crates/test-utils/src/fixture.rs @@ -107,6 +107,11 @@ pub struct Fixture { /// /// Syntax: `env:PATH=/bin,RUST_LOG=debug` pub env: FxHashMap, + /// 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, /// 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()); } diff --git a/docs/book/src/non_cargo_based_projects.md b/docs/book/src/non_cargo_based_projects.md index 74489a9046ba..e7df4a5d7668 100644 --- a/docs/book/src/non_cargo_based_projects.md +++ b/docs/book/src/non_cargo_based_projects.md @@ -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;