diff --git a/crates/pep508-rs/src/lib.rs b/crates/pep508-rs/src/lib.rs index e29a73e35..0ca61add8 100644 --- a/crates/pep508-rs/src/lib.rs +++ b/crates/pep508-rs/src/lib.rs @@ -617,9 +617,48 @@ fn parse_extras(cursor: &mut Cursor) -> Result, Pep508Error> { let Some(bracket_pos) = cursor.eat_char('[') else { return Ok(vec![]); }; + cursor.eat_whitespace(); + let mut extras = Vec::new(); + let mut is_first_iteration = true; loop { + // End of the extras section. (Empty extras are allowed.) + if let Some(']') = cursor.peek_char() { + cursor.next(); + break; + } + + // Comma separator + match (cursor.peek(), is_first_iteration) { + // For the first iteration, we don't expect a comma. + (Some((pos, ',')), true) => { + return Err(Pep508Error { + message: Pep508ErrorSource::String( + "Expected either alphanumerical character (starting the extra name) or ']' (ending the extras section), found ','".to_string() + ), + start: pos, + len: 1, + input: cursor.to_string(), + }); + } + // For the other iterations, the comma is required. + (Some((_, ',')), false) => { + cursor.next(); + } + (Some((pos, other)), false) => { + return Err(Pep508Error { + message: Pep508ErrorSource::String( + format!("Expected either ',' (separating extras) or ']' (ending the extras section), found '{other}'",) + ), + start: pos, + len: 1, + input: cursor.to_string(), + }); + } + _ => {} + } + // wsp* before the identifier cursor.eat_whitespace(); let mut buffer = String::new(); @@ -633,7 +672,7 @@ fn parse_extras(cursor: &mut Cursor) -> Result, Pep508Error> { input: cursor.to_string(), }; - // First char of the identifier + // First char of the identifier. match cursor.next() { // letterOrDigit Some((_, alphanumeric @ ('a'..='z' | 'A'..='Z' | '0'..='9'))) => { @@ -673,33 +712,12 @@ fn parse_extras(cursor: &mut Cursor) -> Result, Pep508Error> { }; // wsp* after the identifier cursor.eat_whitespace(); - // end or next identifier? - match cursor.next() { - Some((_, ',')) => { - extras.push( - ExtraName::new(buffer) - .expect("`ExtraName` validation should match PEP 508 parsing"), - ); - } - Some((_, ']')) => { - extras.push( - ExtraName::new(buffer) - .expect("`ExtraName` validation should match PEP 508 parsing"), - ); - break; - } - Some((pos, other)) => { - return Err(Pep508Error { - message: Pep508ErrorSource::String(format!( - "Expected either ',' (separating extras) or ']' (ending the extras section), found '{other}'" - )), - start: pos, - len: other.len_utf8(), - input: cursor.to_string(), - }); - } - None => return Err(early_eof_error), - } + + // Add the parsed extra + extras.push( + ExtraName::new(buffer).expect("`ExtraName` validation should match PEP 508 parsing"), + ); + is_first_iteration = false; } Ok(extras) @@ -1241,6 +1259,18 @@ mod tests { ); } + #[test] + fn error_extras_illegal_start3() { + assert_err( + "black[,]", + indoc! {" + Expected either alphanumerical character (starting the extra name) or ']' (ending the extras section), found ',' + black[,] + ^" + }, + ); + } + #[test] fn error_extras_illegal_character() { assert_err( @@ -1271,6 +1301,30 @@ mod tests { ); } + #[test] + fn empty_extras() { + let black = Requirement::from_str("black[]").unwrap(); + assert_eq!(black.extras, vec![]); + } + + #[test] + fn empty_extras_with_spaces() { + let black = Requirement::from_str("black[ ]").unwrap(); + assert_eq!(black.extras, vec![]); + } + + #[test] + fn error_extra_with_trailing_comma() { + assert_err( + "black[d,]", + indoc! {" + Expected an alphanumeric character starting the extra name, found ']' + black[d,] + ^" + }, + ); + } + #[test] fn error_parenthesized_pep440() { assert_err( diff --git a/crates/requirements-txt/src/lib.rs b/crates/requirements-txt/src/lib.rs index 0fc7ef9bb..0ab290994 100644 --- a/crates/requirements-txt/src/lib.rs +++ b/crates/requirements-txt/src/lib.rs @@ -1205,7 +1205,7 @@ mod test { }, { insta::assert_display_snapshot!(errors, @r###" Couldn't parse requirement in `` at position 6 - Expected an alphanumeric character starting the extra name, found ',' + Expected either alphanumerical character (starting the extra name) or ']' (ending the extras section), found ',' black[,abcdef] ^ "###);