Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit fbdc0eb

Browse files
hifumi9zkochan
andauthored
fix(version-policy): treat multiple exact-version excludes as equivalent to a single disjunction (#12516)
`minimumReleaseAgeExclude` (and `trustPolicyExclude`) ignored every rule after the first match for a given package, so two separate exact-version entries like `[[email protected], [email protected]]` could still trip the policy for the second version while `[[email protected] || 2.5.6]` (a single disjunction entry) worked. That made list semantics depend on whether the user happened to merge versions into one `||` selector, which is surprising and unsafe for supply-chain exclusion lists. Walk every matching rule and merge consecutive `name@version[...]` matches in source order with duplicates removed. A bare-name or wildcard match still terminates the walk, with first-match precedence between bare and exact rules: a wildcard listed after an exact-version rule no longer silently widens the exclusion to every version of the package, while a bare-name rule listed first keeps its existing `AnyVersion` semantics. Apply the same change to the pacquet port so both stacks stay in sync. Closes #12463 --------- Co-authored-by: Zoltan Kochan <[email protected]>
1 parent ee9fab5 commit fbdc0eb

5 files changed

Lines changed: 130 additions & 17 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@pnpm/config.version-policy": patch
3+
"pnpm": patch
4+
---
5+
6+
Fixed `minimumReleaseAgeExclude` and `trustPolicyExclude` so multiple exact-version entries for the same package behave the same as a single `||` disjunction entry. Previously only the first matching rule's versions were honored, so a config like `[[email protected], [email protected]]` could still flag `[email protected]` as violating `minimumReleaseAge`, while `[[email protected] || 2.5.6]` worked as expected [#12463](https://github.com/pnpm/pnpm/issues/12463).

config/version-policy/src/index.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,16 +72,27 @@ export function expandPackageVersionSpecs (specs: string[]): Set<string> {
7272
}
7373

7474
function evaluateVersionPolicy (rules: VersionPolicyRule[], pkgName: string): boolean | string[] {
75+
let matchedVersions: string[] | undefined
76+
let seen: Set<string> | undefined
7577
for (const { nameMatcher, exactVersions } of rules) {
7678
if (!nameMatcher(pkgName)) {
7779
continue
7880
}
7981
if (exactVersions.length === 0) {
80-
return true
82+
return matchedVersions ?? true
83+
}
84+
if (matchedVersions == null) {
85+
matchedVersions = []
86+
seen = new Set()
87+
}
88+
for (const version of exactVersions) {
89+
if (!seen!.has(version)) {
90+
seen!.add(version)
91+
matchedVersions.push(version)
92+
}
8193
}
82-
return exactVersions
8394
}
84-
return false
95+
return matchedVersions ?? false
8596
}
8697

8798
interface VersionPolicyRule {

config/version-policy/test/index.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,34 @@ test('createPackageVersionPolicy()', () => {
5252
const match = createPackageVersionPolicy(['[email protected]||1.0.1 || 1.0.2'])
5353
expect(match('pkg')).toStrictEqual(['1.0.0', '1.0.1', '1.0.2'])
5454
}
55+
{
56+
const match = createPackageVersionPolicy(['[email protected]', '[email protected]'])
57+
expect(match('form-data')).toStrictEqual(['4.0.6', '2.5.6'])
58+
}
59+
{
60+
const match = createPackageVersionPolicy(['[email protected]', '[email protected] || 2.5.7'])
61+
expect(match('form-data')).toStrictEqual(['4.0.6', '2.5.6', '2.5.7'])
62+
}
63+
{
64+
const match = createPackageVersionPolicy(['[email protected]', '[email protected]'])
65+
expect(match('form-data')).toStrictEqual(['4.0.6'])
66+
}
67+
{
68+
const match = createPackageVersionPolicy(['[email protected]', 'axios'])
69+
expect(match('axios')).toStrictEqual(['1.12.2'])
70+
}
71+
{
72+
const match = createPackageVersionPolicy(['axios', '[email protected]'])
73+
expect(match('axios')).toBe(true)
74+
}
75+
{
76+
const match = createPackageVersionPolicy(['[email protected]', 'ax*'])
77+
expect(match('axios')).toStrictEqual(['1.12.2'])
78+
}
79+
{
80+
const match = createPackageVersionPolicy(['ax*', '[email protected]'])
81+
expect(match('axios')).toBe(true)
82+
}
5583
})
5684

5785
test('createPackageVersionPolicyOrThrow() rewraps parser errors with INVALID_<KEY>', () => {

pacquet/crates/config/src/version_policy.rs

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -111,9 +111,10 @@ pub enum PolicyMatch {
111111
}
112112

113113
/// Matcher-based version policy built from a list of
114-
/// `<name-pattern>[@<version>||<version>...]` rules. Rules are walked
115-
/// in order; the first whose name matcher accepts the input package
116-
/// name wins. Mirrors upstream's [`createPackageVersionPolicy`](https://github.com/pnpm/pnpm/blob/2a9bd897bf/config/version-policy/src/index.ts#L6-L13).
114+
/// `<name-pattern>[@<version>||<version>...]` rules. See
115+
/// [`PackageVersionPolicy::matches`] for the evaluation semantics.
116+
/// Mirrors upstream's
117+
/// [`createPackageVersionPolicy`](https://github.com/pnpm/pnpm/blob/2a9bd897bf/config/version-policy/src/index.ts#L6-L13).
117118
///
118119
/// Used by `minimumReleaseAgeExclude` and `trustPolicyExclude`, both
119120
/// of which need wildcard name patterns (`is-*`, `@scope/*`) AND
@@ -143,23 +144,37 @@ struct VersionPolicyRule {
143144
}
144145

145146
impl PackageVersionPolicy {
146-
/// Evaluate the policy against a package name. Returns the
147-
/// matching rule's payload (`AnyVersion` for a bare-name rule,
148-
/// `ExactVersions` for `name@versions...`), or `PolicyMatch::No`
149-
/// when no rule matched.
147+
/// Evaluate the policy against a package name, merging the exact
148+
/// versions of all matching `name@version[...]` rules.
149+
///
150+
/// A bare-name or wildcard rule matches every version, but never
151+
/// widens exact versions already accumulated from earlier rules: a
152+
/// wildcard listed after an exact-version rule does not silently
153+
/// turn the exclusion into every version of the package.
150154
#[must_use]
151155
pub fn matches(&self, pkg_name: &str) -> PolicyMatch {
156+
let mut merged: Option<(Vec<String>, HashSet<String>)> = None;
152157
for rule in &self.rules {
153158
if !rule.name_matcher.matches(pkg_name) {
154159
continue;
155160
}
156-
return if rule.exact_versions.is_empty() {
157-
PolicyMatch::AnyVersion
158-
} else {
159-
PolicyMatch::ExactVersions(rule.exact_versions.clone())
160-
};
161+
if rule.exact_versions.is_empty() {
162+
return match merged {
163+
Some((versions, _)) => PolicyMatch::ExactVersions(versions),
164+
None => PolicyMatch::AnyVersion,
165+
};
166+
}
167+
let (acc, seen) = merged.get_or_insert_with(|| (Vec::new(), HashSet::new()));
168+
for version in &rule.exact_versions {
169+
if seen.insert(version.clone()) {
170+
acc.push(version.clone());
171+
}
172+
}
173+
}
174+
match merged {
175+
Some((versions, _)) => PolicyMatch::ExactVersions(versions),
176+
None => PolicyMatch::No,
161177
}
162-
PolicyMatch::No
163178
}
164179
}
165180

pacquet/crates/config/src/version_policy/tests.rs

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,13 +116,66 @@ fn create_policy_scoped_bare_name_returns_any_version() {
116116
}
117117

118118
#[test]
119-
fn create_policy_first_matching_rule_wins() {
119+
fn create_policy_distinct_name_rules() {
120120
let policy = create_package_version_policy(["[email protected]", "[email protected]", "is-*"]).unwrap();
121121
assert_eq!(policy.matches("axios"), PolicyMatch::ExactVersions(vec!["1.12.2".to_string()]));
122122
assert_eq!(policy.matches("lodash"), PolicyMatch::ExactVersions(vec!["4.17.21".to_string()]));
123123
assert_eq!(policy.matches("is-odd"), PolicyMatch::AnyVersion);
124124
}
125125

126+
#[test]
127+
fn create_policy_multiple_exact_version_rules_for_same_name_merge() {
128+
let policy = create_package_version_policy(["[email protected]", "[email protected]"]).unwrap();
129+
assert_eq!(
130+
policy.matches("form-data"),
131+
PolicyMatch::ExactVersions(vec!["4.0.6".to_string(), "2.5.6".to_string()]),
132+
);
133+
}
134+
135+
#[test]
136+
fn create_policy_merges_exact_versions_and_unions_for_same_name() {
137+
let policy =
138+
create_package_version_policy(["[email protected]", "[email protected] || 2.5.7"]).unwrap();
139+
assert_eq!(
140+
policy.matches("form-data"),
141+
PolicyMatch::ExactVersions(vec![
142+
"4.0.6".to_string(),
143+
"2.5.6".to_string(),
144+
"2.5.7".to_string(),
145+
]),
146+
);
147+
}
148+
149+
#[test]
150+
fn create_policy_deduplicates_repeated_versions_across_rules() {
151+
let policy = create_package_version_policy(["[email protected]", "[email protected]"]).unwrap();
152+
assert_eq!(policy.matches("form-data"), PolicyMatch::ExactVersions(vec!["4.0.6".to_string()]));
153+
}
154+
155+
#[test]
156+
fn create_policy_bare_rule_after_exact_keeps_exact_versions() {
157+
let policy = create_package_version_policy(["[email protected]", "axios"]).unwrap();
158+
assert_eq!(policy.matches("axios"), PolicyMatch::ExactVersions(vec!["1.12.2".to_string()]));
159+
}
160+
161+
#[test]
162+
fn create_policy_bare_rule_listed_first_wins_over_later_exact() {
163+
let policy = create_package_version_policy(["axios", "[email protected]"]).unwrap();
164+
assert_eq!(policy.matches("axios"), PolicyMatch::AnyVersion);
165+
}
166+
167+
#[test]
168+
fn create_policy_wildcard_after_exact_keeps_exact_versions() {
169+
let policy = create_package_version_policy(["[email protected]", "ax*"]).unwrap();
170+
assert_eq!(policy.matches("axios"), PolicyMatch::ExactVersions(vec!["1.12.2".to_string()]));
171+
}
172+
173+
#[test]
174+
fn create_policy_wildcard_listed_first_wins_over_later_exact() {
175+
let policy = create_package_version_policy(["ax*", "[email protected]"]).unwrap();
176+
assert_eq!(policy.matches("axios"), PolicyMatch::AnyVersion);
177+
}
178+
126179
#[test]
127180
fn create_policy_range_specifier_in_version_errors() {
128181
let err = create_package_version_policy(["lodash@^4.17.0"]).expect_err("must reject");

0 commit comments

Comments
 (0)