Skip to content

fix(idiomatic): use generic parser for idiomatic files#8171

Merged
jdx merged 23 commits into
jdx:mainfrom
risu729:fix-idiomatic
Mar 2, 2026
Merged

fix(idiomatic): use generic parser for idiomatic files#8171
jdx merged 23 commits into
jdx:mainfrom
risu729:fix-idiomatic

Conversation

@risu729

@risu729 risu729 commented Feb 16, 2026

Copy link
Copy Markdown
Contributor

This PR fixes #7379 (comment), 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 package.json 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. package.json (built-in parser)
  2. Plugin-specific parsing (parse_idiomatic_file)
  3. Raw text file (built-in fallback)

Note: Plugins cannot override the parsing logic for package.json if it is handled by the built-in parser. This means specific tools cannot have custom package.json parsing logic that differs from the core implementation (e.g., ignoring packageManager field but respecting devEngines for a specific tool is not possible via plugin override).

Copilot AI review requested due to automatic review settings February 16, 2026 06:07
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello @risu729, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request refactors the handling of idiomatic version files to introduce a more robust and centralized parsing mechanism. The primary goal is to ensure consistent and correct interpretation of version specifications, particularly from package.json files, across different tools and plugins. By establishing a clear parsing hierarchy and moving common parsing logic to a built-in component, the change prevents unexpected behavior and improves the overall reliability of version detection.

Highlights

  • Generic Idiomatic File Parsing: Introduced a centralized, built-in parser for common idiomatic files like package.json and raw text files, addressing issues where some plugins did not correctly handle package.json.
  • Parsing Precedence Defined: Established a clear precedence order for idiomatic file parsing: built-in package.json parser, then plugin-specific parsing, and finally a built-in raw text file fallback. Plugins can no longer override package.json parsing logic.
  • Backend Trait Refactoring: The Backend trait was updated to separate backend-specific idiomatic filenames (_idiomatic_filenames) from the combined list (which now includes registry data). The parse_idiomatic_file method now returns a Vec<String> to support multiple versions.
  • Documentation Updates: The docs/configuration.md file was updated to reflect the expanded support for package.json across various plugins (bun, deno, node, npm, pnpm, yarn).
  • Code Structure Reorganization: The package_json parsing logic was moved into a dedicated module (src/config/config_file/idiomatic_version/package_json.rs) and integrated into the new generic parsing flow.
Changelog
  • docs/configuration.md
    • Updated the idiomatic files table to include package.json for Bun, Deno, Node, NPM, PNPM, and Yarn plugins.
  • src/backend/asdf.rs
    • Renamed idiomatic_filenames to _idiomatic_filenames to align with the new trait structure.
    • Modified parse_idiomatic_file to return Result<Vec<String>> instead of Result<String> and adjusted its logic to handle multiple versions by splitting whitespace.
  • src/backend/mod.rs
    • Refactored idiomatic_filenames trait method to combine backend-specific filenames from _idiomatic_filenames with those from the registry.
    • Introduced a new _idiomatic_filenames trait method for backend-specific idiomatic file declarations.
    • Updated parse_idiomatic_file trait method to return Result<Vec<String>> and removed its direct package.json and raw text parsing logic, delegating it to the generic parser.
  • src/backend/vfox.rs
    • Renamed idiomatic_filenames to _idiomatic_filenames.
    • Modified parse_idiomatic_file to return Result<Vec<String>> and adjusted error handling to return an empty vector if no version is found.
  • src/config/config_file/idiomatic_version/mod.rs
    • Renamed file from src/config/config_file/idiomatic_version.rs to src/config/config_file/idiomatic_version/mod.rs.
    • Added package_json module import.
    • Implemented the new parsing precedence logic within IdiomaticVersionFile::parse, prioritizing package.json parsing, then plugin-specific parsing, and finally raw text file parsing.
    • Added logic to filter tools based on the idiomatic_version_file_enable_tools setting before checking for idiomatic filenames.
  • src/config/config_file/idiomatic_version/package_json.rs
    • Renamed file from src/package_json.rs to src/config/config_file/idiomatic_version/package_json.rs.
    • Refactored PackageJson struct to PackageJsonData and made its methods private.
    • Introduced a new public parse function that handles extracting versions from package.json for specific tools (node, deno, bun, npm, yarn, pnpm).
    • Added new test cases for package.json parsing logic, including bun-specific behavior and ignoring the engines field.
  • src/config/config_file/mod.rs
    • Updated filename_is_idiomatic to use the refactored idiomatic_filenames trait method, simplifying error handling.
  • src/config/mod.rs
    • Renamed load_idiomatic_files to load_idiomatic_filenames and updated its call site.
    • Enabled idiomatic file loading in load_config_files_from_paths for monorepo task contexts.
  • src/main.rs
    • Removed the direct import of the package_json module, as it was moved and refactored.
  • src/plugins/core/bun.rs
    • Renamed idiomatic_filenames to _idiomatic_filenames.
    • Removed package.json parsing logic from parse_idiomatic_file, as it is now handled generically.
  • src/plugins/core/deno.rs
    • Renamed idiomatic_filenames to _idiomatic_filenames.
    • Removed package.json parsing logic from parse_idiomatic_file, as it is now handled generically.
  • src/plugins/core/elixir.rs
    • Renamed idiomatic_filenames to _idiomatic_filenames.
  • src/plugins/core/go.rs
    • Renamed idiomatic_filenames to _idiomatic_filenames.
  • src/plugins/core/java.rs
    • Renamed idiomatic_filenames to _idiomatic_filenames.
    • Modified parse_idiomatic_file to return Result<Vec<String>> and adjusted its return values to wrap single versions in a vector.
  • src/plugins/core/node.rs
    • Renamed idiomatic_filenames to _idiomatic_filenames.
    • Removed package.json parsing logic from parse_idiomatic_file, as it is now handled generically.
    • Modified parse_idiomatic_file to return Result<Vec<String>> and adjusted its return value to wrap the parsed version in a vector.
  • src/plugins/core/python.rs
    • Renamed idiomatic_filenames to _idiomatic_filenames.
  • src/plugins/core/ruby.rs
    • Renamed idiomatic_filenames to _idiomatic_filenames.
    • Modified parse_idiomatic_file to return Result<Vec<String>> and adjusted its return value to wrap the parsed version in a vector.
  • src/plugins/core/ruby_windows.rs
    • Renamed idiomatic_filenames to _idiomatic_filenames.
    • Modified parse_idiomatic_file to return Result<Vec<String>> and adjusted its return value to wrap the parsed version in a vector.
  • src/plugins/core/rust.rs
    • Renamed idiomatic_filenames to _idiomatic_filenames.
    • Modified parse_idiomatic_file to return Result<Vec<String>> and adjusted its return value to wrap the parsed channel in a vector.
  • src/plugins/core/swift.rs
    • Renamed idiomatic_filenames to _idiomatic_filenames.
  • src/plugins/core/zig.rs
    • Renamed idiomatic_filenames to _idiomatic_filenames.
Activity
  • No specific activity (comments, reviews, progress updates) has been recorded for this pull request yet.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the parsing of idiomatic version files to use a more generic approach, with special handling for package.json and a fallback to raw text parsing. The changes are well-structured and consistently applied across the codebase. I've identified a few areas for improvement related to performance and code clarity. Specifically, I've suggested refactoring the parsing logic to avoid redundant file reads and parsing, and a small change to make some of the new code more idiomatic.

Comment thread src/config/config_file/idiomatic_version/package_json.rs Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors idiomatic file parsing to address an issue where vfox and other plugins that don't support package.json parsing experienced unexpected behavior. The change creates a generic parsing system with a defined precedence order: package.json (built-in), plugin-specific parsing, and raw text file fallback.

Changes:

  • Centralized package.json parsing logic into a dedicated module
  • Changed parse_idiomatic_file to return Vec<String> instead of String to support multiple versions
  • Renamed idiomatic_filenames to _idiomatic_filenames in backend implementations to distinguish backend-specific filenames from the combined list

Reviewed changes

Copilot reviewed 21 out of 21 changed files in this pull request and generated no comments.

Show a summary per file
File Description
src/backend/mod.rs Implements new _idiomatic_filenames and refactored parse_idiomatic_file signature
src/backend/asdf.rs Updates to return Vec<String> from parse_idiomatic_file and renames idiomatic_filenames
src/backend/vfox.rs Updates to return Vec<String> from parse_idiomatic_file and renames idiomatic_filenames
src/config/config_file/idiomatic_version.rs Implements parsing precedence logic with built-in package.json parser
src/config/config_file/mod.rs Simplifies idiomatic filename check logic
src/config/mod.rs Renames function to load_idiomatic_filenames and enables idiomatic file support in config hierarchy loading
src/package_json.rs Moves from private module to public module with new parse function that centralizes tool-specific logic
src/plugins/core/*.rs Updates all core plugins to use new _idiomatic_filenames and parse_idiomatic_file signatures
src/main.rs Removes package_json module declaration as it's now in idiomatic_version module
docs/configuration.md Updates documentation with newly supported idiomatic files for various tools

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@risu729 risu729 marked this pull request as draft February 16, 2026 17:08
@risu729 risu729 marked this pull request as ready for review February 17, 2026 11:50

@jdx jdx left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review (AI-generated)

The core idea of centralizing idiomatic file parsing (especially package.json) is solid — nice deduplication across bun, deno, and node backends.

Issues

1. Error propagation change — behavioral regression

The old code logged a trace when parse_idiomatic_file failed and continued to the next plugin. The new code propagates with ?:

let versions = plugin.parse_idiomatic_file(&path).await?;

This means a plugin returning an error will now abort parsing for all remaining plugins for that file, instead of skipping the failed one and trying others. For example, if a vfox plugin fails to parse .node-version, the core node plugin won't get a chance.

2. from_file now filters by enable_tools — duplicating existing filtering

The new enable_tools check in from_file:

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;
    }

This filtering already happens upstream in load_all_config_files. If from_file is also called from other paths this could unexpectedly restrict behavior. If it's intentional for safety, a comment would help.

3. Hardcoded tool name matching in package_json::parse

match tool_name {
    "node" | "deno" => pkg.runtime_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,
};

If someone adds a new tool that uses package.json, they need to modify this match arm too. A comment explaining this is intentional would help.

4. Node parser may return empty string version

In node.rs, if the file is empty or only a comment, body will be "" and it returns vec![""]. The outer code only checks !versions.is_empty(), so an empty string passes through as a "version". The old code masked this via split_whitespace() in the caller. Now with the new pipeline, the raw text fallback is only used when the plugin returns an empty vec, but the node plugin always returns a vec with one element.

5. Duplicate filenames possible

idiomatic_filenames() merges _idiomatic_filenames() with registry entries:

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()));
}

If a backend already returns "package.json" from _idiomatic_filenames() and the registry also has it, the vec will contain duplicates. A .dedup() or BTreeSet would be cleaner.

@risu729

risu729 commented Feb 17, 2026

Copy link
Copy Markdown
Contributor Author

Thanks! Fixed the reviewed points.

@jdx jdx left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

This review was generated by AI (Claude).

Good fix for the core issue — centralizing package.json parsing so vfox plugins don't need to implement it themselves is the right approach. A few things to address:

Bug: raw text fallback triggers when parse_idiomatic_file returns Ok(vec![])

The new fallback logic in idiomatic_version/mod.rs reads the raw file content whenever parse_idiomatic_file returns an empty vec. But backends that implement a custom parser (like node) also return Ok(vec![]) to signal "no version found" — not "use fallback".

Concrete regression: a .nvmrc containing only a comment like # lts will now produce broken versions:

  • Node's parse_idiomatic_file strips comments → returns Ok(vec![])
  • Fallback reads raw text # lts and calls add_version with "#" and "lts" as version strings

vfox behavior change: previously vfox::parse_idiomatic_file returned Err when no version was found (triggering continue/skip). Now it returns Ok(vec![]), which triggers the raw text fallback — the opposite of the intended fix.

The cleanest fix is to move the raw text fallback into the default parse_idiomatic_file implementation (as it was before), and remove it from IdiomaticVersionFile::parse:

// In Backend trait default impl
async fn parse_idiomatic_file(&self, path: &Path) -> eyre::Result<Vec<String>> {
    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())
}

Then IdiomaticVersionFile::parse just uses whatever the backend returns without a separate fallback. Backends with custom parsing override and never fall through.

Lost debug logging in config_file/mod.rs

// Before
Err(e) => {
    debug!("idiomatic_filenames failed for {}: {:?}", b, e);
}

// After (errors silently dropped)
if let Ok(filenames) = b.idiomatic_filenames().await
    && filenames.contains(&file_name)

Worth keeping the debug log. Could be written as:

match b.idiomatic_filenames().await {
    Ok(filenames) if filenames.contains(&file_name) => return true,
    Err(e) => debug!("idiomatic_filenames failed for {}: {:?}", b, e),
    _ => {}
}

Minor: typo in package_json.rs

// "pacakge" → "package"
// We ignore unknown tools in pacakge.json

The overall structure and the package.json centralization are solid. The test for MockBackend is a nice addition. The bonus fix for load_config_files_from_paths (was a TODO) is appreciated.

@risu729

risu729 commented Feb 18, 2026

Copy link
Copy Markdown
Contributor Author

Sorry, I misunderstood the code flow, fixed now.

@jdx jdx left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review (AI-generated)

The core idea of centralizing package.json parsing so vfox/asdf plugins don't need to handle it themselves is solid — nice deduplication across bun, deno, and node backends.

Issues

1. Error-to-fallback semantic change

In idiomatic_version/mod.rs, when parse_idiomatic_file returns Err, the new code falls back to raw text parsing. The old code just skipped the plugin:

// Old behavior
Err(e) => {
    trace!("skipping {} for {}: {e}", plugin.id(), path.display());
    continue;
}

// New behavior  
Err(_) => {
    // Falls through to raw text parsing
    let body = crate::file::read_to_string(&path).unwrap_or_default();
    ...
}

The default trait impl eyre::bail!("no custom parser") intentionally triggers this fallback, which is fine. But real plugin errors (e.g., vfox network failure, malformed plugin) will also hit it, producing garbage versions from raw text instead of being skipped.

The cleanest fix is to move the raw text fallback into the default parse_idiomatic_file impl:

async fn parse_idiomatic_file(&self, path: &Path) -> eyre::Result<Vec<String>> {
    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())
}

Then IdiomaticVersionFile::parse just uses whatever the backend returns without a separate fallback, and backends with custom parsers override normally.

2. Hardcoded tool list in package_json::parse

The match on tool names means adding a new tool that uses package.json requires updating this central match arm — easy to miss:

match tool_name {
    "node" | "deno" => pkg.runtime_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,
};

This should be dispatched to the backends. One approach — add a trait method declaring the extraction strategy:

enum PackageJsonField {
    Runtime,
    PackageManager,
    RuntimeThenPackageManager,
}

// Backend trait — default: no package.json support
fn package_json_field(&self) -> Option<PackageJsonField> {
    None
}

Each backend declares its own strategy (node/deno → Runtime, bun → RuntimeThenPackageManager, npm/yarn/pnpm → PackageManager), and the central parser dispatches on the enum instead of tool name strings. New tools just implement package_json_field().

3. Lost trace logging on parse failures

The old code had trace!("skipping {} for {}: {e}", ...) when a plugin failed to parse. The new fallback path is silent. Worth adding a trace log.

Minor

  • The load_config_files_from_paths change (removing the TODO, now loading idiomatic filenames for monorepo contexts) is a nice bonus fix but is a behavioral change worth calling out in the PR description.

What looks good

  • Vec<String> return type correctly models multiple versions
  • New tests (MockBackend, package_json parse, engines-ignored) are solid
  • unique() call in idiomatic_filenames() handles dedup
  • Docs table update is thorough
  • config_file/mod.rs match simplification retains debug logging

@risu729

risu729 commented Feb 18, 2026

Copy link
Copy Markdown
Contributor Author

What do you think of adding npm, yarn, and pnpm as core tools?
For now, only package.json needs this kind of special parser, so I don't think we don't need this fallback feature if we add them to core.

I'm happy with continuing this PR, but I just want to confirm before.

@jdx

jdx commented Feb 18, 2026

Copy link
Copy Markdown
Owner

ah yeah I forgot they weren't core tools, I guess we can just keep it hardcoded then

@risu729

risu729 commented Feb 18, 2026

Copy link
Copy Markdown
Contributor Author
  1. Error-to-fallback semantic change

It was intended. I intended to support adding idiomatic files like .<tool>-version in the registry, even if vfox plugins have a complicated parser for the tool-specific config or something.

However, the review is correct, so what if we only accept version-like strings, maybe checked by regex?
Or should I remove this fallback completely? I don't have any specific tools need this feature in mind, so this is fine too.

@jdx

jdx commented Feb 21, 2026

Copy link
Copy Markdown
Owner

The error-to-fallback change is a regression. Previously when parse_idiomatic_file returned Err, the plugin was skipped and the next one got a chance:

Err(e) => {
    trace!("skipping {} for {}: {e}", plugin.id(), path.display());
    continue;
}

Now Err triggers the raw text fallback — reads the file and splits on whitespace, treating every token as a version. So a real plugin error (vfox network failure, malformed response, etc.) produces garbage versions instead of being skipped.

The fix suggested in the reviews is the right one: move the raw text fallback into the default parse_idiomatic_file impl on the trait, and have the caller just use whatever the backend returns without a separate fallback path. That way backends without a custom parser get the raw-text behavior automatically, backends with custom parsers override normally, and real errors propagate/skip correctly.

@risu729

risu729 commented Feb 23, 2026

Copy link
Copy Markdown
Contributor Author

Oh, that's true. Sorry I didn't realize that.

@jdx jdx left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Note: This review was AI-generated using Claude Code.

Summary

Centralizes package.json parsing so all tools (including vfox plugins) get consistent behavior. Previously bun, deno, and node each duplicated their own package.json handling, and vfox backends couldn't parse it at all. Changes parse_idiomatic_file to return Vec<String>, moves package.json parsing into a dedicated module, and introduces _idiomatic_filenames() as the backend-specific override with idiomatic_filenames() merging backend + registry results.

Issues

package.json special-casing is in the wrong layer (medium)

In IdiomaticVersionFile::parse(), there's a hardcoded if path.file_name() == "package.json" that bypasses plugin parsing entirely. The old code had this check in the Backend trait's default parse_idiomatic_file — that was a better location because it meant any caller of parse_idiomatic_file got correct package.json handling automatically. With the new code, if anything calls backend.parse_idiomatic_file() directly with a package.json path, it falls through to the raw text parser instead of the JSON parser.

Suggest moving the package.json check back into the default parse_idiomatic_file in Backend.

Positive aspects

  • Good deduplication — package.json parsing was copied in 3+ places
  • Clean test coverage with MockBackend and package_json unit tests
  • Vec<String> return type is more correct since files like .python-versions can list multiple versions
  • Documentation table update is thorough

@risu729

risu729 commented Feb 27, 2026

Copy link
Copy Markdown
Contributor Author

Sorry for taking long. I fixed the reviewed issue.

@jdx

jdx commented Feb 28, 2026

Copy link
Copy Markdown
Owner

I think this is probably a real issue:

In ruby.rs and ruby_windows.rs, when parse_gemfile returns an empty string (no match), this code wraps it:
Ok(vec![v]) // v could be ""
This would create a ToolRequest with an empty version string. The old code had the same issue (Ok(v) with empty string), but the
new split_whitespace in the default implementation would have filtered it out. Since ruby overrides _parse_idiomatic_file, it
bypasses that filtering. Consider adding if v.is_empty() { return Ok(vec![]); }.

Same issue in java.rs line Ok(vec![contents.trim().to_string()]) — if contents is empty after trimming.

@risu729

risu729 commented Feb 28, 2026

Copy link
Copy Markdown
Contributor Author

Sorry, that's so true. I fixed the bug

@jdx

jdx commented Mar 2, 2026

Copy link
Copy Markdown
Owner

sorry @risu729 this needs a rebase

risu729 and others added 6 commits March 2, 2026 15:33
This PR fixes jdx#7379 (comment), 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).
@risu729

risu729 commented Mar 2, 2026

Copy link
Copy Markdown
Contributor Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request is a solid refactoring that genericizes and centralizes the parsing of idiomatic version files. The introduction of a new pattern for backend traits with public and overridable methods, along with special handling for package.json, is a significant improvement. I've identified a couple of potential regressions: the default implementation for parsing idiomatic files no longer handles comments, and the Java plugin's parsing might not correctly handle multiple versions on a single line. I've included specific suggestions to address these points. Overall, this is a well-executed refactoring that enhances the codebase's structure and maintainability.

Comment thread src/backend/mod.rs
Comment thread src/plugins/core/java.rs
@risu729 risu729 marked this pull request as draft March 2, 2026 06:31
@risu729

risu729 commented Mar 2, 2026

Copy link
Copy Markdown
Contributor Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a well-designed generic parser for idiomatic version files, which is a great improvement. It successfully centralizes package.json parsing and provides a sensible default for other text-based version files, reducing code duplication across plugins. The use of _ prefixed methods for backend-specific implementations is a clean and effective pattern. I have one suggestion regarding a potential performance optimization to avoid redundant work when identifying and parsing these files.

Comment on lines +51 to +64
pub async fn from_file(path: &Path) -> Result<Self> {
trace!("parsing idiomatic version: {}", path.display());
let file_name = path.file_name().unwrap().to_string_lossy().to_string();
let mut tools: Vec<Arc<dyn Backend>> = vec![];
for b in backend::list().into_iter() {
if b.idiomatic_filenames()
.await
.is_ok_and(|f| f.contains(&file_name))
{
tools.push(b);
}
}
Self::parse(path.to_path_buf(), tools).await
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This function iterates through all backends to find which ones are associated with the given idiomatic file. This is inefficient because a similar iteration already happens in detect_config_file_type (via filename_is_idiomatic) to identify this as an idiomatic version file in the first place.

To avoid this double work, I suggest a refactoring:

  1. Modify filename_is_idiomatic to return the list of matching backend plugins instead of just a boolean.
  2. Pass this list of plugins through detect_config_file_type to config_file::parse.
  3. In config_file::parse, call IdiomaticVersionFile::parse directly with the list of plugins, which would remove the need for this from_file function.

This would centralize the logic and improve performance by iterating through backends only once.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, but I'm gonna work on this in a separate PR.

@risu729 risu729 marked this pull request as ready for review March 2, 2026 09:03
@greptile-apps

greptile-apps Bot commented Mar 2, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR refactors idiomatic file parsing to use a generic parser that handles package.json natively, fixing issues where plugins like vfox don't support package.json parsing.

Key Changes:

  • Created new module structure with idiomatic_version/package_json.rs containing comprehensive parsing logic for devEngines and packageManager fields
  • Split backend trait methods into public wrappers (idiomatic_filenames, parse_idiomatic_file) and overridable private implementations (_idiomatic_filenames, _parse_idiomatic_file)
  • package.json is now parsed by built-in logic before plugin-specific parsers are called, establishing clear precedence: built-in package.json → plugin-specific → raw text fallback
  • Changed return type from String to Vec<String> to support multiple versions per file
  • Updated all core plugins (node, bun, ruby, etc.) and backend implementations (asdf, vfox) to use new architecture
  • Improved error handling: plugins that fail to parse a file are skipped with trace logging rather than failing the entire operation

Architecture Impact:
Plugins can no longer override package.json parsing logic, which standardizes behavior across all tools but removes flexibility for tool-specific customization of package.json handling.

Confidence Score: 5/5

  • This PR is safe to merge - it's a well-structured refactoring with comprehensive test coverage and clear architectural improvements
  • The code is thoroughly tested with extensive unit tests in package_json.rs covering edge cases, includes proper error handling that gracefully skips failing plugins, maintains backward compatibility while fixing the reported issue, and follows the project's coding patterns consistently across all modified files
  • No files require special attention - all changes are well-implemented with appropriate test coverage

Important Files Changed

Filename Overview
src/config/config_file/idiomatic_version/mod.rs New module structure for idiomatic file parsing with proper error handling - plugins that fail to parse are skipped rather than failing the entire operation
src/config/config_file/idiomatic_version/package_json.rs Comprehensive package.json parser with devEngines and packageManager support, includes extensive test coverage and correct semver simplification
src/backend/mod.rs Added backend trait split between public methods (with package.json special handling) and overridable _private methods, properly uses normalize_idiomatic_contents
src/backend/asdf.rs Updated to use _idiomatic_filenames and _parse_idiomatic_file methods, correctly handles cached versions by splitting on whitespace
src/backend/vfox.rs Implemented _idiomatic_filenames to expose metadata.legacy_filenames, returns empty vec instead of error when no version found (more graceful)
src/plugins/core/bun.rs Removed custom parse_idiomatic_file override since package.json is now handled by built-in parser - simpler and correct
src/plugins/core/node.rs Updated to use _parse_idiomatic_file with line-by-line processing to strip 'v' prefix and handle lts/* replacement

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    Start[IdiomaticVersionFile.from_file] --> GetFilename[Get filename from path]
    GetFilename --> FindBackends[Find all backends with this filename in idiomatic_filenames]
    FindBackends --> Loop{For each backend}
    
    Loop --> CallParse[Call backend.parse_idiomatic_file]
    
    CallParse --> IsPackageJson{Is filename == package.json?}
    IsPackageJson -->|Yes| BuiltInParser[Use built-in package_json::parse]
    IsPackageJson -->|No| CallPrivate[Call backend._parse_idiomatic_file]
    
    BuiltInParser --> ParsePkgJson[Parse devEngines & packageManager fields]
    ParsePkgJson --> SimplifySemver[Simplify semver ranges]
    SimplifySemver --> ReturnVersions[Return Vec&lt;String&gt;]
    
    CallPrivate --> HasOverride{Backend has _parse_idiomatic_file override?}
    HasOverride -->|Yes| CustomParser[Use custom parser logic]
    HasOverride -->|No| DefaultParser[Use default: normalize_idiomatic_contents then split_whitespace]
    
    CustomParser --> ReturnVersions
    DefaultParser --> ReturnVersions
    
    ReturnVersions --> CheckError{Returned Error?}
    CheckError -->|Yes| Skip[Skip this backend, log trace]
    CheckError -->|No| AddVersions[Add versions to ToolRequestSet]
    
    AddVersions --> Loop
    Skip --> Loop
    
    Loop -->|Done| Return[Return IdiomaticVersionFile with all collected versions]
Loading

Last reviewed commit: e37b8a1

@jdx jdx merged commit 95ad050 into jdx:main Mar 2, 2026
35 checks passed
@risu729 risu729 deleted the fix-idiomatic branch March 2, 2026 09:56
jdx pushed a commit that referenced this pull request Mar 2, 2026
### 🚀 Features

- **(hooks)** add task references to hooks and watch_files by @jdx in
[#8400](#8400)
- **(prepare)** add git-submodule built-in provider by @jdx in
[#8407](#8407)
- **(prepare)** add human-readable stale reasons to prepare output by
@jdx in [#8408](#8408)
- **(prepare)** add dependency ordering to prepare steps by @jdx in
[#8401](#8401)
- **(prepare)** add --explain flag for provider diagnostics by @jdx in
[#8409](#8409)
- **(prepare)** add per-provider timeout support by @jdx in
[#8405](#8405)
- **(prepare)** add blake3 content-hash freshness checking by @jdx in
[#8404](#8404)
- **(tasks)** monorepo vars and per-task vars by @halms in
[#8248](#8248)

### 🐛 Bug Fixes

- **(aqua)** restore bin_paths disk cache with fresh_file invalidation
by @jdx in [#8398](#8398)
- **(idiomatic)** use generic parser for idiomatic files by @risu729 in
[#8171](#8171)
- **(install)** apply precompiled options to all platforms in lockfile
by @jdx in [#8396](#8396)
- **(install)** normalize "v" prefix when matching lockfile versions by
@jdx in [#8413](#8413)
- **(prepare)** improve git submodule parser and fix check_staleness
error handling by @jdx in [#8412](#8412)
- **(python)** respect precompiled settings in lock file generation by
@jdx in [#8399](#8399)
- **(python)** clarify uv_venv_auto docs + prevent uv shim recursion in
venv creation by @halms in
[#8402](#8402)
- **(task)** remove deprecated `# mise` task header syntax by @jdx in
[#8403](#8403)
- **(vfox)** avoid eager metadata loading during config file detection
by @jdx in [#8397](#8397)
- clarify GitHub attestations to be artifact ones by @scop in
[#8394](#8394)
- ignore comments in idiomatic version files by @iloveitaly in
[#7682](#7682)

### 🚜 Refactor

- unify archive detection by @risu729 in
[#8137](#8137)

### 📚 Documentation

- remove duplicated docs for npm.package_manager by @risu729 in
[#8414](#8414)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants