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