Skip to content

🪲 [Fix]: Reject values that are not versions in CompareTo and Equals - #55

Open
Marius Storhaug (MariusStorhaug) wants to merge 1 commit into
mainfrom
fix-dead-type-guard
Open

🪲 [Fix]: Reject values that are not versions in CompareTo and Equals#55
Marius Storhaug (MariusStorhaug) wants to merge 1 commit into
mainfrom
fix-dead-type-guard

Conversation

@MariusStorhaug

Copy link
Copy Markdown
Member

Comparing a [PSSemVer] against something that is not a version now behaves predictably instead of silently producing a result.

Fixed: The type guards in CompareTo and Equals actually run

Both methods opened with if (-not $other -is [PSSemVer]). PowerShell applies -not to $other first, so the expression was ((-not $other) -is [PSSemVer]) — a [bool] tested against [PSSemVer], which is always $false. Neither guard ever ran, and both methods carried on reading .Major, .Minor, and .Patch off whatever was passed. Absent properties came back $null, so a comparison against an unrelated object returned a confident, meaningless answer:

([PSSemVer]'1.0.0').CompareTo([guid]::NewGuid())   # before: 1        after: throws ArgumentException
([PSSemVer]'1.0.0').CompareTo('not-a-version')     # before: 1        after: throws ArgumentException
([PSSemVer]'1.0.0').Equals([guid]::NewGuid())      # before: False by accident, after: False by decision

Changed: The comparand is converted rather than type-checked

A strict $other -isnot [PSSemVer] guard would have introduced a worse bug than the one being fixed, so the fix converts instead. The two operators behave differently, which is measurable:

[PSSemVer]'1.0.0' -lt '1.0.1'   # PowerShell converts the operand, CompareTo receives a PSSemVer
[PSSemVer]'1.0.0' -eq '1.0.0'   # PowerShell does NOT convert, Equals receives a raw String

Under a strict guard, -eq against a version string would have silently flipped from True to False — comparisons that quietly stop being true are far more dangerous than a guard that never fires. Converting with -as keeps that working, returns $null for anything that is not a version, and needs no try/catch.

The resulting contract:

Comparand CompareTo Equals
[PSSemVer] or version string compares compares
Hashtable with version properties compares compares
$null returns 1 False
Garbage string, [guid], [datetime], array, hashtable without version properties throws ArgumentException False

$null returning 1 from CompareTo follows the IComparable convention that null sorts before any value; it was previously an accident of the same absent-property arithmetic.

A property-shaped hashtable such as @{ Major = 9; Minor = 0; Patch = 0 } still compares. That is PowerShell's own hashtable-to-class conversion doing real work, which is different in kind from the old behaviour of reading absent properties off any object at all. The linked issue lists that case as a symptom; on inspection it is legitimate, and the tests pin both halves of the boundary so it stays deliberate.

Technical Details

  • src/classes/public/PSSemVer.ps1: both methods convert with $other -as [PSSemVer] and branch on $null; the remaining body reads from the converted $comparand rather than the raw $other.
  • tests/PSSemVer.Tests.ps1: a Type guard context with 13 cases covering the throw, the False, the $null conventions, and — importantly — that -eq, -lt, and Sort-Object against version strings still behave as before.
  • Verified red-then-green by building the module from main and from this branch and running the same suite against both: 4 failures against main, 0 against this branch, with the full suite at 79/79 and PSScriptAnalyzer clean over src/ and tests/ using the repo linter settings.
  • GetHashCode is deliberately untouched. It is a separate defect with its own decision attached to how Equals treats build metadata.
Related issues

Both guards were written as 'if (-not \ -is [PSSemVer])'. PowerShell applies -not to \ first, so the expression was always false and neither guard ever ran. Convert the comparand instead, which keeps -eq and -lt against a version string working and rejects what cannot be a version.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@MariusStorhaug Marius Storhaug (MariusStorhaug) added the Patch Fixes bugs or adds small fixes to existing functionality label Aug 8, 2026
@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown

Super-linter summary

Language Validation result
CHECKOV Pass ✅
GITHUB_ACTIONS Pass ✅
GITLEAKS Pass ✅
GIT_MERGE_CONFLICT_MARKERS Pass ✅
MARKDOWN Pass ✅
NATURAL_LANGUAGE Pass ✅
POWERSHELL Pass ✅
PRE_COMMIT Pass ✅
SPELL_CODESPELL Pass ✅
TRIVY Pass ✅
YAML Pass ✅

All files and directories linted successfully

For more information, see the GitHub Actions workflow run

Powered by Super-linter

@MariusStorhaug
Marius Storhaug (MariusStorhaug) marked this pull request as ready for review August 8, 2026 16:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Patch Fixes bugs or adds small fixes to existing functionality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fix the dead type guard in PSSemVer.CompareTo and PSSemVer.Equals

1 participant