Skip to content

feat(mds-core)!: mark LintDiagnostic #[non_exhaustive] and add constructor [#259] - #286

Merged
dean0x merged 7 commits into
mainfrom
fix/non-exhaustive-lintdiagnostic-259
Aug 9, 2026
Merged

feat(mds-core)!: mark LintDiagnostic #[non_exhaustive] and add constructor [#259]#286
dean0x merged 7 commits into
mainfrom
fix/non-exhaustive-lintdiagnostic-259

Conversation

@dean0x

@dean0x dean0x commented Aug 9, 2026

Copy link
Copy Markdown
Owner

Summary

This is a pre-tag semver window change for the upcoming v0.4.0 release. Adding #[non_exhaustive] to a public struct is a breaking change — any downstream code constructing LintDiagnostic { .. } with struct-literal syntax would fail to compile. Landing it now, before the tag ships, avoids a separate breaking release later.

What Changed

crates/mds-core/src/lint/diagnostic.rs (new file)

  • LintDiagnostic is now #[non_exhaustive]
  • Added LintDiagnostic::new(code, message) — the canonical constructor
  • Builder methods: with_help, with_span, with_file, with_fix_removals, with_fix_edits
  • Added sanitized_for_render() — clones the diagnostic with message and help run through sanitize_control_chars (HUMAN mode escaping); fix_removals/fix_edits are set to None on the clone for allocation avoidance

crates/mds-cli/src/lint.rs and crates/mds-cli/src/output.rs (9 call sites migrated)

  • All LintDiagnostic { .. } struct-literal construction replaced with LintDiagnostic::new(..).with_*() builder chains
  • render_diag_human in mds-cli previously contained the sanitized-clone logic; that inline clone is now deleted and replaced by a call to sanitized_for_render()

Why sanitized_for_render() Belongs in mds-core

PF-014 requires that sanitization of user-controlled content happens as close to the data origin as possible, not at render time in the CLI. Moving the sanitized-clone from mds-cli::render_diag_human into mds-core is the correct layering: the core type owns the sanitization contract, and callers (WASM, native bindings, CLI) all get it consistently. The port is byte-identical — message and help are sanitized via sanitize_control_chars in HUMAN mode; fix_removals/fix_edits are deliberately None on the sanitized clone (no allocations for fields the renderer does not consume).

Local Gate Results

  • cargo fmt --all --check — clean
  • cargo clippy --workspace --all-targets -- -D warnings — clean
  • cargo nextest run --workspace — 1980 tests pass
  • cargo test --doc --workspace — 39 doctests pass

Related

Closes #259

dean0x and others added 7 commits August 9, 2026 08:29
…r and builders [#259]

Add `#[non_exhaustive]` to the public `LintDiagnostic` struct so new fields can be
added in minor releases without a breaking change for external crates.

External crates can no longer construct `LintDiagnostic` via struct literal. Replace
all struct literal construction sites (9 total: 1 production, 2 test fixtures in
mds-cli, 6 integration tests in mds-core) with:

- `LintDiagnostic::new(rule, severity, message)` — creates a diagnostic with all
  optional fields defaulting to `None`.
- `with_help`, `with_span`, `with_file`, `with_fix_removals`, `with_fix_edits` —
  builder methods that set optional fields via method chaining.

Move sanitized-clone logic from the CLI's `render_diag_human` into
`LintDiagnostic::sanitized_for_render(&self) -> Self` in mds-core. This is the
architecturally correct home for render-boundary sanitization per PF-014: the CLI
assembles the sanitized copy by calling the method rather than building the struct
literal itself. Behavior is byte-identical: HUMAN-mode escape on message/help,
fix_removals/fix_edits set to None to avoid unnecessary allocations.

Also export `TextEdit` from `mds-core`'s `lib.rs` public API so callers of
`with_fix_edits` have access to the type.

Co-Authored-By: Claude <noreply@anthropic.com>
…#259]

Mark LintResult, SerializedError, SerializedSpan, TextEdit, FixLineSpan,
ByteEdit, RejectedEdit, FixPlan, and LintConfig as #[non_exhaustive] so
future minor releases can add fields without a breaking change.

Construction paths added:
- LintResult::new(diagnostics, truncated, is_standalone)
- SerializedSpan::new(offset, length) + .with_line() / .with_column()
- TextEdit::new(start, end, new_text)
- FixLineSpan::range(from, to, to_inclusive)  [single() already existed]
- ByteEdit::new(start, end, rule, replacement)
- RejectedEdit::new(edit, reason)
- LintConfig::with_rules(rules)  [default() suffices for empty config]
- FixPlan::default()  [no struct literal; just #[non_exhaustive] added]
- SerializedError: no external constructor (obtained via MdsError::serialize())

All external struct literals in api_surface.rs, build.rs, lint.rs,
mds-napi, mds-wasm, and mds-python migrated to constructors. CHANGELOG
[Unreleased] BREAKING section updated.

Part of review findings on d8c7e48:
- Add #[must_use] to six LintDiagnostic builders and sanitized_for_render
- Fix with_span parameter type (crate::error::SerializedSpan -> crate::SerializedSpan)
- Add unit tests T-SFR-1/T-SFR-2 for sanitized_for_render
- Expand sanitized_for_render rustdoc (one-way escaping, render-only, fix-strip)
- Add file/help/span assertions to lint_types_exist in api_surface.rs
…#[non_exhaustive]; add builders and migrate call sites [#259]

- CompileOptions: add #[non_exhaustive] + with_source_map / with_include_sources_content / with_source_map_base builders
- InvalidOptionsError: add #[non_exhaustive]
- VarsError: add #[non_exhaustive]
- FixTier: add #[non_exhaustive]
- LintResult::new: drop truncated/is_standalone args; add .truncated() and .standalone() builders
- FixLineSpan::range: replace with range_inclusive and range_exclusive (debug_assert from > to)
- ByteEdit::new: replace with deletion and replacement named constructors
- LintConfig::with_rules → from_rules (C-CTOR naming guideline)
- #[must_use] on LintDiagnostic::new, TextEdit::new (+ debug_assert), SerializedSpan::new,
  FixLineSpan::single, LintResult::new, ByteEdit::deletion/replacement, RejectedEdit::new
- FixPlan doc: clarify fields are pub and directly readable/writable
…rename LintConfig::from_rules, add #[must_use] [#259]

Migrate all binding and CLI call sites to the new APIs introduced in the
companion core commit:
- mds-cli/src/build.rs: 3 × CompileOptions literal → builder chain;
  LintConfig::with_rules → from_rules
- mds-cli/src/lint.rs: FixLineSpan::range → range_inclusive; LintResult::new
  3-arg → 1-arg; doc comment updated to reference new constructor names
- mds-napi/src/lib.rs: CompileOptions literal → builder; from_rules; VarsError
  match + wildcard arm (non_exhaustive)
- mds-python/src/lib.rs: CompileOptions literal → builder; from_rules; VarsError
  match + wildcard arm (non_exhaustive)
- mds-wasm/src/lib.rs: 2 × CompileOptions literal → builder; from_rules; VarsError
  match + wildcard arm (non_exhaustive)
…with hostile file input [#259]

- api_surface.rs: migrate all CompileOptions literal → builder, LintResult::new
  3-arg → 1-arg (+.standalone()), LintConfig::with_rules → from_rules,
  ByteEdit::new → ByteEdit::deletion; add F-API-2 test pinning mds::TextEdit
  public nameability and fix_edits JSON emission
- T-SFR-2 (diagnostic.rs): replace plain filename with hostile "a\u{1B}/b\u{202E}.mds";
  use LintDiagnostic::new builder; assert span.line/column survive sanitization;
  assert file is byte-identical including hostile control bytes
- source_map_vfs.rs: all 10 CompileOptions literal → builder
- virtual_fs.rs: 2 × CompileOptions literal → builder
- producer_discipline.rs: CompileOptions literal → builder; remove unused import
…e, fix FixPlan mutation claim [#259]

Update the [Unreleased] section to reflect the new APIs:
- LintResult: new(diagnostics) + .truncated()/.standalone() builder chain
- TextEdit: document that it was previously unnameable from external crates
  (was pub inside a pub(crate) module); this PR re-exports at crate root
- FixLineSpan: range_inclusive / range_exclusive instead of range(from, to, bool)
- ByteEdit: deletion / replacement instead of new(start, end, rule, replacement)
- FixPlan: clarify fields are pub (not just mutation methods)
- LintConfig: from_rules instead of with_rules
- Add sanitized_for_render() entry (PF-014 redesign — render-boundary escape
  logic now lives co-located with the struct definition)
dean0x added a commit that referenced this pull request Aug 9, 2026
PR #240 (de8857d) added a `miette::miette!` call that exceeded the
line-length limit and was not run through `cargo fmt` before merging.
PRs #239 and #240 each passed CI on their own branches, but the
combination left `main` (c8b4062) red on the `cargo fmt --check` gate.

This commit applies the corrective reformat so that PR #287 can pass CI
independently of the merge order for PR #286, which incidentally carries
the same fix on its branch.
@dean0x
dean0x merged commit d8766af into main Aug 9, 2026
19 checks passed
@dean0x
dean0x deleted the fix/non-exhaustive-lintdiagnostic-259 branch August 9, 2026 15:25
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.

PRE-TAG DECISION: mark LintDiagnostic as #[non_exhaustive]

1 participant