use std::str::FromStr;
use pep440_rs::{Version, VersionPattern, VersionSpecifier};
use crate::cursor::Cursor;
use crate::marker::MarkerValueExtra;
use crate::{
ExtraName, ExtraOperator, MarkerExpression, MarkerOperator, MarkerTree, MarkerValue,
MarkerValueVersion, MarkerWarningKind, Pep508Error, Pep508ErrorSource, Pep508Url, Reporter,
};
fn parse_marker_operator<T: Pep508Url>(
cursor: &mut Cursor,
) -> Result<MarkerOperator, Pep508Error<T>> {
let (start, len) = if cursor.peek_char().is_some_and(char::is_alphabetic) {
cursor.take_while(|char| !char.is_whitespace() && char != '\'' && char != '"')
} else {
cursor.take_while(|char| matches!(char, '<' | '=' | '>' | '~' | '!'))
};
let operator = cursor.slice(start, len);
if operator == "not" {
match cursor.next() {
None => {
return Err(Pep508Error {
message: Pep508ErrorSource::String(
"Expected whitespace after 'not', found end of input".to_string(),
),
start: cursor.pos(),
len: 1,
input: cursor.to_string(),
});
}
Some((_, whitespace)) if whitespace.is_whitespace() => {}
Some((pos, other)) => {
return Err(Pep508Error {
message: Pep508ErrorSource::String(format!(
"Expected whitespace after `not`, found `{other}`"
)),
start: pos,
len: other.len_utf8(),
input: cursor.to_string(),
});
}
};
cursor.eat_whitespace();
cursor.next_expect_char('i', cursor.pos())?;
cursor.next_expect_char('n', cursor.pos())?;
return Ok(MarkerOperator::NotIn);
}
MarkerOperator::from_str(operator).map_err(|_| Pep508Error {
message: Pep508ErrorSource::String(format!(
"Expected a valid marker operator (such as `>=` or `not in`), found `{operator}`"
)),
start,
len,
input: cursor.to_string(),
})
}
pub(crate) fn parse_marker_value<T: Pep508Url>(
cursor: &mut Cursor,
) -> Result<MarkerValue, Pep508Error<T>> {
match cursor.peek() {
None => Err(Pep508Error {
message: Pep508ErrorSource::String(
"Expected marker value, found end of dependency specification".to_string(),
),
start: cursor.pos(),
len: 1,
input: cursor.to_string(),
}),
Some((start_pos, quotation_mark @ ('"' | '\''))) => {
cursor.next();
let (start, len) = cursor.take_while(|c| c != quotation_mark);
let value = cursor.slice(start, len).to_string();
cursor.next_expect_char(quotation_mark, start_pos)?;
Ok(MarkerValue::QuotedString(value))
}
Some(_) => {
let (start, len) = cursor.take_while(|char| {
!char.is_whitespace() && !['>', '=', '<', '!', '~', ')'].contains(&char)
});
let key = cursor.slice(start, len);
MarkerValue::from_str(key).map_err(|_| Pep508Error {
message: Pep508ErrorSource::String(format!(
"Expected a quoted string or a valid marker name, found `{key}`"
)),
start,
len,
input: cursor.to_string(),
})
}
}
}
pub(crate) fn parse_marker_key_op_value<T: Pep508Url>(
cursor: &mut Cursor,
reporter: &mut impl Reporter,
) -> Result<Option<MarkerExpression>, Pep508Error<T>> {
cursor.eat_whitespace();
let l_value = parse_marker_value(cursor)?;
cursor.eat_whitespace();
let operator = parse_marker_operator(cursor)?;
cursor.eat_whitespace();
let r_value = parse_marker_value(cursor)?;
let expr = match l_value {
MarkerValue::MarkerEnvVersion(key) => {
let MarkerValue::QuotedString(value) = r_value else {
reporter.report(
MarkerWarningKind::Pep440Error,
format!(
"Expected double quoted PEP 440 version to compare with {key},
found {r_value}, will be ignored"
),
);
return Ok(None);
};
if let Some(expr) = parse_version_in_expr(key.clone(), operator, &value, reporter) {
return Ok(Some(expr));
}
parse_version_expr(key.clone(), operator, &value, reporter)
}
MarkerValue::MarkerEnvString(key) => {
let value = match r_value {
MarkerValue::Extra
| MarkerValue::MarkerEnvVersion(_)
| MarkerValue::MarkerEnvString(_) => {
reporter.report(
MarkerWarningKind::MarkerMarkerComparison,
"Comparing two markers with each other doesn't make any sense,
will be ignored"
.to_string(),
);
return Ok(None);
}
MarkerValue::QuotedString(r_string) => r_string,
};
if operator == MarkerOperator::TildeEqual {
reporter.report(
MarkerWarningKind::LexicographicComparison,
"Can't compare strings with `~=`, will be ignored".to_string(),
);
return Ok(None);
}
Some(MarkerExpression::String {
key,
operator,
value,
})
}
MarkerValue::Extra => {
let value = match r_value {
MarkerValue::MarkerEnvVersion(_)
| MarkerValue::MarkerEnvString(_)
| MarkerValue::Extra => {
reporter.report(
MarkerWarningKind::ExtraInvalidComparison,
"Comparing extra with something other than a quoted string is wrong,
will be ignored"
.to_string(),
);
return Ok(None);
}
MarkerValue::QuotedString(value) => value,
};
parse_extra_expr(operator, &value, reporter)
}
MarkerValue::QuotedString(l_string) => {
match r_value {
MarkerValue::MarkerEnvVersion(key) => {
parse_inverted_version_expr(&l_string, operator, key.clone(), reporter)
}
MarkerValue::MarkerEnvString(key) => Some(MarkerExpression::String {
key,
operator: operator.invert(),
value: l_string,
}),
MarkerValue::Extra => parse_extra_expr(operator, &l_string, reporter),
MarkerValue::QuotedString(_) => {
reporter.report(
MarkerWarningKind::StringStringComparison,
format!(
"Comparing two quoted strings with each other doesn't make sense:
'{l_string}' {operator} {r_value}, will be ignored"
),
);
None
}
}
}
};
Ok(expr)
}
fn parse_version_in_expr(
key: MarkerValueVersion,
operator: MarkerOperator,
value: &str,
reporter: &mut impl Reporter,
) -> Option<MarkerExpression> {
if !matches!(operator, MarkerOperator::In | MarkerOperator::NotIn) {
return None;
}
let negated = matches!(operator, MarkerOperator::NotIn);
let mut cursor = Cursor::new(value);
let mut versions = Vec::new();
loop {
cursor.eat_whitespace();
let (start, len) = cursor.take_while(|c| !c.is_whitespace());
if len == 0 {
break;
}
let version = match Version::from_str(cursor.slice(start, len)) {
Ok(version) => version,
Err(err) => {
reporter.report(
MarkerWarningKind::Pep440Error,
format!(
"Expected PEP 440 versions to compare with {key}, found {value},
will be ignored: {err}"
),
);
return None;
}
};
versions.push(version);
}
Some(MarkerExpression::VersionIn {
key,
versions,
negated,
})
}
fn parse_version_expr(
key: MarkerValueVersion,
marker_operator: MarkerOperator,
value: &str,
reporter: &mut impl Reporter,
) -> Option<MarkerExpression> {
let pattern = match value.parse::<VersionPattern>() {
Ok(pattern) => pattern,
Err(err) => {
reporter.report(
MarkerWarningKind::Pep440Error,
format!(
"Expected PEP 440 version to compare with {key}, found {value},
will be ignored: {err}"
),
);
return None;
}
};
let Some(operator) = marker_operator.to_pep440_operator() else {
reporter.report(
MarkerWarningKind::Pep440Error,
format!(
"Expected PEP 440 version operator to compare {key} with `{version}`,
found `{marker_operator}`, will be ignored",
version = pattern.version()
),
);
return None;
};
let specifier = match VersionSpecifier::from_pattern(operator, pattern) {
Ok(specifier) => specifier,
Err(err) => {
reporter.report(
MarkerWarningKind::Pep440Error,
format!("Invalid operator/version combination: {err}"),
);
return None;
}
};
Some(MarkerExpression::Version { key, specifier })
}
fn parse_inverted_version_expr(
value: &str,
marker_operator: MarkerOperator,
key: MarkerValueVersion,
reporter: &mut impl Reporter,
) -> Option<MarkerExpression> {
let marker_operator = marker_operator.invert();
let version = match value.parse::<Version>() {
Ok(version) => version,
Err(err) => {
reporter.report(
MarkerWarningKind::Pep440Error,
format!(
"Expected PEP 440 version to compare with {key}, found {value},
will be ignored: {err}"
),
);
return None;
}
};
let Some(operator) = marker_operator.to_pep440_operator() else {
reporter.report(
MarkerWarningKind::Pep440Error,
format!(
"Expected PEP 440 version operator to compare {key} with `{version}`,
found `{marker_operator}`, will be ignored"
),
);
return None;
};
let specifier = match VersionSpecifier::from_version(operator, version) {
Ok(specifier) => specifier,
Err(err) => {
reporter.report(
MarkerWarningKind::Pep440Error,
format!("Invalid operator/version combination: {err}"),
);
return None;
}
};
Some(MarkerExpression::Version { key, specifier })
}
fn parse_extra_expr(
operator: MarkerOperator,
value: &str,
reporter: &mut impl Reporter,
) -> Option<MarkerExpression> {
let name = match ExtraName::from_str(value) {
Ok(name) => MarkerValueExtra::Extra(name),
Err(err) => {
reporter.report(
MarkerWarningKind::ExtraInvalidComparison,
format!("Expected extra name (found `{value}`): {err}"),
);
MarkerValueExtra::Arbitrary(value.to_string())
}
};
if let Some(operator) = ExtraOperator::from_marker_operator(operator) {
return Some(MarkerExpression::Extra { operator, name });
}
reporter.report(
MarkerWarningKind::ExtraInvalidComparison,
"Comparing extra with something other than a quoted string is wrong,
will be ignored"
.to_string(),
);
None
}
fn parse_marker_expr<T: Pep508Url>(
cursor: &mut Cursor,
reporter: &mut impl Reporter,
) -> Result<Option<MarkerTree>, Pep508Error<T>> {
cursor.eat_whitespace();
if let Some(start_pos) = cursor.eat_char('(') {
let marker = parse_marker_or(cursor, reporter)?;
cursor.next_expect_char(')', start_pos)?;
Ok(marker)
} else {
Ok(parse_marker_key_op_value(cursor, reporter)?.map(MarkerTree::expression))
}
}
fn parse_marker_and<T: Pep508Url>(
cursor: &mut Cursor,
reporter: &mut impl Reporter,
) -> Result<Option<MarkerTree>, Pep508Error<T>> {
parse_marker_op(cursor, "and", MarkerTree::and, parse_marker_expr, reporter)
}
fn parse_marker_or<T: Pep508Url>(
cursor: &mut Cursor,
reporter: &mut impl Reporter,
) -> Result<Option<MarkerTree>, Pep508Error<T>> {
parse_marker_op(
cursor,
"or",
MarkerTree::or,
|cursor, reporter| parse_marker_and(cursor, reporter),
reporter,
)
}
#[allow(clippy::type_complexity)]
fn parse_marker_op<T: Pep508Url, R: Reporter>(
cursor: &mut Cursor,
op: &str,
apply: fn(&mut MarkerTree, MarkerTree),
parse_inner: fn(&mut Cursor, &mut R) -> Result<Option<MarkerTree>, Pep508Error<T>>,
reporter: &mut R,
) -> Result<Option<MarkerTree>, Pep508Error<T>> {
let mut tree = None;
let first_element = parse_inner(cursor, reporter)?;
if let Some(expression) = first_element {
match tree {
Some(ref mut tree) => apply(tree, expression),
None => tree = Some(expression),
}
}
loop {
cursor.eat_whitespace();
let (start, len) = cursor.peek_while(|c| !c.is_whitespace());
match cursor.slice(start, len) {
value if value == op => {
cursor.take_while(|c| !c.is_whitespace());
if let Some(expression) = parse_inner(cursor, reporter)? {
match tree {
Some(ref mut tree) => apply(tree, expression),
None => tree = Some(expression),
}
}
}
_ => return Ok(tree),
}
}
}
pub(crate) fn parse_markers_cursor<T: Pep508Url>(
cursor: &mut Cursor,
reporter: &mut impl Reporter,
) -> Result<Option<MarkerTree>, Pep508Error<T>> {
let marker = parse_marker_or(cursor, reporter)?;
cursor.eat_whitespace();
if let Some((pos, unexpected)) = cursor.next() {
return Err(Pep508Error {
message: Pep508ErrorSource::String(format!(
"Unexpected character '{unexpected}', expected 'and', 'or' or end of input"
)),
start: pos,
len: cursor.remaining(),
input: cursor.to_string(),
});
};
Ok(marker)
}
pub(crate) fn parse_markers<T: Pep508Url>(
markers: &str,
reporter: &mut impl Reporter,
) -> Result<MarkerTree, Pep508Error<T>> {
let mut chars = Cursor::new(markers);
parse_markers_cursor(&mut chars, reporter).map(|result| result.unwrap_or(MarkerTree::TRUE))
}