From 542b03df8cdb23ec6d8f6bb5e0c167f2e558e88b Mon Sep 17 00:00:00 2001 From: Risu <79110363+risu729@users.noreply.github.com> Date: Mon, 16 Feb 2026 17:06:12 +1100 Subject: [PATCH 01/23] fix(idiomatic): use generic parser for idiomatic files This PR fixes https://github.com/jdx/mise/discussions/7379#discussioncomment-15807370, where unexpected behavior occurred (e.g., yarn with package.json) because some plugins (like vfox) do not support package.json parsing. This change genericizes the parsing of built-in supported idiomatic files (currently and raw text files). If a file is declared as an idiomatic filename for a tool, the built-in parser will be used. The precedence order is: 1. (built-in parser) 2. Plugin-specific parsing () 3. Raw text file (built-in fallback) Note: Plugins cannot override the parsing logic for if it is handled by the built-in parser. This means specific tools cannot have custom parsing logic that differs from the core implementation (e.g., ignoring field but respecting for a specific tool is not possible via plugin override). --- docs/configuration.md | 36 +++-- src/backend/asdf.rs | 14 +- src/backend/mod.rs | 35 +++-- src/backend/vfox.rs | 20 ++- .../mod.rs} | 49 ++++-- .../idiomatic_version}/package_json.rs | 146 +++++++++++++++--- src/config/config_file/mod.rs | 11 +- src/config/mod.rs | 6 +- src/main.rs | 1 - src/plugins/core/bun.rs | 14 +- src/plugins/core/deno.rs | 13 +- src/plugins/core/elixir.rs | 2 +- src/plugins/core/go.rs | 2 +- src/plugins/core/java.rs | 13 +- src/plugins/core/node.rs | 28 ++-- src/plugins/core/python.rs | 2 +- src/plugins/core/ruby.rs | 6 +- src/plugins/core/ruby_windows.rs | 6 +- src/plugins/core/rust.rs | 6 +- src/plugins/core/swift.rs | 2 +- src/plugins/core/zig.rs | 2 +- 21 files changed, 268 insertions(+), 146 deletions(-) rename src/config/config_file/{idiomatic_version.rs => idiomatic_version/mod.rs} (64%) rename src/{ => config/config_file/idiomatic_version}/package_json.rs (72%) diff --git a/docs/configuration.md b/docs/configuration.md index e93a97f565..97d407cf76 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -414,22 +414,26 @@ other developers to use a specific tool like mise or asdf. They support aliases, which means you can have an `.nvmrc` file with `lts/hydrogen` and it will work in mise and nvm. Here are some of the supported idiomatic version files: -| Plugin | Idiomatic Files | -| ---------- | ------------------------------------- | -| atmos | `.atmos-version` | -| crystal | `.crystal-version` | -| elixir | `.exenv-version` | -| go | `.go-version` | -| java | `.java-version`, `.sdkmanrc` | -| node | `.nvmrc`, `.node-version` | -| opentofu | `.opentofu-version` | -| packer | `.packer-version` | -| python | `.python-version`, `.python-versions` | -| ruby | `.ruby-version`, `Gemfile` | -| terraform | `.terraform-version`, `main.tf` | -| terragrunt | `.terragrunt-version` | -| terramate | `.terramate-version` | -| yarn | `.yvmrc` | +| Plugin | Idiomatic Files | +| ---------- | ----------------------------------------- | +| atmos | `.atmos-version` | +| bun | `.bun-version`, `package.json` | +| crystal | `.crystal-version` | +| deno | `.deno-version`, `package.json` | +| elixir | `.exenv-version` | +| go | `.go-version` | +| java | `.java-version`, `.sdkmanrc` | +| node | `.nvmrc`, `.node-version`, `package.json` | +| npm | `package.json` | +| opentofu | `.opentofu-version` | +| packer | `.packer-version` | +| pnpm | `package.json` | +| python | `.python-version`, `.python-versions` | +| ruby | `.ruby-version`, `Gemfile` | +| terraform | `.terraform-version`, `main.tf` | +| terragrunt | `.terragrunt-version` | +| terramate | `.terramate-version` | +| yarn | `.yvmrc`, `package.json` | In mise, these are disabled by default, see for rationale. diff --git a/src/backend/asdf.rs b/src/backend/asdf.rs index aae077a13b..3d81b2ea40 100644 --- a/src/backend/asdf.rs +++ b/src/backend/asdf.rs @@ -308,7 +308,7 @@ impl Backend for AsdfBackend { Ok(aliases) } - async fn idiomatic_filenames(&self) -> Result> { + async fn _idiomatic_filenames(&self) -> Result> { if let Some(data) = &self.toml.list_idiomatic_filenames.data { return Ok(self.plugin.parse_idiomatic_filenames(data)); } @@ -326,9 +326,9 @@ impl Backend for AsdfBackend { .cloned() } - async fn parse_idiomatic_file(&self, idiomatic_file: &Path) -> Result { + async fn parse_idiomatic_file(&self, idiomatic_file: &Path) -> Result> { if let Some(cached) = self.fetch_cached_idiomatic_file(idiomatic_file)? { - return Ok(cached); + return Ok(cached.split_whitespace().map(|s| s.to_string()).collect()); } trace!( "parsing idiomatic file: {}", @@ -343,7 +343,13 @@ impl Backend for AsdfBackend { let idiomatic_version = normalize_idiomatic_contents(&idiomatic_version); self.write_idiomatic_cache(idiomatic_file, &idiomatic_version)?; - Ok(idiomatic_version) + if idiomatic_version.is_empty() { + return Ok(vec![]); + } + Ok(idiomatic_version + .split_whitespace() + .map(|s| s.to_string()) + .collect()) } fn plugin(&self) -> Option<&PluginEnum> { diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 85e619da0e..84863cd293 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -915,22 +915,31 @@ pub trait Backend: Debug + Send + Sync { fn get_aliases(&self) -> eyre::Result> { Ok(BTreeMap::new()) } + + /// Returns a list of idiomatic filenames for this tool. + /// + /// This method is additive: + /// 1. It calls `_idiomatic_filenames` to get backend-specific filenames. + /// 2. It checks the Registry for any additional filenames defined there. async fn idiomatic_filenames(&self) -> Result> { - Ok(REGISTRY - .get(self.id()) - .map(|rt| rt.idiomatic_files.iter().map(|s| s.to_string()).collect()) - .unwrap_or_default()) - } - async fn parse_idiomatic_file(&self, path: &Path) -> eyre::Result { - if path.file_name().is_some_and(|f| f == "package.json") { - let pkg = crate::package_json::PackageJson::parse(path)?; - return pkg - .package_manager_version(self.id()) - .ok_or_else(|| eyre::eyre!("no {} version found in package.json", self.id())); + let mut filenames = self._idiomatic_filenames().await?; + if let Some(rt) = REGISTRY.get(self.id()) { + filenames.extend(rt.idiomatic_files.iter().map(|s| s.to_string())); } - let contents = file::read_to_string(path)?; - Ok(normalize_idiomatic_contents(&contents)) + Ok(filenames) + } + + /// Backend-specific implementation for `idiomatic_filenames`. + /// Override this to provide native idiomatic filenames for the backend. + async fn _idiomatic_filenames(&self) -> Result> { + Ok(vec![]) } + + /// Parses an idiomatic version file to extract the version. + async fn parse_idiomatic_file(&self, _path: &Path) -> eyre::Result> { + Ok(vec![]) + } + fn plugin(&self) -> Option<&PluginEnum> { None } diff --git a/src/backend/vfox.rs b/src/backend/vfox.rs index fce2f0a6ef..1adc68d162 100644 --- a/src/backend/vfox.rs +++ b/src/backend/vfox.rs @@ -171,16 +171,20 @@ impl Backend for VfoxBackend { Some(&self.plugin_enum) } - async fn parse_idiomatic_file(&self, path: &Path) -> eyre::Result { + async fn _idiomatic_filenames(&self) -> eyre::Result> { + let (vfox, _log_rx) = self.plugin.vfox(); + + let metadata = vfox.metadata(&self.pathname).await?; + Ok(metadata.legacy_filenames) + } + + async fn parse_idiomatic_file(&self, path: &Path) -> eyre::Result> { let (vfox, _log_rx) = self.plugin.vfox(); let response = vfox.parse_legacy_file(&self.pathname, path).await?; - response.version.ok_or_else(|| { - eyre::eyre!( - "Version for {} not found in '{}'", - self.pathname, - path.display() - ) - }) + if let Some(version) = response.version { + return Ok(version.split_whitespace().map(|s| s.to_string()).collect()); + } + Ok(vec![]) } async fn get_tarball_url( diff --git a/src/config/config_file/idiomatic_version.rs b/src/config/config_file/idiomatic_version/mod.rs similarity index 64% rename from src/config/config_file/idiomatic_version.rs rename to src/config/config_file/idiomatic_version/mod.rs index f3627dc11c..f68cc5fd83 100644 --- a/src/config/config_file/idiomatic_version.rs +++ b/src/config/config_file/idiomatic_version/mod.rs @@ -10,6 +10,8 @@ use crate::toolset::{ToolRequest, ToolRequestSet, ToolSource}; use super::ConfigFileType; +pub mod package_json; + #[derive(Debug, Clone)] pub struct IdiomaticVersionFile { path: PathBuf, @@ -28,17 +30,35 @@ impl IdiomaticVersionFile { let source = ToolSource::IdiomaticVersionFile(path.clone()); let mut tools = ToolRequestSet::new(); - for plugin in plugins { - let version = match plugin.parse_idiomatic_file(&path).await { - Ok(v) => v, - Err(e) => { - trace!("skipping {} for {}: {e}", plugin.id(), path.display()); - continue; - } - }; - for version in version.split_whitespace() { + let add_version = + |tools: &mut ToolRequestSet, plugin: &Arc, version: &str| -> Result<()> { let tr = ToolRequest::new(plugin.ba().clone(), version, source.clone())?; tools.add_version(tr, &source); + Ok(()) + }; + + for plugin in plugins { + if path.file_name().is_some_and(|f| f == "package.json") { + let versions = package_json::parse(&path, plugin.id())?; + for v in versions { + add_version(&mut tools, &plugin, &v)?; + } + continue; + } + + let versions = plugin.parse_idiomatic_file(&path).await?; + if !versions.is_empty() { + for v in versions { + add_version(&mut tools, &plugin, &v)?; + } + continue; + } + let body = crate::file::read_to_string(&path).unwrap_or_default(); + let body = body.trim(); + if !body.is_empty() { + for v in body.split_whitespace() { + add_version(&mut tools, &plugin, v)?; + } } } @@ -47,12 +67,19 @@ impl IdiomaticVersionFile { pub async fn from_file(path: &Path) -> Result { trace!("parsing idiomatic version: {}", path.display()); - let file_name = &path.file_name().unwrap().to_string_lossy().to_string(); + let file_name = path.file_name().unwrap().to_string_lossy().to_string(); let mut tools: Vec> = vec![]; + let enable_tools = crate::config::Settings::get() + .idiomatic_version_file_enable_tools + .clone(); for b in backend::list().into_iter() { + if !enable_tools.contains(b.id()) { + continue; + } + if b.idiomatic_filenames() .await - .is_ok_and(|f| f.contains(file_name)) + .is_ok_and(|f| f.contains(&file_name)) { tools.push(b); } diff --git a/src/package_json.rs b/src/config/config_file/idiomatic_version/package_json.rs similarity index 72% rename from src/package_json.rs rename to src/config/config_file/idiomatic_version/package_json.rs index 61785f67d2..850799c731 100644 --- a/src/package_json.rs +++ b/src/config/config_file/idiomatic_version/package_json.rs @@ -1,14 +1,12 @@ -use std::path::Path; - +use crate::file; use eyre::Result; use serde::Deserialize; use serde::de::Deserializer; - -use crate::file; +use std::path::Path; #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct PackageJson { +struct PackageJsonData { dev_engines: Option, package_manager: Option, } @@ -50,15 +48,15 @@ where } } -impl PackageJson { - pub fn parse(path: &Path) -> Result { +impl PackageJsonData { + fn parse(path: &Path) -> Result { let contents = file::read_to_string(path)?; - let pkg: PackageJson = serde_json::from_str(&contents)?; + let pkg: PackageJsonData = serde_json::from_str(&contents)?; Ok(pkg) } - /// Extract a runtime version for the given tool name from devEngines.runtime - pub fn runtime_version(&self, tool_name: &str) -> Option { + /// Extract a runtime version for the given tool name. + fn runtime_version(&self, tool_name: &str) -> Option { self.dev_engines .as_ref() .and_then(|de| de.runtime.as_ref()) @@ -70,7 +68,7 @@ impl PackageJson { /// Extract a package manager version for the given tool name. /// Checks devEngines.packageManager first, then falls back to the packageManager field. - pub fn package_manager_version(&self, tool_name: &str) -> Option { + fn package_manager_version(&self, tool_name: &str) -> Option { // Try devEngines.packageManager first self.dev_engines .as_ref() @@ -107,7 +105,7 @@ impl PackageJson { /// This doesn't handle all edge cases correctly. For example, `^20.0.1` should not /// match `20.0.0`, but our simplified approach strips it to `20` which would match. /// Full semver range support may be added in the future. -pub fn simplify_semver(input: &str) -> String { +fn simplify_semver(input: &str) -> String { let input = input.trim(); if input == "*" || input == "x" { return "latest".to_string(); @@ -161,9 +159,32 @@ pub fn simplify_semver(input: &str) -> String { } } +pub fn parse(path: &Path, tool_name: &str) -> Result> { + let pkg = PackageJsonData::parse(path)?; + let v = match tool_name { + "node" | "deno" => pkg.runtime_version(tool_name), + "bun" => { + if let Some(v) = pkg.runtime_version(tool_name) { + Some(v) + } else { + pkg.package_manager_version(tool_name) + } + } + "npm" | "yarn" | "pnpm" => pkg.package_manager_version(tool_name), + _ => None, + }; + if let Some(v) = v { + Ok(vec![v]) + } else { + Ok(vec![]) + } +} + #[cfg(test)] mod tests { use super::*; + use std::fs; + use tempfile::tempdir; #[test] fn test_simplify_semver() { @@ -181,6 +202,47 @@ mod tests { assert_eq!(simplify_semver("=18.0.0"), "18"); } + #[test] + fn test_parse_package_json() { + let dir = tempdir().unwrap(); + let path = dir.path().join("package.json"); + fs::write( + &path, + r#"{ + "devEngines": { + "packageManager": { + "name": "yarn", + "version": "1.22.19" + }, + "runtime": { + "name": "node", + "version": "20.0.0" + } + } + }"#, + ) + .unwrap(); + + assert_eq!(parse(&path, "yarn").unwrap(), vec!["1.22.19".to_string()]); + assert_eq!(parse(&path, "node").unwrap(), vec!["20.0.0".to_string()]); + } + + #[test] + fn test_bun_logic() { + let dir = tempdir().unwrap(); + let path = dir.path().join("package.json"); + fs::write( + &path, + r#"{ + "packageManager": "bun@1.0.0" + }"#, + ) + .unwrap(); + + assert_eq!(parse(&path, "bun").unwrap(), vec!["1.0.0".to_string()]); + assert_eq!(parse(&path, "node").unwrap(), Vec::::new()); + } + #[test] fn test_simplify_semver_upper_bound() { assert_eq!(simplify_semver("<18.0.0"), ""); @@ -197,7 +259,7 @@ mod tests { #[test] fn test_runtime_version() { - let pkg: PackageJson = serde_json::from_str( + let pkg: PackageJsonData = serde_json::from_str( r#"{ "devEngines": { "runtime": { @@ -214,7 +276,7 @@ mod tests { #[test] fn test_runtime_version_bun() { - let pkg: PackageJson = serde_json::from_str( + let pkg: PackageJsonData = serde_json::from_str( r#"{ "devEngines": { "runtime": { @@ -231,7 +293,7 @@ mod tests { #[test] fn test_runtime_version_array_form() { - let pkg: PackageJson = serde_json::from_str( + let pkg: PackageJsonData = serde_json::from_str( r#"{ "devEngines": { "runtime": [ @@ -247,7 +309,7 @@ mod tests { #[test] fn test_runtime_version_missing_name() { - let pkg: PackageJson = serde_json::from_str( + let pkg: PackageJsonData = serde_json::from_str( r#"{ "devEngines": { "runtime": { @@ -262,7 +324,7 @@ mod tests { #[test] fn test_package_manager_version_dev_engines() { - let pkg: PackageJson = serde_json::from_str( + let pkg: PackageJsonData = serde_json::from_str( r#"{ "devEngines": { "packageManager": { @@ -279,7 +341,7 @@ mod tests { #[test] fn test_package_manager_version_field() { - let pkg: PackageJson = serde_json::from_str( + let pkg: PackageJsonData = serde_json::from_str( r#"{ "packageManager": "pnpm@9.1.0+sha256.abcdef" }"#, @@ -294,7 +356,7 @@ mod tests { #[test] fn test_package_manager_version_no_hash() { - let pkg: PackageJson = serde_json::from_str( + let pkg: PackageJsonData = serde_json::from_str( r#"{ "packageManager": "yarn@4.1.0" }"#, @@ -308,7 +370,7 @@ mod tests { #[test] fn test_dev_engines_overrides_package_manager_field() { - let pkg: PackageJson = serde_json::from_str( + let pkg: PackageJsonData = serde_json::from_str( r#"{ "devEngines": { "packageManager": { @@ -325,14 +387,14 @@ mod tests { #[test] fn test_missing_fields() { - let pkg: PackageJson = serde_json::from_str(r#"{}"#).unwrap(); + let pkg: PackageJsonData = serde_json::from_str(r#"{}"#).unwrap(); assert_eq!(pkg.runtime_version("node"), None); assert_eq!(pkg.package_manager_version("pnpm"), None); } #[test] fn test_empty_dev_engines() { - let pkg: PackageJson = serde_json::from_str( + let pkg: PackageJsonData = serde_json::from_str( r#"{ "devEngines": {} }"#, @@ -344,7 +406,7 @@ mod tests { #[test] fn test_bun_as_package_manager() { - let pkg: PackageJson = serde_json::from_str( + let pkg: PackageJsonData = serde_json::from_str( r#"{ "packageManager": "bun@1.2.0" }"#, @@ -359,7 +421,7 @@ mod tests { #[test] fn test_deno_dev_engines() { - let pkg: PackageJson = serde_json::from_str( + let pkg: PackageJsonData = serde_json::from_str( r#"{ "devEngines": { "runtime": { @@ -372,4 +434,40 @@ mod tests { .unwrap(); assert_eq!(pkg.runtime_version("deno"), Some("1.40.0".to_string())); } + + #[test] + fn test_engines_field_ignored() { + let pkg: PackageJsonData = serde_json::from_str( + r#"{ + "engines": { + "node": ">=18.0.0", + "pnpm": "9.0.0" + } + }"#, + ) + .unwrap(); + // Should ignore engines field + assert_eq!(pkg.runtime_version("node"), None); + assert_eq!(pkg.package_manager_version("pnpm"), None); + } + + #[test] + fn test_engines_field_does_not_interfere() { + let pkg: PackageJsonData = serde_json::from_str( + r#"{ + "devEngines": { + "runtime": { + "name": "node", + "version": "20.0.0" + } + }, + "engines": { + "node": "18.0.0" + } + }"#, + ) + .unwrap(); + // Should ignore engines and pick devEngines + assert_eq!(pkg.runtime_version("node"), Some("20.0.0".to_string())); + } } diff --git a/src/config/config_file/mod.rs b/src/config/config_file/mod.rs index 72db6bc941..aecc5f4b2f 100644 --- a/src/config/config_file/mod.rs +++ b/src/config/config_file/mod.rs @@ -516,14 +516,9 @@ fn trust_file_hash(path: &Path) -> eyre::Result { async fn filename_is_idiomatic(file_name: String) -> bool { for b in backend::list() { - match b.idiomatic_filenames().await { - Ok(filenames) => { - if filenames.contains(&file_name) { - return true; - } - } - Err(e) => { - debug!("idiomatic_filenames failed for {}: {:?}", b, e); + if let Ok(filenames) = b.idiomatic_filenames().await { + if filenames.contains(&file_name) { + return true; } } } diff --git a/src/config/mod.rs b/src/config/mod.rs index 7f8f4e199f..2e797e1679 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -125,7 +125,7 @@ impl Config { pub async fn load() -> Result> { backend::load_tools().await?; let idiomatic_files = measure!("config::load idiomatic_files", { - load_idiomatic_files().await + load_idiomatic_filenames().await }); let config_filenames = idiomatic_files .keys() @@ -898,7 +898,7 @@ fn find_monorepo_config(config_files: &ConfigMap) -> Option<&Arc .find(|cf| cf.experimental_monorepo_root() == Some(true)) } -async fn load_idiomatic_files() -> BTreeMap> { +async fn load_idiomatic_filenames() -> BTreeMap> { let enable_tools = Settings::get().idiomatic_version_file_enable_tools.clone(); if enable_tools.is_empty() { return BTreeMap::new(); @@ -1434,7 +1434,7 @@ async fn load_all_config_files( /// Load config files from a list of paths (for monorepo task config contexts) pub async fn load_config_files_from_paths(config_paths: &[PathBuf]) -> Result { backend::load_tools().await?; - let idiomatic_filenames = BTreeMap::new(); // TODO: support idiomatic files in config hierarchy loading + let idiomatic_filenames = load_idiomatic_filenames().await; let mut config_map = ConfigMap::default(); for f in config_paths.iter().unique() { diff --git a/src/main.rs b/src/main.rs index 811c7a95b3..736bbb3ec7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -64,7 +64,6 @@ pub(crate) mod maplit; mod migrate; mod minisign; mod netrc; -mod package_json; pub(crate) mod parallel; mod path; mod path_env; diff --git a/src/plugins/core/bun.rs b/src/plugins/core/bun.rs index 0fca54df3e..edbbab1820 100644 --- a/src/plugins/core/bun.rs +++ b/src/plugins/core/bun.rs @@ -140,22 +140,10 @@ impl Backend for BunPlugin { Ok(versions) } - async fn idiomatic_filenames(&self) -> Result> { + async fn _idiomatic_filenames(&self) -> Result> { Ok(vec![".bun-version".into(), "package.json".into()]) } - async fn parse_idiomatic_file(&self, path: &Path) -> Result { - if path.file_name().is_some_and(|f| f == "package.json") { - let pkg = crate::package_json::PackageJson::parse(path)?; - return pkg - .runtime_version("bun") - .or_else(|| pkg.package_manager_version("bun")) - .ok_or_else(|| eyre::eyre!("no bun version found in package.json")); - } - let contents = file::read_to_string(path)?; - Ok(contents.trim().to_string()) - } - async fn install_version_( &self, ctx: &InstallContext, diff --git a/src/plugins/core/deno.rs b/src/plugins/core/deno.rs index 6b6e610ada..d908c0840c 100644 --- a/src/plugins/core/deno.rs +++ b/src/plugins/core/deno.rs @@ -119,21 +119,10 @@ impl Backend for DenoPlugin { Ok(versions) } - async fn idiomatic_filenames(&self) -> Result> { + async fn _idiomatic_filenames(&self) -> Result> { Ok(vec![".deno-version".into(), "package.json".into()]) } - async fn parse_idiomatic_file(&self, path: &Path) -> Result { - if path.file_name().is_some_and(|f| f == "package.json") { - let pkg = crate::package_json::PackageJson::parse(path)?; - return pkg - .runtime_version("deno") - .ok_or_else(|| eyre::eyre!("no deno version found in package.json")); - } - let body = file::read_to_string(path)?; - Ok(body.trim().to_string()) - } - async fn install_version_( &self, ctx: &InstallContext, diff --git a/src/plugins/core/elixir.rs b/src/plugins/core/elixir.rs index f9e1a1ac4c..a84766ba8a 100644 --- a/src/plugins/core/elixir.rs +++ b/src/plugins/core/elixir.rs @@ -126,7 +126,7 @@ impl Backend for ElixirPlugin { Ok(versions) } - async fn idiomatic_filenames(&self) -> eyre::Result> { + async fn _idiomatic_filenames(&self) -> eyre::Result> { Ok(vec![".exenv-version".into()]) } diff --git a/src/plugins/core/go.rs b/src/plugins/core/go.rs index bad1f004cd..06242302d7 100644 --- a/src/plugins/core/go.rs +++ b/src/plugins/core/go.rs @@ -271,7 +271,7 @@ impl Backend for GoPlugin { }; Ok(versions) } - async fn idiomatic_filenames(&self) -> eyre::Result> { + async fn _idiomatic_filenames(&self) -> eyre::Result> { Ok(vec![".go-version".into()]) } diff --git a/src/plugins/core/java.rs b/src/plugins/core/java.rs index d8faec16cb..148e0b370b 100644 --- a/src/plugins/core/java.rs +++ b/src/plugins/core/java.rs @@ -387,11 +387,11 @@ impl Backend for JavaPlugin { Ok(aliases) } - async fn idiomatic_filenames(&self) -> Result> { + async fn _idiomatic_filenames(&self) -> Result> { Ok(vec![".java-version".into(), ".sdkmanrc".into()]) } - async fn parse_idiomatic_file(&self, path: &Path) -> Result { + async fn parse_idiomatic_file(&self, path: &Path) -> Result> { let contents = file::read_to_string(path)?; if path.file_name() == Some(".sdkmanrc".as_ref()) { let version = contents @@ -402,7 +402,7 @@ impl Backend for JavaPlugin { .unwrap_or_default() .1; if !version.contains('-') { - return Ok(version.to_string()); + return Ok(vec![version.to_string()]); } let (version, vendor) = version.rsplit_once('-').unwrap_or_default(); let vendor = match vendor { @@ -422,9 +422,12 @@ impl Backend for JavaPlugin { if vendor == "zulu" { version = version.split_once('.').unwrap_or_default().0; } - Ok(format!("{vendor}-{version}")) + Ok(vec![format!("{vendor}-{version}")]) } else { - Ok(normalize_idiomatic_contents(&contents)) + Ok(normalize_idiomatic_contents(&contents) + .lines() + .map(|s| s.to_string()) + .collect()) } } diff --git a/src/plugins/core/node.rs b/src/plugins/core/node.rs index 84c3c42174..21af3d68ca 100644 --- a/src/plugins/core/node.rs +++ b/src/plugins/core/node.rs @@ -562,7 +562,7 @@ impl Backend for NodePlugin { Ok(aliases) } - async fn idiomatic_filenames(&self) -> Result> { + async fn _idiomatic_filenames(&self) -> Result> { Ok(vec![ ".node-version".into(), ".nvmrc".into(), @@ -570,19 +570,19 @@ impl Backend for NodePlugin { ]) } - async fn parse_idiomatic_file(&self, path: &Path) -> Result { - if path.file_name().is_some_and(|f| f == "package.json") { - let pkg = crate::package_json::PackageJson::parse(path)?; - return pkg - .runtime_version("node") - .ok_or_else(|| eyre::eyre!("no node version found in package.json")); - } - let body = normalize_idiomatic_contents(&file::read_to_string(path)?); - // trim "v" prefix - let body = body.trim().strip_prefix('v').unwrap_or(&body); - // replace lts/* with lts - let body = body.replace("lts/*", "lts"); - Ok(body.to_string()) + async fn parse_idiomatic_file(&self, path: &Path) -> Result> { + let contents = file::read_to_string(path)?; + let body = normalize_idiomatic_contents(&contents); + + let versions = body + .lines() + .map(|line| { + let mut version = line.trim().strip_prefix('v').unwrap_or(line).to_string(); + version = version.replace("lts/*", "lts"); + version + }) + .collect(); + Ok(versions) } async fn install_version_( diff --git a/src/plugins/core/python.rs b/src/plugins/core/python.rs index b8e54ffdf2..20033be8e1 100644 --- a/src/plugins/core/python.rs +++ b/src/plugins/core/python.rs @@ -572,7 +572,7 @@ impl Backend for PythonPlugin { } } - async fn idiomatic_filenames(&self) -> eyre::Result> { + async fn _idiomatic_filenames(&self) -> eyre::Result> { Ok(vec![ ".python-version".to_string(), ".python-versions".to_string(), diff --git a/src/plugins/core/ruby.rs b/src/plugins/core/ruby.rs index c123269c1a..4a52f9e22a 100644 --- a/src/plugins/core/ruby.rs +++ b/src/plugins/core/ruby.rs @@ -774,11 +774,11 @@ impl Backend for RubyPlugin { .await } - async fn idiomatic_filenames(&self) -> Result> { + async fn _idiomatic_filenames(&self) -> Result> { Ok(vec![".ruby-version".into(), "Gemfile".into()]) } - async fn parse_idiomatic_file(&self, path: &Path) -> Result { + async fn parse_idiomatic_file(&self, path: &Path) -> Result> { let v = match path.file_name() { Some(name) if name == "Gemfile" => parse_gemfile(&file::read_to_string(path)?), _ => { @@ -790,7 +790,7 @@ impl Backend for RubyPlugin { .to_string() } }; - Ok(v) + Ok(vec![v]) } async fn install_version_(&self, ctx: &InstallContext, tv: ToolVersion) -> Result { diff --git a/src/plugins/core/ruby_windows.rs b/src/plugins/core/ruby_windows.rs index d74a56bec2..ae6f6ea05d 100644 --- a/src/plugins/core/ruby_windows.rs +++ b/src/plugins/core/ruby_windows.rs @@ -178,11 +178,11 @@ impl Backend for RubyPlugin { Ok(versions) } - async fn idiomatic_filenames(&self) -> Result> { + async fn _idiomatic_filenames(&self) -> Result> { Ok(vec![".ruby-version".into(), "Gemfile".into()]) } - async fn parse_idiomatic_file(&self, path: &Path) -> Result { + async fn parse_idiomatic_file(&self, path: &Path) -> Result> { let v = match path.file_name() { Some(name) if name == "Gemfile" => parse_gemfile(&file::read_to_string(path)?), _ => { @@ -194,7 +194,7 @@ impl Backend for RubyPlugin { .to_string() } }; - Ok(v) + Ok(vec![v]) } async fn install_version_( diff --git a/src/plugins/core/rust.rs b/src/plugins/core/rust.rs index 4d55e50c42..79ba732173 100644 --- a/src/plugins/core/rust.rs +++ b/src/plugins/core/rust.rs @@ -116,13 +116,13 @@ impl Backend for RustPlugin { Ok(versions) } - async fn idiomatic_filenames(&self) -> Result> { + async fn _idiomatic_filenames(&self) -> Result> { Ok(vec!["rust-toolchain.toml".into()]) } - async fn parse_idiomatic_file(&self, path: &Path) -> Result { + async fn parse_idiomatic_file(&self, path: &Path) -> Result> { let rt = parse_idiomatic_file(path)?; - Ok(rt.channel) + Ok(vec![rt.channel]) } async fn install_version_(&self, ctx: &InstallContext, tv: ToolVersion) -> Result { diff --git a/src/plugins/core/swift.rs b/src/plugins/core/swift.rs index 8fcf7478ef..717b1ccce2 100644 --- a/src/plugins/core/swift.rs +++ b/src/plugins/core/swift.rs @@ -193,7 +193,7 @@ impl Backend for SwiftPlugin { Ok(versions) } - async fn idiomatic_filenames(&self) -> Result> { + async fn _idiomatic_filenames(&self) -> Result> { if Settings::get().experimental { Ok(vec![".swift-version".into()]) } else { diff --git a/src/plugins/core/zig.rs b/src/plugins/core/zig.rs index c6bcafce93..c0c9bf0c49 100644 --- a/src/plugins/core/zig.rs +++ b/src/plugins/core/zig.rs @@ -300,7 +300,7 @@ impl Backend for ZigPlugin { } } - async fn idiomatic_filenames(&self) -> Result> { + async fn _idiomatic_filenames(&self) -> Result> { Ok(vec![".zig-version".into()]) } From f82fa889baf20e867ca5f4126202096816667fd0 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 16 Feb 2026 06:11:56 +0000 Subject: [PATCH 02/23] [autofix.ci] apply automated fixes --- src/config/config_file/mod.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/config/config_file/mod.rs b/src/config/config_file/mod.rs index aecc5f4b2f..89daab6692 100644 --- a/src/config/config_file/mod.rs +++ b/src/config/config_file/mod.rs @@ -516,11 +516,10 @@ fn trust_file_hash(path: &Path) -> eyre::Result { async fn filename_is_idiomatic(file_name: String) -> bool { for b in backend::list() { - if let Ok(filenames) = b.idiomatic_filenames().await { - if filenames.contains(&file_name) { + if let Ok(filenames) = b.idiomatic_filenames().await + && filenames.contains(&file_name) { return true; } - } } false } From 7a8992306e78631d65f61134fa0fce374df3cb28 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 16 Feb 2026 06:15:54 +0000 Subject: [PATCH 03/23] [autofix.ci] apply automated fixes (attempt 2/3) --- src/config/config_file/mod.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/config/config_file/mod.rs b/src/config/config_file/mod.rs index 89daab6692..205ffe6806 100644 --- a/src/config/config_file/mod.rs +++ b/src/config/config_file/mod.rs @@ -517,9 +517,10 @@ fn trust_file_hash(path: &Path) -> eyre::Result { async fn filename_is_idiomatic(file_name: String) -> bool { for b in backend::list() { if let Ok(filenames) = b.idiomatic_filenames().await - && filenames.contains(&file_name) { - return true; - } + && filenames.contains(&file_name) + { + return true; + } } false } From 01512a799dadfb12ef459669969e78dea1ca3e00 Mon Sep 17 00:00:00 2001 From: Risu <79110363+risu729@users.noreply.github.com> Date: Mon, 16 Feb 2026 17:16:49 +1100 Subject: [PATCH 04/23] style: simplify bun parse --- .../config_file/idiomatic_version/package_json.rs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/config/config_file/idiomatic_version/package_json.rs b/src/config/config_file/idiomatic_version/package_json.rs index 850799c731..be308cadd5 100644 --- a/src/config/config_file/idiomatic_version/package_json.rs +++ b/src/config/config_file/idiomatic_version/package_json.rs @@ -163,13 +163,9 @@ pub fn parse(path: &Path, tool_name: &str) -> Result> { let pkg = PackageJsonData::parse(path)?; let v = match tool_name { "node" | "deno" => pkg.runtime_version(tool_name), - "bun" => { - if let Some(v) = pkg.runtime_version(tool_name) { - Some(v) - } else { - pkg.package_manager_version(tool_name) - } - } + "bun" => pkg + .runtime_version(tool_name) + .or_else(|| pkg.package_manager_version(tool_name)), "npm" | "yarn" | "pnpm" => pkg.package_manager_version(tool_name), _ => None, }; From 18a816f796b22ae9080e287c5ddc9ab9f5130d05 Mon Sep 17 00:00:00 2001 From: Risu <79110363+risu729@users.noreply.github.com> Date: Tue, 17 Feb 2026 01:39:26 +1100 Subject: [PATCH 05/23] fix: trim in java.rs --- src/plugins/core/java.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/plugins/core/java.rs b/src/plugins/core/java.rs index 148e0b370b..273b5e7bbb 100644 --- a/src/plugins/core/java.rs +++ b/src/plugins/core/java.rs @@ -424,10 +424,14 @@ impl Backend for JavaPlugin { } Ok(vec![format!("{vendor}-{version}")]) } else { +<<<<<<< HEAD Ok(normalize_idiomatic_contents(&contents) .lines() .map(|s| s.to_string()) .collect()) +======= + Ok(vec![contents.trim().to_string()]) +>>>>>>> 87f3fe68f (fix: trim in java.rs) } } From 5de163936617e229a173ccedc0ada6b68a40e586 Mon Sep 17 00:00:00 2001 From: Risu <79110363+risu729@users.noreply.github.com> Date: Tue, 17 Feb 2026 02:30:12 +1100 Subject: [PATCH 06/23] Revert "fix: trim in java.rs" This reverts commit 4910b7a5cfc8cfbbe867a8bb33b1c928193ef661. --- src/plugins/core/java.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/plugins/core/java.rs b/src/plugins/core/java.rs index 273b5e7bbb..6a8d5f5f5e 100644 --- a/src/plugins/core/java.rs +++ b/src/plugins/core/java.rs @@ -424,6 +424,7 @@ impl Backend for JavaPlugin { } Ok(vec![format!("{vendor}-{version}")]) } else { +<<<<<<< HEAD <<<<<<< HEAD Ok(normalize_idiomatic_contents(&contents) .lines() @@ -432,6 +433,9 @@ impl Backend for JavaPlugin { ======= Ok(vec![contents.trim().to_string()]) >>>>>>> 87f3fe68f (fix: trim in java.rs) +======= + Ok(vec![contents]) +>>>>>>> 079998d53 (Revert "fix: trim in java.rs") } } From 009fcffb96c9f0e4638af9bb00fca5d444c5dd6e Mon Sep 17 00:00:00 2001 From: Risu <79110363+risu729@users.noreply.github.com> Date: Tue, 17 Feb 2026 03:56:01 +1100 Subject: [PATCH 07/23] fix: fallback to other parsers if no versions are found --- src/config/config_file/idiomatic_version/mod.rs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/config/config_file/idiomatic_version/mod.rs b/src/config/config_file/idiomatic_version/mod.rs index f68cc5fd83..4f4e296bbe 100644 --- a/src/config/config_file/idiomatic_version/mod.rs +++ b/src/config/config_file/idiomatic_version/mod.rs @@ -40,19 +40,24 @@ impl IdiomaticVersionFile { for plugin in plugins { if path.file_name().is_some_and(|f| f == "package.json") { let versions = package_json::parse(&path, plugin.id())?; - for v in versions { - add_version(&mut tools, &plugin, &v)?; + for v in versions.iter() { + add_version(&mut tools, &plugin, v)?; + } + if !versions.is_empty() { + continue; } - continue; } let versions = plugin.parse_idiomatic_file(&path).await?; if !versions.is_empty() { - for v in versions { - add_version(&mut tools, &plugin, &v)?; + for v in versions.iter() { + add_version(&mut tools, &plugin, v)?; + } + if !versions.is_empty() { + continue; } - continue; } + let body = crate::file::read_to_string(&path).unwrap_or_default(); let body = body.trim(); if !body.is_empty() { From bddff77667b69e92df62e65a619a7f97de0c74fa Mon Sep 17 00:00:00 2001 From: Risu <79110363+risu729@users.noreply.github.com> Date: Tue, 17 Feb 2026 21:34:15 +1100 Subject: [PATCH 08/23] Revert "fix: fallback to other parsers if no versions are found" This reverts commit a95f34a8b74659982be8ffa4e07334eeaf5cb251. --- src/config/config_file/idiomatic_version/mod.rs | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/config/config_file/idiomatic_version/mod.rs b/src/config/config_file/idiomatic_version/mod.rs index 4f4e296bbe..f68cc5fd83 100644 --- a/src/config/config_file/idiomatic_version/mod.rs +++ b/src/config/config_file/idiomatic_version/mod.rs @@ -40,24 +40,19 @@ impl IdiomaticVersionFile { for plugin in plugins { if path.file_name().is_some_and(|f| f == "package.json") { let versions = package_json::parse(&path, plugin.id())?; - for v in versions.iter() { - add_version(&mut tools, &plugin, v)?; - } - if !versions.is_empty() { - continue; + for v in versions { + add_version(&mut tools, &plugin, &v)?; } + continue; } let versions = plugin.parse_idiomatic_file(&path).await?; if !versions.is_empty() { - for v in versions.iter() { - add_version(&mut tools, &plugin, v)?; - } - if !versions.is_empty() { - continue; + for v in versions { + add_version(&mut tools, &plugin, &v)?; } + continue; } - let body = crate::file::read_to_string(&path).unwrap_or_default(); let body = body.trim(); if !body.is_empty() { From 3ed16a1e06af7b0afde1552be59f4cdd20300ad5 Mon Sep 17 00:00:00 2001 From: Risu <79110363+risu729@users.noreply.github.com> Date: Tue, 17 Feb 2026 21:53:26 +1100 Subject: [PATCH 09/23] fix: trim .java-version content --- src/plugins/core/java.rs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/plugins/core/java.rs b/src/plugins/core/java.rs index 6a8d5f5f5e..148e0b370b 100644 --- a/src/plugins/core/java.rs +++ b/src/plugins/core/java.rs @@ -424,18 +424,10 @@ impl Backend for JavaPlugin { } Ok(vec![format!("{vendor}-{version}")]) } else { -<<<<<<< HEAD -<<<<<<< HEAD Ok(normalize_idiomatic_contents(&contents) .lines() .map(|s| s.to_string()) .collect()) -======= - Ok(vec![contents.trim().to_string()]) ->>>>>>> 87f3fe68f (fix: trim in java.rs) -======= - Ok(vec![contents]) ->>>>>>> 079998d53 (Revert "fix: trim in java.rs") } } From 49bbafdb0148233566c9d4a9a46b1b748566636f Mon Sep 17 00:00:00 2001 From: Risu <79110363+risu729@users.noreply.github.com> Date: Tue, 17 Feb 2026 22:50:10 +1100 Subject: [PATCH 10/23] fix: compile error --- src/plugins/core/node.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/plugins/core/node.rs b/src/plugins/core/node.rs index 21af3d68ca..a6afe3b236 100644 --- a/src/plugins/core/node.rs +++ b/src/plugins/core/node.rs @@ -571,6 +571,7 @@ impl Backend for NodePlugin { } async fn parse_idiomatic_file(&self, path: &Path) -> Result> { +<<<<<<< HEAD let contents = file::read_to_string(path)?; let body = normalize_idiomatic_contents(&contents); @@ -583,6 +584,16 @@ impl Backend for NodePlugin { }) .collect(); Ok(versions) +======= + let body = file::read_to_string(path)?; + // strip comments + let body = body.split('#').next().unwrap_or_default().trim(); + // trim "v" prefix + let body = body.strip_prefix('v').unwrap_or(body); + // replace lts/* with lts + let body = body.replace("lts/*", "lts"); + Ok(vec![body]) +>>>>>>> 3ed60bf99 (fix: compile error) } async fn install_version_( From fe98d1db96c82d2270afd082c10bee11b5da3811 Mon Sep 17 00:00:00 2001 From: Risu <79110363+risu729@users.noreply.github.com> Date: Wed, 18 Feb 2026 00:58:51 +1100 Subject: [PATCH 11/23] fix: allow all idiomatic files in IdiomaticVersionFile::from_file --- src/config/config_file/idiomatic_version/mod.rs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/config/config_file/idiomatic_version/mod.rs b/src/config/config_file/idiomatic_version/mod.rs index f68cc5fd83..c08e99aa6b 100644 --- a/src/config/config_file/idiomatic_version/mod.rs +++ b/src/config/config_file/idiomatic_version/mod.rs @@ -69,14 +69,7 @@ impl IdiomaticVersionFile { trace!("parsing idiomatic version: {}", path.display()); let file_name = path.file_name().unwrap().to_string_lossy().to_string(); let mut tools: Vec> = vec![]; - let enable_tools = crate::config::Settings::get() - .idiomatic_version_file_enable_tools - .clone(); for b in backend::list().into_iter() { - if !enable_tools.contains(b.id()) { - continue; - } - if b.idiomatic_filenames() .await .is_ok_and(|f| f.contains(&file_name)) From 0c4bf846c6845b6c07783ce19161880072c038fd Mon Sep 17 00:00:00 2001 From: Risu <79110363+risu729@users.noreply.github.com> Date: Wed, 18 Feb 2026 01:13:57 +1100 Subject: [PATCH 12/23] style: add comment in package.json --- src/config/config_file/idiomatic_version/package_json.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/config/config_file/idiomatic_version/package_json.rs b/src/config/config_file/idiomatic_version/package_json.rs index be308cadd5..2d56900bc7 100644 --- a/src/config/config_file/idiomatic_version/package_json.rs +++ b/src/config/config_file/idiomatic_version/package_json.rs @@ -161,6 +161,7 @@ fn simplify_semver(input: &str) -> String { pub fn parse(path: &Path, tool_name: &str) -> Result> { let pkg = PackageJsonData::parse(path)?; + // We ignore unknown tools in pacakge.json let v = match tool_name { "node" | "deno" => pkg.runtime_version(tool_name), "bun" => pkg From 29a247ba02e4e72425120369dc6aa485606de83e Mon Sep 17 00:00:00 2001 From: Risu <79110363+risu729@users.noreply.github.com> Date: Wed, 18 Feb 2026 01:21:26 +1100 Subject: [PATCH 13/23] fix(node): return emtpy versions array for empty idiomatic file --- src/plugins/core/node.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/plugins/core/node.rs b/src/plugins/core/node.rs index a6afe3b236..c6d05df26e 100644 --- a/src/plugins/core/node.rs +++ b/src/plugins/core/node.rs @@ -592,6 +592,9 @@ impl Backend for NodePlugin { let body = body.strip_prefix('v').unwrap_or(body); // replace lts/* with lts let body = body.replace("lts/*", "lts"); + if body.is_empty() { + return Ok(vec![]); + } Ok(vec![body]) >>>>>>> 3ed60bf99 (fix: compile error) } From 3320f27eeab3b368af4df4b707a0bda7cdef61a0 Mon Sep 17 00:00:00 2001 From: Risu <79110363+risu729@users.noreply.github.com> Date: Wed, 18 Feb 2026 01:23:03 +1100 Subject: [PATCH 14/23] fix(idimatic): dedup idiomatic filenames for a tool --- src/backend/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 84863cd293..cc55c96588 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -926,6 +926,7 @@ pub trait Backend: Debug + Send + Sync { if let Some(rt) = REGISTRY.get(self.id()) { filenames.extend(rt.idiomatic_files.iter().map(|s| s.to_string())); } + filenames = filenames.into_iter().unique().collect(); Ok(filenames) } From 2fc0446f0cf869672de1838dbe700161bb035236 Mon Sep 17 00:00:00 2001 From: Risu <79110363+risu729@users.noreply.github.com> Date: Wed, 18 Feb 2026 01:44:27 +1100 Subject: [PATCH 15/23] fix: try next plugin if a plugin failed to parse a idiomatic file --- .../config_file/idiomatic_version/mod.rs | 99 ++++++++++++++++++- 1 file changed, 98 insertions(+), 1 deletion(-) diff --git a/src/config/config_file/idiomatic_version/mod.rs b/src/config/config_file/idiomatic_version/mod.rs index c08e99aa6b..021cfda0c2 100644 --- a/src/config/config_file/idiomatic_version/mod.rs +++ b/src/config/config_file/idiomatic_version/mod.rs @@ -46,7 +46,13 @@ impl IdiomaticVersionFile { continue; } - let versions = plugin.parse_idiomatic_file(&path).await?; + let versions = match plugin.parse_idiomatic_file(&path).await { + Ok(versions) => versions, + Err(e) => { + trace!("failed to parse idiomatic file {}: {}", path.display(), e); + continue; + } + }; if !versions.is_empty() { for v in versions { add_version(&mut tools, &plugin, &v)?; @@ -122,3 +128,94 @@ impl ConfigFile for IdiomaticVersionFile { Ok(self.tools.clone()) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::backend::{Backend, VersionInfo}; + use crate::cli::args::{BackendArg, BackendResolution}; + use crate::config::Config; + use crate::install_context::InstallContext; + use crate::toolset::ToolVersion; + use async_trait::async_trait; + use std::sync::Arc; + + #[derive(Debug)] + struct MockBackend { + ba: Arc, + fail: bool, + version: Option, + } + + impl MockBackend { + fn new(short: &str, fail: bool, version: Option) -> Self { + let ba = BackendArg::new_raw( + short.to_string(), + None, + short.to_string(), + None, + BackendResolution::new(false), + ); + Self { + ba: Arc::new(ba), + fail, + version, + } + } + } + + #[async_trait] + impl Backend for MockBackend { + fn ba(&self) -> &Arc { + &self.ba + } + + async fn _list_remote_versions(&self, _config: &Arc) -> Result> { + Ok(vec![]) + } + + async fn install_version_( + &self, + _ctx: &InstallContext, + _tv: ToolVersion, + ) -> Result { + unimplemented!() + } + + async fn parse_idiomatic_file(&self, _path: &Path) -> Result> { + if self.fail { + eyre::bail!("mock error"); + } + if let Some(v) = &self.version { + Ok(vec![v.clone()]) + } else { + Ok(vec![]) + } + } + } + + #[tokio::test] + async fn test_idiomatic_parse_error_propagation() { + let _config = Config::get().await.unwrap(); + let path = PathBuf::from(".tool-versions"); + let backend1 = Arc::new(MockBackend::new("node", true, None)); + let backend2 = Arc::new(MockBackend::new( + "python", + false, + Some("3.10.0".to_string()), + )); + let plugins: BackendList = vec![backend1, backend2]; + + let result = IdiomaticVersionFile::parse(path, plugins).await; + + assert!(result.is_ok(), "Should not propagate error from backend1"); + + let file = result.unwrap(); + let trs = file.to_tool_request_set().unwrap(); + let tools: Vec<_> = trs.into_iter().collect(); + assert_eq!(tools.len(), 1); + let (ba, versions, _) = &tools[0]; + assert_eq!(ba.short, "python"); + assert_eq!(versions[0].version(), "3.10.0"); + } +} From 336d4d8c053c3de3a186e59bdb2496abc381e40f Mon Sep 17 00:00:00 2001 From: Risu <79110363+risu729@users.noreply.github.com> Date: Wed, 18 Feb 2026 15:45:30 +1100 Subject: [PATCH 16/23] fix: fallback to raw text parser only if custom parser isn't implemented or threw an error --- src/backend/mod.rs | 9 ++++++- .../config_file/idiomatic_version/mod.rs | 26 ++++++++----------- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/backend/mod.rs b/src/backend/mod.rs index cc55c96588..50e7946538 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -937,8 +937,15 @@ pub trait Backend: Debug + Send + Sync { } /// Parses an idiomatic version file to extract the version. + /// + /// - Return `Ok(versions)` with extracted version strings (may be empty if + /// the file was parsed successfully but contained no versions). + /// - Return `Err` to signal that this backend has no custom parser for the + /// file, which causes the caller to fall back to raw text parsing. + /// + /// Backends with custom parsing (e.g. node, vfox) should override this. async fn parse_idiomatic_file(&self, _path: &Path) -> eyre::Result> { - Ok(vec![]) + eyre::bail!("no custom parser") } fn plugin(&self) -> Option<&PluginEnum> { diff --git a/src/config/config_file/idiomatic_version/mod.rs b/src/config/config_file/idiomatic_version/mod.rs index 021cfda0c2..a82ba0fc45 100644 --- a/src/config/config_file/idiomatic_version/mod.rs +++ b/src/config/config_file/idiomatic_version/mod.rs @@ -48,23 +48,19 @@ impl IdiomaticVersionFile { let versions = match plugin.parse_idiomatic_file(&path).await { Ok(versions) => versions, - Err(e) => { - trace!("failed to parse idiomatic file {}: {}", path.display(), e); - continue; + Err(_) => { + // Plugin has no custom parser or could not parse this file; + // fall back to splitting raw file text by whitespace. + let body = crate::file::read_to_string(&path).unwrap_or_default(); + let body = body.trim(); + if body.is_empty() { + continue; + } + body.split_whitespace().map(|s| s.to_string()).collect() } }; - if !versions.is_empty() { - for v in versions { - add_version(&mut tools, &plugin, &v)?; - } - continue; - } - let body = crate::file::read_to_string(&path).unwrap_or_default(); - let body = body.trim(); - if !body.is_empty() { - for v in body.split_whitespace() { - add_version(&mut tools, &plugin, v)?; - } + for v in versions { + add_version(&mut tools, &plugin, &v)?; } } From 81e3ad963877380bc506d1a549c7a44a1b19a872 Mon Sep 17 00:00:00 2001 From: Risu <79110363+risu729@users.noreply.github.com> Date: Wed, 18 Feb 2026 15:46:25 +1100 Subject: [PATCH 17/23] style: fix typo --- src/config/config_file/idiomatic_version/package_json.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/config_file/idiomatic_version/package_json.rs b/src/config/config_file/idiomatic_version/package_json.rs index 2d56900bc7..a5bd7ef310 100644 --- a/src/config/config_file/idiomatic_version/package_json.rs +++ b/src/config/config_file/idiomatic_version/package_json.rs @@ -161,7 +161,7 @@ fn simplify_semver(input: &str) -> String { pub fn parse(path: &Path, tool_name: &str) -> Result> { let pkg = PackageJsonData::parse(path)?; - // We ignore unknown tools in pacakge.json + // We ignore unknown tools in package.json let v = match tool_name { "node" | "deno" => pkg.runtime_version(tool_name), "bun" => pkg From 5607857e35c4c1709ab13ee7cb8ff5f0e5281d8f Mon Sep 17 00:00:00 2001 From: Risu <79110363+risu729@users.noreply.github.com> Date: Wed, 18 Feb 2026 15:46:47 +1100 Subject: [PATCH 18/23] fix: restore debug logging --- src/config/config_file/mod.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/config/config_file/mod.rs b/src/config/config_file/mod.rs index 205ffe6806..bdd8c9303f 100644 --- a/src/config/config_file/mod.rs +++ b/src/config/config_file/mod.rs @@ -516,10 +516,10 @@ fn trust_file_hash(path: &Path) -> eyre::Result { async fn filename_is_idiomatic(file_name: String) -> bool { for b in backend::list() { - if let Ok(filenames) = b.idiomatic_filenames().await - && filenames.contains(&file_name) - { - return true; + match b.idiomatic_filenames().await { + Ok(filenames) if filenames.contains(&file_name) => return true, + Err(e) => debug!("idiomatic_filenames failed for {}: {:?}", b, e), + _ => {} } } false From 38d120f2b836aeefe562a04952e13319a428b36d Mon Sep 17 00:00:00 2001 From: Risu <79110363+risu729@users.noreply.github.com> Date: Mon, 23 Feb 2026 12:43:52 +1100 Subject: [PATCH 19/23] fix: don't fall back to raw text parser --- src/backend/mod.rs | 21 +++++++++++-------- .../config_file/idiomatic_version/mod.rs | 18 +++++++--------- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 50e7946538..8910d61326 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -937,15 +937,18 @@ pub trait Backend: Debug + Send + Sync { } /// Parses an idiomatic version file to extract the version. - /// - /// - Return `Ok(versions)` with extracted version strings (may be empty if - /// the file was parsed successfully but contained no versions). - /// - Return `Err` to signal that this backend has no custom parser for the - /// file, which causes the caller to fall back to raw text parsing. - /// - /// Backends with custom parsing (e.g. node, vfox) should override this. - async fn parse_idiomatic_file(&self, _path: &Path) -> eyre::Result> { - eyre::bail!("no custom parser") + /// Default implementation reads the file and treats each whitespace-separated token as a version. + /// Override to provide format-specific parsing; return `Err` on real failures so the plugin is skipped. + async fn parse_idiomatic_file(&self, path: &Path) -> eyre::Result> { + let contents = file::read_to_string(path)?; + let trimmed = contents.trim(); + if trimmed.is_empty() { + return Ok(vec![]); + } + Ok(trimmed + .split_whitespace() + .map(|s| s.to_string()) + .collect()) } fn plugin(&self) -> Option<&PluginEnum> { diff --git a/src/config/config_file/idiomatic_version/mod.rs b/src/config/config_file/idiomatic_version/mod.rs index a82ba0fc45..901e55870a 100644 --- a/src/config/config_file/idiomatic_version/mod.rs +++ b/src/config/config_file/idiomatic_version/mod.rs @@ -48,19 +48,15 @@ impl IdiomaticVersionFile { let versions = match plugin.parse_idiomatic_file(&path).await { Ok(versions) => versions, - Err(_) => { - // Plugin has no custom parser or could not parse this file; - // fall back to splitting raw file text by whitespace. - let body = crate::file::read_to_string(&path).unwrap_or_default(); - let body = body.trim(); - if body.is_empty() { - continue; - } - body.split_whitespace().map(|s| s.to_string()).collect() + Err(e) => { + trace!("skipping {} for {}: {}", path.display(), plugin.id(), e); + continue; } }; - for v in versions { - add_version(&mut tools, &plugin, &v)?; + if !versions.is_empty() { + for v in versions { + add_version(&mut tools, &plugin, &v)?; + } } } From 7ce45c885a1e33994e042ba2d72955cfd30675ed Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 01:55:06 +0000 Subject: [PATCH 20/23] [autofix.ci] apply automated fixes --- src/backend/mod.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 8910d61326..6d6ec4f82e 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -945,10 +945,7 @@ pub trait Backend: Debug + Send + Sync { if trimmed.is_empty() { return Ok(vec![]); } - Ok(trimmed - .split_whitespace() - .map(|s| s.to_string()) - .collect()) + Ok(trimmed.split_whitespace().map(|s| s.to_string()).collect()) } fn plugin(&self) -> Option<&PluginEnum> { From 0871d67bb7474e21f67270c7a69fd57dd252afe2 Mon Sep 17 00:00:00 2001 From: Risu <79110363+risu729@users.noreply.github.com> Date: Sat, 28 Feb 2026 04:18:53 +1100 Subject: [PATCH 21/23] refactor: move package.json parse into backend/mod.rs --- docs/configuration.md | 1 + src/backend/asdf.rs | 2 +- src/backend/mod.rs | 17 +++++++++++- src/backend/vfox.rs | 2 +- .../config_file/idiomatic_version/mod.rs | 27 +++++-------------- src/plugins/core/dotnet.rs | 6 ++--- src/plugins/core/java.rs | 2 +- src/plugins/core/node.rs | 16 +---------- src/plugins/core/ruby.rs | 2 +- src/plugins/core/ruby_windows.rs | 2 +- src/plugins/core/rust.rs | 2 +- 11 files changed, 33 insertions(+), 46 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 97d407cf76..c9de82e999 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -420,6 +420,7 @@ in mise and nvm. Here are some of the supported idiomatic version files: | bun | `.bun-version`, `package.json` | | crystal | `.crystal-version` | | deno | `.deno-version`, `package.json` | +| dotnet | `global.json` | | elixir | `.exenv-version` | | go | `.go-version` | | java | `.java-version`, `.sdkmanrc` | diff --git a/src/backend/asdf.rs b/src/backend/asdf.rs index 3d81b2ea40..6f22d907e5 100644 --- a/src/backend/asdf.rs +++ b/src/backend/asdf.rs @@ -326,7 +326,7 @@ impl Backend for AsdfBackend { .cloned() } - async fn parse_idiomatic_file(&self, idiomatic_file: &Path) -> Result> { + async fn _parse_idiomatic_file(&self, idiomatic_file: &Path) -> Result> { if let Some(cached) = self.fetch_cached_idiomatic_file(idiomatic_file)? { return Ok(cached.split_whitespace().map(|s| s.to_string()).collect()); } diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 6d6ec4f82e..70cde21753 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -937,9 +937,24 @@ pub trait Backend: Debug + Send + Sync { } /// Parses an idiomatic version file to extract the version. + /// + /// This handles special files like `package.json` which are parsed natively to avoid + /// every backend needing to implement `package.json` support. For other files, it + /// delegates to `_parse_idiomatic_file`. + async fn parse_idiomatic_file(&self, path: &Path) -> eyre::Result> { + if path.file_name().is_some_and(|f| f == "package.json") { + return crate::config::config_file::idiomatic_version::package_json::parse( + path, + self.id(), + ); + } + self._parse_idiomatic_file(path).await + } + + /// Backend-specific implementation for `parse_idiomatic_file`. /// Default implementation reads the file and treats each whitespace-separated token as a version. /// Override to provide format-specific parsing; return `Err` on real failures so the plugin is skipped. - async fn parse_idiomatic_file(&self, path: &Path) -> eyre::Result> { + async fn _parse_idiomatic_file(&self, path: &Path) -> eyre::Result> { let contents = file::read_to_string(path)?; let trimmed = contents.trim(); if trimmed.is_empty() { diff --git a/src/backend/vfox.rs b/src/backend/vfox.rs index 1adc68d162..de3c091721 100644 --- a/src/backend/vfox.rs +++ b/src/backend/vfox.rs @@ -178,7 +178,7 @@ impl Backend for VfoxBackend { Ok(metadata.legacy_filenames) } - async fn parse_idiomatic_file(&self, path: &Path) -> eyre::Result> { + async fn _parse_idiomatic_file(&self, path: &Path) -> eyre::Result> { let (vfox, _log_rx) = self.plugin.vfox(); let response = vfox.parse_legacy_file(&self.pathname, path).await?; if let Some(version) = response.version { diff --git a/src/config/config_file/idiomatic_version/mod.rs b/src/config/config_file/idiomatic_version/mod.rs index 901e55870a..1dcce1e8bb 100644 --- a/src/config/config_file/idiomatic_version/mod.rs +++ b/src/config/config_file/idiomatic_version/mod.rs @@ -30,33 +30,18 @@ impl IdiomaticVersionFile { let source = ToolSource::IdiomaticVersionFile(path.clone()); let mut tools = ToolRequestSet::new(); - let add_version = - |tools: &mut ToolRequestSet, plugin: &Arc, version: &str| -> Result<()> { - let tr = ToolRequest::new(plugin.ba().clone(), version, source.clone())?; - tools.add_version(tr, &source); - Ok(()) - }; - for plugin in plugins { - if path.file_name().is_some_and(|f| f == "package.json") { - let versions = package_json::parse(&path, plugin.id())?; - for v in versions { - add_version(&mut tools, &plugin, &v)?; + match plugin.parse_idiomatic_file(&path).await { + Ok(versions) => { + for v in versions { + let tr = ToolRequest::new(plugin.ba().clone(), &v, source.clone())?; + tools.add_version(tr, &source); + } } - continue; - } - - let versions = match plugin.parse_idiomatic_file(&path).await { - Ok(versions) => versions, Err(e) => { trace!("skipping {} for {}: {}", path.display(), plugin.id(), e); continue; } - }; - if !versions.is_empty() { - for v in versions { - add_version(&mut tools, &plugin, &v)?; - } } } diff --git a/src/plugins/core/dotnet.rs b/src/plugins/core/dotnet.rs index bfade63849..6fe292b755 100644 --- a/src/plugins/core/dotnet.rs +++ b/src/plugins/core/dotnet.rs @@ -95,17 +95,17 @@ impl Backend for DotnetPlugin { .collect()) } - async fn idiomatic_filenames(&self) -> Result> { + async fn _idiomatic_filenames(&self) -> Result> { Ok(vec!["global.json".into()]) } - async fn parse_idiomatic_file(&self, path: &Path) -> Result { + async fn _parse_idiomatic_file(&self, path: &Path) -> Result> { let content = file::read_to_string(path)?; let global_json: GlobalJson = serde_json::from_str(&content)?; let sdk = global_json .sdk .ok_or_else(|| eyre::eyre!("no sdk.version found in {}", path.display()))?; - Ok(sdk.version) + Ok(vec![sdk.version]) } async fn install_version_(&self, ctx: &InstallContext, tv: ToolVersion) -> Result { diff --git a/src/plugins/core/java.rs b/src/plugins/core/java.rs index 148e0b370b..04151dfba6 100644 --- a/src/plugins/core/java.rs +++ b/src/plugins/core/java.rs @@ -391,7 +391,7 @@ impl Backend for JavaPlugin { Ok(vec![".java-version".into(), ".sdkmanrc".into()]) } - async fn parse_idiomatic_file(&self, path: &Path) -> Result> { + async fn _parse_idiomatic_file(&self, path: &Path) -> Result> { let contents = file::read_to_string(path)?; if path.file_name() == Some(".sdkmanrc".as_ref()) { let version = contents diff --git a/src/plugins/core/node.rs b/src/plugins/core/node.rs index c6d05df26e..2e3b4ed313 100644 --- a/src/plugins/core/node.rs +++ b/src/plugins/core/node.rs @@ -570,8 +570,7 @@ impl Backend for NodePlugin { ]) } - async fn parse_idiomatic_file(&self, path: &Path) -> Result> { -<<<<<<< HEAD + async fn _parse_idiomatic_file(&self, path: &Path) -> Result> { let contents = file::read_to_string(path)?; let body = normalize_idiomatic_contents(&contents); @@ -584,19 +583,6 @@ impl Backend for NodePlugin { }) .collect(); Ok(versions) -======= - let body = file::read_to_string(path)?; - // strip comments - let body = body.split('#').next().unwrap_or_default().trim(); - // trim "v" prefix - let body = body.strip_prefix('v').unwrap_or(body); - // replace lts/* with lts - let body = body.replace("lts/*", "lts"); - if body.is_empty() { - return Ok(vec![]); - } - Ok(vec![body]) ->>>>>>> 3ed60bf99 (fix: compile error) } async fn install_version_( diff --git a/src/plugins/core/ruby.rs b/src/plugins/core/ruby.rs index 4a52f9e22a..5797015419 100644 --- a/src/plugins/core/ruby.rs +++ b/src/plugins/core/ruby.rs @@ -778,7 +778,7 @@ impl Backend for RubyPlugin { Ok(vec![".ruby-version".into(), "Gemfile".into()]) } - async fn parse_idiomatic_file(&self, path: &Path) -> Result> { + async fn _parse_idiomatic_file(&self, path: &Path) -> Result> { let v = match path.file_name() { Some(name) if name == "Gemfile" => parse_gemfile(&file::read_to_string(path)?), _ => { diff --git a/src/plugins/core/ruby_windows.rs b/src/plugins/core/ruby_windows.rs index ae6f6ea05d..c2f0158ac7 100644 --- a/src/plugins/core/ruby_windows.rs +++ b/src/plugins/core/ruby_windows.rs @@ -182,7 +182,7 @@ impl Backend for RubyPlugin { Ok(vec![".ruby-version".into(), "Gemfile".into()]) } - async fn parse_idiomatic_file(&self, path: &Path) -> Result> { + async fn _parse_idiomatic_file(&self, path: &Path) -> Result> { let v = match path.file_name() { Some(name) if name == "Gemfile" => parse_gemfile(&file::read_to_string(path)?), _ => { diff --git a/src/plugins/core/rust.rs b/src/plugins/core/rust.rs index 79ba732173..6b37266aab 100644 --- a/src/plugins/core/rust.rs +++ b/src/plugins/core/rust.rs @@ -120,7 +120,7 @@ impl Backend for RustPlugin { Ok(vec!["rust-toolchain.toml".into()]) } - async fn parse_idiomatic_file(&self, path: &Path) -> Result> { + async fn _parse_idiomatic_file(&self, path: &Path) -> Result> { let rt = parse_idiomatic_file(path)?; Ok(vec![rt.channel]) } From 2baa1f6ac5c3dfa9edda9545379239fbb44c4a3b Mon Sep 17 00:00:00 2001 From: Risu <79110363+risu729@users.noreply.github.com> Date: Sun, 1 Mar 2026 03:41:06 +1100 Subject: [PATCH 22/23] fix: ignore empty strings in idiomatic version files --- src/plugins/core/dotnet.rs | 3 +++ src/plugins/core/python.rs | 12 ++++++++++++ src/plugins/core/ruby.rs | 3 +++ src/plugins/core/ruby_windows.rs | 3 +++ src/plugins/core/rust.rs | 3 +++ 5 files changed, 24 insertions(+) diff --git a/src/plugins/core/dotnet.rs b/src/plugins/core/dotnet.rs index 6fe292b755..e5e8b183ab 100644 --- a/src/plugins/core/dotnet.rs +++ b/src/plugins/core/dotnet.rs @@ -105,6 +105,9 @@ impl Backend for DotnetPlugin { let sdk = global_json .sdk .ok_or_else(|| eyre::eyre!("no sdk.version found in {}", path.display()))?; + if sdk.version.is_empty() { + return Ok(vec![]); + } Ok(vec![sdk.version]) } diff --git a/src/plugins/core/python.rs b/src/plugins/core/python.rs index 20033be8e1..d9951616d3 100644 --- a/src/plugins/core/python.rs +++ b/src/plugins/core/python.rs @@ -579,6 +579,18 @@ impl Backend for PythonPlugin { ]) } + async fn _parse_idiomatic_file(&self, path: &Path) -> eyre::Result> { + let contents = file::read_to_string(path)?; + let normalized = crate::backend::normalize_idiomatic_contents(&contents); + if normalized.is_empty() { + return Ok(vec![]); + } + Ok(normalized + .split_whitespace() + .map(|s| s.to_string()) + .collect()) + } + async fn install_version_(&self, ctx: &InstallContext, tv: ToolVersion) -> Result { if cfg!(windows) || Settings::get().python.compile != Some(true) { self.install_precompiled(ctx, &tv).await?; diff --git a/src/plugins/core/ruby.rs b/src/plugins/core/ruby.rs index 5797015419..31057924fc 100644 --- a/src/plugins/core/ruby.rs +++ b/src/plugins/core/ruby.rs @@ -790,6 +790,9 @@ impl Backend for RubyPlugin { .to_string() } }; + if v.is_empty() { + return Ok(vec![]); + } Ok(vec![v]) } diff --git a/src/plugins/core/ruby_windows.rs b/src/plugins/core/ruby_windows.rs index c2f0158ac7..967d528d71 100644 --- a/src/plugins/core/ruby_windows.rs +++ b/src/plugins/core/ruby_windows.rs @@ -194,6 +194,9 @@ impl Backend for RubyPlugin { .to_string() } }; + if v.is_empty() { + return Ok(vec![]); + } Ok(vec![v]) } diff --git a/src/plugins/core/rust.rs b/src/plugins/core/rust.rs index 6b37266aab..77d92f4c81 100644 --- a/src/plugins/core/rust.rs +++ b/src/plugins/core/rust.rs @@ -122,6 +122,9 @@ impl Backend for RustPlugin { async fn _parse_idiomatic_file(&self, path: &Path) -> Result> { let rt = parse_idiomatic_file(path)?; + if rt.channel.is_empty() { + return Ok(vec![]); + } Ok(vec![rt.channel]) } From e37b8a1df44fca30eb840a4fd131327f2c034b1b Mon Sep 17 00:00:00 2001 From: Risu <79110363+risu729@users.noreply.github.com> Date: Mon, 2 Mar 2026 19:50:34 +1100 Subject: [PATCH 23/23] fix(idiomatic): restore normalize_idiomatic_contents to default idiomatic file parser --- src/backend/mod.rs | 9 ++++++--- src/plugins/core/python.rs | 12 ------------ 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 70cde21753..378ae8d9fc 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -956,11 +956,14 @@ pub trait Backend: Debug + Send + Sync { /// Override to provide format-specific parsing; return `Err` on real failures so the plugin is skipped. async fn _parse_idiomatic_file(&self, path: &Path) -> eyre::Result> { let contents = file::read_to_string(path)?; - let trimmed = contents.trim(); - if trimmed.is_empty() { + let normalized = normalize_idiomatic_contents(&contents); + if normalized.is_empty() { return Ok(vec![]); } - Ok(trimmed.split_whitespace().map(|s| s.to_string()).collect()) + Ok(normalized + .split_whitespace() + .map(|s| s.to_string()) + .collect()) } fn plugin(&self) -> Option<&PluginEnum> { diff --git a/src/plugins/core/python.rs b/src/plugins/core/python.rs index d9951616d3..20033be8e1 100644 --- a/src/plugins/core/python.rs +++ b/src/plugins/core/python.rs @@ -579,18 +579,6 @@ impl Backend for PythonPlugin { ]) } - async fn _parse_idiomatic_file(&self, path: &Path) -> eyre::Result> { - let contents = file::read_to_string(path)?; - let normalized = crate::backend::normalize_idiomatic_contents(&contents); - if normalized.is_empty() { - return Ok(vec![]); - } - Ok(normalized - .split_whitespace() - .map(|s| s.to_string()) - .collect()) - } - async fn install_version_(&self, ctx: &InstallContext, tv: ToolVersion) -> Result { if cfg!(windows) || Settings::get().python.compile != Some(true) { self.install_precompiled(ctx, &tv).await?;