Skip to content

Commit 28e2bfc

Browse files
committed
Implement new Nuget Version Range parser
1 parent 43f994a commit 28e2bfc

2 files changed

Lines changed: 187 additions & 104 deletions

File tree

ModuleFast.ps1

Lines changed: 137 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,9 @@ function Get-ModuleFastPlan {
284284
# HACK: I should be using the Id provided by the server, for now I'm just guessing because
285285
# I need to add it to the ComparableModuleSpec class
286286
Write-Debug "$currentModuleSpec`: Processing dependencies"
287-
[List[ModuleFastSpec]]$dependencies = $dependencyInfo | Parse-NugetDependency
287+
[List[ModuleFastSpec]]$dependencies = $dependencyInfo | ForEach-Object {
288+
[ModuleFastSpec]::new($PSItem.id, [NuGetRange]$PSItem.range)
289+
}
288290
Write-Debug "$currentModuleSpec has $($dependencies.count) dependencies"
289291

290292
# TODO: Where loop filter maybe
@@ -702,8 +704,8 @@ class ModuleFastSpec : IComparable {
702704
static [SemanticVersion]$MaxVersion = '{0}.{0}.{0}' -f [int32]::MaxValue
703705
#Special string we use to translate between Version and SemanticVersion since SemanticVersion doesnt support Semver 2.0 properly and doesnt allow + only
704706
#Someone actually using this string may cause a conflict, it's not foolproof but it's better than nothing
705-
static [string]$VersionBuildIdentifier = 'MFbuild'
706-
hidden static [string]$buildVersionRegex = '^\d+\.\d+\.\d+\.\d+$'
707+
hidden static [string]$SYSTEM_VERSION_LABEL = 'SYSTEMVERSION'
708+
hidden static [string]$SYSTEM_VERSION_REGEX = '^(?<major>\d+)\.(?<minor>\d+)\.(?<build>\d+)\.(?<revision>\d+)$'
707709

708710
#These properties are effectively read only thanks to some wizardry
709711
hidden [uri]$_DownloadLink
@@ -779,23 +781,23 @@ class ModuleFastSpec : IComparable {
779781
$this.Initialize($Name, $requiredVersion, $requiredVersion, $null, $null)
780782
$this._DownloadLink = [uri]$DownloadLink
781783
}
782-
783784
ModuleFastSpec([string]$Name, [string]$Min, [string]$Max) {
784785
[SemanticVersion]$minVer = $min ? [ModuleFastSpec]::ParseVersionString($min) : $null
785786
[SemanticVersion]$maxVer = $max ? [ModuleFastSpec]::ParseVersionString($max) : $null
786787
$this.Initialize($Name, $minVer, $maxVer, $null, $null)
787788
}
788789

789-
790790
# These can be used for performance to avoid parsing to string and back. Probably makes little difference
791791
ModuleFastSpec([string]$Name, [SemanticVersion]$Required) {
792792
$this.Initialize($Name, $Required, $Required, $null, $null)
793793
}
794-
794+
ModuleFastSpec([string]$Name, [NugetRange]$Range) {
795+
$Range.Min
796+
$this.Initialize($Name, $range.Min, $range.Max, $null, $null)
797+
}
795798

796799

797800
#TODO: Version versions maybe? Probably should just use the parser and let those go to string
798-
799801
ModuleFastSpec([ModuleSpecification]$ModuleSpec) {
800802
$this.Initialize($null, $null, $null, $null, $ModuleSpec)
801803
}
@@ -833,10 +835,10 @@ class ModuleFastSpec : IComparable {
833835

834836
# Parses either a assembly version or semver to a semver string
835837
static [SemanticVersion] ParseVersionString([string]$Version) {
836-
if ($null -eq $Version) { return $null }
837-
$result = if ($Version -match [ModuleFastSpec]::buildVersionRegex) {
838-
[ModuleFastSpec]::ParseVersion($Version)
839-
} else { $Version }
838+
if (-not $Version) { throw [NotSupportedException]'Null or empty strings are not supported' }
839+
$result = $Version -match [ModuleFastSpec]::SYSTEM_VERSION_REGEX `
840+
? [SemanticVersion]::ParseVersion([Version]$Version)
841+
: [SemanticVersion]$Version
840842
return $result
841843
}
842844

@@ -845,47 +847,61 @@ class ModuleFastSpec : IComparable {
845847
# Needed because SemVer can't parse builds correctly
846848
#https://github.com/PowerShell/PowerShell/issues/14605
847849
static [SemanticVersion]ParseVersion([Version]$Version) {
848-
if ($null -eq $Version) { return $null }
850+
if (-not $Version) { throw [NotSupportedException]'Null or empty strings are not supported' }
849851

852+
[list[string]]$buildLabels = @()
853+
$buildVersion = $null
854+
if ($Version.Build -eq -1) { $buildLabels.Add('NOBUILD'); $buildVersion = 0 }
855+
if ($Version.Revision -ne -1) {
856+
$buildLabels.Add('HASREVISION')
857+
}
858+
if ($buildLabels.count -eq 0) {
859+
#This version maps directly to semantic version and we can return early
860+
return [SemanticVersion]::new($Version.Major, $Version.Minor, $Version.Build)
861+
}
862+
863+
#Otherwise we need to explicitly note this came from a system version for when we parse it back
864+
$buildLabels.Add([ModuleFastSpec]::SYSTEM_VERSION_LABEL)
865+
$preReleaseLabel = $null
850866
if ($Version.Revision -ge 0) {
851-
[SemanticVersion]::new(
852-
$Version.Major,
853-
$Version.Minor -eq -1 ? 0 : $Version.Minor,
854-
$Version.Build -eq -1 ? 0 : $Version.Build,
855-
[ModuleFastSpec]::VersionBuildIdentifier,
856-
$Version.Revision
857-
)
858-
} else {
859-
[SemanticVersion]::new(
860-
$Version.Major,
861-
$Version.Minor -eq -1 ? 0 : $Version.Minor,
862-
$Version.Build -eq -1 ? 0 : $Version.Build
863-
)
867+
#We do this so that the sort order is correct in semver (prereleases sort before major versions and is lexically sorted)
868+
#Revision can't be 0 while build is -1, so we can skip any evaluation logic there.
869+
$preReleaseLabel = $Version.Revision.ToString().PadLeft(10, '0')
870+
$buildVersion = $Version.Build + 1
864871
}
865-
$versionWith4SectionsRegex = '^\d+\.\d+\.\d+\.\d+$'
866-
$parsedVersion = $Version -match $versionWith4SectionsRegex `
867-
? '{0}.{1}.{2}-{4}+{3}' -f $Version.Major, $Version.Minor, $Version.Build, $Version.Revision, [ModuleFastSpec]::VersionBuildIdentifier
868-
: $Version
869-
return [SemanticVersion]$parsedVersion
872+
$buildLabels.Reverse()
873+
[string]$buildLabel = $buildLabels -join '.'
874+
#Nulls will return as 0, which we want. Major and Minor cannot be -1
875+
return [SemanticVersion]::new($Version.Major, $Version.Minor, $buildVersion, $preReleaseLabel, $buildLabel)
870876
}
871877

872-
# A way to go back from SemanticVersion
878+
# A way to go back from SemanticVersion, the anticedent to ParseVersion
873879
static [Version]ParseSemanticVersion([SemanticVersion]$Version) {
874-
if ($null -eq $Version) { return $null }
875-
return [Version]('{0}.{1}.{2}{3}' -f
876-
$Version.Major,
877-
($Version.Minor ?? 0),
878-
($Version.Patch ?? 0),
879-
(($Version.PreReleaseLabel -eq [ModuleFastSpec]::VersionBuildIdentifier -and $Version.BuildLabel -gt 0) ? ".$($Version.BuildLabel)" : $null)
880-
)
880+
if ($null -eq $Version) { throw [NotSupportedException]'Null or empty strings are not supported' }
881+
882+
[string[]]$buildFlags = $Version.BuildLabel -split '\.'
883+
if ($BuildFlags -notcontains [ModuleFastSpec]::SYSTEM_VERSION_LABEL) {
884+
#This is a semantic-compatible version, we can just return it
885+
return [Version]::new($Version.Major, $Version.Minor, $Version.Patch)
886+
}
887+
if ($buildFlags -contains 'NOBUILD') {
888+
return [Version]::new($Version.Major, $Version.Minor)
889+
}
890+
#It is not possible to have no build version but have a revision version, we dont have to test for that
891+
if ($buildFlags -contains 'HASREVISION') {
892+
#A null prerelease label will map to 0, so this will correctly be for example 3.2.1.0 if it is null but NOREVISION wasnt flagged
893+
return [Version]::new($Version.Major, $Version.Minor, $Version.Patch - 1, $Version.PreReleaseLabel)
894+
}
895+
896+
throw [InvalidDataException]"Unexpected situation when parsing SemanticVersion $Version to Version. This is a bug in ModuleFastSpec and should be reported"
881897
}
898+
882899
[Version] ToVersion() {
883900
if (-not $this.Required) { throw [NotSupportedException]'You can only convert Required specs to a version.' }
884901
#Warning: Return type is not enforced by the method, that's why we did it explicitly here.
885902
return [Version][ModuleFastSpec]::ParseSemanticVersion($this.Required)
886903
}
887904

888-
889905
###Implicit Methods
890906

891907
#This string will be unique for each spec type, and can (probably)? Be safely used as a hashcode
@@ -1002,6 +1018,88 @@ class ModuleFastSpec : IComparable {
10021018
}
10031019
}
10041020

1021+
#This is a helper function that processes nuget ranges.
1022+
#Reference: https://github.com/NuGet/NuGet.Client/blob/035850255a15b60437d22f9178c4206bafe0b6a9/src/NuGet.Core/NuGet.Versioning/VersionRangeFactory.cs#L91-L265
1023+
class NugetRange {
1024+
[SemanticVersion]$Min
1025+
[SemanticVersion]$Max
1026+
[boolean]$MinInclusive = $true
1027+
[boolean]$MaxInclusive = $true
1028+
1029+
NugetRange([string]$string) {
1030+
# Use a regex to parse a semantic version range inclusive
1031+
# of the NuGet versioning spec.
1032+
# Reference: https://docs.microsoft.com/en-us/nuget/concepts/package-versioning#version-ranges-and-wildcards
1033+
if ($string -as [SemanticVersion]) {
1034+
$this.Min = $string
1035+
$this.Max = $string
1036+
return
1037+
}
1038+
1039+
#Matches for beginning and ending parens or brackets
1040+
#If it doesnt match this, we've already evaluted the possible other solution
1041+
if ($string -notmatch '^(\(|\[)(.+)(\)|\])$') {
1042+
throw "Invalid Nuget Range: $string"
1043+
}
1044+
$left, $range, $right = $Matches[1..3]
1045+
1046+
$this.MinInclusive = $left -eq '['
1047+
$this.MaxInclusive = $right -eq ']'
1048+
1049+
if ($range -notmatch '\,') {
1050+
$req = [String]::IsNullOrWhiteSpace($range) ? [ModuleFastSpec]::MinVersion : [SemanticVersion]$range
1051+
$this.Min = $req
1052+
$this.Max = $req
1053+
return
1054+
}
1055+
$minString, $maxString = $range.split(',')
1056+
if (-not [String]::IsNullOrWhiteSpace($minString.trim())) { $minString.trim() }
1057+
if (-not [String]::IsNullOrWhiteSpace($maxString.trim())) { $maxString.trim() }
1058+
}
1059+
1060+
static [SemanticVersion] Decrement([SemanticVersion]$version) {
1061+
if ($version.BuildLabel -or $version.PreReleaseLabel) {
1062+
Write-Warning 'Decrementing a version with a build or prerelease label is not supported as the Powershell Semantic Version class cannot compare them anyways. We will decrement the patch version instead and strip the prerelease headers. Do not rely on this behavior, it will change. https://github.com/PowerShell/PowerShell/issues/18489'
1063+
}
1064+
if ($version.Patch -gt 0) {
1065+
return [SemanticVersion]::new($version.Major, $version.Minor, $version.Patch - 1)
1066+
}
1067+
if ($version.Minor -gt 0) {
1068+
if ($version.Patch -eq 0) {
1069+
return [SemanticVersion]::new($version.Major, $version.Minor - 1, [int]::MaxValue)
1070+
}
1071+
return [SemanticVersion]::new($version.Major, $version.Minor - 1, $version.Patch)
1072+
}
1073+
if ($version.Major -gt 0) {
1074+
if ($version.Minor -eq 0 -and $version.Patch -eq 0) {
1075+
return [SemanticVersion]::new($version.Major - 1, [int]::MaxValue, [int]::MaxValue)
1076+
}
1077+
}
1078+
throw [ArgumentOutOfRangeException]'Unexpected Decrement Scenario Occurred, this should never happen and is a bug in ModuleFastSpec'
1079+
}
1080+
1081+
static [SemanticVersion] Increment([SemanticVersion]$version) {
1082+
if ($version.BuildLabel -or $version.PreReleaseLabel) {
1083+
Write-Warning 'Incrementing a version with a build or prerelease label is not supported as the Powershell Semantic Version class cannot compare them anyways. We will decrement the patch version instead and strip the prerelease headers. Do not rely on this behavior, it will change. https://github.com/PowerShell/PowerShell/issues/18489'
1084+
}
1085+
if ($version.Patch -le [int]::MaxValue) {
1086+
return [SemanticVersion]::new($version.Major, $version.Minor, $version.Patch + 1)
1087+
}
1088+
if ($version.Minor -gt 0) {
1089+
if ($version.Patch -eq [int]::MaxValue) {
1090+
return [SemanticVersion]::new($version.Major, $version.Minor + 1, 0)
1091+
}
1092+
return [SemanticVersion]::new($version.Major, $version.Minor + 1, $version.Patch)
1093+
}
1094+
if ($version.Major -gt 0) {
1095+
if ($version.Minor -eq 0 -and $version.Patch -eq 0) {
1096+
return [SemanticVersion]::new($version.Major - 1, [int]::MaxValue, [int]::MaxValue)
1097+
}
1098+
}
1099+
throw [ArgumentOutOfRangeException]'Unexpected Increment Scenario Occurred, this should never happen and is a bug in ModuleFastSpec'
1100+
}
1101+
}
1102+
10051103
#This is a module helper to create "getters" in classes
10061104
function Add-Getters {
10071105
Get-Member -InputObject $this -MemberType Method -Force |
@@ -1071,62 +1169,6 @@ function Get-ModuleInfoAsync {
10711169
return $HttpClient.GetStringAsync($uri, $CancellationToken)
10721170
}
10731171

1074-
filter Parse-NugetDependency ([Parameter(Mandatory, ValueFromPipeline)]$Dependency) {
1075-
#TODO: Dependency should be more strictly typed
1076-
1077-
#NOTE: This can't be a modulespecification from the start because modulespecs are immutable
1078-
$dep = @{
1079-
ModuleName = $Dependency.id
1080-
}
1081-
$Version = $Dependency.range
1082-
1083-
# Treat a null result as "any"
1084-
if ([String]::IsNullOrEmpty($Version)) {
1085-
$dep.ModuleVersion = '0.0.0'
1086-
return [ModuleFastSpec]$dep
1087-
}
1088-
1089-
#If it is a direct module specification, treat this as a required version.
1090-
$exactVersion = $null
1091-
if ([Version]::TryParse($Version, [ref]$exactVersion)) {
1092-
$dep.RequiredVersion = $exactVersion
1093-
return [ModuleFastSpec]$dep
1094-
}
1095-
1096-
#If it is an open bound, set ModuleVersion to 0.0.0 (meaning any version)
1097-
if ($version -eq '(, )') {
1098-
$dep.ModuleVersion = '0.0.0'
1099-
return [ModuleFastSpec]$dep
1100-
}
1101-
1102-
#If it is an exact match version (has brackets and doesn't have a comma), set version accordingly
1103-
$ExactVersionRegex = '\[([^,]+)\]'
1104-
if ($version -match $ExactVersionRegex) {
1105-
$dep.RequiredVersion = $matches[1]
1106-
return [ModuleFastSpec]$dep
1107-
}
1108-
1109-
#Parse all other remainder options. For this purpose we ignore inclusive vs. exclusive
1110-
#TODO: Add inclusive/exclusive parsing
1111-
$version = $version -replace '[\[\(\)\]]', '' -split ','
1112-
1113-
$minimumVersion = $version[0].trim()
1114-
$maximumVersion = $version[1].trim()
1115-
if ($minimumVersion -and $maximumVersion -and ($minimumVersion -eq $maximumVersion)) {
1116-
#If the minimum and maximum versions match, we treat this as an explicit version
1117-
$dep.RequiredVersion = $minimumVersion
1118-
return [ModuleFastSpec]$dep
1119-
} elseif ($minimumVersion -or $maximumVersion) {
1120-
if ($minimumVersion) { $dep.ModuleVersion = $minimumVersion }
1121-
if ($maximumVersion) { $dep.MaximumVersion = $maximumVersion }
1122-
} else {
1123-
#If no matching version works, just set dep to a string of the modulename
1124-
Write-Warning "$($dep.ModuleName) has an invalid version spec, falling back to maximum version."
1125-
}
1126-
1127-
return [ModuleFastSpec]$dep
1128-
}
1129-
11301172
<#
11311173
.SYNOPSIS
11321174
Adds an existing PowerShell Modules path to the current session as well as the profile
@@ -1272,7 +1314,6 @@ function Limit-ModuleFastSpecs {
12721314
}
12731315
-not $Highest ? $Versions : @($Versions | Sort-Object -Descending | Select-Object -First 1)
12741316
}
1275-
12761317
#endregion Helpers
12771318

12781319
# Export-ModuleMember Get-ModuleFast

0 commit comments

Comments
 (0)