Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'crates/hir-expand/src/lib.rs')
| -rw-r--r-- | crates/hir-expand/src/lib.rs | 571 |
1 files changed, 485 insertions, 86 deletions
diff --git a/crates/hir-expand/src/lib.rs b/crates/hir-expand/src/lib.rs index 0ba04bc395..719a38edf4 100644 --- a/crates/hir-expand/src/lib.rs +++ b/crates/hir-expand/src/lib.rs @@ -33,17 +33,20 @@ use thin_vec::ThinVec; use triomphe::Arc; use core::fmt; -use std::{hash::Hash, ops}; +use std::{borrow::Cow, ops}; use base_db::Crate; use either::Either; +use mbe::MatchedArmIndex; use span::{ - Edition, ErasedFileAstId, FileAstId, NO_DOWNMAP_ERASED_FILE_AST_ID_MARKER, Span, SyntaxContext, + AstIdMap, Edition, ErasedFileAstId, FileAstId, NO_DOWNMAP_ERASED_FILE_AST_ID_MARKER, Span, + SyntaxContext, }; use syntax::{ - SyntaxNode, SyntaxToken, TextRange, TextSize, + Parse, SyntaxError, SyntaxNode, SyntaxToken, T, TextRange, TextSize, ast::{self, AstNode}, }; +use syntax_bridge::DocCommentDesugarMode; use crate::{ attrs::AttrId, @@ -51,8 +54,10 @@ use crate::{ BuiltinAttrExpander, BuiltinDeriveExpander, BuiltinFnLikeExpander, EagerExpander, include_input_to_file_id, }, + cfg_process::attr_macro_input_to_token_tree, db::ExpandDatabase, - mod_path::ModPath, + fixup::SyntaxFixupUndoInfo, + hygiene::{span_with_call_site_ctxt, span_with_def_site_ctxt, span_with_mixed_site_ctxt}, proc_macro::{CustomProcMacroExpander, ProcMacroKind, ProcMacros}, span_map::{ExpansionSpanMap, SpanMap}, }; @@ -67,6 +72,18 @@ pub use mbe::{DeclarativeMacro, MacroCallStyle, MacroCallStyles, ValueResult}; pub use tt; +/// This is just to ensure the types of [`MacroCallId::macro_arg_considering_derives`] +/// and [`MacroCallId::macro_arg`] are the same. +type MacroArgResult = (tt::TopSubtree, SyntaxFixupUndoInfo, Span); + +/// Total limit on the number of tokens produced by any macro invocation. +/// +/// If an invocation produces more tokens than this limit, it will not be stored in the database and +/// an error will be emitted. +/// +/// Actual max for `analysis-stats .` at some point: 30672. +const TOKEN_LIMIT: usize = 2_097_152; + #[macro_export] macro_rules! impl_intern_lookup { ($db:ident, $id:ident, $loc:ident) => { @@ -378,75 +395,6 @@ impl MacroCallKind { } } -impl HirFileId { - pub fn edition(self, db: &dyn ExpandDatabase) -> Edition { - match self { - HirFileId::FileId(file_id) => file_id.edition(db), - HirFileId::MacroFile(m) => m.loc(db).def.edition, - } - } - pub fn original_file(self, db: &dyn ExpandDatabase) -> EditionedFileId { - let mut file_id = self; - loop { - match file_id { - HirFileId::FileId(id) => break id, - HirFileId::MacroFile(macro_call_id) => { - file_id = macro_call_id.loc(db).kind.file_id() - } - } - } - } - - pub fn original_file_respecting_includes(mut self, db: &dyn ExpandDatabase) -> EditionedFileId { - loop { - match self { - HirFileId::FileId(id) => break id, - HirFileId::MacroFile(file) => { - let loc = file.loc(db); - if loc.def.is_include() - && let MacroCallKind::FnLike { eager: Some(eager), .. } = &loc.kind - && let Ok(it) = include_input_to_file_id(db, file, &eager.arg) - { - break it; - } - self = loc.kind.file_id(); - } - } - } - } - - pub fn original_call_node(self, db: &dyn ExpandDatabase) -> Option<InRealFile<SyntaxNode>> { - let mut call = self.macro_file()?.loc(db).to_node(db); - loop { - match call.file_id { - HirFileId::FileId(file_id) => { - break Some(InRealFile { file_id, value: call.value }); - } - HirFileId::MacroFile(macro_call_id) => { - call = macro_call_id.loc(db).to_node(db); - } - } - } - } - - pub fn call_node(self, db: &dyn ExpandDatabase) -> Option<InFile<SyntaxNode>> { - Some(self.macro_file()?.loc(db).to_node(db)) - } - - pub fn as_builtin_derive_attr_node( - &self, - db: &dyn ExpandDatabase, - ) -> Option<InFile<ast::Attr>> { - let macro_file = self.macro_file()?; - let loc = macro_file.loc(db); - let attr = match loc.def.kind { - MacroDefKind::BuiltInDerive(..) => loc.to_node(db), - _ => return None, - }; - Some(attr.with_value(ast::Attr::cast(attr.value.clone())?)) - } -} - #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum MacroKind { /// `macro_rules!` or Macros 2.0 macro. @@ -537,6 +485,357 @@ impl MacroCallId { } } +#[salsa::tracked] +impl MacroCallId { + /// Implementation of [`HirFileId::parse_or_expand`] for the macro case. + // FIXME: We should verify that the parsed node is one of the many macro node variants we expect + // instead of having it be untyped + #[salsa::tracked(returns(ref), lru = 512)] + pub fn parse_macro_expansion( + self, + db: &dyn ExpandDatabase, + ) -> ExpandResult<(Parse<SyntaxNode>, ExpansionSpanMap)> { + let _p = tracing::info_span!("parse_macro_expansion").entered(); + let loc = self.loc(db); + let expand_to = loc.expand_to(); + let mbe::ValueResult { value: (tt, matched_arm), err } = self.macro_expand(db, loc); + + let (parse, mut rev_token_map) = token_tree_to_syntax_node(db, &tt, expand_to); + rev_token_map.matched_arm = matched_arm; + + ExpandResult { value: (parse, rev_token_map), err } + } + + pub fn parse_macro_expansion_error( + self, + db: &dyn ExpandDatabase, + ) -> Option<ExpandResult<Arc<[SyntaxError]>>> { + let e: ExpandResult<Arc<[SyntaxError]>> = + self.parse_macro_expansion(db).as_ref().map(|it| Arc::from(it.0.errors())); + if e.value.is_empty() && e.err.is_none() { None } else { Some(e) } + } + + /// This resolves the [MacroCallId] to check if it is a derive macro if so get the [macro_arg] for the derive. + /// Other wise return the [macro_arg] for the macro_call_id. + /// + /// This is not connected to the database so it does not cache the result. However, the inner [macro_arg] query is + /// + /// [macro_arg]: Self::macro_arg + #[allow(deprecated)] // we are macro_arg_considering_derives + pub fn macro_arg_considering_derives<'db>( + self, + db: &'db dyn ExpandDatabase, + kind: &MacroCallKind, + ) -> &'db MacroArgResult { + match kind { + // Get the macro arg for the derive macro + MacroCallKind::Derive { derive_macro_id, .. } => derive_macro_id.macro_arg(db), + // Normal macro arg + _ => self.macro_arg(db), + } + } + + /// Lowers syntactic macro call to a token tree representation. That's a firewall + /// query, only typing in the macro call itself changes the returned + /// subtree. + #[salsa::tracked(returns(ref))] + fn macro_arg(self, db: &dyn ExpandDatabase) -> MacroArgResult { + let loc = self.loc(db); + + if let MacroCallLoc { + def: MacroDefId { kind: MacroDefKind::BuiltInEager(..), .. }, + kind: MacroCallKind::FnLike { eager: Some(eager), .. }, + .. + } = &loc + { + return (eager.arg.clone(), SyntaxFixupUndoInfo::NONE, eager.span); + } + + let (parse, map) = loc.kind.file_id().parse_with_map(db); + let root = parse.syntax_node(); + + let (is_derive, censor_item_tree_attr_ids, item_node, span) = match &loc.kind { + MacroCallKind::FnLike { ast_id, .. } => { + let node = &ast_id.to_ptr(db).to_node(&root); + let path_range = node + .path() + .map_or_else(|| node.syntax().text_range(), |path| path.syntax().text_range()); + let span = map.span_for_range(path_range); + + let dummy_tt = |kind| { + ( + tt::TopSubtree::from_token_trees( + tt::Delimiter { open: span, close: span, kind }, + tt::TokenTreesView::empty(), + ), + SyntaxFixupUndoInfo::default(), + span, + ) + }; + + let Some(tt) = node.token_tree() else { + return dummy_tt(tt::DelimiterKind::Invisible); + }; + let first = tt.left_delimiter_token().map(|it| it.kind()).unwrap_or(T!['(']); + let last = tt.right_delimiter_token().map(|it| it.kind()).unwrap_or(T![.]); + + let mismatched_delimiters = !matches!( + (first, last), + (T!['('], T![')']) | (T!['['], T![']']) | (T!['{'], T!['}']) + ); + if mismatched_delimiters { + // Don't expand malformed (unbalanced) macro invocations. This is + // less than ideal, but trying to expand unbalanced macro calls + // sometimes produces pathological, deeply nested code which breaks + // all kinds of things. + // + // So instead, we'll return an empty subtree here + cov_mark::hit!(issue9358_bad_macro_stack_overflow); + + let kind = match first { + _ if loc.def.is_proc_macro() => tt::DelimiterKind::Invisible, + T!['('] => tt::DelimiterKind::Parenthesis, + T!['['] => tt::DelimiterKind::Bracket, + T!['{'] => tt::DelimiterKind::Brace, + _ => tt::DelimiterKind::Invisible, + }; + return dummy_tt(kind); + } + + let mut tt = syntax_bridge::syntax_node_to_token_tree( + tt.syntax(), + map, + span, + if loc.def.is_proc_macro() { + DocCommentDesugarMode::ProcMacro + } else { + DocCommentDesugarMode::Mbe + }, + ); + if loc.def.is_proc_macro() { + // proc macros expect their inputs without parentheses, MBEs expect it with them included + tt.set_top_subtree_delimiter_kind(tt::DelimiterKind::Invisible); + } + return (tt, SyntaxFixupUndoInfo::NONE, span); + } + // MacroCallKind::Derive should not be here. As we are getting the argument for the derive macro + MacroCallKind::Derive { .. } => { + unreachable!("`ExpandDatabase::macro_arg` called with `MacroCallKind::Derive`") + } + MacroCallKind::Attr { ast_id, censored_attr_ids: attr_ids, .. } => { + let node = ast_id.to_ptr(db).to_node(&root); + let (_, attr) = + attr_ids.invoc_attr().find_attr_range_with_source(db, loc.krate, &node); + let range = attr + .path() + .map(|path| path.syntax().text_range()) + .unwrap_or_else(|| attr.syntax().text_range()); + let span = map.span_for_range(range); + + let is_derive = matches!(loc.def.kind, MacroDefKind::BuiltInAttr(_, expander) if expander.is_derive()); + (is_derive, &**attr_ids, node, span) + } + }; + + let (mut tt, undo_info) = attr_macro_input_to_token_tree( + db, + item_node.syntax(), + map, + span, + is_derive, + censor_item_tree_attr_ids, + loc.krate, + ); + + if loc.def.is_proc_macro() { + // proc macros expect their inputs without parentheses, MBEs expect it with them included + tt.set_top_subtree_delimiter_kind(tt::DelimiterKind::Invisible); + } + + (tt, undo_info, span) + } + + fn macro_expand<'db>( + self, + db: &'db dyn ExpandDatabase, + loc: &MacroCallLoc, + ) -> ExpandResult<(Cow<'db, tt::TopSubtree>, MatchedArmIndex)> { + let _p = tracing::info_span!("macro_expand").entered(); + + let (ExpandResult { value: (tt, matched_arm), err }, span) = match loc.def.kind { + MacroDefKind::ProcMacro(..) => { + return self.expand_proc_macro(db).as_ref().map(|it| (Cow::Borrowed(it), None)); + } + _ => { + let (macro_arg, undo_info, span) = + self.macro_arg_considering_derives(db, &loc.kind); + let span = *span; + + let arg = macro_arg; + let res = match loc.def.kind { + MacroDefKind::Declarative(id, _) => { + db.decl_macro_expander(loc.def.krate, id).expand(db, arg, self, span) + } + MacroDefKind::BuiltIn(_, it) => { + it.expand(db, self, arg, span).map_err(Into::into).zip_val(None) + } + MacroDefKind::BuiltInDerive(_, it) => { + it.expand(db, self, arg, span).map_err(Into::into).zip_val(None) + } + MacroDefKind::UnimplementedBuiltIn(_) => { + expand_unimplemented_builtin_macro(span).zip_val(None) + } + MacroDefKind::BuiltInEager(_, it) => { + // This might look a bit odd, but we do not expand the inputs to eager macros here. + // Eager macros inputs are expanded, well, eagerly when we collect the macro calls. + // That kind of expansion uses the ast id map of an eager macros input though which goes through + // the HirFileId machinery. As eager macro inputs are assigned a macro file id that query + // will end up going through here again, whereas we want to just want to inspect the raw input. + // As such we just return the input subtree here. + let eager = match &loc.kind { + MacroCallKind::FnLike { eager: None, .. } => { + return ExpandResult::ok(Cow::Borrowed(macro_arg)).zip_val(None); + } + MacroCallKind::FnLike { eager: Some(eager), .. } => Some(&**eager), + _ => None, + }; + + let mut res = it.expand(db, self, arg, span).map_err(Into::into); + + if let Some(EagerCallInfo { error, .. }) = eager { + // FIXME: We should report both errors! + res.err = error.clone().or(res.err); + } + res.zip_val(None) + } + MacroDefKind::BuiltInAttr(_, it) => { + let mut res = it.expand(db, self, arg, span); + fixup::reverse_fixups(&mut res.value, undo_info); + res.zip_val(None) + } + MacroDefKind::ProcMacro(_, _, _) => unreachable!(), + }; + (res, span) + } + }; + + // Skip checking token tree limit for include! macro call + if !loc.def.is_include() { + // Set a hard limit for the expanded tt + if let Err(value) = check_tt_count(&tt) { + return value + .map(|()| Cow::Owned(tt::TopSubtree::empty(tt::DelimSpan::from_single(span)))) + .zip_val(matched_arm); + } + } + + ExpandResult { value: (Cow::Owned(tt), matched_arm), err } + } + + /// Special case of [`Self::macro_expand`] for procedural macros. We can't LRU + /// proc macros, since they are not deterministic in general, and + /// non-determinism breaks salsa in a very, very, very bad way. + /// @edwin0cheng heroically debugged this once! See #4315 for details + #[salsa::tracked(returns(ref))] + fn expand_proc_macro(self, db: &dyn ExpandDatabase) -> ExpandResult<tt::TopSubtree> { + let loc = self.loc(db); + let (macro_arg, undo_info, span) = self.macro_arg_considering_derives(db, &loc.kind); + + let (ast, expander) = match loc.def.kind { + MacroDefKind::ProcMacro(ast, expander, _) => (ast, expander), + _ => unreachable!(), + }; + + let attr_arg = match &loc.kind { + MacroCallKind::Attr { attr_args: Some(attr_args), .. } => Some(&**attr_args), + _ => None, + }; + + let ExpandResult { value: mut tt, err } = { + let span = proc_macro_span(db, ast); + expander.expand( + db, + loc.def.krate, + loc.krate, + macro_arg, + attr_arg, + span_with_def_site_ctxt(db, span, self.into(), loc.def.edition), + span_with_call_site_ctxt(db, span, self.into(), loc.def.edition), + span_with_mixed_site_ctxt(db, span, self.into(), loc.def.edition), + ) + }; + + // Set a hard limit for the expanded tt + if let Err(value) = check_tt_count(&tt) { + return value.map(|()| tt::TopSubtree::empty(tt::DelimSpan::from_single(*span))); + } + + fixup::reverse_fixups(&mut tt, undo_info); + + ExpandResult { value: tt, err } + } +} + +fn expand_unimplemented_builtin_macro(span: Span) -> ExpandResult<tt::TopSubtree> { + ExpandResult::new( + tt::TopSubtree::empty(tt::DelimSpan::from_single(span)), + ExpandError::other(span, "this built-in macro is not implemented"), + ) +} + +/// Retrieves the span to be used for a proc-macro expansions spans. +/// This is a firewall query as it requires parsing the file, which we don't want proc-macros to +/// directly depend on as that would cause to frequent invalidations, mainly because of the +/// parse queries being LRU cached. If they weren't the invalidations would only happen if the +/// user wrote in the file that defines the proc-macro. +fn proc_macro_span(db: &dyn ExpandDatabase, ast: AstId<ast::Fn>) -> Span { + #[salsa::tracked] + fn proc_macro_span(db: &dyn ExpandDatabase, ast: AstId<ast::Fn>, _: ()) -> Span { + let root = ast.file_id.parse_or_expand(db); + let ast_id_map = ast.file_id.ast_id_map(db); + let span_map = db.span_map(ast.file_id); + + let node = ast_id_map.get(ast.value).to_node(&root); + let range = ast::HasName::name(&node) + .map_or_else(|| node.syntax().text_range(), |name| name.syntax().text_range()); + span_map.span_for_range(range) + } + proc_macro_span(db, ast, ()) +} + +pub(crate) fn token_tree_to_syntax_node( + db: &dyn ExpandDatabase, + tt: &tt::TopSubtree, + expand_to: ExpandTo, +) -> (Parse<SyntaxNode>, ExpansionSpanMap) { + let entry_point = match expand_to { + ExpandTo::Statements => syntax_bridge::TopEntryPoint::MacroStmts, + ExpandTo::Items => syntax_bridge::TopEntryPoint::MacroItems, + ExpandTo::Pattern => syntax_bridge::TopEntryPoint::Pattern, + ExpandTo::Type => syntax_bridge::TopEntryPoint::Type, + ExpandTo::Expr => syntax_bridge::TopEntryPoint::Expr, + }; + syntax_bridge::token_tree_to_syntax_node(tt, entry_point, &mut |ctx| ctx.edition(db)) +} + +fn check_tt_count(tt: &tt::TopSubtree) -> Result<(), ExpandResult<()>> { + let tt = tt.top_subtree(); + let count = tt.count(); + if count <= TOKEN_LIMIT { + Ok(()) + } else { + Err(ExpandResult { + value: (), + err: Some(ExpandError::other( + tt.delimiter.open, + format!( + "macro invocation exceeds token limit: produced {count} tokens, limit is {TOKEN_LIMIT}", + ), + )), + }) + } +} + impl MacroDefId { pub fn make_call( self, @@ -556,10 +855,10 @@ impl MacroDefId { | MacroDefKind::BuiltInDerive(id, _) | MacroDefKind::BuiltInEager(id, _) | MacroDefKind::UnimplementedBuiltIn(id) => { - id.with_value(db.ast_id_map(id.file_id).get(id.value).text_range()) + id.with_value(id.file_id.ast_id_map(db).get(id.value).text_range()) } MacroDefKind::ProcMacro(id, _, _) => { - id.with_value(db.ast_id_map(id.file_id).get(id.value).text_range()) + id.with_value(id.file_id.ast_id_map(db).get(id.value).text_range()) } } } @@ -932,7 +1231,7 @@ impl<'db> ExpansionInfo<'db> { let arg_tt = loc.kind.arg(db); let arg_map = db.span_map(arg_tt.file_id); - let (parse, exp_map) = &db.parse_macro_expansion(macro_file).value; + let (parse, exp_map) = ¯o_file.parse_macro_expansion(db).value; let expanded = InMacroFile { file_id: macro_file, value: parse.syntax_node() }; ExpansionInfo { expanded, loc, arg: arg_tt, exp_map, arg_map } @@ -1059,8 +1358,6 @@ impl ExpandTo { } } -intern::impl_internable!(ModPath); - /// Macro ids. That's probably the tricksiest bit in rust-analyzer, and the /// reason why we use salsa at all. /// @@ -1107,6 +1404,17 @@ impl From<MacroCallId> for HirFileId { } } +impl PartialEq<EditionedFileId> for HirFileId { + fn eq(&self, &other: &EditionedFileId) -> bool { + *self == HirFileId::from(other) + } +} +impl PartialEq<HirFileId> for EditionedFileId { + fn eq(&self, &other: &HirFileId) -> bool { + other == HirFileId::from(*self) + } +} + impl HirFileId { #[inline] pub fn macro_file(self) -> Option<MacroCallId> { @@ -1134,19 +1442,110 @@ impl HirFileId { HirFileId::FileId(_) => SyntaxContext::root(edition), HirFileId::MacroFile(m) => { let kind = &m.loc(db).kind; - db.macro_arg_considering_derives(m, kind).2.ctx + m.macro_arg_considering_derives(db, kind).2.ctx } } } -} -impl PartialEq<EditionedFileId> for HirFileId { - fn eq(&self, &other: &EditionedFileId) -> bool { - *self == HirFileId::from(other) + pub fn edition(self, db: &dyn ExpandDatabase) -> Edition { + match self { + HirFileId::FileId(file_id) => file_id.edition(db), + HirFileId::MacroFile(m) => m.loc(db).def.edition, + } + } + + pub fn original_file(self, db: &dyn ExpandDatabase) -> EditionedFileId { + let mut file_id = self; + loop { + match file_id { + HirFileId::FileId(id) => break id, + HirFileId::MacroFile(macro_call_id) => { + file_id = macro_call_id.loc(db).kind.file_id() + } + } + } + } + + pub fn original_file_respecting_includes(mut self, db: &dyn ExpandDatabase) -> EditionedFileId { + loop { + match self { + HirFileId::FileId(id) => break id, + HirFileId::MacroFile(file) => { + let loc = file.loc(db); + if loc.def.is_include() + && let MacroCallKind::FnLike { eager: Some(eager), .. } = &loc.kind + && let Ok(it) = include_input_to_file_id(db, file, &eager.arg) + { + break it; + } + self = loc.kind.file_id(); + } + } + } + } + + pub fn original_call_node(self, db: &dyn ExpandDatabase) -> Option<InRealFile<SyntaxNode>> { + let mut call = self.macro_file()?.loc(db).to_node(db); + loop { + match call.file_id { + HirFileId::FileId(file_id) => { + break Some(InRealFile { file_id, value: call.value }); + } + HirFileId::MacroFile(macro_call_id) => { + call = macro_call_id.loc(db).to_node(db); + } + } + } + } + + pub fn call_node(self, db: &dyn ExpandDatabase) -> Option<InFile<SyntaxNode>> { + Some(self.macro_file()?.loc(db).to_node(db)) + } + + pub fn as_builtin_derive_attr_node( + &self, + db: &dyn ExpandDatabase, + ) -> Option<InFile<ast::Attr>> { + let macro_file = self.macro_file()?; + let loc = macro_file.loc(db); + let attr = match loc.def.kind { + MacroDefKind::BuiltInDerive(..) => loc.to_node(db), + _ => return None, + }; + Some(attr.with_value(ast::Attr::cast(attr.value.clone())?)) + } + + /// Main public API -- parses a hir file, not caring whether it's a real + /// file or a macro expansion. + pub fn parse_or_expand(self, db: &dyn ExpandDatabase) -> SyntaxNode { + match self { + HirFileId::FileId(file_id) => file_id.parse(db).syntax_node(), + HirFileId::MacroFile(macro_file) => { + macro_file.parse_macro_expansion(db).value.0.syntax_node() + } + } + } + + pub(crate) fn parse_with_map( + self, + db: &dyn ExpandDatabase, + ) -> (Parse<SyntaxNode>, SpanMap<'_>) { + match self { + HirFileId::FileId(file_id) => { + (file_id.parse(db).to_syntax(), SpanMap::RealSpanMap(db.real_span_map(file_id))) + } + HirFileId::MacroFile(macro_file) => { + let (parse, map) = ¯o_file.parse_macro_expansion(db).value; + (parse.clone(), SpanMap::ExpansionSpanMap(map)) + } + } } } -impl PartialEq<HirFileId> for EditionedFileId { - fn eq(&self, &other: &HirFileId) -> bool { - other == HirFileId::from(*self) + +#[salsa::tracked] +impl HirFileId { + #[salsa::tracked(lru = 1024, returns(ref))] + pub fn ast_id_map(self, db: &dyn ExpandDatabase) -> AstIdMap { + AstIdMap::from_source(&self.parse_or_expand(db)) } } |