Range improvements#7
Merged
thetechnick merged 14 commits intoMay 20, 2026
Merged
Conversation
- Fix Range.Contains(OR) to check ALL branches instead of ANY - Implement OR.Contains(OR) with proper branch comparison - Implement AND.Contains(AND) with constraint matching - Add 30+ edge case tests to prevent regressions Fixed bugs: 1. Range.Contains(OR): Was returning true if ANY branch was contained, now correctly checks that ALL branches are contained 2. OR.Contains(OR): Implemented proper branch-by-branch comparison 3. AND.Contains(AND): Implemented proper constraint matching for AND with multiple != constraints All edge cases now correctly handled including: - OR with multiple branches extending beyond range bounds - OR contains OR with disjoint ranges - AND with multiple != exclusion constraints - Mixed AND/OR with exclusions - Tilde (~) and Caret (^) ranges in complex expressions - Unbounded ranges (>=, <=, >, <) with OR/AND Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Add validation to detect when version constraints are impossible to satisfy (over-constrained) and reject them with clear error messages that include the position where the issue was detected. Detected cases: - Individual ranges with min > max - Non-overlapping ranges in AND (e.g., 1-2 && 3-4) - Conflicting bounds (e.g., >=5.0.0 && <=3.0.0, >=2 && <1) - NOT excluding exact version (e.g., =1.0.0 && !=1.0.0) Error messages include precise column positions and clear descriptions of why the constraint is impossible, helping users fix their constraints. Examples: - `>=2.0.0 && <1.0.0` now errors with "over-constrained, ranges do not overlap" - `=1.0.0 && !=1.0.0` now errors with "excludes all versions" - `1-2 && 3-4` now errors with "ranges do not overlap" This prevents the parser from accepting constraints that would never match any version, making errors visible at parse time rather than at runtime when the constraint unexpectedly matches nothing. Test coverage: - 45 new test cases for over-constraint detection - Updated existing tests that had incorrect expectations - Fixed test case using AND instead of OR for disjoint ranges Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Added 38 test cases covering all four range sorters (AscendingMin, AscendingMax, DescendingMin, DescendingMax) with edge cases including empty slices, single elements, already sorted data, and version component differences. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
erdii
reviewed
May 19, 2026
Member
erdii
left a comment
There was a problem hiding this comment.
Found some issues. Here's a set of reproducers / regression tests to discuss.
./review_regression_test.go:
package semver
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// Bug: compactAndValidateLogicalAND merges adjacent ranges as a union
// (1-2 && 2-3 → 1-3) when AND semantics require intersection.
func TestANDCompaction_Check(t *testing.T) {
t.Parallel()
// Adjacent AND must not be compacted into a single Range.
c, err := NewConstraint("1 - 2 && 2 - 3")
require.NoError(t, err)
oic := c.(*originalInputConstraint)
_, isSingleRange := oic.Constraint.(*Range)
assert.False(t, isSingleRange,
"adjacent AND should not compact to a single Range (union); AND means intersection")
tests := []struct {
name string
constraint string
version string
expected bool
}{
// Adjacent ranges [1,2] ∩ [2,3] = {2.0.0}
{"adjacent AND matches at boundary", "1 - 2 && 2 - 3", "2.0.0", true},
{"adjacent AND rejects left-only version", "1 - 2 && 2 - 3", "1.5.0", false},
{"adjacent AND rejects right-only version", "1 - 2 && 2 - 3", "2.5.0", false},
{"adjacent AND rejects left boundary", "1 - 2 && 2 - 3", "1.0.0", false},
{"adjacent AND rejects right boundary", "1 - 2 && 2 - 3", "3.0.0", false},
{"adjacent AND rejects below both", "1 - 2 && 2 - 3", "0.5.0", false},
{"adjacent AND rejects above both", "1 - 2 && 2 - 3", "3.5.0", false},
// Overlapping ranges [1,3] ∩ [2,4] = [2,3]
{"overlapping AND matches intersection start", "1.0.0 - 3.0.0 && 2.0.0 - 4.0.0", "2.0.0", true},
{"overlapping AND matches intersection mid", "1.0.0 - 3.0.0 && 2.0.0 - 4.0.0", "2.5.0", true},
{"overlapping AND matches intersection end", "1.0.0 - 3.0.0 && 2.0.0 - 4.0.0", "3.0.0", true},
{"overlapping AND rejects left-only", "1.0.0 - 3.0.0 && 2.0.0 - 4.0.0", "1.5.0", false},
{"overlapping AND rejects right-only", "1.0.0 - 3.0.0 && 2.0.0 - 4.0.0", "3.5.0", false},
// Same range AND'd with itself
{"identical AND matches inside", "1.0.0 - 2.0.0 && 1.0.0 - 2.0.0", "1.5.0", true},
{"identical AND rejects outside", "1.0.0 - 2.0.0 && 1.0.0 - 2.0.0", "2.5.0", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
c, err := NewConstraint(tt.constraint)
require.NoError(t, err)
assert.Equal(t, tt.expected, c.Check(MustNewVersion(tt.version)))
})
}
}
// Bug: Contains() returns wrong results after union-based AND compaction.
func TestANDCompaction_Contains(t *testing.T) {
t.Parallel()
tests := []struct {
name string
rA string
rB string
expected bool
}{
{
// [1,2] ∩ [2,3] = {2.0.0}, which does not contain [1,3].
name: "adjacent AND does not contain full union range",
rA: "2 - 3 && 1 - 2",
rB: "1 - 3",
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
assert.Equal(t, tt.expected,
MustNewConstraint(tt.rA).Contains(MustNewConstraint(tt.rB)))
})
}
}
// Bug: not.Contains uses !other.Contains(¬.Range) which produces wrong
// results for not-vs-not comparisons. Also, and.coversConstraint falls back
// to String() comparison which breaks when semantically equivalent constraints
// have different string representations (e.g. !=2 vs !=2.0.0).
func TestNotContains(t *testing.T) {
t.Parallel()
tests := []struct {
name string
rA string
rB string
expected bool
}{
{
// !=2 excludes all 2.x.x, !=2.0.0 excludes only 2.0.0.
// (1-5 && !=2) is more restrictive — it should not contain (1-5 && !=2.0.0).
name: "wider exclusion does not contain narrower exclusion",
rA: "1 - 5 && !=2",
rB: "1 - 5 && !=2.0.0",
expected: false,
},
{
// Every version allowed by (1-5 && !=2) is also allowed by (1-5 && !=2.0.0).
name: "narrower exclusion contains wider exclusion",
rA: "1 - 5 && !=2.0.0",
rB: "1 - 5 && !=2",
expected: true,
},
{
// !=2.0.0 allows all versions except 2.0.0; !=2 allows all except 2.x.x.
// Every version in !=2 is also in !=2.0.0.
name: "narrow not contains wide not",
rA: "!=2.0.0",
rB: "!=2",
expected: true,
},
{
// !=2.0.0 allows 2.1.0, but !=2 does not.
name: "wide not does not contain narrow not",
rA: "!=2",
rB: "!=2.0.0",
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
assert.Equal(t, tt.expected,
MustNewConstraint(tt.rA).Contains(MustNewConstraint(tt.rB)))
})
}
}
// Bug: or.Contains checks each branch independently and misses cases where
// overlapping or adjacent branches together cover the queried constraint.
func TestORContains_OverlappingBranches(t *testing.T) {
t.Parallel()
tests := []struct {
name string
rA string
rB string
expected bool
}{
{
// [1,3] ∪ [2,5] = [1,5], which contains [1,5].
name: "overlapping OR branches cover full range",
rA: "1 - 3 || 2 - 5",
rB: "1 - 5",
expected: true,
},
{
// [1,2] ∪ [2,3] = [1,3], which contains [1,3].
name: "adjacent OR branches cover full range",
rA: "1 - 2 || 2 - 3",
rB: "1 - 3",
expected: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
assert.Equal(t, tt.expected,
MustNewConstraint(tt.rA).Contains(MustNewConstraint(tt.rB)))
})
}
}
// Bug: over-constrained error messages have doubled position prefix
// ("col 16: AND col 16: ...") due to wrapping in parse() + validateNotOverConstrained.
func TestOverConstrainedErrorFormat(t *testing.T) {
t.Parallel()
_, err := NewConstraint("2 - 3 && 5 - 6 && 1 - 2")
require.Error(t, err)
assert.NotRegexp(t, `col \d+: AND col \d+:`, err.Error(),
"error should not have doubled position prefix")
}Co-authored-by: Josh Gwosdz <me@erdii.net>
Co-authored-by: Josh Gwosdz <me@erdii.net>
Co-authored-by: Josh Gwosdz <me@erdii.net>
Co-authored-by: Josh Gwosdz <me@erdii.net>
Co-authored-by: Josh Gwosdz <me@erdii.net>
Previously, the parser incorrectly combined adjacent ranges in AND operations, treating them as unions instead of intersections. For example, "1-2 && 2-3" was incorrectly combined into "1-3", when it should represent only "2.0.0" (the intersection). Changes: - Removed range combining logic from AND operations - AND represents intersection - Added compactLogicalOR function to properly merge adjacent/overlapping ranges in OR operations - Updated test expectations to reflect correct AND semantics - OR operations now correctly combine: "1-2 || 2-3" → "1-3" (union) - AND operations now correctly preserve: "1-2 && 2-3" → intersection at 2.0.0 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Added test cases to verify: - OR operations combine adjacent ranges: "1-2 || 2-3" → "1-3" - AND operations preserve both ranges: "1-2 && 2-3" → intersection Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
When multiple ranges in an AND operation only overlap at a single version, simplify them to a single range representing that exact version. Examples: - "1-2 && 2-3" → "2-2" (equivalent to =2.0.0) - "1-5 && 3-7 && 5-9" → "5-5" (equivalent to =5.0.0) - ">=2.5.0 && <=2.5.0" → "2.5.0-2.5.0" (equivalent to =2.5.0) Ranges with broader overlaps are preserved as AND constraints to maintain the intersection semantics, with the actual intersection calculated at runtime during version checking. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
erdii
approved these changes
May 20, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes a range of range compare issues and improves error messages.