diff --git a/crates/pep508-rs/src/lib.rs b/crates/pep508-rs/src/lib.rs index d495748dee8d..ebde80ab9abf 100644 --- a/crates/pep508-rs/src/lib.rs +++ b/crates/pep508-rs/src/lib.rs @@ -564,19 +564,33 @@ impl Display for Cursor<'_> { fn parse_name(cursor: &mut Cursor) -> Result { // https://peps.python.org/pep-0508/#names // ^([A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9])$ with re.IGNORECASE + let start = cursor.pos(); let mut name = String::new(); + if let Some((index, char)) = cursor.next() { if matches!(char, 'A'..='Z' | 'a'..='z' | '0'..='9') { name.push(char); } else { - return Err(Pep508Error { - message: Pep508ErrorSource::String(format!( + // Check if the user added a filesystem path 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 looks_like_file_path(&mut clone) { + Err(Pep508Error { + message: Pep508ErrorSource::UnsupportedRequirement("URL requirement must be preceded by a package name. Add the name of the package before the URL (e.g., `package_name @ /path/to/file`).".to_string()), + start, + len: clone.pos() - start, + input: clone.to_string(), + }) + } else { + Err(Pep508Error { + message: Pep508ErrorSource::String(format!( "Expected package name starting with an alphanumeric character, found '{char}'" )), - start: index, - len: char.len_utf8(), - input: cursor.to_string(), - }); + start: index, + len: char.len_utf8(), + input: cursor.to_string(), + }) + }; } } else { return Err(Pep508Error { @@ -752,6 +766,35 @@ fn parse_url(cursor: &mut Cursor, working_dir: Option<&Path>) -> Result bool { + let Some((_, first_char)) = cursor.next() else { + return false; + }; + + // Ex) `/bin/ls` + if first_char == '\\' || first_char == '/' || first_char == '.' { + // Read until the end of the path. + cursor.take_while(|char| !char.is_whitespace()); + return true; + } + + // Ex) `C:` + if first_char.is_alphabetic() { + if let Some((_, second_char)) = cursor.next() { + if second_char == ':' { + // Read until the end of the path. + cursor.take_while(|char| !char.is_whitespace()); + return true; + } + } + } + + false +} + /// Create a `VerbatimUrl` to represent the requirement. fn preprocess_url( url: &str, @@ -1491,6 +1534,18 @@ mod tests { ); } + #[test] + fn error_bare_file_path() { + assert_snapshot!( + parse_err(r"/path/to/flask.tar.gz"), + @r###" + URL requirement must be preceded by a package name. Add the name of the package before the URL (e.g., `package_name @ /path/to/file`). + /path/to/flask.tar.gz + ^^^^^^^^^^^^^^^^^^^^^ + "### + ); + } + #[test] fn error_no_comma_between_extras() { assert_snapshot!(