diff --git a/crates/base-db/src/fixture.rs b/crates/base-db/src/fixture.rs index 3f5ccb621c76..c5fe769a5c78 100644 --- a/crates/base-db/src/fixture.rs +++ b/crates/base-db/src/fixture.rs @@ -349,6 +349,7 @@ pub fn identity(_attr: TokenStream, item: TokenStream) -> TokenStream { ProcMacro { name: "identity".into(), kind: crate::ProcMacroKind::Attr, + ignored: false, expander: sync::Arc::new(IdentityProcMacroExpander), }, ), @@ -363,6 +364,7 @@ pub fn derive_identity(item: TokenStream) -> TokenStream { ProcMacro { name: "DeriveIdentity".into(), kind: crate::ProcMacroKind::CustomDerive, + ignored: false, expander: sync::Arc::new(IdentityProcMacroExpander), }, ), @@ -377,6 +379,7 @@ pub fn input_replace(attr: TokenStream, _item: TokenStream) -> TokenStream { ProcMacro { name: "input_replace".into(), kind: crate::ProcMacroKind::Attr, + ignored: false, expander: sync::Arc::new(AttributeInputReplaceProcMacroExpander), }, ), @@ -391,6 +394,7 @@ pub fn mirror(input: TokenStream) -> TokenStream { ProcMacro { name: "mirror".into(), kind: crate::ProcMacroKind::FuncLike, + ignored: false, expander: sync::Arc::new(MirrorProcMacroExpander), }, ), @@ -405,6 +409,7 @@ pub fn shorten(input: TokenStream) -> TokenStream { ProcMacro { name: "shorten".into(), kind: crate::ProcMacroKind::FuncLike, + ignored: false, expander: sync::Arc::new(ShortenProcMacroExpander), }, ), diff --git a/crates/base-db/src/input.rs b/crates/base-db/src/input.rs index 65db5c0fc7d3..43da186e9bc5 100644 --- a/crates/base-db/src/input.rs +++ b/crates/base-db/src/input.rs @@ -271,6 +271,9 @@ pub type TargetLayoutLoadResult = Result, Arc>; pub struct ProcMacro { pub name: SmolStr, pub kind: ProcMacroKind, + /// Ignored proc macros should only be expanded + /// when we are explicitly asked to do so by the user. + pub ignored: bool, pub expander: sync::Arc, } diff --git a/crates/hir-def/src/data.rs b/crates/hir-def/src/data.rs index 72ccc17486f0..c4d7d4784724 100644 --- a/crates/hir-def/src/data.rs +++ b/crates/hir-def/src/data.rs @@ -635,11 +635,11 @@ impl<'a> AssocItemCollector<'a> { attr, ) { Ok(ResolvedAttr::Macro(call_id)) => { - self.attr_calls.push((ast_id, call_id)); // If proc attribute macro expansion is disabled, skip expanding it here if !self.db.expand_proc_attr_macros() { continue 'attrs; } + let loc = self.db.lookup_intern_macro_call(call_id); if let MacroDefKind::ProcMacro(exp, ..) = loc.def.kind { // If there's no expander for the proc macro (e.g. the @@ -650,8 +650,21 @@ impl<'a> AssocItemCollector<'a> { if exp.is_dummy() { continue 'attrs; } + + // We check whether the proc macro is ignored, if it is, we don't expand it. + let proc_macros = self.db.proc_macros(); + let proc_macro = proc_macros.get(&loc.def.krate).and_then(|m| { + m.as_ref().ok().and_then(|m| m.get(exp.proc_macro_id().0 as usize)) + }); + if let Some(proc_macro) = proc_macro { + if proc_macro.ignored { + continue 'attrs; + } + } } + self.attr_calls.push((ast_id, call_id)); + let res = self.expander.enter_expand_id::(self.db, call_id); self.collect_macro_items(res, &|| loc.kind.clone()); diff --git a/crates/hir-def/src/macro_expansion_tests/mod.rs b/crates/hir-def/src/macro_expansion_tests/mod.rs index 8adced4e0824..df5b84cda718 100644 --- a/crates/hir-def/src/macro_expansion_tests/mod.rs +++ b/crates/hir-def/src/macro_expansion_tests/mod.rs @@ -55,6 +55,7 @@ pub fn identity_when_valid(_attr: TokenStream, item: TokenStream) -> TokenStream ProcMacro { name: "identity_when_valid".into(), kind: base_db::ProcMacroKind::Attr, + ignored: false, expander: sync::Arc::new(IdentityWhenValidProcMacroExpander), }, )]; diff --git a/crates/hir-def/src/nameres/collector.rs b/crates/hir-def/src/nameres/collector.rs index 2d4586146db0..2ffb3bc5d946 100644 --- a/crates/hir-def/src/nameres/collector.rs +++ b/crates/hir-def/src/nameres/collector.rs @@ -1128,6 +1128,41 @@ impl DefCollector<'_> { resolver_def_id, ); if let Ok(Some(call_id)) = call_id { + let loc: MacroCallLoc = self.db.lookup_intern_macro_call(call_id); + + if let MacroDefKind::ProcMacro(expander, _, _) = loc.def.kind { + if expander.is_dummy() { + // If there's no expander for the proc macro (e.g. + // because proc macros are disabled, or building the + // proc macro crate failed), report this and skip + // expansion like we would if it was disabled + self.def_map.diagnostics.push( + DefDiagnostic::unresolved_proc_macro( + directive.module_id, + loc.kind, + loc.def.krate, + ), + ); + + res = ReachedFixedPoint::No; + return false; + } + + // We check whether the proc macro is ignored, if it is, we don't expand it. + let proc_macros = self.db.proc_macros(); + let proc_macro = proc_macros.get(&loc.def.krate).and_then(|m| { + m.as_ref() + .ok() + .and_then(|m| m.get(expander.proc_macro_id().0 as usize)) + }); + if let Some(proc_macro) = proc_macro { + if proc_macro.ignored { + res = ReachedFixedPoint::No; + return false; + } + } + } + push_resolved(directive, call_id); res = ReachedFixedPoint::No; @@ -1306,30 +1341,45 @@ impl DefCollector<'_> { return recollect_without(self); } - // Skip #[test]/#[bench] expansion, which would merely result in more memory usage - // due to duplicating functions into macro expansions - if matches!( - loc.def.kind, + match loc.def.kind { + // Skip #[test]/#[bench] expansion, which would merely result in more memory usage + // due to duplicating functions into macro expansions MacroDefKind::BuiltInAttr(expander, _) - if expander.is_test() || expander.is_bench() - ) { - return recollect_without(self); - } + if expander.is_test() || expander.is_bench() => + { + return recollect_without(self); + } + MacroDefKind::ProcMacro(expander, _, _) => { + if expander.is_dummy() { + // If there's no expander for the proc macro (e.g. + // because proc macros are disabled, or building the + // proc macro crate failed), report this and skip + // expansion like we would if it was disabled + self.def_map.diagnostics.push( + DefDiagnostic::unresolved_proc_macro( + directive.module_id, + loc.kind, + loc.def.krate, + ), + ); - if let MacroDefKind::ProcMacro(exp, ..) = loc.def.kind { - if exp.is_dummy() { - // If there's no expander for the proc macro (e.g. - // because proc macros are disabled, or building the - // proc macro crate failed), report this and skip - // expansion like we would if it was disabled - self.def_map.diagnostics.push(DefDiagnostic::unresolved_proc_macro( - directive.module_id, - loc.kind, - loc.def.krate, - )); + return recollect_without(self); + } - return recollect_without(self); + // We check whether the proc macro is ignored, if it is, we don't expand it. + let proc_macros = self.db.proc_macros(); + let proc_macro = proc_macros.get(&loc.def.krate).and_then(|m| { + m.as_ref() + .ok() + .and_then(|m| m.get(expander.proc_macro_id().0 as usize)) + }); + if let Some(proc_macro) = proc_macro { + if proc_macro.ignored { + return recollect_without(self); + } + } } + _ => {} } self.def_map.modules[directive.module_id] diff --git a/crates/hir-expand/src/proc_macro.rs b/crates/hir-expand/src/proc_macro.rs index 41675c630dcf..bc670f2b6da3 100644 --- a/crates/hir-expand/src/proc_macro.rs +++ b/crates/hir-expand/src/proc_macro.rs @@ -26,6 +26,10 @@ impl ProcMacroExpander { self.proc_macro_id.0 == DUMMY_ID } + pub fn proc_macro_id(&self) -> ProcMacroId { + self.proc_macro_id + } + pub fn expand( self, db: &dyn ExpandDatabase, diff --git a/crates/rust-analyzer/src/config.rs b/crates/rust-analyzer/src/config.rs index f28f6ffb8748..d8e4a59ea787 100644 --- a/crates/rust-analyzer/src/config.rs +++ b/crates/rust-analyzer/src/config.rs @@ -1166,7 +1166,7 @@ impl Config { Some(AbsPathBuf::try_from(path).unwrap_or_else(|path| self.root_path.join(&path))) } - pub fn dummy_replacements(&self) -> &FxHashMap, Box<[Box]>> { + pub fn ignored_proc_macros(&self) -> &FxHashMap, Box<[Box]>> { &self.data.procMacro_ignored } diff --git a/crates/rust-analyzer/src/reload.rs b/crates/rust-analyzer/src/reload.rs index 8dba83ed5edd..114f8b32b1e4 100644 --- a/crates/rust-analyzer/src/reload.rs +++ b/crates/rust-analyzer/src/reload.rs @@ -281,13 +281,13 @@ impl GlobalState { pub(crate) fn fetch_proc_macros(&mut self, cause: Cause, paths: Vec) { tracing::info!(%cause, "will load proc macros"); - let dummy_replacements = self.config.dummy_replacements().clone(); + let ignored_proc_macros = self.config.ignored_proc_macros().clone(); let proc_macro_clients = self.proc_macro_clients.clone(); self.task_pool.handle.spawn_with_sender(ThreadIntent::Worker, move |sender| { sender.send(Task::LoadProcMacros(ProcMacroProgress::Begin)).unwrap(); - let dummy_replacements = &dummy_replacements; + let ignored_proc_macros = &ignored_proc_macros; let progress = { let sender = sender.clone(); &move |msg| { @@ -315,7 +315,13 @@ impl GlobalState { crate_name .as_deref() .and_then(|crate_name| { - dummy_replacements.get(crate_name).map(|v| &**v) + ignored_proc_macros.iter().find_map(|c| { + if eq_ignore_underscore(&*c.0, crate_name) { + Some(&**c.1) + } else { + None + } + }) }) .unwrap_or_default(), ) @@ -622,6 +628,247 @@ impl GlobalState { } } +#[derive(Default)] +pub(crate) struct ProjectFolders { + pub(crate) load: Vec, + pub(crate) watch: Vec, + pub(crate) source_root_config: SourceRootConfig, +} + +impl ProjectFolders { + pub(crate) fn new( + workspaces: &[ProjectWorkspace], + global_excludes: &[AbsPathBuf], + ) -> ProjectFolders { + let mut res = ProjectFolders::default(); + let mut fsc = FileSetConfig::builder(); + let mut local_filesets = vec![]; + + // Dedup source roots + // Depending on the project setup, we can have duplicated source roots, or for example in + // the case of the rustc workspace, we can end up with two source roots that are almost the + // same but not quite, like: + // PackageRoot { is_local: false, include: [AbsPathBuf(".../rust/src/tools/miri/cargo-miri")], exclude: [] } + // PackageRoot { + // is_local: true, + // include: [AbsPathBuf(".../rust/src/tools/miri/cargo-miri"), AbsPathBuf(".../rust/build/x86_64-pc-windows-msvc/stage0-tools/x86_64-pc-windows-msvc/release/build/cargo-miri-85801cd3d2d1dae4/out")], + // exclude: [AbsPathBuf(".../rust/src/tools/miri/cargo-miri/.git"), AbsPathBuf(".../rust/src/tools/miri/cargo-miri/target")] + // } + // + // The first one comes from the explicit rustc workspace which points to the rustc workspace itself + // The second comes from the rustc workspace that we load as the actual project workspace + // These `is_local` differing in this kind of way gives us problems, especially when trying to filter diagnostics as we don't report diagnostics for external libraries. + // So we need to deduplicate these, usually it would be enough to deduplicate by `include`, but as the rustc example shows here that doesn't work, + // so we need to also coalesce the includes if they overlap. + + let mut roots: Vec<_> = workspaces + .iter() + .flat_map(|ws| ws.to_roots()) + .update(|root| root.include.sort()) + .sorted_by(|a, b| a.include.cmp(&b.include)) + .collect(); + + // map that tracks indices of overlapping roots + let mut overlap_map = FxHashMap::<_, Vec<_>>::default(); + let mut done = false; + + while !mem::replace(&mut done, true) { + // maps include paths to indices of the corresponding root + let mut include_to_idx = FxHashMap::default(); + // Find and note down the indices of overlapping roots + for (idx, root) in roots.iter().enumerate().filter(|(_, it)| !it.include.is_empty()) { + for include in &root.include { + match include_to_idx.entry(include) { + Entry::Occupied(e) => { + overlap_map.entry(*e.get()).or_default().push(idx); + } + Entry::Vacant(e) => { + e.insert(idx); + } + } + } + } + for (k, v) in overlap_map.drain() { + done = false; + for v in v { + let r = mem::replace( + &mut roots[v], + PackageRoot { is_local: false, include: vec![], exclude: vec![] }, + ); + roots[k].is_local |= r.is_local; + roots[k].include.extend(r.include); + roots[k].exclude.extend(r.exclude); + } + roots[k].include.sort(); + roots[k].exclude.sort(); + roots[k].include.dedup(); + roots[k].exclude.dedup(); + } + } + + for root in roots.into_iter().filter(|it| !it.include.is_empty()) { + let file_set_roots: Vec = + root.include.iter().cloned().map(VfsPath::from).collect(); + + let entry = { + let mut dirs = vfs::loader::Directories::default(); + dirs.extensions.push("rs".into()); + dirs.include.extend(root.include); + dirs.exclude.extend(root.exclude); + for excl in global_excludes { + if dirs + .include + .iter() + .any(|incl| incl.starts_with(excl) || excl.starts_with(incl)) + { + dirs.exclude.push(excl.clone()); + } + } + + vfs::loader::Entry::Directories(dirs) + }; + + if root.is_local { + res.watch.push(res.load.len()); + } + res.load.push(entry); + + if root.is_local { + local_filesets.push(fsc.len()); + } + fsc.add_file_set(file_set_roots) + } + + let fsc = fsc.build(); + res.source_root_config = SourceRootConfig { fsc, local_filesets }; + + res + } +} + +#[derive(Default, Debug)] +pub(crate) struct SourceRootConfig { + pub(crate) fsc: FileSetConfig, + pub(crate) local_filesets: Vec, +} + +impl SourceRootConfig { + pub(crate) fn partition(&self, vfs: &vfs::Vfs) -> Vec { + let _p = profile::span("SourceRootConfig::partition"); + self.fsc + .partition(vfs) + .into_iter() + .enumerate() + .map(|(idx, file_set)| { + let is_local = self.local_filesets.contains(&idx); + if is_local { + SourceRoot::new_local(file_set) + } else { + SourceRoot::new_library(file_set) + } + }) + .collect() + } +} + +/// Load the proc-macros for the given lib path, marking them as ignored if needed. +pub(crate) fn load_proc_macro( + server: &ProcMacroServer, + path: &AbsPath, + ignored_macros: &[Box], +) -> ProcMacroLoadResult { + let res: Result, String> = (|| { + let dylib = MacroDylib::new(path.to_path_buf()); + let vec = server.load_dylib(dylib).map_err(|e| format!("{e}"))?; + if vec.is_empty() { + return Err("proc macro library returned no proc macros".to_string()); + } + Ok(vec + .into_iter() + .map(|expander| expander_to_proc_macro(expander, ignored_macros)) + .collect()) + })(); + return match res { + Ok(proc_macros) => { + tracing::info!( + "Loaded proc-macros for {path}: {:?}", + proc_macros.iter().map(|it| it.name.clone()).collect::>() + ); + Ok(proc_macros) + } + Err(e) => { + tracing::warn!("proc-macro loading for {path} failed: {e}"); + Err(e) + } + }; + + fn expander_to_proc_macro( + expander: proc_macro_api::ProcMacro, + ignored: &[Box], + ) -> ProcMacro { + let name = SmolStr::from(expander.name()); + let kind = match expander.kind() { + proc_macro_api::ProcMacroKind::CustomDerive => ProcMacroKind::CustomDerive, + proc_macro_api::ProcMacroKind::FuncLike => ProcMacroKind::FuncLike, + proc_macro_api::ProcMacroKind::Attr => ProcMacroKind::Attr, + }; + + let ignored = ignored.iter().any(|ignored| &**ignored == name); + + let expander: sync::Arc = sync::Arc::new(Expander(expander)); + ProcMacro { name, kind, expander, ignored } + } + + #[derive(Debug)] + struct Expander(proc_macro_api::ProcMacro); + + impl ProcMacroExpander for Expander { + fn expand( + &self, + subtree: &tt::Subtree, + attrs: Option<&tt::Subtree>, + env: &Env, + ) -> Result { + let env = env.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect(); + match self.0.expand(subtree, attrs, env) { + Ok(Ok(subtree)) => Ok(subtree), + Ok(Err(err)) => Err(ProcMacroExpansionError::Panic(err.0)), + Err(err) => Err(ProcMacroExpansionError::System(err.to_string())), + } + } + } + + /// Dummy identity expander, used for attribute proc-macros that are deliberately ignored by the user. + #[derive(Debug)] + struct IdentityExpander; + + impl ProcMacroExpander for IdentityExpander { + fn expand( + &self, + subtree: &tt::Subtree, + _: Option<&tt::Subtree>, + _: &Env, + ) -> Result { + Ok(subtree.clone()) + } + } + + /// Empty expander, used for proc-macros that are deliberately ignored by the user. + #[derive(Debug)] + struct EmptyExpander; + + impl ProcMacroExpander for EmptyExpander { + fn expand( + &self, + _: &tt::Subtree, + _: Option<&tt::Subtree>, + _: &Env, + ) -> Result { + Ok(tt::Subtree::empty()) + } + } +} + pub(crate) fn should_refresh_for_change(path: &AbsPath, change_kind: ChangeKind) -> bool { const IMPLICIT_TARGET_FILES: &[&str] = &["build.rs", "src/main.rs", "src/lib.rs"]; const IMPLICIT_TARGET_DIRS: &[&str] = &["src/bin", "examples", "tests", "benches"]; @@ -666,3 +913,18 @@ pub(crate) fn should_refresh_for_change(path: &AbsPath, change_kind: ChangeKind) } false } + +/// Similar to [`str::eq_ignore_ascii_case`] but instead of ignoring +/// case, we say that `-` and `_` are equal. +fn eq_ignore_underscore(s1: &str, s2: &str) -> bool { + if s1.len() != s2.len() { + return false; + } + + s1.as_bytes().iter().zip(s2.as_bytes()).all(|(s1, s2)| { + let s1_underscore = s1 == &b'_' || s1 == &b'-'; + let s2_underscore = s2 == &b'_' || s2 == &b'-'; + + s1 == s2 || (s1_underscore && s2_underscore) + }) +}