Skip to content

W_INDEX_ORPHAN false positive: index link parsing uses rfind(')'), so a later ')' on the line hides the reference #60

Description

@fohara

Summary

lash lint reports W_INDEX_ORPHAN for files that the root index does reference, whenever the referencing line contains a ) anywhere after the markdown link. Annotating an index entry with a parenthetical is enough to trigger it:

- [Alpha](tasks/alpha.md) (historical, superseded)

The warning says the file is not referenced in the root index, so the natural response is to go looking for a missing line. The line is right there, and nothing points at the trailing parenthesis as the cause. I spent a while assuming I had mistyped a path.

Version: lash 0.4.0.

Reproduction

Create a project with five task files and this index:

# Demo Project

@id: index

## Epic Task Files

- [Alpha](tasks/alpha.md) (trailing parenthetical)
- [Beta](tasks/beta.md) — trailing em dash
- [Gamma](tasks/gamma.md) plain trailing words
- [Delta](tasks/delta.md) mentions (a parenthetical) mid-sentence
- [Epsilon](tasks/epsilon.md)(immediately adjacent)

Each tasks/*.md is a minimal file with an @id and one open task.

Then run:

lash index
lash lint

Expected: no warnings. All five files are referenced.

Actual: W_INDEX_ORPHAN for alpha, delta, and epsilon. Beta and gamma pass. The distinguishing factor is not the parenthetical itself but whether a ) appears later on the line.

Second symptom, same cause

Two links on one line breaks both of them:

- [Alpha](tasks/alpha.md) and [Beta](tasks/beta.md) on one line

This reports both alpha and beta as orphans.

Cause

extract_markdown_link_path in crates/lash-core/src/linter/rules/crossfile/orphaned_files.rs:163:

let open_paren = text.find("](")?;
let close_paren = text.rfind(')')?;

if open_paren + 2 < close_paren {
    let path = &text[open_paren + 2..close_paren];

The start comes from the first ]( and the end from rfind(')'), which is the last ) on the line rather than the one that closes the link. Everything between them is taken as the path. For the alpha case that yields tasks/alpha.md) (trailing parenthetical, which matches no file on disk.

The .md extension guard below it does not catch this. Path::new("tasks/alpha.md) (trailing parenthetical").extension() returns None so alpha is simply dropped, but in the two-link case the corrupted string is tasks/alpha.md) and [Beta](tasks/beta.md, whose final component still ends in .md. The guard passes and a nonsense path gets recorded as a legitimate reference, so both real files fall through to the orphan check.

Using find for the opening delimiter also means only the first link on any line is ever considered, which is the other half of the two-link failure.

Suggested fix

Scan forward from ]( to the next ) instead of calling rfind, and iterate across the line so every link is collected rather than just the first. Nested parentheses inside a path are rare enough that a first-match scan is probably sufficient, though matching balanced delimiters would also handle them.

A cheap partial improvement, if the parser is not worth changing right now: reject candidate paths containing ) or whitespace before recording them. That would stop the two-link case from registering a bogus reference, but it would not make either line link correctly.

Impact

Low severity and easy to work around once you know the rule, which is to annotate index entries with a comma or a dash instead of parentheses. It is expensive to diagnose the first time because the diagnostic points at the orphaned file rather than at the index line that failed to parse, and because the obvious repair (adding the file to the index) is something you have already done.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions