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