Remove need for vendored format/cformat code (#1573)

This commit is contained in:
Oliver Margetts 2023-01-03 00:37:31 +00:00 committed by GitHub
parent 0c05488740
commit cd5882c66d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 35 additions and 814 deletions

10
Cargo.lock generated
View File

@ -2005,7 +2005,7 @@ dependencies = [
[[package]]
name = "rustpython-ast"
version = "0.1.0"
source = "git+https://github.com/RustPython/RustPython.git?rev=71becd4059fdce4bce7010f1208ed3b1c883abba#71becd4059fdce4bce7010f1208ed3b1c883abba"
source = "git+https://github.com/RustPython/RustPython.git?rev=4d53c7cb27c0379adf8b51c4d3d0d2174f41d590#4d53c7cb27c0379adf8b51c4d3d0d2174f41d590"
dependencies = [
"num-bigint",
"rustpython-common",
@ -2015,11 +2015,13 @@ dependencies = [
[[package]]
name = "rustpython-common"
version = "0.0.0"
source = "git+https://github.com/RustPython/RustPython.git?rev=71becd4059fdce4bce7010f1208ed3b1c883abba#71becd4059fdce4bce7010f1208ed3b1c883abba"
source = "git+https://github.com/RustPython/RustPython.git?rev=4d53c7cb27c0379adf8b51c4d3d0d2174f41d590#4d53c7cb27c0379adf8b51c4d3d0d2174f41d590"
dependencies = [
"ascii",
"bitflags",
"cfg-if 1.0.0",
"hexf-parse",
"itertools",
"lexical-parse-float",
"libc",
"lock_api",
@ -2038,7 +2040,7 @@ dependencies = [
[[package]]
name = "rustpython-compiler-core"
version = "0.1.2"
source = "git+https://github.com/RustPython/RustPython.git?rev=71becd4059fdce4bce7010f1208ed3b1c883abba#71becd4059fdce4bce7010f1208ed3b1c883abba"
source = "git+https://github.com/RustPython/RustPython.git?rev=4d53c7cb27c0379adf8b51c4d3d0d2174f41d590#4d53c7cb27c0379adf8b51c4d3d0d2174f41d590"
dependencies = [
"bincode",
"bitflags",
@ -2055,7 +2057,7 @@ dependencies = [
[[package]]
name = "rustpython-parser"
version = "0.1.2"
source = "git+https://github.com/RustPython/RustPython.git?rev=71becd4059fdce4bce7010f1208ed3b1c883abba#71becd4059fdce4bce7010f1208ed3b1c883abba"
source = "git+https://github.com/RustPython/RustPython.git?rev=4d53c7cb27c0379adf8b51c4d3d0d2174f41d590#4d53c7cb27c0379adf8b51c4d3d0d2174f41d590"
dependencies = [
"ahash",
"anyhow",

View File

@ -53,9 +53,9 @@ regex = { version = "1.6.0" }
ropey = { version = "1.5.0", features = ["cr_lines", "simd"], default-features = false }
ruff_macros = { version = "0.0.207", path = "ruff_macros" }
rustc-hash = { version = "1.1.0" }
rustpython-ast = { features = ["unparse"], git = "https://github.com/RustPython/RustPython.git", rev = "71becd4059fdce4bce7010f1208ed3b1c883abba" }
rustpython-common = { git = "https://github.com/RustPython/RustPython.git", rev = "71becd4059fdce4bce7010f1208ed3b1c883abba" }
rustpython-parser = { features = ["lalrpop"], git = "https://github.com/RustPython/RustPython.git", rev = "71becd4059fdce4bce7010f1208ed3b1c883abba" }
rustpython-ast = { features = ["unparse"], git = "https://github.com/RustPython/RustPython.git", rev = "4d53c7cb27c0379adf8b51c4d3d0d2174f41d590" }
rustpython-common = { git = "https://github.com/RustPython/RustPython.git", rev = "4d53c7cb27c0379adf8b51c4d3d0d2174f41d590" }
rustpython-parser = { features = ["lalrpop"], git = "https://github.com/RustPython/RustPython.git", rev = "4d53c7cb27c0379adf8b51c4d3d0d2174f41d590" }
schemars = { version = "0.8.11" }
semver = { version = "1.0.16" }
serde = { version = "1.0.147", features = ["derive"] }

View File

@ -11,9 +11,9 @@ itertools = { version = "0.10.5" }
libcst = { git = "https://github.com/charliermarsh/LibCST", rev = "f2f0b7a487a8725d161fe8b3ed73a6758b21e177" }
once_cell = { version = "1.16.0" }
ruff = { path = ".." }
rustpython-ast = { features = ["unparse"], git = "https://github.com/RustPython/RustPython.git", rev = "71becd4059fdce4bce7010f1208ed3b1c883abba" }
rustpython-common = { git = "https://github.com/RustPython/RustPython.git", rev = "71becd4059fdce4bce7010f1208ed3b1c883abba" }
rustpython-parser = { features = ["lalrpop"], git = "https://github.com/RustPython/RustPython.git", rev = "71becd4059fdce4bce7010f1208ed3b1c883abba" }
rustpython-ast = { features = ["unparse"], git = "https://github.com/RustPython/RustPython.git", rev = "4d53c7cb27c0379adf8b51c4d3d0d2174f41d590" }
rustpython-common = { git = "https://github.com/RustPython/RustPython.git", rev = "4d53c7cb27c0379adf8b51c4d3d0d2174f41d590" }
rustpython-parser = { features = ["lalrpop"], git = "https://github.com/RustPython/RustPython.git", rev = "4d53c7cb27c0379adf8b51c4d3d0d2174f41d590" }
schemars = { version = "0.8.11" }
serde_json = {version="1.0.91"}
strum = { version = "0.24.1", features = ["strum_macros"] }

View File

@ -7,6 +7,7 @@ use log::error;
use nohash_hasher::IntMap;
use rustc_hash::{FxHashMap, FxHashSet};
use rustpython_ast::{Located, Location};
use rustpython_common::cformat::{CFormatError, CFormatErrorType};
use rustpython_parser::ast::{
Arg, Arguments, Constant, Excepthandler, ExcepthandlerKind, Expr, ExprContext, ExprKind,
KeywordData, Operator, Stmt, StmtKind, Suite,
@ -34,7 +35,6 @@ use crate::settings::types::PythonVersion;
use crate::settings::{flags, Settings};
use crate::source_code_locator::SourceCodeLocator;
use crate::source_code_style::SourceCodeStyleDetector;
use crate::vendor::cformat::{CFormatError, CFormatErrorType};
use crate::visibility::{module_visibility, transition_scope, Modifier, Visibility, VisibleScope};
use crate::{
docstrings, flake8_2020, flake8_annotations, flake8_bandit, flake8_blind_except,
@ -1744,7 +1744,7 @@ where
if self.settings.enabled.contains(&CheckCode::F521) {
self.add_check(Check::new(
CheckKind::StringDotFormatInvalidFormat(
e.to_string(),
pyflakes::format::error_to_string(&e),
),
location,
));

View File

@ -3,8 +3,7 @@ use std::convert::TryFrom;
use std::str::FromStr;
use rustc_hash::FxHashSet;
use crate::vendor::cformat::{
use rustpython_common::cformat::{
CFormatError, CFormatPart, CFormatQuantity, CFormatSpec, CFormatString,
};
@ -24,11 +23,11 @@ impl TryFrom<&str> for CFormatSummary {
let mut num_positional = 0;
let mut keywords = FxHashSet::default();
for format_part in format_string.parts {
for format_part in format_string.iter() {
let CFormatPart::Spec(CFormatSpec {
mapping_key,
min_field_width,
precision,
ref mapping_key,
ref min_field_width,
ref precision,
..
}) = format_part.1 else
{
@ -36,17 +35,17 @@ impl TryFrom<&str> for CFormatSummary {
};
match mapping_key {
Some(k) => {
keywords.insert(k);
keywords.insert(k.clone());
}
None => {
num_positional += 1;
}
};
if min_field_width == Some(CFormatQuantity::FromValuesTuple) {
if min_field_width == &Some(CFormatQuantity::FromValuesTuple) {
num_positional += 1;
starred = true;
}
if precision == Some(CFormatQuantity::FromValuesTuple) {
if precision == &Some(CFormatQuantity::FromValuesTuple) {
num_positional += 1;
starred = true;
}

View File

@ -1,29 +1,24 @@
//! Implements helper functions for using vendored/format.rs
use std::convert::TryFrom;
use std::fmt;
use rustc_hash::FxHashSet;
use crate::vendor::format::{
use rustpython_common::format::{
FieldName, FieldType, FormatParseError, FormatPart, FormatString, FromTemplate,
};
impl fmt::Display for FormatParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let message = match self {
FormatParseError::EmptyAttribute => "Empty attribute in format string",
FormatParseError::InvalidCharacterAfterRightBracket => {
"Only '.' or '[' may follow ']' in format field specifier"
}
FormatParseError::InvalidFormatSpecifier => "Max string recursion exceeded",
FormatParseError::MissingStartBracket => "Single '}' encountered in format string",
FormatParseError::MissingRightBracket => "Expected '}' before end of string",
FormatParseError::UnmatchedBracket => "Single '{' encountered in format string",
_ => "Unexpected error parsing format string",
};
write!(f, "{message}")
pub(crate) fn error_to_string(err: &FormatParseError) -> String {
match err {
FormatParseError::EmptyAttribute => "Empty attribute in format string",
FormatParseError::InvalidCharacterAfterRightBracket => {
"Only '.' or '[' may follow ']' in format field specifier"
}
FormatParseError::InvalidFormatSpecifier => "Max string recursion exceeded",
FormatParseError::MissingStartBracket => "Single '}' encountered in format string",
FormatParseError::MissingRightBracket => "Expected '}' before end of string",
FormatParseError::UnmatchedBracket => "Single '{' encountered in format string",
_ => "Unexpected error parsing format string",
}
.to_string()
}
pub(crate) struct FormatSummary {
@ -82,7 +77,6 @@ impl TryFrom<&str> for FormatSummary {
#[cfg(test)]
mod tests {
use super::*;
use crate::vendor::format::FromTemplate;
#[test]
fn test_format_summary() {

412
src/vendor/cformat.rs vendored
View File

@ -1,412 +0,0 @@
//! Implementation of Printf-Style string formatting
//! as per the [Python Docs](https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting).
//! Vendored from [cformat.rs in rustpython-vm](https://github.com/RustPython/RustPython/blob/f54b5556e28256763c5506813ea977c9e1445af0/vm/src/cformat.rs).
//! The only changes we make are to remove dead code and code involving the VM.
use std::fmt;
use std::iter::{Enumerate, Peekable};
use std::str::FromStr;
#[derive(Debug, PartialEq)]
pub(crate) enum CFormatErrorType {
UnmatchedKeyParentheses,
MissingModuloSign,
UnsupportedFormatChar(char),
IncompleteFormat,
IntTooBig,
// Unimplemented,
}
// also contains how many chars the parsing function consumed
type ParsingError = (CFormatErrorType, usize);
#[derive(Debug, PartialEq)]
pub(crate) struct CFormatError {
pub(crate) typ: CFormatErrorType,
index: usize,
}
impl fmt::Display for CFormatError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use CFormatErrorType::{
IntTooBig, MissingModuloSign, UnmatchedKeyParentheses, UnsupportedFormatChar,
};
match self.typ {
UnmatchedKeyParentheses => write!(f, "incomplete format key"),
CFormatErrorType::IncompleteFormat => write!(f, "incomplete format"),
UnsupportedFormatChar(c) => write!(
f,
"unsupported format character '{}' ({:#x}) at index {}",
c, c as u32, self.index
),
IntTooBig => write!(f, "width/precision too big"),
MissingModuloSign => write!(f, "unexpected error parsing format string"),
}
}
}
#[derive(Debug, PartialEq)]
pub(crate) enum CFormatQuantity {
Amount(usize),
FromValuesTuple,
}
#[derive(Debug, PartialEq)]
pub(crate) struct CFormatSpec {
pub mapping_key: Option<String>,
pub min_field_width: Option<CFormatQuantity>,
pub precision: Option<CFormatQuantity>,
}
impl CFormatSpec {
fn parse<T, I>(iter: &mut ParseIter<I>) -> Result<Self, ParsingError>
where
T: Into<char> + Copy,
I: Iterator<Item = T>,
{
let mapping_key = parse_spec_mapping_key(iter)?;
consume_flags(iter);
let min_field_width = parse_quantity(iter)?;
let precision = parse_precision(iter)?;
consume_length(iter);
parse_format_type(iter)?;
Ok(CFormatSpec {
mapping_key,
min_field_width,
precision,
})
}
}
#[derive(Debug, PartialEq)]
pub(crate) enum CFormatPart<T> {
Literal(T),
Spec(CFormatSpec),
}
#[derive(Debug, PartialEq)]
pub(crate) struct CFormatString {
pub parts: Vec<(usize, CFormatPart<String>)>,
}
impl FromStr for CFormatString {
type Err = CFormatError;
fn from_str(text: &str) -> Result<Self, Self::Err> {
let mut iter = text.chars().enumerate().peekable();
Self::parse(&mut iter)
}
}
impl CFormatString {
pub(crate) fn parse<I: Iterator<Item = char>>(
iter: &mut ParseIter<I>,
) -> Result<Self, CFormatError> {
let mut parts = vec![];
let mut literal = String::new();
let mut part_index = 0;
while let Some((index, c)) = iter.next() {
if c == '%' {
if let Some(&(_, second)) = iter.peek() {
if second == '%' {
iter.next().unwrap();
literal.push('%');
continue;
}
if !literal.is_empty() {
parts.push((
part_index,
CFormatPart::Literal(std::mem::take(&mut literal)),
));
}
let spec = CFormatSpec::parse(iter).map_err(|err| CFormatError {
typ: err.0,
index: err.1,
})?;
parts.push((index, CFormatPart::Spec(spec)));
if let Some(&(index, _)) = iter.peek() {
part_index = index;
}
} else {
return Err(CFormatError {
typ: CFormatErrorType::IncompleteFormat,
index: index + 1,
});
}
} else {
literal.push(c);
}
}
if !literal.is_empty() {
parts.push((part_index, CFormatPart::Literal(literal)));
}
Ok(Self { parts })
}
}
type ParseIter<I> = Peekable<Enumerate<I>>;
fn parse_quantity<T, I>(iter: &mut ParseIter<I>) -> Result<Option<CFormatQuantity>, ParsingError>
where
T: Into<char> + Copy,
I: Iterator<Item = T>,
{
#![allow(clippy::cast_possible_wrap)] // A single digit will never overflow
if let Some(&(_, c)) = iter.peek() {
let c: char = c.into();
if c == '*' {
iter.next().unwrap();
return Ok(Some(CFormatQuantity::FromValuesTuple));
}
if let Some(i) = c.to_digit(10) {
let mut num = i as i32;
iter.next().unwrap();
while let Some(&(index, c)) = iter.peek() {
if let Some(i) = c.into().to_digit(10) {
num = num
.checked_mul(10)
.and_then(|num| num.checked_add(i as i32))
.ok_or((CFormatErrorType::IntTooBig, index))?;
iter.next().unwrap();
} else {
break;
}
}
return Ok(Some(CFormatQuantity::Amount(num.unsigned_abs() as usize)));
}
}
Ok(None)
}
fn parse_precision<T, I>(iter: &mut ParseIter<I>) -> Result<Option<CFormatQuantity>, ParsingError>
where
T: Into<char> + Copy,
I: Iterator<Item = T>,
{
if let Some(&(_, c)) = iter.peek() {
if c.into() == '.' {
iter.next().unwrap();
return parse_quantity(iter);
}
}
Ok(None)
}
fn parse_text_inside_parentheses<T, I>(iter: &mut ParseIter<I>) -> Option<String>
where
T: Into<char>,
I: Iterator<Item = T>,
{
let mut counter: i32 = 1;
let mut contained_text = String::new();
loop {
let (_, c) = iter.next()?;
let c = c.into();
match c {
_ if c == '(' => {
counter += 1;
}
_ if c == ')' => {
counter -= 1;
}
_ => (),
}
if counter > 0 {
contained_text.push(c);
} else {
break;
}
}
Some(contained_text)
}
fn parse_spec_mapping_key<T, I>(iter: &mut ParseIter<I>) -> Result<Option<String>, ParsingError>
where
T: Into<char> + Copy,
I: Iterator<Item = T>,
{
if let Some(&(index, c)) = iter.peek() {
if c.into() == '(' {
iter.next().unwrap();
return match parse_text_inside_parentheses(iter) {
Some(key) => Ok(Some(key)),
None => Err((CFormatErrorType::UnmatchedKeyParentheses, index)),
};
}
}
Ok(None)
}
fn consume_flags<T, I>(iter: &mut ParseIter<I>)
where
T: Into<char> + Copy,
I: Iterator<Item = T>,
{
while let Some(&(_, c)) = iter.peek() {
match c.into() {
'#' | '0' | '-' | ' ' | '+' => {
iter.next().unwrap();
continue;
}
_ => break,
};
}
}
fn consume_length<T, I>(iter: &mut ParseIter<I>)
where
T: Into<char> + Copy,
I: Iterator<Item = T>,
{
if let Some(&(_, c)) = iter.peek() {
let c = c.into();
if c == 'h' || c == 'l' || c == 'L' {
iter.next().unwrap();
}
}
}
fn parse_format_type<T, I>(iter: &mut ParseIter<I>) -> Result<(), ParsingError>
where
T: Into<char>,
I: Iterator<Item = T>,
{
let (index, c) = match iter.next() {
Some((index, c)) => (index, c.into()),
None => {
return Err((
CFormatErrorType::IncompleteFormat,
iter.peek().map_or(0, |x| x.0),
));
}
};
match c {
'd' | 'i' | 'u' | 'o' | 'x' | 'X' | 'e' | 'E' | 'f' | 'F' | 'g' | 'G' | 'c' | 'r' | 's'
| 'b' | 'a' => Ok(()),
_ => Err((CFormatErrorType::UnsupportedFormatChar(c), index)),
}
}
impl FromStr for CFormatSpec {
type Err = ParsingError;
fn from_str(text: &str) -> Result<Self, Self::Err> {
let mut chars = text.chars().enumerate().peekable();
if chars.next().map(|x| x.1) != Some('%') {
return Err((CFormatErrorType::MissingModuloSign, 1));
}
CFormatSpec::parse(&mut chars)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_key() {
let expected = Ok(CFormatSpec {
mapping_key: Some("amount".to_owned()),
min_field_width: None,
precision: None,
});
assert_eq!("%(amount)d".parse::<CFormatSpec>(), expected);
let expected = Ok(CFormatSpec {
mapping_key: Some("m((u(((l((((ti))))p)))l))e".to_owned()),
min_field_width: None,
precision: None,
});
assert_eq!(
"%(m((u(((l((((ti))))p)))l))e)d".parse::<CFormatSpec>(),
expected
);
}
#[test]
fn test_format_parse_key_fail() {
assert_eq!(
"%(aged".parse::<CFormatString>(),
Err(CFormatError {
typ: CFormatErrorType::UnmatchedKeyParentheses,
index: 1
})
);
}
#[test]
fn test_format_parse_type_fail() {
assert_eq!(
"Hello %n".parse::<CFormatString>(),
Err(CFormatError {
typ: CFormatErrorType::UnsupportedFormatChar('n'),
index: 7
})
);
}
#[test]
fn test_incomplete_format_fail() {
assert_eq!(
"Hello %".parse::<CFormatString>(),
Err(CFormatError {
typ: CFormatErrorType::IncompleteFormat,
index: 7
})
);
}
#[test]
fn test_consume_flags() {
let expected = Ok(CFormatSpec {
min_field_width: Some(CFormatQuantity::Amount(10)),
precision: None,
mapping_key: None,
});
let parsed = "% 0 -+++###10d".parse::<CFormatSpec>();
assert_eq!(parsed, expected);
}
#[test]
fn test_parse_string() {
assert!("%5.4s".parse::<CFormatSpec>().is_ok());
assert!("%-5.4s".parse::<CFormatSpec>().is_ok());
}
#[test]
fn test_format_parse() {
let fmt = "Hello, my name is %s and I'm %d years old";
let expected = Ok(CFormatString {
parts: vec![
(0, CFormatPart::Literal("Hello, my name is ".to_owned())),
(
18,
CFormatPart::Spec(CFormatSpec {
mapping_key: None,
min_field_width: None,
precision: None,
}),
),
(20, CFormatPart::Literal(" and I'm ".to_owned())),
(
29,
CFormatPart::Spec(CFormatSpec {
mapping_key: None,
min_field_width: None,
precision: None,
}),
),
(31, CFormatPart::Literal(" years old".to_owned())),
],
});
let result = fmt.parse::<CFormatString>();
assert_eq!(
result, expected,
"left = {result:#?} \n\n\n right = {expected:#?}"
);
}
}

360
src/vendor/format.rs vendored
View File

@ -1,360 +0,0 @@
//! Vendored from [format.rs in rustpython-vm](https://github.com/RustPython/RustPython/blob/f54b5556e28256763c5506813ea977c9e1445af0/vm/src/format.rs).
//! The only changes we make are to remove dead code and code involving the VM.
use itertools::{Itertools, PeekingNext};
#[derive(Debug, PartialEq)]
pub(crate) enum FormatParseError {
UnmatchedBracket,
MissingStartBracket,
UnescapedStartBracketInLiteral,
InvalidFormatSpecifier,
UnknownConversion,
EmptyAttribute,
MissingRightBracket,
InvalidCharacterAfterRightBracket,
}
#[derive(Debug, PartialEq)]
pub(crate) enum FieldNamePart {
Attribute(String),
Index(usize),
StringIndex(String),
}
impl FieldNamePart {
fn parse_part(
chars: &mut impl PeekingNext<Item = char>,
) -> Result<Option<FieldNamePart>, FormatParseError> {
chars
.next()
.map(|ch| match ch {
'.' => {
let mut attribute = String::new();
for ch in chars.peeking_take_while(|ch| *ch != '.' && *ch != '[') {
attribute.push(ch);
}
if attribute.is_empty() {
Err(FormatParseError::EmptyAttribute)
} else {
Ok(FieldNamePart::Attribute(attribute))
}
}
'[' => {
let mut index = String::new();
for ch in chars {
if ch == ']' {
return if index.is_empty() {
Err(FormatParseError::EmptyAttribute)
} else if let Ok(index) = index.parse::<usize>() {
Ok(FieldNamePart::Index(index))
} else {
Ok(FieldNamePart::StringIndex(index))
};
}
index.push(ch);
}
Err(FormatParseError::MissingRightBracket)
}
_ => Err(FormatParseError::InvalidCharacterAfterRightBracket),
})
.transpose()
}
}
#[derive(Debug, PartialEq)]
pub(crate) enum FieldType {
Auto,
Index(usize),
Keyword(String),
}
#[derive(Debug, PartialEq)]
pub(crate) struct FieldName {
pub field_type: FieldType,
pub parts: Vec<FieldNamePart>,
}
impl FieldName {
pub(crate) fn parse(text: &str) -> Result<FieldName, FormatParseError> {
let mut chars = text.chars().peekable();
let mut first = String::new();
for ch in chars.peeking_take_while(|ch| *ch != '.' && *ch != '[') {
first.push(ch);
}
let field_type = if first.is_empty() {
FieldType::Auto
} else if let Ok(index) = first.parse::<usize>() {
FieldType::Index(index)
} else {
FieldType::Keyword(first)
};
let mut parts = Vec::new();
while let Some(part) = FieldNamePart::parse_part(&mut chars)? {
parts.push(part);
}
Ok(FieldName { field_type, parts })
}
}
#[derive(Debug, PartialEq)]
pub(crate) enum FormatPart {
Field {
field_name: String,
preconversion_spec: Option<char>,
format_spec: String,
},
Literal(String),
}
#[derive(Debug, PartialEq)]
pub(crate) struct FormatString {
pub format_parts: Vec<FormatPart>,
}
impl FormatString {
fn parse_literal_single(text: &str) -> Result<(char, &str), FormatParseError> {
let mut chars = text.chars();
// This should never be called with an empty str
let first_char = chars.next().unwrap();
// isn't this detectable only with bytes operation?
if first_char == '{' || first_char == '}' {
let maybe_next_char = chars.next();
// if we see a bracket, it has to be escaped by doubling up to be in a literal
return if maybe_next_char.is_none() || maybe_next_char.unwrap() != first_char {
Err(FormatParseError::UnescapedStartBracketInLiteral)
} else {
Ok((first_char, chars.as_str()))
};
}
Ok((first_char, chars.as_str()))
}
fn parse_literal(text: &str) -> Result<(FormatPart, &str), FormatParseError> {
let mut cur_text = text;
let mut result_string = String::new();
while !cur_text.is_empty() {
match FormatString::parse_literal_single(cur_text) {
Ok((next_char, remaining)) => {
result_string.push(next_char);
cur_text = remaining;
}
Err(err) => {
return if result_string.is_empty() {
Err(err)
} else {
Ok((FormatPart::Literal(result_string), cur_text))
};
}
}
}
Ok((FormatPart::Literal(result_string), ""))
}
fn parse_part_in_brackets(text: &str) -> Result<FormatPart, FormatParseError> {
let parts: Vec<&str> = text.splitn(2, ':').collect();
// before the comma is a keyword or arg index, after the comma is maybe a spec.
let arg_part = parts[0];
let format_spec = if parts.len() > 1 {
parts[1].to_owned()
} else {
String::new()
};
// On parts[0] can still be the preconversor (!r, !s, !a)
let parts: Vec<&str> = arg_part.splitn(2, '!').collect();
// before the bang is a keyword or arg index, after the comma is maybe a
// conversor spec.
let arg_part = parts[0];
let preconversion_spec = parts
.get(1)
.map(|conversion| {
// conversions are only every one character
conversion
.chars()
.exactly_one()
.map_err(|_| FormatParseError::UnknownConversion)
})
.transpose()?;
Ok(FormatPart::Field {
field_name: arg_part.to_owned(),
preconversion_spec,
format_spec,
})
}
fn parse_spec(text: &str) -> Result<(FormatPart, &str), FormatParseError> {
let mut nested = false;
let mut end_bracket_pos = None;
let mut left = String::new();
// There may be one layer nesting brackets in spec
for (idx, c) in text.chars().enumerate() {
if idx == 0 {
if c != '{' {
return Err(FormatParseError::MissingStartBracket);
}
} else if c == '{' {
if nested {
return Err(FormatParseError::InvalidFormatSpecifier);
}
nested = true;
left.push(c);
continue;
} else if c == '}' {
if nested {
nested = false;
left.push(c);
continue;
}
end_bracket_pos = Some(idx);
break;
} else {
left.push(c);
}
}
if let Some(pos) = end_bracket_pos {
let (_, right) = text.split_at(pos);
let format_part = FormatString::parse_part_in_brackets(&left)?;
Ok((format_part, &right[1..]))
} else {
Err(FormatParseError::UnmatchedBracket)
}
}
}
pub(crate) trait FromTemplate<'a>: Sized {
type Err;
fn from_str(s: &'a str) -> Result<Self, Self::Err>;
}
impl<'a> FromTemplate<'a> for FormatString {
type Err = FormatParseError;
fn from_str(text: &'a str) -> Result<Self, Self::Err> {
let mut cur_text: &str = text;
let mut parts: Vec<FormatPart> = Vec::new();
while !cur_text.is_empty() {
// Try to parse both literals and bracketed format parts until we
// run out of text
cur_text = FormatString::parse_literal(cur_text)
.or_else(|_| FormatString::parse_spec(cur_text))
.map(|(part, new_text)| {
parts.push(part);
new_text
})?;
}
Ok(FormatString {
format_parts: parts,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_format_parse() {
let expected = Ok(FormatString {
format_parts: vec![
FormatPart::Literal("abcd".to_owned()),
FormatPart::Field {
field_name: "1".to_owned(),
preconversion_spec: None,
format_spec: String::new(),
},
FormatPart::Literal(":".to_owned()),
FormatPart::Field {
field_name: "key".to_owned(),
preconversion_spec: None,
format_spec: String::new(),
},
],
});
assert_eq!(FormatString::from_str("abcd{1}:{key}"), expected);
}
#[test]
fn test_format_parse_fail() {
assert_eq!(
FormatString::from_str("{s"),
Err(FormatParseError::UnmatchedBracket)
);
}
#[test]
fn test_format_parse_escape() {
let expected = Ok(FormatString {
format_parts: vec![
FormatPart::Literal("{".to_owned()),
FormatPart::Field {
field_name: "key".to_owned(),
preconversion_spec: None,
format_spec: String::new(),
},
FormatPart::Literal("}ddfe".to_owned()),
],
});
assert_eq!(FormatString::from_str("{{{key}}}ddfe"), expected);
}
#[test]
fn test_parse_field_name() {
assert_eq!(
FieldName::parse(""),
Ok(FieldName {
field_type: FieldType::Auto,
parts: Vec::new(),
})
);
assert_eq!(
FieldName::parse("0"),
Ok(FieldName {
field_type: FieldType::Index(0),
parts: Vec::new(),
})
);
assert_eq!(
FieldName::parse("key"),
Ok(FieldName {
field_type: FieldType::Keyword("key".to_owned()),
parts: Vec::new(),
})
);
assert_eq!(
FieldName::parse("key.attr[0][string]"),
Ok(FieldName {
field_type: FieldType::Keyword("key".to_owned()),
parts: vec![
FieldNamePart::Attribute("attr".to_owned()),
FieldNamePart::Index(0),
FieldNamePart::StringIndex("string".to_owned())
],
})
);
assert_eq!(
FieldName::parse("key.."),
Err(FormatParseError::EmptyAttribute)
);
assert_eq!(
FieldName::parse("key[]"),
Err(FormatParseError::EmptyAttribute)
);
assert_eq!(
FieldName::parse("key["),
Err(FormatParseError::MissingRightBracket)
);
assert_eq!(
FieldName::parse("key[0]after"),
Err(FormatParseError::InvalidCharacterAfterRightBracket)
);
}
}

2
src/vendor/mod.rs vendored
View File

@ -1,4 +1,2 @@
pub mod bytes;
pub mod cformat;
pub mod format;
pub mod str;