Skip to main content

shuck_linter/suppression/
shellcheck_map.rs

1use rustc_hash::FxHashMap;
2
3use crate::{Rule, code_to_rule};
4
5include!(concat!(env!("OUT_DIR"), "/shellcheck_map_data.rs"));
6
7const SUPPRESSION_ALIAS_CODES: &[(u32, Rule)] = &[
8    // Older ShellCheck compatibility codes still appear in user suppressions.
9    (2268, Rule::BackslashBeforeCommand),
10    (2250, Rule::PatternWithVariable),
11    (2350, Rule::XargsWithInlineReplace),
12    (2316, Rule::BacktickInCommandPosition),
13    (2362, Rule::LocalDeclareCombined),
14    (2321, Rule::FunctionKeywordInSh),
15    (2234, Rule::SingleTestSubshell),
16    (2351, Rule::XPrefixInTest),
17    (3084, Rule::SourceInsideFunctionInSh),
18    (2365, Rule::UnreachableAfterExit),
19];
20
21/// Maps ShellCheck SC codes to Shuck rules.
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct ShellCheckCodeMap {
24    sc_to_rule: FxHashMap<u32, Rule>,
25    rule_to_sc: FxHashMap<Rule, u32>,
26    suppression_aliases: Vec<(u32, Rule)>,
27}
28
29impl ShellCheckCodeMap {
30    pub fn new() -> Self {
31        Self::default()
32    }
33
34    pub fn mappings(&self) -> impl Iterator<Item = (u32, Rule)> + '_ {
35        self.sc_to_rule
36            .iter()
37            .map(|(sc_code, rule)| (*sc_code, *rule))
38    }
39
40    pub fn code_for_rule(&self, rule: Rule) -> Option<u32> {
41        self.rule_to_sc.get(&rule).copied()
42    }
43
44    /// Look up a ShellCheck code like `SC2086`.
45    pub fn resolve(&self, sc_code: &str) -> Option<Rule> {
46        Self::parse_code(sc_code).and_then(|number| self.sc_to_rule.get(&number).copied())
47    }
48
49    /// Look up all ShellCheck mappings for a code like `SC2086`.
50    pub fn resolve_all(&self, sc_code: &str) -> Vec<Rule> {
51        let Some(number) = Self::parse_code(sc_code) else {
52            return Vec::new();
53        };
54
55        let mut rules = self
56            .sc_to_rule
57            .get(&number)
58            .copied()
59            .into_iter()
60            .collect::<Vec<_>>();
61        for &(code, rule) in &self.suppression_aliases {
62            if code == number && !rules.contains(&rule) {
63                rules.push(rule);
64            }
65        }
66        rules
67    }
68
69    fn parse_code(sc_code: &str) -> Option<u32> {
70        sc_code
71            .strip_prefix("SC")
72            .or_else(|| sc_code.strip_prefix("sc"))
73            .unwrap_or(sc_code)
74            .parse()
75            .ok()
76    }
77}
78
79impl Default for ShellCheckCodeMap {
80    fn default() -> Self {
81        let mut sc_to_rule = FxHashMap::default();
82        let mut rule_to_sc = FxHashMap::default();
83
84        for &(rule_code, sc_code) in RULE_SHELLCHECK_CODES {
85            let Some(rule) = code_to_rule(rule_code) else {
86                continue;
87            };
88
89            let old_rule = sc_to_rule.insert(sc_code, rule);
90            assert!(
91                old_rule.is_none(),
92                "duplicate ShellCheck mapping for SC{sc_code}: {old_rule:?} and {rule:?}"
93            );
94
95            let old_code = rule_to_sc.insert(rule, sc_code);
96            assert!(
97                old_code.is_none(),
98                "duplicate Shuck mapping for {rule:?}: {old_code:?} and {sc_code}"
99            );
100        }
101
102        Self {
103            sc_to_rule,
104            rule_to_sc,
105            suppression_aliases: SUPPRESSION_ALIAS_CODES.to_vec(),
106        }
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use std::collections::HashSet;
113
114    use super::*;
115
116    #[test]
117    fn resolves_known_codes_and_ignores_unknown_ones() {
118        let map = ShellCheckCodeMap::default();
119
120        assert_eq!(map.resolve("SC2034"), Some(Rule::UnusedAssignment));
121        assert_eq!(map.resolve("2034"), Some(Rule::UnusedAssignment));
122        assert_eq!(map.resolve("sc2154"), Some(Rule::UndefinedVariable));
123        assert_eq!(map.resolve("SC2086"), Some(Rule::UnquotedExpansion));
124        assert_eq!(map.resolve("SC1012"), Some(Rule::LiteralControlEscape));
125        assert_eq!(map.resolve("SC1046"), Some(Rule::UnterminatedIf));
126        assert_eq!(map.resolve("SC2238"), Some(Rule::RedirectToCommandName));
127        assert_eq!(map.resolve("SC2063"), Some(Rule::LeadingGlobInGrepPattern));
128        assert_eq!(map.resolve("SC2211"), Some(Rule::BareGlobCommandPath));
129        assert_eq!(map.resolve("SC2215"), Some(Rule::DiffMarkerLine));
130        assert_eq!(map.resolve("SC3034"), Some(Rule::BashFileSlurp));
131        assert_eq!(map.resolve("SC2096"), Some(Rule::DuplicateShebangFlag));
132        assert_eq!(map.resolve("SC2318"), Some(Rule::LocalCrossReference));
133        assert_eq!(map.resolve("SC2252"), Some(Rule::TautologyChain));
134        assert_eq!(map.resolve("SC2261"), Some(Rule::DuplicateRedirect));
135        assert_eq!(map.resolve("SC2260"), Some(Rule::RedirectBeforePipe));
136        assert_eq!(
137            map.resolve("SC1087"),
138            Some(Rule::BraceVariableBeforeBracket)
139        );
140        assert_eq!(map.resolve("SC2280"), Some(Rule::AssignSpecialZero));
141        assert_eq!(map.resolve("SC2317"), Some(Rule::UnreachableAfterExit));
142        assert_eq!(
143            map.resolve("SC2218"),
144            Some(Rule::FunctionCalledBeforeDefined)
145        );
146        assert_eq!(map.resolve("SC7777"), None);
147    }
148
149    #[test]
150    fn resolve_all_keeps_legacy_suppression_aliases() {
151        let map = ShellCheckCodeMap::default();
152
153        assert_eq!(map.resolve_all("SC2086"), vec![Rule::UnquotedExpansion]);
154        assert_eq!(
155            map.resolve_all("SC2268"),
156            vec![Rule::XPrefixInTest, Rule::BackslashBeforeCommand]
157        );
158        assert_eq!(
159            map.resolve_all("SC2316"),
160            vec![Rule::LocalDeclareCombined, Rule::BacktickInCommandPosition]
161        );
162        assert_eq!(map.resolve_all("SC2362"), vec![Rule::LocalDeclareCombined]);
163        assert_eq!(map.resolve_all("SC2250"), vec![Rule::PatternWithVariable]);
164        assert_eq!(
165            map.resolve_all("SC2267"),
166            vec![Rule::XargsWithInlineReplace]
167        );
168        assert_eq!(
169            map.resolve_all("SC2321"),
170            vec![Rule::ArrayIndexArithmetic, Rule::FunctionKeywordInSh]
171        );
172        assert_eq!(map.resolve_all("SC2234"), vec![Rule::SingleTestSubshell]);
173        assert_eq!(
174            map.resolve_all("SC3084"),
175            vec![Rule::SourceInsideFunctionInSh]
176        );
177        assert_eq!(map.resolve_all("SC2260"), vec![Rule::RedirectBeforePipe]);
178        assert_eq!(map.resolve_all("SC2365"), vec![Rule::UnreachableAfterExit]);
179    }
180
181    #[test]
182    fn mappings_are_unique_in_both_directions() {
183        let map = ShellCheckCodeMap::default();
184
185        let sc_codes = map
186            .mappings()
187            .map(|(sc_code, _)| sc_code)
188            .collect::<Vec<_>>();
189        let rules = map.mappings().map(|(_, rule)| rule).collect::<Vec<_>>();
190
191        assert_eq!(
192            sc_codes.len(),
193            sc_codes.iter().copied().collect::<HashSet<_>>().len()
194        );
195        assert_eq!(
196            rules.len(),
197            rules.iter().copied().collect::<HashSet<_>>().len()
198        );
199    }
200
201    #[test]
202    fn can_reverse_lookup_canonical_codes() {
203        let map = ShellCheckCodeMap::default();
204
205        assert_eq!(map.code_for_rule(Rule::UnusedAssignment), Some(2034));
206        assert_eq!(map.code_for_rule(Rule::RedirectToCommandName), Some(2238));
207        assert_eq!(map.code_for_rule(Rule::DuplicateShebangFlag), Some(2096));
208        assert_eq!(map.code_for_rule(Rule::BashFileSlurp), Some(3034));
209        assert_eq!(map.code_for_rule(Rule::UnreachableAfterExit), Some(2317));
210        assert_eq!(map.code_for_rule(Rule::LiteralControlEscape), Some(1012));
211        assert_eq!(
212            map.code_for_rule(Rule::BraceVariableBeforeBracket),
213            Some(1087)
214        );
215        assert_eq!(
216            map.code_for_rule(Rule::FunctionCalledBeforeDefined),
217            Some(2218)
218        );
219        assert_eq!(map.code_for_rule(Rule::BackslashBeforeCommand), None);
220        assert_eq!(
221            map.code_for_rule(Rule::LeadingGlobInGrepPattern),
222            Some(2063)
223        );
224        assert_eq!(map.code_for_rule(Rule::MixedAndOrInCondition), None);
225    }
226}