feat(mds-core)!: mark LintDiagnostic #[non_exhaustive] and add constructor [#259] - #286
Merged
Merged
Conversation
…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.
This was referenced Aug 9, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 constructingLintDiagnostic { .. }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)LintDiagnosticis now#[non_exhaustive]LintDiagnostic::new(code, message)— the canonical constructorwith_help,with_span,with_file,with_fix_removals,with_fix_editssanitized_for_render()— clones the diagnostic with message and help run throughsanitize_control_chars(HUMAN mode escaping);fix_removals/fix_editsare set toNoneon the clone for allocation avoidancecrates/mds-cli/src/lint.rsandcrates/mds-cli/src/output.rs(9 call sites migrated)LintDiagnostic { .. }struct-literal construction replaced withLintDiagnostic::new(..).with_*()builder chainsrender_diag_humaninmds-clipreviously contained the sanitized-clone logic; that inline clone is now deleted and replaced by a call tosanitized_for_render()Why
sanitized_for_render()Belongs in mds-corePF-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_humanintomds-coreis 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 viasanitize_control_charsin HUMAN mode;fix_removals/fix_editsare deliberatelyNoneon the sanitized clone (no allocations for fields the renderer does not consume).Local Gate Results
cargo fmt --all --check— cleancargo clippy --workspace --all-targets -- -D warnings— cleancargo nextest run --workspace— 1980 tests passcargo test --doc --workspace— 39 doctests passRelated
Closes #259