Skip to content

Improve YAML parse error diagnostics#127

Closed
leynos wants to merge 2 commits intomainfrom
codex/improve-yaml-error-message-clarity
Closed

Improve YAML parse error diagnostics#127
leynos wants to merge 2 commits intomainfrom
codex/improve-yaml-error-message-clarity

Conversation

@leynos
Copy link
Copy Markdown
Owner

@leynos leynos commented Aug 18, 2025

Summary

  • provide line-aware YAML parse errors with suggestions for common mistakes
  • test YAML syntax diagnostics for tabs and missing colons

closes #49

Testing

  • make check-fmt
  • make lint
  • make test

https://chatgpt.com/codex/tasks/task_e_68a2c9e1467483229721c7ed29af854b

Summary by Sourcery

Provide detailed YAML parse errors by including line/column information and context-sensitive hints for common mistakes, replace the generic initial parse error handling, and add tests to verify the new diagnostics.

New Features:

  • Include line and column information and context-driven hints (tabs, list dashes, missing colons) in YAML parse errors

Enhancements:

  • Introduce format_yaml_error to wrap serde_yml errors with enhanced messaging
  • Use format_yaml_error in manifest::from_str instead of the generic parse context

Tests:

  • Add tests for tab indentation hint and missing colon suggestion in YAML error diagnostics

@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai Bot commented Aug 18, 2025

Reviewer's Guide

Enhance the manifest parser’s YAML error handling by mapping serde_yml errors through a custom formatter that reports line/column information and context-aware hints, and verify these diagnostics with dedicated unit tests.

Sequence diagram for YAML parse error reporting with hints

sequenceDiagram
    participant ManifestParser
    participant YamlErrorFormatter
    participant User
    User->>ManifestParser: Provide YAML manifest string
    ManifestParser->>YamlErrorFormatter: On parse error, call format_yaml_error(err, src)
    YamlErrorFormatter-->>ManifestParser: Return formatted error with line/column and hint
    ManifestParser-->>User: Return error with diagnostics and suggestions
Loading

Class diagram for improved YAML error handling in manifest parser

classDiagram
    class ManifestParser {
        +from_str(yaml: &str) Result<NetsukeManifest>
    }
    class YamlErrorFormatter {
        +format_yaml_error(err: YamlError, src: &str) -> anyhow::Error
    }
    ManifestParser --> YamlErrorFormatter: uses
    class YamlError {
        +location() -> Option<Location>
        +to_string() -> String
    }
    YamlErrorFormatter --> YamlError: formats
    class NetsukeManifest
    ManifestParser --> NetsukeManifest: parses
Loading

File-Level Changes

Change Details Files
Introduce a custom YAML error formatter with location and hint logic
  • Import YamlError from serde_yml
  • Define format_yaml_error to extract line/column and generate hints
  • Implement hint detection for tabs, list syntax, and missing colons
  • Compose the final anyhow error message including hints
src/manifest.rs
Swap out generic YAML parse error context for the new formatter in from_str
  • Remove ERR_INITIAL_YAML_PARSE constant
  • Replace .context call with .map_err using format_yaml_error
src/manifest.rs
Add tests verifying enhanced YAML diagnostics
  • Create tests for tab-indentation hint
  • Create tests for missing-colon suggestion
tests/yaml_error_tests.rs

Assessment against linked issues

Issue Objective Addressed Explanation
#49 Provide YAML syntax error messages that include specific line and column numbers where the error occurred.
#49 Include a description of the type of YAML syntax error encountered in error messages.
#49 Add suggestions for how to fix common YAML syntax issues in error messages.

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Aug 18, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Warning

Rate limit exceeded

@leynos has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 2 minutes and 18 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 44ce507 and a46b9a0.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (3)
  • Cargo.toml (1 hunks)
  • docs/netsuke-design.md (3 hunks)
  • src/manifest.rs (2 hunks)

Summary by CodeRabbit

  • New Features
    • Richer YAML parse error messages with line/column details and contextual hints (e.g., tabs vs spaces for indentation, missing list dash, missing colon) to aid diagnosing manifest issues.
  • Tests
    • Added unit tests validating error reporting for tab-based indentation and missing-colon scenarios.

Walkthrough

Introduce a dedicated YAML parse error formatter and wire it into manifest::from_str to surface detailed messages with location and hints. Remove the old generic YAML parse error constant. Add unit tests asserting line/column and hint content for tab indentation and missing colon cases.

Changes

Cohort / File(s) Summary
Manifest YAML error handling
src/manifest.rs
Add format_yaml_error helper using serde_yml::Error; map YAML parse failures to enriched messages (line/column, context hints). Remove ERR_INITIAL_YAML_PARSE. Keep overall parsing/rendering flow unchanged.
YAML error tests
tests/yaml_error_tests.rs
Add tests verifying error messages include precise location and actionable hints for tab indentation and missing colon scenarios.

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant Manifest as manifest::from_str
  participant Serde as serde_yml::from_str
  participant Formatter as format_yaml_error

  Caller->>Manifest: from_str(yaml)
  Manifest->>Serde: parse YAML
  Serde-->>Manifest: Ok(Value) or Err(YamlError)
  alt YAML error
    Manifest->>Formatter: format_yaml_error(error, yaml)
    Formatter-->>Manifest: Detailed error message
    Manifest-->>Caller: Err(detailed message)
  else Success
    Manifest-->>Caller: Ok(Manifest)
  end
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Assessment against linked issues

Objective Addressed Explanation
Provide descriptive YAML syntax errors with line and column (#49)
Describe the type of syntax error encountered (#49)
Suggest fixes for common YAML issues (tabs vs spaces, missing colon, list dash) (#49)

Poem

A tab snuck in, a colon fled—
The parser frowned, “Look here instead!”
With lines and columns shining bright,
Hints now guide the fix at night.
YAML whispers, errors tame—
You press build, it purrs by name.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch codex/improve-yaml-error-message-clarity

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

codescene-delta-analysis[bot]

This comment was marked as outdated.

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Hey there - I've reviewed your changes and found some issues that need to be addressed.

  • Instead of constructing a fresh anyhow! error in format_yaml_error, consider using with_context or error.source() to attach the original YamlError as the cause so you don’t lose the original error chain.
  • You might extract the hint-detection logic (matching substrings in err_str) into a dedicated lookup table or helper to make it easier to extend and maintain as you add more YAML error cases.
  • For even clearer diagnostics, consider including the actual source line (and a caret at the column) in the error message so users can visually spot the problem in context.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- Instead of constructing a fresh anyhow! error in format_yaml_error, consider using with_context or error.source() to attach the original YamlError as the cause so you don’t lose the original error chain.
- You might extract the hint-detection logic (matching substrings in err_str) into a dedicated lookup table or helper to make it easier to extend and maintain as you add more YAML error cases.
- For even clearer diagnostics, consider including the actual source line (and a caret at the column) in the error message so users can visually spot the problem in context.

## Individual Comments

### Comment 1
<location> `tests/yaml_error_tests.rs:3` </location>
<code_context>
+use netsuke::manifest;
+
+#[test]
+fn reports_line_and_column_with_tab_hint() {
+    let yaml = "targets:\n\t- name: test\n";
</code_context>

<issue_to_address>
Consider adding a test for YAML parse errors without location information.

Existing tests only cover errors with location details. Please add a test for errors lacking this information to verify correct error formatting.

Suggested implementation:

```rust
#[test]

```

```rust
#[test]
fn reports_error_without_location() {
    // This YAML is invalid in a way that does not produce location info (e.g., empty input)
    let yaml = "";
    let err = manifest::from_str(yaml).expect_err("parse should fail");
    let msg = err.to_string();
    // Should not mention line/column
    assert!(
        !msg.contains("line"),
        "unexpected location info in error: {msg}"
    );
    assert!(
        !msg.contains("column"),
        "unexpected location info in error: {msg}"
    );
    // Should still be a useful error message
    assert!(
        !msg.is_empty(),
        "error message should not be empty"
    );
}

```
</issue_to_address>

### Comment 2
<location> `tests/yaml_error_tests.rs:16` </location>
<code_context>
+}
+
+#[test]
+fn suggests_colon_when_missing() {
+    let yaml = "targets:\n  - name: hi\n    command echo\n";
+    let err = manifest::from_str(yaml).expect_err("parse should fail");
+    let msg = err.to_string();
+    assert!(msg.contains("line 3"), "missing line info: {msg}");
+    assert!(msg.contains("expected ':'"), "missing error detail: {msg}");
+    assert!(
+        msg.contains("Ensure each key is followed by ':'"),
+        "missing suggestion: {msg}"
+    );
+}
</code_context>

<issue_to_address>
Add a test for the 'did not find expected -' error hint.

Please include a test with a YAML input that triggers the 'did not find expected -' error to confirm the hint appears as intended.
</issue_to_address>

### Comment 3
<location> `src/manifest.rs:16` </location>
<code_context>
-const ERR_INITIAL_YAML_PARSE: &str = "initial YAML parse error";
 const ERR_MANIFEST_PARSE: &str = "manifest parse error";

+fn format_yaml_error(err: &YamlError, src: &str) -> anyhow::Error {
+    let location = err.location().map(|l| (l.line(), l.column()));
+    let err_str = err.to_string();
</code_context>

<issue_to_address>
Consider replacing the nested if/else chain with a lookup table for YAML error hints to improve maintainability.

Here’s one way to collapse that nested‐`if`/`else` chain into a small lookup table. You still get the same hints, but it’s far easier to extend or change later:

```rust
// near the top of the file
// (all patterns must be lowercase)
const YAML_HINTS: &[(&str, &str)] = &[
    ("did not find expected '-'", "Start list items with '-' and ensure proper indentation."),
    ("expected ':'",            "Ensure each key is followed by ':' separating key and value."),
];

fn format_yaml_error(err: &YamlError, src: &str) -> anyhow::Error {
    let location = err.location().map(|l| (l.line(), l.column()));
    let err_str   = err.to_string();
    let mut msg   = match location {
        Some((line, col)) => format!("YAML parse error at line {line}, column {col}: {err_str}"),
        None              => format!("YAML parse error: {err_str}"),
    };

    // build hints via a small table + a special tab‐case
    let lower = err_str.to_lowercase();
    let hint = if src.contains('\t') {
        Some("Use spaces for indentation; tabs are invalid in YAML.")
    } else {
        YAML_HINTS
            .iter()
            .find_map(|(pat, hint)| lower.contains(pat).then(|| *hint))
    };

    if let Some(h) = hint {
        use std::fmt::Write;
        write!(&mut msg, " Hint: {h}").unwrap();
    }

    anyhow!(msg)
}
```

Advantages:

- All your patterns live in one small `YAML_HINTS` array.
- The `find_map` replaces the manual chain of `if/else if`.
- It’s trivial to add/remove hints or special‐case rules.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread tests/yaml_error_tests.rs
@@ -0,0 +1,26 @@
use netsuke::manifest;

#[test]
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.

suggestion (testing): Consider adding a test for YAML parse errors without location information.

Existing tests only cover errors with location details. Please add a test for errors lacking this information to verify correct error formatting.

Suggested implementation:

#[test]
#[test]
fn reports_error_without_location() {
    // This YAML is invalid in a way that does not produce location info (e.g., empty input)
    let yaml = "";
    let err = manifest::from_str(yaml).expect_err("parse should fail");
    let msg = err.to_string();
    // Should not mention line/column
    assert!(
        !msg.contains("line"),
        "unexpected location info in error: {msg}"
    );
    assert!(
        !msg.contains("column"),
        "unexpected location info in error: {msg}"
    );
    // Should still be a useful error message
    assert!(
        !msg.is_empty(),
        "error message should not be empty"
    );
}

Comment thread tests/yaml_error_tests.rs
assert!(
msg.contains("Use spaces for indentation"),
"missing hint: {msg}"
);
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.

suggestion (testing): Add a test for the 'did not find expected -' error hint.

Please include a test with a YAML input that triggers the 'did not find expected -' error to confirm the hint appears as intended.

Comment thread src/manifest.rs Outdated
const ERR_INITIAL_YAML_PARSE: &str = "initial YAML parse error";
const ERR_MANIFEST_PARSE: &str = "manifest parse error";

fn format_yaml_error(err: &YamlError, src: &str) -> anyhow::Error {
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.

issue (complexity): Consider replacing the nested if/else chain with a lookup table for YAML error hints to improve maintainability.

Here’s one way to collapse that nested‐if/else chain into a small lookup table. You still get the same hints, but it’s far easier to extend or change later:

// near the top of the file
// (all patterns must be lowercase)
const YAML_HINTS: &[(&str, &str)] = &[
    ("did not find expected '-'", "Start list items with '-' and ensure proper indentation."),
    ("expected ':'",            "Ensure each key is followed by ':' separating key and value."),
];

fn format_yaml_error(err: &YamlError, src: &str) -> anyhow::Error {
    let location = err.location().map(|l| (l.line(), l.column()));
    let err_str   = err.to_string();
    let mut msg   = match location {
        Some((line, col)) => format!("YAML parse error at line {line}, column {col}: {err_str}"),
        None              => format!("YAML parse error: {err_str}"),
    };

    // build hints via a small table + a special tab‐case
    let lower = err_str.to_lowercase();
    let hint = if src.contains('\t') {
        Some("Use spaces for indentation; tabs are invalid in YAML.")
    } else {
        YAML_HINTS
            .iter()
            .find_map(|(pat, hint)| lower.contains(pat).then(|| *hint))
    };

    if let Some(h) = hint {
        use std::fmt::Write;
        write!(&mut msg, " Hint: {h}").unwrap();
    }

    anyhow!(msg)
}

Advantages:

  • All your patterns live in one small YAML_HINTS array.
  • The find_map replaces the manual chain of if/else if.
  • It’s trivial to add/remove hints or special‐case rules.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

📜 Review details

Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro

💡 Knowledge Base configuration:

  • Jira integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between a1b648b and 44ce507.

📒 Files selected for processing (2)
  • src/manifest.rs (2 hunks)
  • tests/yaml_error_tests.rs (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.rs

📄 CodeRabbit Inference Engine (AGENTS.md)

**/*.rs: Comment why, not what; document assumptions, edge cases, trade-offs, and complexity without restating obvious code
Extract reusable logic into functions; prefer composition and declarative constructs when readable
Keep functions small, single-responsibility, and observe command/query separation
Use precise, descriptive names; boolean names should start with is/has/should
Use en-GB-oxendict spelling/grammar in comments (except external API references)
Function docs must include clear usage examples; avoid redundant examples in test docs
No Rust source file may exceed 400 lines; split long switches/tables; move large test data to external files
Fix test warnings in code; do not silence them
Extract helpers when functions are too long; group many parameters into well-named structs
Consider using Arc when returning large errors to reduce data movement
Document public APIs using Rustdoc (///) so cargo doc can generate documentation
Prefer immutable data; avoid unnecessary mut bindings
Prefer Result-based error handling over panicking where feasible
Avoid unsafe code unless absolutely necessary and document any usage clearly
Place function attributes after doc comments
Do not use return in single-line functions
Use predicate helper functions when conditionals have more than two branches
Do not silence lints except as a last resort; suppressions must be tightly scoped and include a clear reason
Prefer #[expect(...)] over #[allow(...)] for lint management
Prefer .expect() over .unwrap()
Use conditional compilation (#[cfg]/#[cfg_attr]) for functions unused under specific feature sets
Use concat!() to join long string literals rather than using backslash-newline escapes
Prefer single-line function bodies where appropriate (e.g., pub fn new(id: u64) -> Self { Self(id) })
Prefer semantic error enums deriving std::error::Error via thiserror for caller-inspectable conditions

Files:

  • tests/yaml_error_tests.rs
  • src/manifest.rs

⚙️ CodeRabbit Configuration File

**/*.rs: * Seek to keep the cyclomatic complexity of functions no more than 12.

  • Adhere to single responsibility and CQRS

  • Place function attributes after doc comments.

  • Do not use return in single-line functions.

  • Move conditionals with >2 branches into a predicate function.

  • Avoid unsafe unless absolutely necessary.

  • Every module must begin with a //! doc comment that explains the module's purpose and utility.

  • Comments and docs must follow en-GB-oxendict (-ize / -our) spelling and grammar

  • Lints must not be silenced except as a last resort.

    • #[allow] is forbidden.
    • Only narrowly scoped #[expect(lint, reason = "...")] is allowed.
    • No lint groups, no blanket or file-wide suppression.
    • Include FIXME: with link if a fix is expected.
  • Where code is only used by specific features, it must be conditionally compiled or a conditional expectation for unused_code applied.

  • Use rstest fixtures for shared setup and to avoid repetition between tests.

  • Replace duplicated tests with #[rstest(...)] parameterised cases.

  • Prefer mockall for mocks/stubs.

  • Prefer .expect() over .unwrap()

  • Ensure that any API or behavioural changes are reflected in the documentation in docs/

  • Ensure that any completed roadmap steps are recorded in the appropriate roadmap in docs/

  • Files must not exceed 400 lines in length

    • Large modules must be decomposed
    • Long match statements or dispatch tables should be decomposed by domain and collocated with targets
    • Large blocks of inline data (e.g., test fixtures, constants or templates) must be moved to external files and inlined at compile-time or loaded at run-time.

Files:

  • tests/yaml_error_tests.rs
  • src/manifest.rs
tests/**/*.rs

📄 CodeRabbit Inference Engine (AGENTS.md)

tests/**/*.rs: Use rstest fixtures for shared setup and replace duplicated tests with #[rstest(...)] parameterised cases
Prefer mockall for mocks/stubs
Mock non-deterministic dependencies via dependency injection (e.g., Env/Clock traits) using mockable crate; follow docs/reliable-testing-in-rust-via-dependency-injection.md

Files:

  • tests/yaml_error_tests.rs
src/**/*.rs

📄 CodeRabbit Inference Engine (AGENTS.md)

src/**/*.rs: Every module must begin with a //! module-level comment explaining its purpose and utility
Never export eyre::Report from library code; convert to domain error enums at API boundaries

Files:

  • src/manifest.rs
🧬 Code Graph Analysis (1)
tests/yaml_error_tests.rs (1)
src/manifest.rs (1)
  • from_str (50-71)
🔍 Remote MCP Context7, Deepwiki

Summary of Additional Context for PR #127 Review:

  1. Original YAML parsing in manifest::from_str used serde_yml::from_str with generic context:

    • Errors were wrapped with .context("YAML parse error") via anyhow for generic messaging.
  2. No existing format_yaml_error function found in pre-PR code or documentation:

    • Searches in codebase and wiki yielded no direct references to format_yaml_error before PR changes. Error handling relied on anyhow contexts and thiserror for friendly messages, as described in docs/netsuke-design.md and src/manifest.rs.
  3. Design philosophy for YAML error handling:

    • The five-stage pipeline’s “YAML Parse” stage aims to surface line/column and actionable hints (e.g., spaces vs tabs), implemented using anyhow::Context.

No direct code diff for format_yaml_error could be retrieved, indicating its addition is new and isolated to this PR. Review should focus on verifying that the new function correctly captures error location, context hints, and integrates with existing error-handling patterns.

⏰ Context from checks skipped due to timeout of 120000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build-test
🔇 Additional comments (2)
src/manifest.rs (1)

10-11: Alias the YAML error type locally — LGTM

Keep the alias; it improves readability at call sites and in the formatter.

tests/yaml_error_tests.rs (1)

3-13: Assert location and hint — LGTM; keep this as a regression guard

The test captures both the precise location and the indentation hint. This guards the intended UX.

Comment thread src/manifest.rs Outdated
Comment on lines +16 to +40
fn format_yaml_error(err: &YamlError, src: &str) -> anyhow::Error {
let location = err.location().map(|l| (l.line(), l.column()));
let err_str = err.to_string();
let mut msg = match location {
Some((line, column)) => {
format!("YAML parse error at line {line}, column {column}: {err_str}")
}
None => format!("YAML parse error: {err_str}"),
};
let lower = err_str.to_lowercase();
let hint = if src.contains('\t') {
Some("Use spaces for indentation; tabs are invalid in YAML.")
} else if lower.contains("did not find expected '-'") {
Some("Start list items with '-' and ensure proper indentation.")
} else if lower.contains("expected ':'") {
Some("Ensure each key is followed by ':' separating key and value.")
} else {
None
};
if let Some(h) = hint {
use std::fmt::Write;
write!(&mut msg, " Hint: {h}").expect("string write");
}
anyhow!(msg)
}
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.

🛠️ Refactor suggestion

Refine tab detection to indentation-only and avoid panicking on write

Prevent false-positive “tabs” hints when a tab appears in quoted scalars or non-indentation positions. Also avoid the unnecessary write! + expect by appending directly to the String. While here, cover the common “mapping values are not allowed in this context” variant for a missing colon.

Apply this diff within the shown range:

@@
-    let lower = err_str.to_lowercase();
-    let hint = if src.contains('\t') {
+    let lower = err_str.to_lowercase();
+    let hint = if has_tab_in_indentation(src) {
         Some("Use spaces for indentation; tabs are invalid in YAML.")
     } else if lower.contains("did not find expected '-'") {
         Some("Start list items with '-' and ensure proper indentation.")
-    } else if lower.contains("expected ':'") {
+    } else if lower.contains("expected ':'") || lower.contains("mapping values are not allowed in this context") {
         Some("Ensure each key is followed by ':' separating key and value.")
     } else {
         None
     };
     if let Some(h) = hint {
-        use std::fmt::Write;
-        write!(&mut msg, " Hint: {h}").expect("string write");
+        msg.push_str(" Hint: ");
+        msg.push_str(h);
     }

Add this helper anywhere in the module (e.g., just above format_yaml_error) to scope the hint to indentation:

fn has_tab_in_indentation(src: &str) -> bool {
    src.lines()
        .any(|line| line.chars().take_while(|c| c.is_whitespace()).any(|c| c == '\t'))
}
🤖 Prompt for AI Agents
In src/manifest.rs around lines 16 to 40, the YAML error formatting currently
flags any tab anywhere in the source and uses write! with expect to append the
hint; change this so tab detection only considers indentation by adding the
helper fn has_tab_in_indentation(src: &str) -> bool { src.lines().any(|line|
line.chars().take_while(|c| c.is_whitespace()).any(|c| c == '\t')) } (place it
in the module above format_yaml_error), replace the src.contains('\t') check
with has_tab_in_indentation(src), replace the write!(&mut msg, ...) + expect
with direct msg.push_str(...) to append the hint, and also add a hint branch for
the error message variant containing "mapping values are not allowed in this
context" to suggest a missing colon.

Comment thread src/manifest.rs Outdated
/// Returns an error if YAML parsing or Jinja evaluation fails.
pub fn from_str(yaml: &str) -> Result<NetsukeManifest> {
let mut doc: YamlValue = serde_yml::from_str(yaml).context(ERR_INITIAL_YAML_PARSE)?;
let mut doc: YamlValue = serde_yml::from_str(yaml).map_err(|e| format_yaml_error(&e, yaml))?;
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.

🧹 Nitpick (assertive)

Retain the original error as the source for better error chains

Mapping to a freshly formatted anyhow::Error discards the original serde_yml::Error as the source. Preserve the cause chain so callers can inspect/log the underlying parser error with full details. Introduce a small error wrapper that implements std::error::Error with the source set to the original error, or switch to a typed error enum exposing both message and source.

I can sketch a minimal thiserror-based ManifestYamlError { msg, #[source] source: serde_yml::Error } and wire it here without touching call sites. Want me to draft it?

Comment thread tests/yaml_error_tests.rs
Comment on lines +15 to +26
#[test]
fn suggests_colon_when_missing() {
let yaml = "targets:\n - name: hi\n command echo\n";
let err = manifest::from_str(yaml).expect_err("parse should fail");
let msg = err.to_string();
assert!(msg.contains("line 3"), "missing line info: {msg}");
assert!(msg.contains("expected ':'"), "missing error detail: {msg}");
assert!(
msg.contains("Ensure each key is followed by ':'"),
"missing suggestion: {msg}"
);
}
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.

🧹 Nitpick (assertive)

Broaden coverage: add tests for missing list dash and avoid false-positive tab hints

Extend the suite to cover the “expected '-'” path and ensure tab hints are not emitted when tabs appear in quoted scalars (non-indentation). This protects against regressions in the heuristic.

Add the following tests below in this file:

#[test]
fn suggests_dash_when_missing_for_sequence_items() {
    // Missing '-' before 'one' under deps should produce a list-dash hint.
    let yaml = "targets:\n  - name: hi\n    deps:\n      one\n";
    let err = manifest::from_str(yaml).expect_err("parse should fail");
    let msg = err.to_string();
    assert!(msg.to_lowercase().contains("expected '-'") || msg.to_lowercase().contains("did not find expected '-'"),
            "missing parser detail about expected '-' : {msg}");
    assert!(msg.contains("Start list items with '-'"), "missing list dash hint: {msg}");
}

#[test]
fn does_not_hint_tabs_for_tabs_inside_quotes() {
    // Tabs inside quoted scalars are valid; if another error occurs, do not emit the “tabs” hint.
    // Force an unrelated error (missing ':') while including a quoted tab.
    let yaml = "targets:\n  - name: \"tab\\tinside\"\n    command echo\n";
    let err = manifest::from_str(yaml).expect_err("parse should fail");
    let msg = err.to_string();
    assert!(!msg.contains("tabs are invalid in YAML"), "tab hint should not trigger for quoted tabs: {msg}");
    assert!(msg.contains("expected ':'"), "missing colon diagnostic: {msg}");
}
🤖 Prompt for AI Agents
In tests/yaml_error_tests.rs around lines 15–26, add two new unit tests below
the existing test: one that verifies the parser emits an "expected '-'" (or "did
not find expected '-'") message and the "Start list items with '-'" suggestion
when a sequence dash is missing, and another that ensures when a quoted scalar
contains a tab the diagnostics do not include the "tabs are invalid in YAML"
hint while still reporting the missing ':' error; insert the exact assertions
shown in the review comment, using to_lowercase() for the dash message check and
negating the tab hint check so the suite covers both the missing-list-dash path
and avoids false-positive tab hints for quoted tabs.

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Aug 18, 2025

Superseded by #129

@leynos leynos closed this Aug 18, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add friendlier YAML syntax error messages

1 participant