shuck_linter/suppression/
shellcheck_map.rs1use 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 (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#[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 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 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}