diff --git a/crates/pep508-rs/src/lib.rs b/crates/pep508-rs/src/lib.rs index 441548b81..eae8015ac 100644 --- a/crates/pep508-rs/src/lib.rs +++ b/crates/pep508-rs/src/lib.rs @@ -415,6 +415,15 @@ impl<'a> Cursor<'a> { } } + /// Returns a new cursor starting at the given position. + pub fn at(self, pos: usize) -> Self { + Self { + input: self.input, + chars: self.input[pos..].chars(), + pos, + } + } + /// Returns the current byte position of the cursor. fn pos(&self) -> usize { self.pos @@ -778,6 +787,8 @@ fn parse_version_specifier_parentheses( /// Parse a [dependency specifier](https://packaging.python.org/en/latest/specifications/dependency-specifiers) fn parse(cursor: &mut Cursor) -> Result { + let start = cursor.pos(); + // Technically, the grammar is: // ```text // name_req = name wsp* extras? wsp* versionspec? wsp* quoted_marker? @@ -810,27 +821,30 @@ fn parse(cursor: &mut Cursor) -> Result { Some('(') => parse_version_specifier_parentheses(cursor)?, Some('<' | '=' | '>' | '~' | '!') => parse_version_specifier(cursor)?, Some(';') | None => None, - // Ex) `https://...` or `git+https://...` - Some(':') | Some('+') => { - return Err(Pep508Error { - message: Pep508ErrorSource::String( - "URL requirement is missing a package name; expected: `package_name @`" - .to_string(), - ), - start: cursor.pos(), - len: 1, - input: cursor.to_string(), - }); - } Some(other) => { - return Err(Pep508Error { - message: Pep508ErrorSource::String(format!( - "Expected one of `@`, `(`, `<`, `=`, `>`, `~`, `!`, `;`, found `{other}`" - )), - start: cursor.pos(), - len: other.len_utf8(), - input: cursor.to_string(), - }) + // Rewind to the start of the version specifier, to see if the user added a URL without + // a package name. pip supports this in `requirements.txt`, but it doesn't adhere to + // the PEP 508 grammar. + let mut clone = cursor.clone().at(start); + return if let Ok(url) = parse_url(&mut clone) { + Err(Pep508Error { + message: Pep508ErrorSource::String(format!( + "URL requirement is missing a package name; expected: `package_name @ {url}`", + )), + start, + len: clone.pos() - start, + input: clone.to_string(), + }) + } else { + Err(Pep508Error { + message: Pep508ErrorSource::String(format!( + "Expected one of `@`, `(`, `<`, `=`, `>`, `~`, `!`, `;`, found `{other}`" + )), + start: cursor.pos(), + len: other.len_utf8(), + input: cursor.to_string(), + }) + }; } }; @@ -1286,6 +1300,18 @@ mod tests { ); } + #[test] + fn error_bare_url() { + assert_err( + r#"git+https://github.com/pallets/flask.git"#, + indoc! {" + URL requirement is missing a package name; expected: `package_name @ git+https://github.com/pallets/flask.git` + git+https://github.com/pallets/flask.git + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^" + }, + ); + } + #[test] fn error_no_comma_between_extras() { assert_err(