kittenipc/kitcom/internal/tsgo/semver/version_range.go
2025-10-15 10:12:44 +03:00

439 lines
10 KiB
Go

package semver
import (
"regexp"
"strings"
)
// https://github.com/npm/node-semver#range-grammar
//
// range-set ::= range ( logical-or range ) *
// range ::= hyphen | simple ( ' ' simple ) * | ”
// logical-or ::= ( ' ' ) * '||' ( ' ' ) *
var (
logicalOrRegExp = regexp.MustCompile(`\|\|`)
whitespaceRegExp = regexp.MustCompile(`\s+`)
)
// https://github.com/npm/node-semver#range-grammar
//
// partial ::= xr ( '.' xr ( '.' xr qualifier ? )? )?
// xr ::= 'x' | 'X' | '*' | nr
// nr ::= '0' | ['1'-'9'] ( ['0'-'9'] ) *
// qualifier ::= ( '-' pre )? ( '+' build )?
// pre ::= parts
// build ::= parts
// parts ::= part ( '.' part ) *
// part ::= nr | [-0-9A-Za-z]+
var partialRegExp = regexp.MustCompile(`(?i)^([x*0]|[1-9]\d*)(?:\.([x*0]|[1-9]\d*)(?:\.([x*0]|[1-9]\d*)(?:-([a-z0-9-.]+))?(?:\+([a-z0-9-.]+))?)?)?$`)
// https://github.com/npm/node-semver#range-grammar
//
// hyphen ::= partial ' - ' partial
var hyphenRegExp = regexp.MustCompile(`(?i)^\s*([a-z0-9-+.*]+)\s+-\s+([a-z0-9-+.*]+)\s*$`)
// https://github.com/npm/node-semver#range-grammar
//
// simple ::= primitive | partial | tilde | caret
// primitive ::= ( '<' | '>' | '>=' | '<=' | '=' ) partial
// tilde ::= '~' partial
// caret ::= '^' partial
var rangeRegExp = regexp.MustCompile(`(?i)^([~^<>=]|<=|>=)?\s*([a-z0-9-+.*]+)$`)
type VersionRange struct {
alternatives [][]versionComparator
}
type versionComparator struct {
operator comparatorOperator
operand Version
}
type comparatorOperator string
const (
rangeLessThan comparatorOperator = "<"
rangeLessThanEqual comparatorOperator = "<="
rangeEqual comparatorOperator = "="
rangeGreaterThanEqual comparatorOperator = ">="
rangeGreaterThan comparatorOperator = ">"
)
func (v *VersionRange) String() string {
var sb strings.Builder
formatDisjunction(&sb, v.alternatives)
return sb.String()
}
func formatDisjunction(sb *strings.Builder, alternatives [][]versionComparator) {
origLen := sb.Len()
for i, alternative := range alternatives {
if i > 0 {
sb.WriteString(" || ")
}
formatAlternative(sb, alternative)
}
if sb.Len() == origLen {
sb.WriteString("*")
}
}
func formatAlternative(sb *strings.Builder, comparators []versionComparator) {
for i, comparator := range comparators {
if i > 0 {
sb.WriteByte(' ')
}
formatComparator(sb, comparator)
}
}
func formatComparator(sb *strings.Builder, comparator versionComparator) {
sb.WriteString(string(comparator.operator))
sb.WriteString(comparator.operand.String())
}
func (v *VersionRange) Test(version *Version) bool {
return testDisjunction(v.alternatives, version)
}
func testDisjunction(alternatives [][]versionComparator, version *Version) bool {
// an empty disjunction is treated as "*" (all versions)
if len(alternatives) == 0 {
return true
}
for _, alternative := range alternatives {
if testAlternative(alternative, version) {
return true
}
}
return false
}
func testAlternative(alternative []versionComparator, version *Version) bool {
for _, comparator := range alternative {
if !testComparator(comparator, version) {
return false
}
}
return true
}
func testComparator(comparator versionComparator, version *Version) bool {
cmp := version.Compare(&comparator.operand)
switch comparator.operator {
case rangeLessThan:
return cmp < 0
case rangeLessThanEqual:
return cmp <= 0
case rangeEqual:
return cmp == 0
case rangeGreaterThanEqual:
return cmp >= 0
case rangeGreaterThan:
return cmp > 0
default:
panic("Unexpected operator: " + comparator.operator)
}
}
func TryParseVersionRange(text string) (VersionRange, bool) {
alternatives, ok := parseAlternatives(text)
return VersionRange{alternatives: alternatives}, ok
}
func parseAlternatives(text string) ([][]versionComparator, bool) {
var alternatives [][]versionComparator
text = strings.TrimSpace(text)
ranges := logicalOrRegExp.Split(text, -1)
for _, r := range ranges {
r = strings.TrimSpace(r)
if r == "" {
continue
}
var comparators []versionComparator
if hyphenMatch := hyphenRegExp.FindStringSubmatch(r); hyphenMatch != nil {
if parsedComparators, ok := parseHyphen(hyphenMatch[1], hyphenMatch[2]); ok {
comparators = append(comparators, parsedComparators...)
} else {
return nil, false
}
} else {
for _, simple := range whitespaceRegExp.Split(r, -1) {
match := rangeRegExp.FindStringSubmatch(strings.TrimSpace(simple))
if match == nil {
return nil, false
}
if parsedComparators, ok := parseComparator(match[1], match[2]); ok {
comparators = append(comparators, parsedComparators...)
} else {
return nil, false
}
}
}
alternatives = append(alternatives, comparators)
}
return alternatives, true
}
func parseHyphen(left, right string) ([]versionComparator, bool) {
leftResult, leftOk := parsePartial(left)
if !leftOk {
return nil, false
}
rightResult, rightOk := parsePartial(right)
if !rightOk {
return nil, false
}
var comparators []versionComparator
if !isWildcard(leftResult.majorStr) {
// `MAJOR.*.*-...` gives us `>=MAJOR.0.0 ...`
comparators = append(comparators, versionComparator{
operator: rangeGreaterThanEqual,
operand: leftResult.version,
})
}
if !isWildcard(rightResult.majorStr) {
var operator comparatorOperator
operand := rightResult.version
switch {
case isWildcard(rightResult.minorStr):
// `...-MAJOR.*.*` gives us `... <(MAJOR+1).0.0`
operand = operand.incrementMajor()
operator = rangeLessThan
case isWildcard(rightResult.patchStr):
// `...-MAJOR.MINOR.*` gives us `... <MAJOR.(MINOR+1).0`
operand = operand.incrementMinor()
operator = rangeLessThan
default:
// `...-MAJOR.MINOR.PATCH` gives us `... <=MAJOR.MINOR.PATCH`
operator = rangeLessThanEqual
}
comparators = append(comparators, versionComparator{
operator: operator,
operand: operand,
})
}
return comparators, true
}
type partialVersion struct {
version Version
majorStr string
minorStr string
patchStr string
}
// Produces a "partial" version
func parsePartial(text string) (partialVersion, bool) {
match := partialRegExp.FindStringSubmatch(text)
if match == nil {
return partialVersion{}, false
}
majorStr := match[1]
minorStr := match[2]
patchStr := match[3]
prereleaseStr := match[4]
buildStr := match[5]
if minorStr == "" {
minorStr = "*"
}
if patchStr == "" {
patchStr = "*"
}
var majorNumeric, minorNumeric, patchNumeric uint32
var err error
if isWildcard(majorStr) {
majorNumeric = 0
minorNumeric = 0
patchNumeric = 0
} else {
majorNumeric, err = getUintComponent(majorStr)
if err != nil {
return partialVersion{}, false
}
if isWildcard(minorStr) {
minorNumeric = 0
patchNumeric = 0
} else {
minorNumeric, err = getUintComponent(minorStr)
if err != nil {
return partialVersion{}, false
}
if isWildcard(patchStr) {
patchNumeric = 0
} else {
patchNumeric, err = getUintComponent(patchStr)
if err != nil {
return partialVersion{}, false
}
}
}
}
var prerelease []string
if prereleaseStr != "" {
prerelease = strings.Split(prereleaseStr, ".")
}
var build []string
if buildStr != "" {
build = strings.Split(buildStr, ".")
}
result := partialVersion{
version: Version{
major: majorNumeric,
minor: minorNumeric,
patch: patchNumeric,
prerelease: prerelease,
build: build,
},
majorStr: majorStr,
minorStr: minorStr,
patchStr: patchStr,
}
return result, true
}
func parseComparator(op string, text string) ([]versionComparator, bool) {
operator := comparatorOperator(op)
result, ok := parsePartial(text)
if !ok {
return nil, false
}
var comparatorsResult []versionComparator
if !isWildcard(result.majorStr) {
switch operator {
case "~":
first := versionComparator{rangeGreaterThanEqual, result.version}
var secondVersion Version
if isWildcard(result.minorStr) {
secondVersion = result.version.incrementMajor()
} else {
secondVersion = result.version.incrementMinor()
}
second := versionComparator{rangeLessThan, secondVersion}
comparatorsResult = []versionComparator{first, second}
case "^":
first := versionComparator{rangeGreaterThanEqual, result.version}
var secondVersion Version
if result.version.major > 0 || isWildcard(result.minorStr) {
secondVersion = result.version.incrementMajor()
} else if result.version.minor > 0 || isWildcard(result.patchStr) {
secondVersion = result.version.incrementMinor()
} else {
secondVersion = result.version.incrementPatch()
}
second := versionComparator{rangeLessThan, secondVersion}
comparatorsResult = []versionComparator{first, second}
case "<", ">=":
version := result.version
if isWildcard(result.minorStr) || isWildcard(result.patchStr) {
version.prerelease = []string{"0"}
}
comparatorsResult = []versionComparator{
{operator, version},
}
case "<=", ">":
version := result.version
if isWildcard(result.minorStr) {
if operator == rangeLessThanEqual {
operator = rangeLessThan
} else {
operator = rangeGreaterThanEqual
}
version = version.incrementMajor()
version.prerelease = []string{"0"}
} else if isWildcard(result.patchStr) {
if operator == rangeLessThanEqual {
operator = rangeLessThan
} else {
operator = rangeGreaterThanEqual
}
version = version.incrementMinor()
version.prerelease = []string{"0"}
}
comparatorsResult = []versionComparator{
{operator, version},
}
case "=", "":
// normalize empty string to `=`
operator = rangeEqual
if isWildcard(result.minorStr) || isWildcard(result.patchStr) {
originalVersion := result.version
firstVersion := originalVersion
firstVersion.prerelease = []string{"0"}
var secondVersion Version
if isWildcard(result.minorStr) {
secondVersion = originalVersion.incrementMajor()
} else {
secondVersion = originalVersion.incrementMinor()
}
secondVersion.prerelease = []string{"0"}
comparatorsResult = []versionComparator{
{rangeGreaterThanEqual, firstVersion},
{rangeLessThan, secondVersion},
}
} else {
comparatorsResult = []versionComparator{
{operator, result.version},
}
}
default:
panic("Unexpected operator: " + operator)
}
} else {
if operator == "<" || operator == ">" {
comparatorsResult = []versionComparator{
// < 0.0.0-0
{rangeLessThan, versionZero},
}
}
}
return comparatorsResult, true
}
func isWildcard(text string) bool {
return text == "*" || text == "x" || text == "X"
}