1use std::collections::BTreeMap;
4
5use serde::Deserialize;
6
7use crate::span::SourceLoc;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
11pub enum LintId {
12 Unwrap,
14 Expect,
16 FormatSpec,
18 VariableIndex,
20 IdentificationCtor,
22}
23
24impl LintId {
25 #[must_use]
27 pub const fn as_str(self) -> &'static str {
28 match self {
29 Self::Unwrap => "unwrap",
30 Self::Expect => "expect",
31 Self::FormatSpec => "format_spec",
32 Self::VariableIndex => "variable_index",
33 Self::IdentificationCtor => "identification_ctor",
34 }
35 }
36
37 #[must_use]
39 pub const fn code(self) -> &'static str {
40 match self {
41 Self::Unwrap => "E0001",
42 Self::Expect => "E0002",
43 Self::FormatSpec => "E0003",
44 Self::VariableIndex => "E0004",
45 Self::IdentificationCtor => "E0005",
46 }
47 }
48
49 #[must_use]
55 pub const fn default_level(self) -> LintLevel {
56 match self {
57 Self::FormatSpec => LintLevel::Warn,
58 Self::Unwrap | Self::Expect | Self::VariableIndex | Self::IdentificationCtor => {
59 LintLevel::Deny
60 }
61 }
62 }
63
64 #[must_use]
66 pub const fn help(self) -> &'static str {
67 match self {
68 Self::Unwrap => "use `if let Some(x) = ...` (or set `[lints] unwrap = \"allow\"`)",
69 Self::Expect => "use `if let Some(x) = ...` (or set `[lints] expect = \"allow\"`)",
70 #[allow(clippy::literal_string_with_formatting_args)]
71 Self::FormatSpec => "only `{}`, `{:?}`, and `{:#?}` are supported when lowering",
72 Self::VariableIndex => {
73 "literal indices are shifted `n -> n+1`; pass a 1-based index or use a literal"
74 }
75 Self::IdentificationCtor => {
76 "pass a payload with `.into()` instead, e.g. `force.into()` or `\"enemy\".into()`"
77 }
78 }
79 }
80
81 #[must_use]
83 pub const fn all() -> &'static [Self] {
84 &[
85 Self::Unwrap,
86 Self::Expect,
87 Self::FormatSpec,
88 Self::VariableIndex,
89 Self::IdentificationCtor,
90 ]
91 }
92
93 #[must_use]
95 pub fn from_config_str(name: &str) -> Option<Self> {
96 Self::all().iter().copied().find(|id| id.as_str() == name)
97 }
98}
99
100impl std::fmt::Display for LintId {
101 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
102 f.write_str(self.code())
103 }
104}
105
106#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)]
108#[serde(rename_all = "snake_case")]
109pub enum LintLevel {
110 Allow,
112 Warn,
114 #[default]
116 Deny,
117}
118
119impl LintLevel {
120 #[must_use]
121 pub const fn as_str(self) -> &'static str {
122 match self {
123 Self::Allow => "allow",
124 Self::Warn => "warn",
125 Self::Deny => "deny",
126 }
127 }
128}
129
130#[derive(Debug, Clone, PartialEq, Eq)]
132pub struct LintConfig {
133 levels: BTreeMap<LintId, LintLevel>,
134}
135
136impl Default for LintConfig {
137 fn default() -> Self {
138 Self {
139 levels: LintId::all()
140 .iter()
141 .map(|id| (*id, id.default_level()))
142 .collect(),
143 }
144 }
145}
146
147impl LintConfig {
148 #[must_use]
150 pub fn allow_all() -> Self {
151 Self {
152 levels: LintId::all()
153 .iter()
154 .map(|id| (*id, LintLevel::Allow))
155 .collect(),
156 }
157 }
158
159 pub fn with_overrides(
164 mut self,
165 overrides: &BTreeMap<String, LintLevel>,
166 ) -> Result<Self, String> {
167 for (name, level) in overrides {
168 let Some(id) = LintId::from_config_str(name) else {
169 let known = LintId::all()
170 .iter()
171 .copied()
172 .map(LintId::as_str)
173 .collect::<Vec<_>>()
174 .join(", ");
175 return Err(format!("unknown lint `{name}` (known: {known})"));
176 };
177 self.levels.insert(id, *level);
178 }
179 Ok(self)
180 }
181
182 #[must_use]
183 pub fn level(&self, id: LintId) -> LintLevel {
184 self.levels
185 .get(&id)
186 .copied()
187 .unwrap_or_else(|| id.default_level())
188 }
189
190 #[must_use]
191 pub fn is_allowed(&self, id: LintId) -> bool {
192 matches!(self.level(id), LintLevel::Allow)
193 }
194
195 #[must_use]
197 pub fn allowing(mut self, id: LintId) -> Self {
198 self.levels.insert(id, LintLevel::Allow);
199 self
200 }
201}
202
203#[derive(Debug, Clone, PartialEq, Eq)]
205pub struct Diagnostic {
206 pub id: LintId,
207 pub level: LintLevel,
208 pub message: String,
209 pub loc: SourceLoc,
210}
211
212impl Diagnostic {
213 #[must_use]
214 pub fn new(
215 id: LintId,
216 level: LintLevel,
217 message: impl Into<String>,
218 loc: impl Into<SourceLoc>,
219 ) -> Self {
220 Self {
221 id,
222 level,
223 message: message.into(),
224 loc: loc.into(),
225 }
226 }
227
228 #[must_use]
229 pub const fn is_error(&self) -> bool {
230 matches!(self.level, LintLevel::Deny)
231 }
232}
233
234impl std::fmt::Display for Diagnostic {
235 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
236 write!(
237 f,
238 "{} {}: {} ({}) at {}",
239 self.level.as_str(),
240 self.id.code(),
241 self.message,
242 self.id.as_str(),
243 self.loc
244 )
245 }
246}
247
248#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
250pub struct LintsTable {
251 #[serde(flatten)]
252 pub levels: BTreeMap<String, LintLevel>,
253}
254
255impl LintsTable {
256 pub fn into_config(self) -> Result<LintConfig, String> {
261 LintConfig::default().with_overrides(&self.levels)
262 }
263}
264
265#[cfg(test)]
266mod tests {
267 use super::*;
268
269 #[test]
270 fn default_levels() {
271 let config = LintConfig::default();
272 assert_eq!(config.level(LintId::Unwrap), LintLevel::Deny);
273 assert_eq!(config.level(LintId::FormatSpec), LintLevel::Warn);
274 }
275
276 #[test]
277 fn overrides_allow_unwrap() {
278 let mut table = BTreeMap::new();
279 table.insert("unwrap".to_string(), LintLevel::Allow);
280 let config = LintConfig::default().with_overrides(&table).unwrap();
281 assert!(config.is_allowed(LintId::Unwrap));
282 assert!(!config.is_allowed(LintId::Expect));
283 }
284
285 #[test]
286 fn rejects_unknown_lint_name() {
287 let mut table = BTreeMap::new();
288 table.insert("not_a_lint".to_string(), LintLevel::Allow);
289 let err = LintConfig::default().with_overrides(&table).unwrap_err();
290 assert!(err.contains("not_a_lint"));
291 }
292
293 #[test]
294 fn lint_codes_are_stable() {
295 assert_eq!(LintId::Unwrap.code(), "E0001");
296 assert_eq!(LintId::Expect.code(), "E0002");
297 assert_eq!(LintId::FormatSpec.code(), "E0003");
298 assert_eq!(LintId::VariableIndex.code(), "E0004");
299 assert_eq!(LintId::IdentificationCtor.code(), "E0005");
300 }
301}