astral-sh/ruff · error

Expected a Title

Error message

Expected a Title

What it means

ruff_annotate_snippets is the vendored annotate-snippets renderer used for ruff/ty diagnostics. When a Renderer is built with .short_message(true), Renderer::render() delegates to render_short_message(), which emits rustc-style one-line messages ('path:line:col: level[id]: title'). That path requires the first group to carry a title (set via Level::primary_title()/secondary_title()); a report built without one violates the invariant and panics with 'Expected a Title'.

Source

Thrown at crates/ruff_annotate_snippets/src/renderer/render.rs:244

            }
            buffer
                .render(&level, &renderer.stylesheet, &mut out_string)
                .unwrap();
            if g != group_len - 1 {
                out_string.push('\n');
            }
        }
        out_string
    }
}

fn render_short_message(renderer: &Renderer, groups: &[Group<'_>]) -> Result<String, fmt::Error> {
    let mut buffer = StyledBuffer::new();
    let mut labels = None;
    let group = groups.first().expect("Expected at least one group");

    let Some(title) = &group.title else {
        panic!("Expected a Title");
    };

    if let Some(Element::Cause(cause)) = group
        .elements
        .iter()
        .find(|e| matches!(e, Element::Cause(_)))
    {
        let labels_inner = cause
            .markers
            .iter()
            .filter_map(|ann| match &ann.label {
                Some(msg) if ann.kind.is_primary() => {
                    if !msg.trim().is_empty() {
                        Some(msg.to_string())
                    } else {
                        None
                    }
                }

View on GitHub (pinned to d1087a4b9e)

Solutions

  1. Attach a title to the Level before adding elements: Level::ERROR.primary_title("...") (or secondary_title).
  2. If no title exists, render with the normal (non-short) renderer instead of short_message(true).
  3. Add a unit test that renders the diagnostic with a short_message(true) renderer so the invariant is enforced in CI.

Example fix

// before: no title on the Level -> panic: Expected a Title
let input = &[Level::ERROR
    .element(Snippet::source(src)
        .annotation(AnnotationKind::Primary.span(0..5)))];
let out = Renderer::plain().short_message(true).render(input);

// after
let input = &[Level::ERROR
    .primary_title("E501 line too long")
    .element(Snippet::source(src)
        .annotation(AnnotationKind::Primary.span(0..5)))];
let out = Renderer::plain().short_message(true).render(input);
Defensive patterns

Strategy: validation

Validate before calling

// Lock the invariant with a test that renders every diagnostic in short mode.
#[test]
fn diagnostics_render_short_message() {
    for report in all_diagnostic_reports() { // each report must carry a title
        let _ = Renderer::plain().short_message(true).render(report);
    }
}

Try / catch

let rendered = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
    renderer.render(&report)
}))
.unwrap_or_else(|_| report.title_only_fallback());

Prevention

When it happens

Trigger: Calling Renderer::plain().short_message(true).render(...) with a Level whose group has no title — e.g. a new diagnostic wired into concise/short rendering that sets only .element(Snippet...) annotations and never .primary_title(...).

Common situations: Adding a new diagnostic and forgetting the title; refactoring titles to be optional in shared rendering code; writing snapshot tests for the short format without a title.

Related errors


AI-assisted analysis of astral-sh/ruff@d1087a4b9e (2026-08-20). Data as JSON: /api/errors/f5e142987e09bf18. Report an issue: GitHub.