Unnamed repository; edit this file 'description' to name the repository.
Merge pull request #21282 from J3m3/crate-attrs
feat: introduce `crate_attrs` field in `rust-project.json`
Chayim Refael Friedman 4 months ago
parent efdb3de · parent 7f6858f · commit ea1d299
-rw-r--r--crates/base-db/src/input.rs19
-rw-r--r--crates/hir-def/src/attrs.rs92
-rw-r--r--crates/hir-def/src/item_tree.rs52
-rw-r--r--crates/hir-def/src/item_tree/attrs.rs59
-rw-r--r--crates/hir-def/src/item_tree/tests.rs42
-rw-r--r--crates/hir-def/src/nameres/collector.rs13
-rw-r--r--crates/hir-def/src/nameres/tests/incremental.rs2
-rw-r--r--crates/hir/src/semantics.rs16
-rw-r--r--crates/ide-diagnostics/src/handlers/unused_variables.rs40
-rw-r--r--crates/ide-diagnostics/src/lib.rs23
-rw-r--r--crates/ide/src/doc_links/tests.rs15
-rw-r--r--crates/ide/src/lib.rs2
-rw-r--r--crates/ide/src/status.rs2
-rw-r--r--crates/project-model/src/project_json.rs5
-rw-r--r--crates/project-model/src/tests.rs9
-rw-r--r--crates/project-model/src/workspace.rs5
-rw-r--r--crates/project-model/test_data/crate-attrs.json13
-rw-r--r--crates/project-model/test_data/output/cargo_hello_world_project_model.txt5
-rw-r--r--crates/project-model/test_data/output/cargo_hello_world_project_model_with_selective_overrides.txt5
-rw-r--r--crates/project-model/test_data/output/cargo_hello_world_project_model_with_wildcard_overrides.txt5
-rw-r--r--crates/project-model/test_data/output/rust_project_cfg_groups.txt2
-rw-r--r--crates/project-model/test_data/output/rust_project_crate_attrs.txt54
-rw-r--r--crates/project-model/test_data/output/rust_project_hello_world_project_model.txt1
-rw-r--r--crates/test-fixture/src/lib.rs6
-rw-r--r--crates/test-utils/src/fixture.rs18
-rw-r--r--docs/book/src/non_cargo_based_projects.md9
26 files changed, 473 insertions, 41 deletions
diff --git a/crates/base-db/src/input.rs b/crates/base-db/src/input.rs
index 14649dde64..240f126491 100644
--- a/crates/base-db/src/input.rs
+++ b/crates/base-db/src/input.rs
@@ -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(),
diff --git a/crates/hir-def/src/attrs.rs b/crates/hir-def/src/attrs.rs
index febc794b5a..34a9230794 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<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));
+ }
}
diff --git a/crates/hir-def/src/item_tree.rs b/crates/hir-def/src/item_tree.rs
index 2a104fff2b..6eab8888d9 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<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
diff --git a/crates/hir-def/src/item_tree/attrs.rs b/crates/hir-def/src/item_tree/attrs.rs
index 5c635a4b38..81a9b28b62 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<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)]
diff --git a/crates/hir-def/src/item_tree/tests.rs b/crates/hir-def/src/item_tree/tests.rs
index a57432f33c..1926ed74e8 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 08edf41c56..10581378be 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 5724334601..225ba95863 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 80fac3512e..b65a24d61c 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<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 {
diff --git a/crates/ide-diagnostics/src/handlers/unused_variables.rs b/crates/ide-diagnostics/src/handlers/unused_variables.rs
index b7ec8fa53f..52a2f44fd0 100644
--- a/crates/ide-diagnostics/src/handlers/unused_variables.rs
+++ b/crates/ide-diagnostics/src/handlers/unused_variables.rs
@@ -390,4 +390,44 @@ fn f(S { field }: error) {
"#,
);
}
+
+ #[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 2b8474c316..0b32144249 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<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,
diff --git a/crates/ide/src/doc_links/tests.rs b/crates/ide/src/doc_links/tests.rs
index 8594a0a224..a61a6c677f 100644
--- a/crates/ide/src/doc_links/tests.rs
+++ b/crates/ide/src/doc_links/tests.rs
@@ -659,6 +659,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(
r#"
diff --git a/crates/ide/src/lib.rs b/crates/ide/src/lib.rs
index 9436264904..0066ceed21 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 cfcd76d2aa..7f377e416b 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<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
diff --git a/crates/project-model/src/project_json.rs b/crates/project-model/src/project_json.rs
index 041b9accf4..b3478d2cfe 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<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>,
diff --git a/crates/project-model/src/tests.rs b/crates/project-model/src/tests.rs
index 1908fc0290..a03ed562e1 100644
--- a/crates/project-model/src/tests.rs
+++ b/crates/project-model/src/tests.rs
@@ -199,6 +199,15 @@ fn rust_project_cfg_groups() {
}
#[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 747aa68f08..fa3a79e041 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 0000000000..b2a7e37150
--- /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 4f6ce4dc95..a895ef53af 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 4f6ce4dc95..a895ef53af 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 6862918e09..9eb47947b6 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 28ad3236ae..32f9206a3e 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 0000000000..21b484bc0c
--- /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 dabb3aa674..de793115b9 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 5e8b250c24..01e4215cfb 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<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,
}
}
diff --git a/crates/test-utils/src/fixture.rs b/crates/test-utils/src/fixture.rs
index 831d2b30c1..1f6262c897 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<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());
}
diff --git a/docs/book/src/non_cargo_based_projects.md b/docs/book/src/non_cargo_based_projects.md
index 74489a9046..e7df4a5d76 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;