Report unresolved idents for implicit captures in format_args!()

And also a bit of cleanup by storing the capture's span with the open quote included.
This commit is contained in:
Chayim Refael Friedman 2024-12-16 07:45:25 +02:00
parent 27e824fad4
commit 54ce1dda3a
7 changed files with 110 additions and 63 deletions

View file

@ -18,6 +18,7 @@ use smallvec::SmallVec;
use span::{Edition, MacroFileId}; use span::{Edition, MacroFileId};
use syntax::{ast, AstPtr, SyntaxNodePtr}; use syntax::{ast, AstPtr, SyntaxNodePtr};
use triomphe::Arc; use triomphe::Arc;
use tt::TextRange;
use crate::{ use crate::{
db::DefDatabase, db::DefDatabase,
@ -143,15 +144,7 @@ pub struct BodySourceMap {
pub types: TypesSourceMap, pub types: TypesSourceMap,
// FIXME: Make this a sane struct. template_map: Option<Box<FormatTemplate>>,
template_map: Option<
Box<(
// format_args!
FxHashMap<ExprId, (HygieneId, Vec<(syntax::TextRange, Name)>)>,
// asm!
FxHashMap<ExprId, Vec<Vec<(syntax::TextRange, usize)>>>,
)>,
>,
expansions: FxHashMap<InFile<AstPtr<ast::MacroCall>>, MacroFileId>, expansions: FxHashMap<InFile<AstPtr<ast::MacroCall>>, MacroFileId>,
@ -160,6 +153,20 @@ pub struct BodySourceMap {
diagnostics: Vec<BodyDiagnostic>, diagnostics: Vec<BodyDiagnostic>,
} }
#[derive(Default, Debug, Eq, PartialEq)]
struct FormatTemplate {
/// A map from `format_args!()` expressions to their captures.
format_args_to_captures: FxHashMap<ExprId, (HygieneId, Vec<(syntax::TextRange, Name)>)>,
/// A map from `asm!()` expressions to their captures.
asm_to_captures: FxHashMap<ExprId, Vec<Vec<(syntax::TextRange, usize)>>>,
/// A map from desugared expressions of implicit captures to their source.
///
/// The value stored for each capture is its template literal and offset inside it. The template literal
/// is from the `format_args[_nl]!()` macro and so needs to be mapped up once to go to the user-written
/// template.
implicit_capture_to_source: FxHashMap<ExprId, InFile<(AstPtr<ast::Expr>, TextRange)>>,
}
#[derive(Debug, Eq, PartialEq)] #[derive(Debug, Eq, PartialEq)]
pub enum BodyDiagnostic { pub enum BodyDiagnostic {
InactiveCode { node: InFile<SyntaxNodePtr>, cfg: CfgExpr, opts: CfgOptions }, InactiveCode { node: InFile<SyntaxNodePtr>, cfg: CfgExpr, opts: CfgOptions },
@ -798,18 +805,29 @@ impl BodySourceMap {
node: InFile<&ast::FormatArgsExpr>, node: InFile<&ast::FormatArgsExpr>,
) -> Option<(HygieneId, &[(syntax::TextRange, Name)])> { ) -> Option<(HygieneId, &[(syntax::TextRange, Name)])> {
let src = node.map(AstPtr::new).map(AstPtr::upcast::<ast::Expr>); let src = node.map(AstPtr::new).map(AstPtr::upcast::<ast::Expr>);
let (hygiene, names) = let (hygiene, names) = self
self.template_map.as_ref()?.0.get(&self.expr_map.get(&src)?.as_expr()?)?; .template_map
.as_ref()?
.format_args_to_captures
.get(&self.expr_map.get(&src)?.as_expr()?)?;
Some((*hygiene, &**names)) Some((*hygiene, &**names))
} }
pub fn format_args_implicit_capture(
&self,
capture_expr: ExprId,
) -> Option<InFile<(AstPtr<ast::Expr>, TextRange)>> {
self.template_map.as_ref()?.implicit_capture_to_source.get(&capture_expr).copied()
}
pub fn asm_template_args( pub fn asm_template_args(
&self, &self,
node: InFile<&ast::AsmExpr>, node: InFile<&ast::AsmExpr>,
) -> Option<(ExprId, &[Vec<(syntax::TextRange, usize)>])> { ) -> Option<(ExprId, &[Vec<(syntax::TextRange, usize)>])> {
let src = node.map(AstPtr::new).map(AstPtr::upcast::<ast::Expr>); let src = node.map(AstPtr::new).map(AstPtr::upcast::<ast::Expr>);
let expr = self.expr_map.get(&src)?.as_expr()?; let expr = self.expr_map.get(&src)?.as_expr()?;
Some(expr).zip(self.template_map.as_ref()?.1.get(&expr).map(std::ops::Deref::deref)) Some(expr)
.zip(self.template_map.as_ref()?.asm_to_captures.get(&expr).map(std::ops::Deref::deref))
} }
/// Get a reference to the body source map's diagnostics. /// Get a reference to the body source map's diagnostics.
@ -835,8 +853,14 @@ impl BodySourceMap {
types, types,
} = self; } = self;
if let Some(template_map) = template_map { if let Some(template_map) = template_map {
template_map.0.shrink_to_fit(); let FormatTemplate {
template_map.1.shrink_to_fit(); format_args_to_captures,
asm_to_captures,
implicit_capture_to_source,
} = &mut **template_map;
format_args_to_captures.shrink_to_fit();
asm_to_captures.shrink_to_fit();
implicit_capture_to_source.shrink_to_fit();
} }
expr_map.shrink_to_fit(); expr_map.shrink_to_fit();
expr_map_back.shrink_to_fit(); expr_map_back.shrink_to_fit();

View file

@ -1957,8 +1957,10 @@ impl ExprCollector<'_> {
_ => None, _ => None,
}); });
let mut mappings = vec![]; let mut mappings = vec![];
let (fmt, hygiene) = match template.and_then(|it| self.expand_macros_to_string(it)) { let (fmt, hygiene) = match template.and_then(|template| {
Some((s, is_direct_literal)) => { self.expand_macros_to_string(template.clone()).map(|it| (it, template))
}) {
Some(((s, is_direct_literal), template)) => {
let call_ctx = self.expander.syntax_context(); let call_ctx = self.expander.syntax_context();
let hygiene = self.hygiene_id_for(s.syntax().text_range().start()); let hygiene = self.hygiene_id_for(s.syntax().text_range().start());
let fmt = format_args::parse( let fmt = format_args::parse(
@ -1966,8 +1968,18 @@ impl ExprCollector<'_> {
fmt_snippet, fmt_snippet,
args, args,
is_direct_literal, is_direct_literal,
|name| { |name, range| {
let expr_id = self.alloc_expr_desugared(Expr::Path(Path::from(name))); let expr_id = self.alloc_expr_desugared(Expr::Path(Path::from(name)));
if let Some(range) = range {
self.source_map
.template_map
.get_or_insert_with(Default::default)
.implicit_capture_to_source
.insert(
expr_id,
self.expander.in_file((AstPtr::new(&template), range)),
);
}
if !hygiene.is_root() { if !hygiene.is_root() {
self.body.expr_hygiene.insert(expr_id, hygiene); self.body.expr_hygiene.insert(expr_id, hygiene);
} }
@ -2139,7 +2151,7 @@ impl ExprCollector<'_> {
self.source_map self.source_map
.template_map .template_map
.get_or_insert_with(Default::default) .get_or_insert_with(Default::default)
.0 .format_args_to_captures
.insert(idx, (hygiene, mappings)); .insert(idx, (hygiene, mappings));
idx idx
} }

View file

@ -6,7 +6,7 @@ use syntax::{
ast::{self, HasName, IsString}, ast::{self, HasName, IsString},
AstNode, AstPtr, AstToken, T, AstNode, AstPtr, AstToken, T,
}; };
use tt::{TextRange, TextSize}; use tt::TextRange;
use crate::{ use crate::{
body::lower::{ExprCollector, FxIndexSet}, body::lower::{ExprCollector, FxIndexSet},
@ -224,7 +224,7 @@ impl ExprCollector<'_> {
TextRange::new( TextRange::new(
inner_span.start.try_into().unwrap(), inner_span.start.try_into().unwrap(),
inner_span.end.try_into().unwrap(), inner_span.end.try_into().unwrap(),
) - TextSize::from(str_style.map(|it| it + 1).unwrap_or(0) as u32 + 1) )
}) })
}; };
for piece in unverified_pieces { for piece in unverified_pieces {
@ -268,7 +268,11 @@ impl ExprCollector<'_> {
Expr::InlineAsm(InlineAsm { operands: operands.into_boxed_slice(), options }), Expr::InlineAsm(InlineAsm { operands: operands.into_boxed_slice(), options }),
syntax_ptr, syntax_ptr,
); );
self.source_map.template_map.get_or_insert_with(Default::default).1.insert(idx, mappings); self.source_map
.template_map
.get_or_insert_with(Default::default)
.asm_to_captures
.insert(idx, mappings);
idx idx
} }
} }

View file

@ -1,5 +1,6 @@
//! Parses `format_args` input. //! Parses `format_args` input.
use either::Either;
use hir_expand::name::Name; use hir_expand::name::Name;
use intern::Symbol; use intern::Symbol;
use rustc_parse_format as parse; use rustc_parse_format as parse;
@ -7,7 +8,7 @@ use span::SyntaxContextId;
use stdx::TupleExt; use stdx::TupleExt;
use syntax::{ use syntax::{
ast::{self, IsString}, ast::{self, IsString},
TextRange, TextSize, TextRange,
}; };
use crate::hir::ExprId; use crate::hir::ExprId;
@ -33,7 +34,7 @@ pub enum FormatArgsPiece {
Placeholder(FormatPlaceholder), Placeholder(FormatPlaceholder),
} }
#[derive(Copy, Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct FormatPlaceholder { pub struct FormatPlaceholder {
/// Index into [`FormatArgs::arguments`]. /// Index into [`FormatArgs::arguments`].
pub argument: FormatArgPosition, pub argument: FormatArgPosition,
@ -45,11 +46,11 @@ pub struct FormatPlaceholder {
pub format_options: FormatOptions, pub format_options: FormatOptions,
} }
#[derive(Copy, Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct FormatArgPosition { pub struct FormatArgPosition {
/// Which argument this position refers to (Ok), /// Which argument this position refers to (Ok),
/// or would've referred to if it existed (Err). /// or would've referred to if it existed (Err).
pub index: Result<usize, usize>, pub index: Result<usize, Either<usize, Name>>,
/// What kind of position this is. See [`FormatArgPositionKind`]. /// What kind of position this is. See [`FormatArgPositionKind`].
pub kind: FormatArgPositionKind, pub kind: FormatArgPositionKind,
/// The span of the name or number. /// The span of the name or number.
@ -88,7 +89,7 @@ pub enum FormatTrait {
UpperHex, UpperHex,
} }
#[derive(Copy, Clone, Default, Debug, PartialEq, Eq)] #[derive(Clone, Default, Debug, PartialEq, Eq)]
pub struct FormatOptions { pub struct FormatOptions {
/// The width. E.g. `{:5}` or `{:width$}`. /// The width. E.g. `{:5}` or `{:width$}`.
pub width: Option<FormatCount>, pub width: Option<FormatCount>,
@ -133,7 +134,7 @@ pub enum FormatAlignment {
Center, Center,
} }
#[derive(Copy, Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub enum FormatCount { pub enum FormatCount {
/// `{:5}` or `{:.5}` /// `{:5}` or `{:.5}`
Literal(usize), Literal(usize),
@ -173,7 +174,7 @@ pub(crate) fn parse(
fmt_snippet: Option<String>, fmt_snippet: Option<String>,
mut args: FormatArgumentsCollector, mut args: FormatArgumentsCollector,
is_direct_literal: bool, is_direct_literal: bool,
mut synth: impl FnMut(Name) -> ExprId, mut synth: impl FnMut(Name, Option<TextRange>) -> ExprId,
mut record_usage: impl FnMut(Name, Option<TextRange>), mut record_usage: impl FnMut(Name, Option<TextRange>),
call_ctx: SyntaxContextId, call_ctx: SyntaxContextId,
) -> FormatArgs { ) -> FormatArgs {
@ -192,7 +193,6 @@ pub(crate) fn parse(
} }
None => None, None => None,
}; };
let mut parser = let mut parser =
parse::Parser::new(&text, str_style, fmt_snippet, false, parse::ParseMode::Format); parse::Parser::new(&text, str_style, fmt_snippet, false, parse::ParseMode::Format);
@ -217,7 +217,6 @@ pub(crate) fn parse(
let to_span = |inner_span: parse::InnerSpan| { let to_span = |inner_span: parse::InnerSpan| {
is_source_literal.then(|| { is_source_literal.then(|| {
TextRange::new(inner_span.start.try_into().unwrap(), inner_span.end.try_into().unwrap()) TextRange::new(inner_span.start.try_into().unwrap(), inner_span.end.try_into().unwrap())
- TextSize::from(str_style.map(|it| it + 1).unwrap_or(0) as u32 + 1)
}) })
}; };
@ -245,8 +244,8 @@ pub(crate) fn parse(
Ok(index) Ok(index)
} else { } else {
// Doesn't exist as an explicit argument. // Doesn't exist as an explicit argument.
invalid_refs.push((index, span, used_as, kind)); invalid_refs.push((Either::Left(index), span, used_as, kind));
Err(index) Err(Either::Left(index))
} }
} }
ArgRef::Name(name, span) => { ArgRef::Name(name, span) => {
@ -265,14 +264,17 @@ pub(crate) fn parse(
// For the moment capturing variables from format strings expanded from macros is // For the moment capturing variables from format strings expanded from macros is
// disabled (see RFC #2795) // disabled (see RFC #2795)
// FIXME: Diagnose // FIXME: Diagnose
invalid_refs.push((Either::Right(name.clone()), span, used_as, kind));
Err(Either::Right(name))
} else {
record_usage(name.clone(), span);
Ok(args.add(FormatArgument {
kind: FormatArgumentKind::Captured(name.clone()),
// FIXME: This is problematic, we might want to synthesize a dummy
// expression proper and/or desugar these.
expr: synth(name, span),
}))
} }
record_usage(name.clone(), span);
Ok(args.add(FormatArgument {
kind: FormatArgumentKind::Captured(name.clone()),
// FIXME: This is problematic, we might want to synthesize a dummy
// expression proper and/or desugar these.
expr: synth(name),
}))
} }
} }
}; };

View file

@ -262,7 +262,7 @@ pub struct UnresolvedAssocItem {
#[derive(Debug)] #[derive(Debug)]
pub struct UnresolvedIdent { pub struct UnresolvedIdent {
pub expr_or_pat: InFile<AstPtr<Either<ast::Expr, ast::Pat>>>, pub node: InFile<(AstPtr<Either<ast::Expr, ast::Pat>>, Option<TextRange>)>,
} }
#[derive(Debug)] #[derive(Debug)]
@ -550,11 +550,10 @@ impl AnyDiagnostic {
source_map: &hir_def::body::BodySourceMap, source_map: &hir_def::body::BodySourceMap,
) -> Option<AnyDiagnostic> { ) -> Option<AnyDiagnostic> {
let expr_syntax = |expr| { let expr_syntax = |expr| {
source_map.expr_syntax(expr).inspect_err(|_| tracing::error!("synthetic syntax")).ok() source_map.expr_syntax(expr).inspect_err(|_| stdx::never!("synthetic syntax")).ok()
};
let pat_syntax = |pat| {
source_map.pat_syntax(pat).inspect_err(|_| tracing::error!("synthetic syntax")).ok()
}; };
let pat_syntax =
|pat| source_map.pat_syntax(pat).inspect_err(|_| stdx::never!("synthetic syntax")).ok();
let expr_or_pat_syntax = |id| match id { let expr_or_pat_syntax = |id| match id {
ExprOrPatId::ExprId(expr) => expr_syntax(expr).map(|it| it.map(AstPtr::wrap_left)), ExprOrPatId::ExprId(expr) => expr_syntax(expr).map(|it| it.map(AstPtr::wrap_left)),
ExprOrPatId::PatId(pat) => pat_syntax(pat), ExprOrPatId::PatId(pat) => pat_syntax(pat),
@ -626,8 +625,16 @@ impl AnyDiagnostic {
UnresolvedAssocItem { expr_or_pat }.into() UnresolvedAssocItem { expr_or_pat }.into()
} }
&InferenceDiagnostic::UnresolvedIdent { id } => { &InferenceDiagnostic::UnresolvedIdent { id } => {
let expr_or_pat = expr_or_pat_syntax(id)?; let node = match id {
UnresolvedIdent { expr_or_pat }.into() ExprOrPatId::ExprId(id) => match source_map.expr_syntax(id) {
Ok(syntax) => syntax.map(|it| (it.wrap_left(), None)),
Err(SyntheticSyntax) => source_map
.format_args_implicit_capture(id)?
.map(|(node, range)| (node.wrap_left(), Some(range))),
},
ExprOrPatId::PatId(id) => pat_syntax(id)?.map(|it| (it, None)),
};
UnresolvedIdent { node }.into()
} }
&InferenceDiagnostic::BreakOutsideOfLoop { expr, is_break, bad_value_break } => { &InferenceDiagnostic::BreakOutsideOfLoop { expr, is_break, bad_value_break } => {
let expr = expr_syntax(expr)?; let expr = expr_syntax(expr)?;

View file

@ -38,7 +38,7 @@ use span::{AstIdMap, EditionedFileId, FileId, HirFileIdRepr, SyntaxContextId};
use stdx::TupleExt; use stdx::TupleExt;
use syntax::{ use syntax::{
algo::skip_trivia_token, algo::skip_trivia_token,
ast::{self, HasAttrs as _, HasGenericParams, IsString as _}, ast::{self, HasAttrs as _, HasGenericParams},
AstNode, AstToken, Direction, SyntaxKind, SyntaxNode, SyntaxNodePtr, SyntaxToken, TextRange, AstNode, AstToken, Direction, SyntaxKind, SyntaxNode, SyntaxNodePtr, SyntaxToken, TextRange,
TextSize, TextSize,
}; };
@ -643,8 +643,7 @@ impl<'db> SemanticsImpl<'db> {
&self, &self,
string: &ast::String, string: &ast::String,
) -> Option<Vec<(TextRange, Option<Either<PathResolution, InlineAsmOperand>>)>> { ) -> Option<Vec<(TextRange, Option<Either<PathResolution, InlineAsmOperand>>)>> {
let quote = string.open_quote_text_range()?; let string_start = string.syntax().text_range().start();
let token = self.wrap_token_infile(string.syntax().clone()).into_real_file().ok()?; let token = self.wrap_token_infile(string.syntax().clone()).into_real_file().ok()?;
self.descend_into_macros_breakable(token, |token, _| { self.descend_into_macros_breakable(token, |token, _| {
(|| { (|| {
@ -658,7 +657,7 @@ impl<'db> SemanticsImpl<'db> {
let format_args = self.wrap_node_infile(format_args); let format_args = self.wrap_node_infile(format_args);
let res = source_analyzer let res = source_analyzer
.as_format_args_parts(self.db, format_args.as_ref())? .as_format_args_parts(self.db, format_args.as_ref())?
.map(|(range, res)| (range + quote.end(), res.map(Either::Left))) .map(|(range, res)| (range + string_start, res.map(Either::Left)))
.collect(); .collect();
Some(res) Some(res)
} else { } else {
@ -672,7 +671,7 @@ impl<'db> SemanticsImpl<'db> {
.iter() .iter()
.map(|&(range, index)| { .map(|&(range, index)| {
( (
range + quote.end(), range + string_start,
Some(Either::Right(InlineAsmOperand { owner, expr, index })), Some(Either::Right(InlineAsmOperand { owner, expr, index })),
) )
}) })
@ -690,17 +689,16 @@ impl<'db> SemanticsImpl<'db> {
original_token: SyntaxToken, original_token: SyntaxToken,
offset: TextSize, offset: TextSize,
) -> Option<(TextRange, Option<Either<PathResolution, InlineAsmOperand>>)> { ) -> Option<(TextRange, Option<Either<PathResolution, InlineAsmOperand>>)> {
let original_string = ast::String::cast(original_token.clone())?; let string_start = original_token.text_range().start();
let original_token = self.wrap_token_infile(original_token).into_real_file().ok()?; let original_token = self.wrap_token_infile(original_token).into_real_file().ok()?;
let quote = original_string.open_quote_text_range()?;
self.descend_into_macros_breakable(original_token, |token, _| { self.descend_into_macros_breakable(original_token, |token, _| {
(|| { (|| {
let token = token.value; let token = token.value;
self.resolve_offset_in_format_args( self.resolve_offset_in_format_args(
ast::String::cast(token)?, ast::String::cast(token)?,
offset.checked_sub(quote.end())?, offset.checked_sub(string_start)?,
) )
.map(|(range, res)| (range + quote.end(), res)) .map(|(range, res)| (range + string_start, res))
})() })()
.map_or(ControlFlow::Continue(()), ControlFlow::Break) .map_or(ControlFlow::Continue(()), ControlFlow::Break)
}) })

View file

@ -7,20 +7,19 @@ pub(crate) fn unresolved_ident(
ctx: &DiagnosticsContext<'_>, ctx: &DiagnosticsContext<'_>,
d: &hir::UnresolvedIdent, d: &hir::UnresolvedIdent,
) -> Diagnostic { ) -> Diagnostic {
Diagnostic::new_with_syntax_node_ptr( let mut range =
ctx, ctx.sema.diagnostics_display_range(d.node.map(|(node, _)| node.syntax_node_ptr()));
DiagnosticCode::RustcHardError("E0425"), if let Some(in_node_range) = d.node.value.1 {
"no such value in this scope", range.range = in_node_range + range.range.start();
d.expr_or_pat.map(Into::into), }
) Diagnostic::new(DiagnosticCode::RustcHardError("E0425"), "no such value in this scope", range)
.experimental() .experimental()
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::tests::check_diagnostics; use crate::tests::check_diagnostics;
// FIXME: This should show a diagnostic
#[test] #[test]
fn feature() { fn feature() {
check_diagnostics( check_diagnostics(
@ -28,6 +27,7 @@ mod tests {
//- minicore: fmt //- minicore: fmt
fn main() { fn main() {
format_args!("{unresolved}"); format_args!("{unresolved}");
// ^^^^^^^^^^ error: no such value in this scope
} }
"#, "#,
) )