From 037ade901efabe7acf070d9f21f9b0fdfb97f721 Mon Sep 17 00:00:00 2001 From: arpitjain099 Date: Sun, 19 Jul 2026 00:58:33 +0900 Subject: [PATCH] Fix UnboundLocalError on single-clause npm version ranges NpmVersionRange.from_native() routes wildcard tokens such as ">=1.x" and "*.x" through get_npm_version_constraints_from_semver_npm_spec(). When NpmSpec(...).clause.simplify() collapses to a single node-semver Range clause (rather than an AnyOf or AllOf), the helper never bound anyof_constraints and returned it, raising a raw UnboundLocalError out of the parser. Handle the single Range clause by building one VersionConstraint from its operator and target, and raise InvalidVersionRange for any other unexpected clause type. Multi-clause ranges are unchanged. Signed-off-by: arpitjain099 --- src/univers/version_range.py | 26 ++++++++++++++++---------- tests/test_version_range.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 10 deletions(-) diff --git a/src/univers/version_range.py b/src/univers/version_range.py index c50bc87c..2a5da022 100644 --- a/src/univers/version_range.py +++ b/src/univers/version_range.py @@ -13,6 +13,7 @@ from packaging.specifiers import SpecifierSet from semantic_version.base import AllOf from semantic_version.base import AnyOf +from semantic_version.base import Range from univers import gem from univers import maven @@ -324,16 +325,21 @@ def get_npm_version_constraints_from_semver_npm_spec(string, cls): """ spec = semantic_version.NpmSpec(string) clause = spec.clause.simplify() - if isinstance(clause, (AnyOf, AllOf)): - anyof_constraints = [] - if isinstance(clause, AnyOf): - for allof_clause in clause.clauses: - anyof_constraints.extend(get_allof_constraints(cls, allof_clause)) - elif isinstance(clause, AllOf): - alloc = get_allof_constraints(cls, clause) - anyof_constraints.extend(alloc) - else: - raise ValueError(f"Unknown clause type: {spec!r}") + anyof_constraints = [] + if isinstance(clause, AnyOf): + for allof_clause in clause.clauses: + anyof_constraints.extend(get_allof_constraints(cls, allof_clause)) + elif isinstance(clause, AllOf): + anyof_constraints.extend(get_allof_constraints(cls, clause)) + elif isinstance(clause, Range): + # A simplified NpmSpec can collapse to a single Range clause, for + # example ">=1.x" or "*.x". Handle it here so we do not fall through + # with an unbound result. + comparator = cls.vers_by_native_comparators[clause.operator] + version = cls.version_class(str(clause.target)) + anyof_constraints.append(VersionConstraint(comparator=comparator, version=version)) + else: + raise InvalidVersionRange(f"Unknown clause type: {clause!r}") return anyof_constraints diff --git a/tests/test_version_range.py b/tests/test_version_range.py index b2cb3c4a..4999bdbc 100644 --- a/tests/test_version_range.py +++ b/tests/test_version_range.py @@ -18,6 +18,7 @@ from univers.version_range import IntdotVersionRange from univers.version_range import InvalidVersionRange from univers.version_range import MattermostVersionRange +from univers.version_range import NpmVersionRange from univers.version_range import OpensslVersionRange from univers.version_range import PypiVersionRange from univers.version_range import VersionRange @@ -295,6 +296,34 @@ def test_PypiVersionRange_raises_ivr_for_unsupported_and_invalid_ranges(range, w assert expected == str(PypiVersionRange.from_native(range)) +@pytest.mark.parametrize( + "native, expected", + [ + (">=1.x", "vers:npm/>=1.0.0"), + ("*.x", "vers:npm/>=0.0.0"), + (">1.x", "vers:npm/>=2.0.0"), + ("<=2.x", "vers:npm/<3.0.0"), + ], +) +def test_npm_range_from_native_single_clause(native, expected): + # These wildcard ranges simplify to a single node-semver clause and used to + # raise an UnboundLocalError instead of returning a VersionRange. + assert str(NpmVersionRange.from_native(native)) == expected + + +@pytest.mark.parametrize( + "native, expected", + [ + ("1.x", "vers:npm/>=1.0.0|<2.0.0"), + ("0.x", "vers:npm/>=0.0.0|<1.0.0"), + ("~1.x", "vers:npm/>=1.0.0|<2.0.0"), + ("1.2.x - 2.0.0", "vers:npm/>=1.2.0|<=2.0.0"), + ], +) +def test_npm_range_from_native_multi_clause_still_works(native, expected): + assert str(NpmVersionRange.from_native(native)) == expected + + def test_invert(): vers_with_equal_operator = VersionRange.from_string("vers:gem/1.0") assert str(vers_with_equal_operator.invert()) == "vers:gem/!=1.0"