1#![deny(unstable_features)]
11#![doc(test(attr(deny(warnings), allow(internal_features))))]
12use std::ops::Range;
15
16pub use Alignment::*;
17pub use Count::*;
18pub use Position::*;
19
20#[derive(Copy, Clone, Debug, Eq, PartialEq)]
22pub enum ParseMode {
23 Format,
25 InlineAsm,
27 Diagnostic,
32}
33
34#[derive(Clone, Debug, PartialEq)]
37pub enum Piece<'input> {
38 Lit(&'input str),
40 NextArgument(Box<Argument<'input>>),
43}
44
45#[derive(Clone, Debug, PartialEq)]
47pub struct Argument<'input> {
48 pub position: Position<'input>,
50 pub position_span: Range<usize>,
53 pub format: FormatSpec<'input>,
55}
56
57impl<'input> Argument<'input> {
58 pub fn is_identifier(&self) -> bool {
59 matches!(self.position, Position::ArgumentNamed(_)) && self.format == FormatSpec::default()
60 }
61}
62
63#[derive(Clone, Debug, PartialEq, Default)]
65pub struct FormatSpec<'input> {
66 pub fill: Option<char>,
68 pub fill_span: Option<Range<usize>>,
70 pub align: Alignment,
72 pub sign: Option<Sign>,
74 pub alternate: bool,
76 pub zero_pad: bool,
78 pub debug_hex: Option<DebugHex>,
80 pub precision: Count<'input>,
82 pub precision_span: Option<Range<usize>>,
84 pub width: Count<'input>,
86 pub width_span: Option<Range<usize>>,
88 pub ty: &'input str,
92 pub ty_span: Option<Range<usize>>,
94}
95
96#[derive(Clone, Debug, PartialEq)]
98pub enum Position<'input> {
99 ArgumentImplicitlyIs(usize),
101 ArgumentIs(usize),
103 ArgumentNamed(&'input str),
105}
106
107impl Position<'_> {
108 pub fn index(&self) -> Option<usize> {
109 match self {
110 ArgumentIs(i, ..) | ArgumentImplicitlyIs(i) => Some(*i),
111 _ => None,
112 }
113 }
114}
115
116#[derive(Copy, Clone, Debug, PartialEq, Default)]
118pub enum Alignment {
119 AlignLeft,
121 AlignRight,
123 AlignCenter,
125 #[default]
127 AlignUnknown,
128}
129
130#[derive(Copy, Clone, Debug, PartialEq)]
132pub enum Sign {
133 Plus,
135 Minus,
137}
138
139#[derive(Copy, Clone, Debug, PartialEq)]
141pub enum DebugHex {
142 Lower,
144 Upper,
146}
147
148#[derive(Clone, Debug, PartialEq, Default)]
151pub enum Count<'input> {
152 CountIs(u16),
154 CountIsName(&'input str, Range<usize>),
156 CountIsParam(usize),
158 CountIsStar(usize),
160 #[default]
162 CountImplied,
163}
164
165pub struct ParseError {
166 pub description: String,
167 pub note: Option<String>,
168 pub label: String,
169 pub span: Range<usize>,
170 pub secondary_label: Option<(String, Range<usize>)>,
171 pub suggestion: Suggestion,
172}
173
174pub enum Suggestion {
175 None,
176 UsePositional,
179 RemoveRawIdent(Range<usize>),
182 ReorderFormatParameter(Range<usize>, String),
187 AddMissingColon(Range<usize>),
190 UseRustDebugPrintingMacro,
193}
194
195pub struct Parser<'input> {
202 mode: ParseMode,
203 input: &'input str,
205 input_vec: Vec<(Range<usize>, usize, char)>,
207 input_vec_index: usize,
209 pub errors: Vec<ParseError>,
211 pub curarg: usize,
213 pub arg_places: Vec<Range<usize>>,
215 last_open_brace: Option<Range<usize>>,
217 pub is_source_literal: bool,
221 end_of_snippet: usize,
223 cur_line_start: usize,
225 pub line_spans: Vec<Range<usize>>,
228}
229
230impl<'input> Iterator for Parser<'input> {
231 type Item = Piece<'input>;
232
233 fn next(&mut self) -> Option<Piece<'input>> {
234 if let Some((Range { start, end }, idx, ch)) = self.peek() {
235 match ch {
236 '{' => {
237 self.input_vec_index += 1;
238 if let Some((_, i, '{')) = self.peek() {
239 self.input_vec_index += 1;
240 Some(Piece::Lit(self.string(i)))
243 } else {
244 self.last_open_brace = Some(start..end);
246 let arg = self.argument();
247 self.ws();
248 if let Some((close_brace_range, _)) = self.consume_pos('}') {
249 if self.is_source_literal {
250 self.arg_places.push(start..close_brace_range.end);
251 }
252 } else {
253 self.missing_closing_brace(&arg);
254 }
255
256 Some(Piece::NextArgument(Box::new(arg)))
257 }
258 }
259 '}' => {
260 self.input_vec_index += 1;
261 if let Some((_, i, '}')) = self.peek() {
262 self.input_vec_index += 1;
263 Some(Piece::Lit(self.string(i)))
266 } else {
267 self.errors.push(ParseError {
269 description: "unmatched `}` found".into(),
270 note: Some(
271 "if you intended to print `}`, you can escape it using `}}`".into(),
272 ),
273 label: "unmatched `}`".into(),
274 span: start..end,
275 secondary_label: None,
276 suggestion: Suggestion::None,
277 });
278 None
279 }
280 }
281 _ => Some(Piece::Lit(self.string(idx))),
282 }
283 } else {
284 if self.is_source_literal {
286 let span = self.cur_line_start..self.end_of_snippet;
287 if self.line_spans.last() != Some(&span) {
288 self.line_spans.push(span);
289 }
290 }
291 None
292 }
293 }
294}
295
296impl<'input> Parser<'input> {
297 pub fn new(
303 input: &'input str,
304 style: Option<usize>,
305 snippet: Option<String>,
306 appended_newline: bool,
307 mode: ParseMode,
308 ) -> Self {
309 let quote_offset = style.map_or(1, |nr_hashes| nr_hashes + 2);
310
311 let (is_source_literal, end_of_snippet, pre_input_vec) = if let Some(snippet) = snippet {
312 if let Some(nr_hashes) = style {
313 let prefix_len = nr_hashes + 2; let suffix_len = nr_hashes + 1; let snippet_bytes = snippet.as_bytes();
320 let content_end = snippet.len() - suffix_len;
321 if snippet.len() >= prefix_len + suffix_len && snippet_bytes[0] == b'r'
323 && snippet_bytes[1..1 + nr_hashes].iter().all(|&c| c == b'#')
324 && snippet_bytes[1 + nr_hashes] == b'"'
325 && snippet_bytes[content_end] == b'"'
326 && snippet_bytes[content_end + 1..].iter().all(|&c| c == b'#')
327 {
328 let snippet_without_quotes = &snippet[prefix_len..content_end];
329 let input_without_newline =
330 if appended_newline { &input[..input.len() - 1] } else { input };
331 if snippet_without_quotes == input_without_newline {
332 (true, snippet.len() - suffix_len, vec![])
333 } else {
334 (false, snippet.len(), vec![])
335 }
336 } else {
337 (false, snippet.len(), vec![])
338 }
339 } else {
340 if snippet.starts_with('"') {
342 let snippet_without_quotes = &snippet[1..snippet.len() - 1];
345 let (mut ok, mut vec) = (true, vec![]);
346 let mut chars = input.chars();
347 rustc_literal_escaper::unescape_str(snippet_without_quotes, |range, res| {
348 match res {
349 Ok(ch) if ok && chars.next().is_some_and(|c| ch == c) => {
350 vec.push((range, ch));
351 }
352 _ => {
353 ok = false;
354 vec = vec![];
355 }
356 }
357 });
358 let end = vec.last().map(|(r, _)| r.end).unwrap_or(0);
359 if ok {
360 if appended_newline {
361 if chars.as_str() == "\n" {
362 vec.push((end..end + 1, '\n'));
363 (true, 1 + end, vec)
364 } else {
365 (false, snippet.len(), vec![])
366 }
367 } else if chars.as_str() == "" {
368 (true, 1 + end, vec)
369 } else {
370 (false, snippet.len(), vec![])
371 }
372 } else {
373 (false, snippet.len(), vec![])
374 }
375 } else {
376 (false, snippet.len(), vec![])
378 }
379 }
380 } else {
381 (false, input.len() - if appended_newline { 1 } else { 0 }, vec![])
383 };
384
385 let input_vec: Vec<(Range<usize>, usize, char)> = if pre_input_vec.is_empty() {
386 input
389 .char_indices()
390 .map(|(idx, c)| {
391 let i = idx + quote_offset;
392 (i..i + c.len_utf8(), idx, c)
393 })
394 .collect()
395 } else {
396 input
398 .char_indices()
399 .zip(pre_input_vec)
400 .map(|((i, c), (r, _))| (r.start + quote_offset..r.end + quote_offset, i, c))
401 .collect()
402 };
403
404 Parser {
405 mode,
406 input,
407 input_vec,
408 input_vec_index: 0,
409 errors: vec![],
410 curarg: 0,
411 arg_places: vec![],
412 last_open_brace: None,
413 is_source_literal,
414 end_of_snippet,
415 cur_line_start: quote_offset,
416 line_spans: vec![],
417 }
418 }
419
420 pub fn peek(&self) -> Option<(Range<usize>, usize, char)> {
422 self.input_vec.get(self.input_vec_index).cloned()
423 }
424
425 pub fn peek_ahead(&self) -> Option<(Range<usize>, usize, char)> {
427 self.input_vec.get(self.input_vec_index + 1).cloned()
428 }
429
430 fn consume(&mut self, c: char) -> bool {
434 self.consume_pos(c).is_some()
435 }
436
437 fn consume_pos(&mut self, ch: char) -> Option<(Range<usize>, usize)> {
442 if let Some((r, i, c)) = self.peek()
443 && ch == c
444 {
445 self.input_vec_index += 1;
446 return Some((r, i));
447 }
448
449 None
450 }
451
452 fn missing_closing_brace(&mut self, arg: &Argument<'_>) {
454 let (range, description) = if let Some((r, _, c)) = self.peek() {
455 (r.start..r.start, format!("expected `}}`, found `{}`", c.escape_debug()))
456 } else {
457 (
458 self.end_of_snippet..self.end_of_snippet,
460 "expected `}` but string was terminated".to_owned(),
461 )
462 };
463
464 let (note, secondary_label) = if arg.format.fill == Some('}') {
465 (
466 Some("the character `}` is interpreted as a fill character because of the `:` that precedes it".to_owned()),
467 arg.format.fill_span.clone().map(|sp| ("this is not interpreted as a formatting closing brace".to_owned(), sp)),
468 )
469 } else {
470 (
471 Some("if you intended to print `{`, you can escape it using `{{`".to_owned()),
472 self.last_open_brace
473 .clone()
474 .map(|sp| ("because of this opening brace".to_owned(), sp)),
475 )
476 };
477
478 self.errors.push(ParseError {
479 description,
480 note,
481 label: "expected `}`".to_owned(),
482 span: range.start..range.start,
483 secondary_label,
484 suggestion: Suggestion::None,
485 });
486
487 if let (Some((_, _, c)), Some((_, _, nc))) = (self.peek(), self.peek_ahead()) {
488 match (c, nc) {
489 ('?', '}') => self.missing_colon_before_debug_formatter(),
490 ('?', _) => self.suggest_format_debug(),
491 ('<' | '^' | '>', _) => self.suggest_format_align(c),
492 (',', _) => self.suggest_unsupported_python_numeric_grouping(),
493 ('=', '}') => self.suggest_rust_debug_printing_macro(),
494 ('+', _) => self.suggest_format_missing_colon_for_sign(),
495 _ => self.suggest_positional_arg_instead_of_captured_arg(arg),
496 }
497 }
498 }
499
500 fn ws(&mut self) {
502 let rest = &self.input_vec[self.input_vec_index..];
503 let step = rest.iter().position(|&(_, _, c)| !c.is_whitespace()).unwrap_or(rest.len());
504 self.input_vec_index += step;
505 }
506
507 fn string(&mut self, start: usize) -> &'input str {
510 while let Some((r, i, c)) = self.peek() {
511 match c {
512 '{' | '}' => {
513 return &self.input[start..i];
514 }
515 '\n' if self.is_source_literal => {
516 self.input_vec_index += 1;
517 self.line_spans.push(self.cur_line_start..r.start);
518 self.cur_line_start = r.end;
519 }
520 _ => {
521 self.input_vec_index += 1;
522 if self.is_source_literal && r.start == self.cur_line_start && c.is_whitespace()
523 {
524 self.cur_line_start = r.end;
525 }
526 }
527 }
528 }
529 &self.input[start..]
530 }
531
532 fn argument(&mut self) -> Argument<'input> {
534 let start_idx = self.input_vec_index;
535
536 let position = self.position();
537 self.ws();
538
539 let end_idx = self.input_vec_index;
540
541 let format = match self.mode {
542 ParseMode::Format => self.format(),
543 ParseMode::InlineAsm => self.inline_asm(),
544 ParseMode::Diagnostic => self.diagnostic(),
545 };
546
547 let position = position.unwrap_or_else(|| {
549 let i = self.curarg;
550 self.curarg += 1;
551 ArgumentImplicitlyIs(i)
552 });
553
554 let position_span =
555 self.input_vec_index2range(start_idx).start..self.input_vec_index2range(end_idx).start;
556 Argument { position, position_span, format }
557 }
558
559 fn position(&mut self) -> Option<Position<'input>> {
564 if let Some(i) = self.integer() {
565 Some(ArgumentIs(i.into()))
566 } else {
567 match self.peek() {
568 Some((range, _, c)) if rustc_lexer::is_id_start(c) => {
569 let start = range.start;
570 let word = self.word();
571
572 if word == "r"
574 && let Some((r, _, '#')) = self.peek()
575 && self.peek_ahead().is_some_and(|(_, _, c)| rustc_lexer::is_id_start(c))
576 {
577 self.input_vec_index += 1;
578 let prefix_end = r.end;
579 let word = self.word();
580 let prefix_span = start..prefix_end;
581 let full_span =
582 start..self.input_vec_index2range(self.input_vec_index).start;
583 self.errors.insert(0, ParseError {
584 description: "raw identifiers are not supported".to_owned(),
585 note: Some("identifiers in format strings can be keywords and don't need to be prefixed with `r#`".to_string()),
586 label: "raw identifier used here".to_owned(),
587 span: full_span,
588 secondary_label: None,
589 suggestion: Suggestion::RemoveRawIdent(prefix_span),
590 });
591 return Some(ArgumentNamed(word));
592 }
593
594 Some(ArgumentNamed(word))
595 }
596 _ => None,
600 }
601 }
602 }
603
604 fn input_vec_index2pos(&self, index: usize) -> usize {
605 if let Some((_, pos, _)) = self.input_vec.get(index) { *pos } else { self.input.len() }
606 }
607
608 fn input_vec_index2range(&self, index: usize) -> Range<usize> {
609 if let Some((r, _, _)) = self.input_vec.get(index) {
610 r.clone()
611 } else {
612 self.end_of_snippet..self.end_of_snippet
613 }
614 }
615
616 fn format(&mut self) -> FormatSpec<'input> {
619 let mut spec = FormatSpec::default();
620
621 if !self.consume(':') {
622 return spec;
623 }
624
625 if let (Some((r, _, c)), Some((_, _, '>' | '<' | '^'))) = (self.peek(), self.peek_ahead()) {
627 self.input_vec_index += 1;
628 spec.fill = Some(c);
629 spec.fill_span = Some(r);
630 }
631 if self.consume('<') {
633 spec.align = AlignLeft;
634 } else if self.consume('>') {
635 spec.align = AlignRight;
636 } else if self.consume('^') {
637 spec.align = AlignCenter;
638 }
639 if self.consume('+') {
641 spec.sign = Some(Sign::Plus);
642 } else if self.consume('-') {
643 spec.sign = Some(Sign::Minus);
644 }
645 if self.consume('#') {
647 spec.alternate = true;
648 }
649 let mut havewidth = false;
651
652 if let Some((range, _)) = self.consume_pos('0') {
653 if let Some((r, _)) = self.consume_pos('$') {
658 spec.width = CountIsParam(0);
659 spec.width_span = Some(range.start..r.end);
660 havewidth = true;
661 } else {
662 spec.zero_pad = true;
663 }
664 }
665
666 if !havewidth {
667 let start_idx = self.input_vec_index;
668 spec.width = self.count();
669 if spec.width != CountImplied {
670 let end = self.input_vec_index2range(self.input_vec_index).start;
671 spec.width_span = Some(self.input_vec_index2range(start_idx).start..end);
672 }
673 }
674
675 if let Some((range, _)) = self.consume_pos('.') {
676 if self.consume('*') {
677 let i = self.curarg;
680 self.curarg += 1;
681 spec.precision = CountIsStar(i);
682 } else {
683 spec.precision = self.count();
684 }
685 spec.precision_span =
686 Some(range.start..self.input_vec_index2range(self.input_vec_index).start);
687 }
688
689 let start_idx = self.input_vec_index;
690 if self.consume('x') {
692 if self.consume('?') {
693 spec.debug_hex = Some(DebugHex::Lower);
694 spec.ty = "?";
695 } else {
696 spec.ty = "x";
697 }
698 } else if self.consume('X') {
699 if self.consume('?') {
700 spec.debug_hex = Some(DebugHex::Upper);
701 spec.ty = "?";
702 } else {
703 spec.ty = "X";
704 }
705 } else if let Some((range, _)) = self.consume_pos('?') {
706 spec.ty = "?";
707 if let Some((r, _, c @ ('#' | 'x' | 'X'))) = self.peek() {
708 self.errors.insert(
709 0,
710 ParseError {
711 description: format!("expected `}}`, found `{c}`"),
712 note: None,
713 label: "expected `'}'`".into(),
714 span: r.clone(),
715 secondary_label: None,
716 suggestion: Suggestion::ReorderFormatParameter(
717 range.start..r.end,
718 format!("{c}?"),
719 ),
720 },
721 );
722 }
723 } else {
724 spec.ty = self.word();
725 if !spec.ty.is_empty() {
726 let start = self.input_vec_index2range(start_idx).start;
727 let end = self.input_vec_index2range(self.input_vec_index).start;
728 spec.ty_span = Some(start..end);
729 }
730 }
731 spec
732 }
733
734 fn inline_asm(&mut self) -> FormatSpec<'input> {
737 let mut spec = FormatSpec::default();
738
739 if !self.consume(':') {
740 return spec;
741 }
742
743 let start_idx = self.input_vec_index;
744 spec.ty = self.word();
745 if !spec.ty.is_empty() {
746 let start = self.input_vec_index2range(start_idx).start;
747 let end = self.input_vec_index2range(self.input_vec_index).start;
748 spec.ty_span = Some(start..end);
749 }
750
751 spec
752 }
753
754 fn diagnostic(&mut self) -> FormatSpec<'input> {
756 let mut spec = FormatSpec::default();
757
758 let Some((Range { start, .. }, _)) = self.consume_pos(':') else {
759 return spec;
760 };
761
762 spec.ty = self.string(self.input_vec_index);
763 spec.ty_span = {
764 let end = self.input_vec_index2range(self.input_vec_index).start;
765 Some(start..end)
766 };
767 spec
768 }
769
770 fn count(&mut self) -> Count<'input> {
774 if let Some(i) = self.integer() {
775 if self.consume('$') { CountIsParam(i.into()) } else { CountIs(i) }
776 } else {
777 let start_idx = self.input_vec_index;
778 let word = self.word();
779 if word.is_empty() {
780 CountImplied
781 } else if let Some((r, _)) = self.consume_pos('$') {
782 CountIsName(word, self.input_vec_index2range(start_idx).start..r.start)
783 } else {
784 self.input_vec_index = start_idx;
785 CountImplied
786 }
787 }
788 }
789
790 fn word(&mut self) -> &'input str {
793 let index = self.input_vec_index;
794 match self.peek() {
795 Some((ref r, i, c)) if rustc_lexer::is_id_start(c) => {
796 self.input_vec_index += 1;
797 (r.start, i)
798 }
799 _ => {
800 return "";
801 }
802 };
803 let (err_end, end): (usize, usize) = loop {
804 if let Some((ref r, i, c)) = self.peek() {
805 if rustc_lexer::is_id_continue(c) {
806 self.input_vec_index += 1;
807 } else {
808 break (r.start, i);
809 }
810 } else {
811 break (self.end_of_snippet, self.input.len());
812 }
813 };
814
815 let word = &self.input[self.input_vec_index2pos(index)..end];
816 if word == "_" {
817 self.errors.push(ParseError {
818 description: "invalid argument name `_`".into(),
819 note: Some("argument name cannot be a single underscore".into()),
820 label: "invalid argument name".into(),
821 span: self.input_vec_index2range(index).start..err_end,
822 secondary_label: None,
823 suggestion: Suggestion::None,
824 });
825 }
826 word
827 }
828
829 fn integer(&mut self) -> Option<u16> {
830 let mut cur: u16 = 0;
831 let mut found = false;
832 let mut overflow = false;
833 let start_index = self.input_vec_index;
834 while let Some((_, _, c)) = self.peek() {
835 if let Some(i) = c.to_digit(10) {
836 self.input_vec_index += 1;
837 let (tmp, mul_overflow) = cur.overflowing_mul(10);
838 let (tmp, add_overflow) = tmp.overflowing_add(i as u16);
839 if mul_overflow || add_overflow {
840 overflow = true;
841 }
842 cur = tmp;
843 found = true;
844 } else {
845 break;
846 }
847 }
848
849 if overflow {
850 let overflowed_int = &self.input[self.input_vec_index2pos(start_index)
851 ..self.input_vec_index2pos(self.input_vec_index)];
852 self.errors.push(ParseError {
853 description: format!(
854 "integer `{}` does not fit into the type `u16` whose range is `0..={}`",
855 overflowed_int,
856 u16::MAX
857 ),
858 note: None,
859 label: "integer out of range for `u16`".into(),
860 span: self.input_vec_index2range(start_index).start
861 ..self.input_vec_index2range(self.input_vec_index).end,
862 secondary_label: None,
863 suggestion: Suggestion::None,
864 });
865 }
866
867 found.then_some(cur)
868 }
869
870 fn suggest_format_debug(&mut self) {
871 if let (Some((range, _)), Some(_)) = (self.consume_pos('?'), self.consume_pos(':')) {
872 let word = self.word();
873 self.errors.insert(
874 0,
875 ParseError {
876 description: "expected format parameter to occur after `:`".to_owned(),
877 note: Some(format!("`?` comes after `:`, try `{}:{}` instead", word, "?")),
878 label: "expected `?` to occur after `:`".to_owned(),
879 span: range,
880 secondary_label: None,
881 suggestion: Suggestion::None,
882 },
883 );
884 }
885 }
886
887 fn missing_colon_before_debug_formatter(&mut self) {
888 if let Some((range, _)) = self.consume_pos('?') {
889 let span = range.clone();
890 self.errors.insert(
891 0,
892 ParseError {
893 description: "expected `}`, found `?`".to_owned(),
894 note: Some(format!("to print `{{`, you can escape it using `{{{{`",)),
895 label: "expected `:` before `?` to format with `Debug`".to_owned(),
896 span: range,
897 secondary_label: None,
898 suggestion: Suggestion::AddMissingColon(span),
899 },
900 );
901 }
902 }
903
904 fn suggest_rust_debug_printing_macro(&mut self) {
905 if let Some((range, _)) = self.consume_pos('=') {
906 self.errors.insert(
907 0,
908 ParseError {
909 description:
910 "python's f-string debug `=` is not supported in rust, use `dbg(x)` instead"
911 .to_owned(),
912 note: Some(format!("to print `{{`, you can escape it using `{{{{`",)),
913 label: "expected `}`".to_owned(),
914 span: range,
915 secondary_label: self
916 .last_open_brace
917 .clone()
918 .map(|sp| ("because of this opening brace".to_owned(), sp)),
919 suggestion: Suggestion::UseRustDebugPrintingMacro,
920 },
921 );
922 }
923 }
924
925 fn suggest_format_align(&mut self, alignment: char) {
926 if let Some((range, _)) = self.consume_pos(alignment) {
927 self.errors.insert(
928 0,
929 ParseError {
930 description:
931 "expected alignment specifier after `:` in format string; example: `{:>?}`"
932 .to_owned(),
933 note: None,
934 label: format!("expected `{}` to occur after `:`", alignment),
935 span: range,
936 secondary_label: None,
937 suggestion: Suggestion::None,
938 },
939 );
940 }
941 }
942
943 fn suggest_format_missing_colon_for_sign(&mut self) {
944 if let Some((range, _)) = self.consume_pos('+') {
945 self.errors.insert(
946 0,
947 ParseError {
948 description: "the `+` sign flag must appear after `:` in a format string"
949 .to_owned(),
950 note: Some("`+` comes after `:`, try `{:+}` instead of `{+}`".to_owned()),
951 label: "expected `:` before `+` sign flag".to_owned(),
952 span: range,
953 secondary_label: None,
954 suggestion: Suggestion::None,
955 },
956 );
957 }
958 }
959
960 fn suggest_positional_arg_instead_of_captured_arg(&mut self, arg: &Argument<'_>) {
961 if !arg.is_identifier() {
963 return;
964 }
965
966 if let Some((_range, _pos)) = self.consume_pos('.') {
967 let field = self.argument();
968 if !self.consume('}') {
971 return;
972 }
973 if let ArgumentNamed(_) = arg.position {
974 match field.position {
975 ArgumentNamed(_) => {
976 self.errors.insert(
977 0,
978 ParseError {
979 description: "field access isn't supported".to_string(),
980 note: Some(
981 "consider moving this expression to a local variable and then \
982 using the local here instead"
983 .to_owned(),
984 ),
985 label: "not supported".to_string(),
986 span: arg.position_span.start..field.position_span.end,
987 secondary_label: None,
988 suggestion: Suggestion::UsePositional,
989 },
990 );
991 }
992 ArgumentIs(_) => {
993 self.errors.insert(
994 0,
995 ParseError {
996 description: "tuple index access isn't supported".to_string(),
997 note: Some(
998 "consider moving this expression to a local variable and then \
999 using the local here instead"
1000 .to_owned(),
1001 ),
1002 label: "not supported".to_string(),
1003 span: arg.position_span.start..field.position_span.end,
1004 secondary_label: None,
1005 suggestion: Suggestion::UsePositional,
1006 },
1007 );
1008 }
1009 _ => {}
1010 };
1011 }
1012 }
1013 }
1014
1015 fn suggest_unsupported_python_numeric_grouping(&mut self) {
1016 if let Some((range, _)) = self.consume_pos(',') {
1017 self.errors.insert(
1018 0,
1019 ParseError {
1020 description:
1021 "python's numeric grouping `,` is not supported in rust format strings"
1022 .to_owned(),
1023 note: Some(format!("to print `{{`, you can escape it using `{{{{`",)),
1024 label: "expected `}`".to_owned(),
1025 span: range,
1026 secondary_label: self
1027 .last_open_brace
1028 .clone()
1029 .map(|sp| ("because of this opening brace".to_owned(), sp)),
1030 suggestion: Suggestion::None,
1031 },
1032 );
1033 }
1034 }
1035}
1036
1037#[cfg(all(test, target_pointer_width = "64"))]
1039rustc_index::static_assert_size!(Piece<'_>, 16);
1040
1041#[cfg(test)]
1042mod tests;