Skip to content

var/renameStep writes optional-group notation into documents, silently deleting the assertion #116

Description

@aslakhellesoy

Found while reworking 15 oaths in a real project (@varar/varar + @varar/vitest + @varar/lsp 0.8.1).

var/renameStep renders the new expression's source text into every matching sentence, rather than the text that would actually match it. When the new expression contains an optional group, the notation lands in the document verbatim — and checkout(s) matches neither checkout nor checkouts.

The assertion does not fail. It stops existing.

Repro

Step definition:

sensor('sent to {int} checkout(s)', async (world) =>
  world.adapters.paymentProcessorWeb.getAllCheckouts().length)

Document, before — note Varar rendered the optional correctly when the sentence was written by hand:

...they are sent to 1 checkout, and the checkout lists:

Rename sent to {int} checkout(s) → handed {int} checkout(s) to pay. The LSP reports 1 site(s) and writes:

...they are handed 1 checkout(s) to pay, and the checkout lists:

Why this is worse than a broken example

The sentence still contains other steps that match, so the paragraph stays live. It is not a delimiter, the example is not split, and drift sees nothing wrong. The sensor simply never runs again:

Document says: "they are handed 99 checkout(s) to pay"
System creates: 1 checkout
Result:         Tests  3 passed (3)

I changed 1 to 99 specifically to check, and the suite stayed green. A rename — the one operation the LSP exists to make safe — silently converted a verified claim into decorative prose. That is the failure mode explanation/no-theatre is built to prevent, arriving through the refactoring tool.

Suggestions

In rough order of how much I'd want them:

  1. Render per site from the matched text. renameStep already has each site's paramValues and span. For an optional group it has to decide checkout vs checkouts; the old site's text is the evidence — if the old expression had the same optional group and the site rendered it as checkout, keep checkout. renderExpressionText presumably needs the same treatment.
  2. Refuse rather than corrupt. If rendering an optional (or alternation) is ambiguous, return { ok: false, error }. Refusing a rename is a minor annoyance; silently deleting an assertion is not.
  3. Verify the write. After computing each site's new text, re-match it against the new expression and fail the whole rename if any site no longer matches. That is a cheap invariant — a rename must leave every site matching — and it would have caught this class of bug generally, not just optionals.

(3) feels like the one worth having regardless of (1), since it turns "the rename tool can silently break documents" into "it cannot".

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions