Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions e2e/config/test_semver_range_warning
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,16 @@ EOF

assert_contains "mise ls 2>&1" 'semver range "^20.0.0" is not supported for node (source: ~/workdir/mise.toml); use a concrete version or version prefix'
assert_contains "mise ls 2>&1" 'semver range "^3.1.0" is not supported for npm:prettier (source: ~/workdir/mise.toml); use a concrete version or version prefix'

# A value mangled before it got here is not a range. TOML reads `\t` as a tab and `\n` as a
# newline inside a basic string, and `split_whitespace` counts those as separators, so this used
# to be reported as a semver range -- pointing at versions when the path is what is wrong.
cat <<'EOF' >mise.toml
[tools]
node = { path = "C:\tools\node" }
EOF

# `mise ls` exits non-zero here, so these use the failure helpers rather than the ones above,
# which treat a non-zero status as the test failing.
assert_fail_contains "mise ls" 'invalid tool path'
assert "mise ls 2>&1 | grep -c 'semver range' || true" "0"
51 changes: 49 additions & 2 deletions src/semver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,14 @@ pub fn is_npm_semver_range_query(query: &str) -> bool {
if query.is_empty() || query.eq_ignore_ascii_case("latest") {
return false;
}
// A real npm range never contains a control character, but `split_whitespace` below counts
// tabs and newlines as separators, so a string that was mangled before it got here looks like
// a multi-part range. TOML reading `\t` as a tab in `path = "C:\tools\node"` is how that
// happens in practice. Calling such a value a semver range points the reader at versions when
// the problem is the value itself; the validation that follows already names the real cause.
if query.chars().any(|c| c.is_control()) {
return false;
}
if query == "*" || query.eq_ignore_ascii_case("x") {
return true;
}
Expand Down Expand Up @@ -118,8 +126,8 @@ pub fn semver_is_at_least(version: &str, minimum: &str) -> Option<bool> {
#[cfg(test)]
mod tests {
use super::{
chunkify_version, npm_semver_range_filter, semver_cmp, semver_is_at_least,
semver_is_older_than, semver_triplet, split_version_prefix,
chunkify_version, is_npm_semver_range_query, npm_semver_range_filter, semver_cmp,
semver_is_at_least, semver_is_older_than, semver_triplet, split_version_prefix,
};
use std::cmp::Ordering;

Expand Down Expand Up @@ -241,6 +249,45 @@ mod tests {
);
}

#[test]
fn test_npm_semver_range_query_ignores_mangled_values() {
// A value broken before it reached here is not a range. TOML reads `\t` in
// `path = "C:\tools\node"` as a tab and `\n` as a newline, which `split_whitespace`
// counts as separators -- so it used to be reported as a multi-part semver range, with
// no mention of the path that was actually wrong.
assert!(!is_npm_semver_range_query("path:C:\tools\node"));
assert!(!is_npm_semver_range_query("1.0\t2.0"));
assert!(!is_npm_semver_range_query("1.0\n2.0"));
}

#[test]
fn test_npm_semver_range_query_still_detects_real_ranges() {
// The control for the test above: skipping control characters must not stop any range
// from being detected. None of these contain one.
for q in [
"^1.2.3",
">=1.0 <2.0",
"1.x",
"*",
"1.2 - 1.5",
"1.0 || 2.0",
] {
assert!(is_npm_semver_range_query(q), "expected {q:?} to be a range");
}
// Ordinary values stay non-ranges, including a well-formed `path:`.
for q in [
"1.0.0",
"latest",
"path:C:/tools/node",
"path:/opt/homebrew/opt/node@20",
] {
assert!(
!is_npm_semver_range_query(q),
"expected {q:?} not to be a range"
);
}
}

#[test]
fn test_semver_triplet() {
assert_eq!(semver_triplet("1.2.3"), Some((1, 2, 3)));
Expand Down
Loading