From 93004861e5bdc1ad847463d124ac97b478d27c4b Mon Sep 17 00:00:00 2001 From: David Pfeffer Date: Fri, 23 Oct 2026 10:30:04 -0400 Subject: [PATCH] fix: strip quotes from parsed package names --- .changeset/quoted-package-names.md | 5 ++++ README.md | 2 +- src/change.rs | 47 ++++++++++++++++++++---------- tests/change.rs | 37 +++++++++++++++++++++++ tests/change_set.rs | 20 +++++++++++++ 5 files changed, 94 insertions(+), 17 deletions(-) create mode 100644 .changeset/quoted-package-names.md diff --git a/.changeset/quoted-package-names.md b/.changeset/quoted-package-names.md new file mode 100644 index 0000000..a9e083a --- /dev/null +++ b/.changeset/quoted-package-names.md @@ -0,0 +1,5 @@ +--- +changesets: major +--- + +Strip a matching pair of single or double quotes from package names when parsing change files. Quoted package names now produce the unquoted package name in `Change::versioning`. diff --git a/README.md b/README.md index 1c7e97e..0c41202 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ The part of a changeset that applies to a single package and determines how that Change files are Markdown files whose names _must_ end with `.md`. The content of the file must be as follows: 1. A line containing `---` (three dashes) on its own line. -2. Any number of lines containing `package: change type` pairs where `package` defines a package that this change impacts and `change type` is a [change type](#change-type). One pair per line. The first `:` is used to determine the separation between package and change type, so the package name may not contain a `:`. +2. Any number of lines containing `package: change type` pairs where `package` defines a package that this change impacts and `change type` is a [change type](#change-type). One pair per line. The first `:` is used to determine the separation between package and change type, so the package name may not contain a `:`. Package names may be enclosed in a matching pair of single or double quotes; the quotes are removed when parsing. 3. A line containing `---` (three dashes) on its own line. 4. The rest of the file can contain any valid Markdown text. diff --git a/src/change.rs b/src/change.rs index 6946a15..2bcad9b 100644 --- a/src/change.rs +++ b/src/change.rs @@ -78,24 +78,39 @@ impl Change { if first_line.trim() != "---" { return Err(ParsingError::MissingFrontMatter); } - let versioning_iter = lines - .clone() - .take_while(|line| line.trim() != "---") - .map(|line| { - let parts = line - .split_once(':') - .ok_or(ParsingError::InvalidFrontMatter)?; - let package_name = PackageName::from(parts.0.trim()); - let change_type = ChangeType::from(parts.1.trim()); - Ok((package_name, change_type)) - }) - .collect::, ParsingError>>()?; - let versioning = Versioning::try_from_iter(versioning_iter)?; - let mut lines = lines.skip(versioning.len()); - let end_front_matter = lines.next().ok_or(ParsingError::InvalidFrontMatter)?; - if end_front_matter.trim() != "---" { + let mut versioning_iter = Vec::new(); + let mut end_front_matter = false; + for line in lines.by_ref() { + if line.trim() == "---" { + end_front_matter = true; + break; + } + let parts = line + .split_once(':') + .ok_or(ParsingError::InvalidFrontMatter)?; + let package_name = parts.0.trim(); + let package_name = if let Some(inner) = package_name + .strip_prefix('\'') + .and_then(|name| name.strip_suffix('\'')) + { + inner + } else if let Some(inner) = package_name + .strip_prefix('"') + .and_then(|name| name.strip_suffix('"')) + { + inner + } else { + package_name + }; + versioning_iter.push(( + PackageName::from(package_name), + ChangeType::from(parts.1.trim()), + )); + } + if !end_front_matter { return Err(ParsingError::InvalidFrontMatter); } + let versioning = Versioning::try_from_iter(versioning_iter)?; let summary = lines .skip_while(|line| line.trim().is_empty()) .collect::>() diff --git a/tests/change.rs b/tests/change.rs index 5a0275e..1978142 100644 --- a/tests/change.rs +++ b/tests/change.rs @@ -61,3 +61,40 @@ fn load_change() { Versioning::from(("my_package", ChangeType::Minor)) ); } + +#[test] +fn load_quoted_package_names() { + for (key, expected) in [ + (" '@scope/package' ", "@scope/package"), + ("\"@scope/package\"", "@scope/package"), + ("'unmatched", "'unmatched"), + ("'package\"", "'package\""), + ("package'embedded", "package'embedded"), + ("'embedded'quote'", "embedded'quote"), + ] { + let content = format!("---\n{key}: patch\n---\n\nSummary\n"); + let change = Change::from_file_name_and_content("test.md", &content).unwrap(); + assert!( + change + .versioning + .iter() + .any(|(name, kind)| { name == expected && *kind == ChangeType::Patch }) + ); + assert_eq!(change.summary, "Summary"); + } +} + +#[test] +fn normalized_duplicate_keys_keep_last_value_and_summary() { + let change = Change::from_file_name_and_content( + "test.md", + "---\n'package': patch\npackage: major\n---\n\nSummary\n", + ) + .unwrap(); + assert_eq!(change.versioning.len(), 1); + assert_eq!( + change.versioning.iter().next().unwrap(), + (&String::from("package"), &ChangeType::Major) + ); + assert_eq!(change.summary, "Summary"); +} diff --git a/tests/change_set.rs b/tests/change_set.rs index 169e83a..cbfd2de 100644 --- a/tests/change_set.rs +++ b/tests/change_set.rs @@ -74,3 +74,23 @@ fn load_changeset() { second_release.changes ); } + +#[test] +fn quoted_and_unquoted_package_names_share_a_release() { + let dir = tempdir().unwrap(); + std::fs::write( + dir.path().join("quoted.md"), + "---\n'@scope/package': minor\n---\n\nQuoted summary\n", + ) + .unwrap(); + std::fs::write( + dir.path().join("unquoted.md"), + "---\n@scope/package: patch\n---\n\nUnquoted summary\n", + ) + .unwrap(); + + let releases: Vec = ChangeSet::from_directory(&dir).unwrap().into(); + assert_eq!(releases.len(), 1); + assert_eq!(releases[0].package_name, "@scope/package"); + assert_eq!(releases[0].changes.len(), 2); +}