mirror of https://github.com/astral-sh/ruff
Remove need for vendored format/cformat code (#1573)
This commit is contained in:
parent
0c05488740
commit
cd5882c66d
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"] }
|
||||
|
|
|
|||
|
|
@ -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"] }
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
));
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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:#?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,2 @@
|
|||
pub mod bytes;
|
||||
pub mod cformat;
|
||||
pub mod format;
|
||||
pub mod str;
|
||||
|
|
|
|||
Loading…
Reference in New Issue