Skip to main content

mdwright_lint/
stdlib.rs

1//! The standard library of lint rules.
2//!
3//! Every rule here implements [`crate::LintRule`]. The constructors
4//! [`defaults`] and [`all`] return ready-made [`RuleSet`]s; [`by_name`]
5//! looks up a fresh instance of one stdlib rule by its kebab-case
6//! name, used by the CLI's `--rules` parser.
7//!
8//! ## Rules
9//!
10//! | Name | Default | Advisory |
11//! | --- | --- | --- |
12//! | `unbalanced-backtick`      | yes | no  |
13//! | `math/unbalanced-delim`    | yes | no  |
14//! | `math/unbalanced-env`      | yes | no  |
15//! | `math/unbalanced-braces`   | yes | no  |
16//! | `math/render-compat`       | no  | no  |
17//! | `adjacent-code-no-space`   | yes | no  |
18//! | `heading-punctuation`      | yes | no  |
19//! | `orphan-reference-link`    | yes | no  |
20//! | `duplicate-link-label`     | yes | no  |
21//! | `bare-url`                 | yes | no  |
22//! | `trailing-whitespace`      | yes | no  |
23//! | `inconsistent-list-marker` | yes | no  |
24//! | `table-pipe-spacing`       | yes | no  |
25//! | `list-tightness-flipped`   | no  | yes |
26//! | `duplicate-heading`        | yes | no  |
27//! | `unicodeable-subscript`    | yes | yes |
28//! | `info-string-typo`         | yes | yes |
29//! | `stray-dollar`             | no  | no  |
30//! | `latex-command`            | no  | no  |
31//! | `escaped-emphasis`         | no  | no  |
32//! | `subscript-damage`         | no  | no  |
33
34mod adjacent_code;
35mod bare_url;
36mod duplicate_heading;
37mod duplicate_link_label;
38mod escaped_emphasis;
39mod heading_punctuation;
40mod inconsistent_list_marker;
41mod info_string_typo;
42mod latex_command;
43mod list_tightness_flipped;
44mod math_render;
45mod math_unbalanced_braces;
46mod math_unbalanced_delim;
47mod math_unbalanced_env;
48mod orphan_reference_link;
49mod stray_dollar;
50mod subscript_damage;
51mod table_pipe_spacing;
52mod trailing_whitespace;
53mod unbalanced_backtick;
54mod unicodeable_subscript;
55
56use crate::rule::LintRule;
57use crate::rule_set::RuleSet;
58
59pub use adjacent_code::AdjacentCodeNoSpace;
60pub use bare_url::BareUrl;
61pub use duplicate_heading::DuplicateHeading;
62pub use duplicate_link_label::DuplicateLinkLabel;
63pub use escaped_emphasis::EscapedEmphasis;
64pub use heading_punctuation::HeadingPunctuation;
65pub use inconsistent_list_marker::InconsistentListMarker;
66pub use info_string_typo::InfoStringTypo;
67pub use latex_command::LatexCommand;
68pub use list_tightness_flipped::ListTightnessFlipped;
69pub use math_render::RenderCompat;
70pub use math_unbalanced_braces::MathUnbalancedBraces;
71pub use math_unbalanced_delim::MathUnbalancedDelim;
72pub use math_unbalanced_env::MathUnbalancedEnv;
73pub use orphan_reference_link::OrphanReferenceLink;
74pub use stray_dollar::StrayDollar;
75pub use subscript_damage::SubscriptDamage;
76pub use table_pipe_spacing::TablePipeSpacing;
77pub use trailing_whitespace::TrailingWhitespace;
78pub use unbalanced_backtick::UnbalancedBacktick;
79pub use unicodeable_subscript::UnicodeableSubscript;
80
81/// Every stdlib rule's kebab-case name, in registration order.
82///
83/// Parallel to the boxed-rule registry: the [`LintRule::name`] trait signature
84/// returns `&str` (not `&'static str`) so user rules can borrow from
85/// `self`, which means stdlib names can't be lifted off the rule
86/// instances at compile time. The test
87/// `names_match_all_boxed` catches drift between this array and the
88/// rules themselves.
89pub const NAMES: &[&str] = &[
90    "unbalanced-backtick",
91    "math/unbalanced-delim",
92    "math/unbalanced-env",
93    "math/unbalanced-braces",
94    "math/render-compat",
95    "adjacent-code-no-space",
96    "heading-punctuation",
97    "orphan-reference-link",
98    "duplicate-link-label",
99    "bare-url",
100    "trailing-whitespace",
101    "inconsistent-list-marker",
102    "table-pipe-spacing",
103    "list-tightness-flipped",
104    "duplicate-heading",
105    "unicodeable-subscript",
106    "info-string-typo",
107    "stray-dollar",
108    "latex-command",
109    "escaped-emphasis",
110    "subscript-damage",
111];
112
113/// Iterator over every stdlib rule's kebab-case name. Used by the
114/// suppression-map builder to validate names in `<!-- mdwright: ... -->`
115/// comments without instantiating every rule.
116pub fn names() -> impl Iterator<Item = &'static str> {
117    NAMES.iter().copied()
118}
119
120/// Construct every stdlib rule once. Used as the source-of-truth for
121/// [`all`], [`defaults`], and [`by_name`].
122fn all_boxed() -> Vec<Box<dyn LintRule>> {
123    vec![
124        Box::new(UnbalancedBacktick),
125        Box::new(MathUnbalancedDelim),
126        Box::new(MathUnbalancedEnv),
127        Box::new(MathUnbalancedBraces),
128        Box::new(RenderCompat::new()),
129        Box::new(AdjacentCodeNoSpace),
130        Box::new(HeadingPunctuation),
131        Box::new(OrphanReferenceLink),
132        Box::new(DuplicateLinkLabel),
133        Box::new(BareUrl),
134        Box::new(TrailingWhitespace),
135        Box::new(InconsistentListMarker),
136        Box::new(TablePipeSpacing),
137        Box::new(ListTightnessFlipped),
138        Box::new(DuplicateHeading),
139        Box::new(UnicodeableSubscript),
140        Box::new(InfoStringTypo::new()),
141        Box::new(StrayDollar),
142        Box::new(LatexCommand),
143        Box::new(EscapedEmphasis),
144        Box::new(SubscriptDamage),
145    ]
146}
147
148/// Every stdlib rule, including the default-off ones.
149#[must_use]
150pub fn all() -> RuleSet {
151    let mut rs = RuleSet::new();
152    for rule in all_boxed() {
153        // Stdlib rules have stable, unique names. Duplicate
154        // registration here would be a programming error in this
155        // crate, not a user mistake.
156        // Stdlib rules have unique kebab-case names by construction;
157        // a duplicate here is a programming error in this crate.
158        let _unused = rs.add(rule);
159    }
160    rs
161}
162
163/// The curated default-on subset.
164#[must_use]
165pub fn defaults() -> RuleSet {
166    let mut rs = RuleSet::new();
167    for rule in all_boxed() {
168        if rule.is_default() {
169            // Stdlib rules have unique kebab-case names by construction;
170            // a duplicate here is a programming error in this crate.
171            let _unused = rs.add(rule);
172        }
173    }
174    rs
175}
176
177/// Construct a fresh instance of one stdlib rule by kebab-case name.
178/// Returns `None` if `name` is not a stdlib rule. Used by the CLI's
179/// `--rules` parser to look up `+rule-name` modifiers.
180#[must_use]
181pub fn by_name(name: &str) -> Option<Box<dyn LintRule>> {
182    all_boxed().into_iter().find(|r| r.name() == name)
183}
184
185#[cfg(test)]
186mod tests {
187    use super::{all, by_name, defaults};
188
189    #[test]
190    fn defaults_excludes_opt_in() {
191        let rs = defaults();
192        assert!(rs.contains("unbalanced-backtick"));
193        assert!(rs.contains("heading-punctuation"));
194        assert!(!rs.contains("stray-dollar"));
195        assert!(!rs.contains("latex-command"));
196        assert!(!rs.contains("escaped-emphasis"));
197        assert!(!rs.contains("subscript-damage"));
198    }
199
200    #[test]
201    fn all_includes_everything() {
202        let rs = all();
203        assert!(rs.contains("stray-dollar"));
204        assert!(rs.contains("subscript-damage"));
205        assert!(rs.contains("math/unbalanced-delim"));
206        assert!(rs.contains("math/unbalanced-env"));
207        assert!(rs.contains("math/unbalanced-braces"));
208        assert!(rs.contains("math/render-compat"));
209        assert!(rs.contains("table-pipe-spacing"));
210        assert!(rs.len() == 21);
211    }
212
213    #[test]
214    fn by_name_known() {
215        assert!(by_name("unbalanced-backtick").is_some());
216        assert!(by_name("escaped-emphasis").is_some());
217        assert!(by_name("does-not-exist").is_none());
218    }
219
220    #[test]
221    fn names_match_all_boxed() {
222        let from_rules: std::collections::BTreeSet<String> =
223            super::all_boxed().iter().map(|r| r.name().to_owned()).collect();
224        let from_const: std::collections::BTreeSet<String> = super::NAMES.iter().map(|s| (*s).to_owned()).collect();
225        assert_eq!(from_rules, from_const, "stdlib::NAMES drift");
226    }
227}