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 OptionIf,
24 AmbiguousTry,
26 AmbiguousMethod,
28 SkippedMod,
30}
31
32impl LintId {
33 #[must_use]
35 pub const fn as_str(self) -> &'static str {
36 match self {
37 Self::Unwrap => "unwrap",
38 Self::Expect => "expect",
39 Self::FormatSpec => "format_spec",
40 Self::VariableIndex => "variable_index",
41 Self::IdentificationCtor => "identification_ctor",
42 Self::OptionIf => "option_if",
43 Self::AmbiguousTry => "ambiguous_try",
44 Self::AmbiguousMethod => "ambiguous_method",
45 Self::SkippedMod => "skipped_mod",
46 }
47 }
48
49 #[must_use]
51 pub const fn code(self) -> &'static str {
52 match self {
53 Self::Unwrap => "E0001",
54 Self::Expect => "E0002",
55 Self::FormatSpec => "E0003",
56 Self::VariableIndex => "E0004",
57 Self::IdentificationCtor => "E0005",
58 Self::OptionIf => "E0006",
59 Self::AmbiguousTry => "E0007",
60 Self::AmbiguousMethod => "E0008",
61 Self::SkippedMod => "E0009",
62 }
63 }
64
65 #[must_use]
71 pub const fn default_level(self) -> LintLevel {
72 match self {
73 Self::FormatSpec => LintLevel::Warn,
74 Self::Unwrap
75 | Self::Expect
76 | Self::VariableIndex
77 | Self::IdentificationCtor
78 | Self::OptionIf
79 | Self::AmbiguousTry
80 | Self::AmbiguousMethod
81 | Self::SkippedMod => LintLevel::Deny,
82 }
83 }
84
85 #[must_use]
87 pub const fn help(self) -> &'static str {
88 match self {
89 Self::Unwrap => "use `if let Some(x) = ...` (or set `[lints] unwrap = \"allow\"`)",
90 Self::Expect => "use `if let Some(x) = ...` (or set `[lints] expect = \"allow\"`)",
91 #[allow(clippy::literal_string_with_formatting_args)]
92 Self::FormatSpec => "only `{}`, `{:?}`, and `{:#?}` are supported when lowering",
93 Self::VariableIndex => {
94 "literal indices are shifted `n -> n+1`; pass a 1-based index or use a literal"
95 }
96 Self::IdentificationCtor => {
97 "pass a payload with `.into()` instead, e.g. `force.into()` or `\"enemy\".into()`"
98 }
99 Self::OptionIf => {
100 "use `if let Some(x) = opt` or `opt.is_some()` (Lua truthiness skips `Some(false)`)"
101 }
102 Self::AmbiguousTry => {
103 "annotate as `Result` / `Option`, or convert with `.ok_or(...)?` for Options"
104 }
105 Self::AmbiguousMethod => {
106 "bind with an explicit `Result` / `Option` type so the correct helper is chosen"
107 }
108 Self::SkippedMod => {
109 "add `#[factorio_rs::export]` on the inline mod, or move items to a file module"
110 }
111 }
112 }
113
114 #[must_use]
116 pub const fn all() -> &'static [Self] {
117 &[
118 Self::Unwrap,
119 Self::Expect,
120 Self::FormatSpec,
121 Self::VariableIndex,
122 Self::IdentificationCtor,
123 Self::OptionIf,
124 Self::AmbiguousTry,
125 Self::AmbiguousMethod,
126 Self::SkippedMod,
127 ]
128 }
129
130 #[must_use]
132 pub fn from_config_str(name: &str) -> Option<Self> {
133 Self::all().iter().copied().find(|id| id.as_str() == name)
134 }
135}
136
137impl std::fmt::Display for LintId {
138 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
139 f.write_str(self.code())
140 }
141}
142
143#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)]
145#[serde(rename_all = "snake_case")]
146pub enum LintLevel {
147 Allow,
149 Warn,
151 #[default]
153 Deny,
154}
155
156impl LintLevel {
157 #[must_use]
158 pub const fn as_str(self) -> &'static str {
159 match self {
160 Self::Allow => "allow",
161 Self::Warn => "warn",
162 Self::Deny => "deny",
163 }
164 }
165}
166
167#[derive(Debug, Clone, PartialEq, Eq)]
169pub struct LintConfig {
170 levels: BTreeMap<LintId, LintLevel>,
171}
172
173impl Default for LintConfig {
174 fn default() -> Self {
175 Self {
176 levels: LintId::all()
177 .iter()
178 .map(|id| (*id, id.default_level()))
179 .collect(),
180 }
181 }
182}
183
184impl LintConfig {
185 #[must_use]
187 pub fn allow_all() -> Self {
188 Self {
189 levels: LintId::all()
190 .iter()
191 .map(|id| (*id, LintLevel::Allow))
192 .collect(),
193 }
194 }
195
196 pub fn with_overrides(
201 mut self,
202 overrides: &BTreeMap<String, LintLevel>,
203 ) -> Result<Self, String> {
204 for (name, level) in overrides {
205 let Some(id) = LintId::from_config_str(name) else {
206 let known = LintId::all()
207 .iter()
208 .copied()
209 .map(LintId::as_str)
210 .collect::<Vec<_>>()
211 .join(", ");
212 return Err(format!("unknown lint `{name}` (known: {known})"));
213 };
214 self.levels.insert(id, *level);
215 }
216 Ok(self)
217 }
218
219 #[must_use]
220 pub fn level(&self, id: LintId) -> LintLevel {
221 self.levels
222 .get(&id)
223 .copied()
224 .unwrap_or_else(|| id.default_level())
225 }
226
227 #[must_use]
228 pub fn is_allowed(&self, id: LintId) -> bool {
229 matches!(self.level(id), LintLevel::Allow)
230 }
231
232 #[must_use]
234 pub fn allowing(mut self, id: LintId) -> Self {
235 self.levels.insert(id, LintLevel::Allow);
236 self
237 }
238}
239
240#[derive(Debug, Clone, PartialEq, Eq)]
242pub struct Diagnostic {
243 pub id: LintId,
244 pub level: LintLevel,
245 pub message: String,
246 pub loc: SourceLoc,
247}
248
249impl Diagnostic {
250 #[must_use]
251 pub fn new(
252 id: LintId,
253 level: LintLevel,
254 message: impl Into<String>,
255 loc: impl Into<SourceLoc>,
256 ) -> Self {
257 Self {
258 id,
259 level,
260 message: message.into(),
261 loc: loc.into(),
262 }
263 }
264
265 #[must_use]
266 pub const fn is_error(&self) -> bool {
267 matches!(self.level, LintLevel::Deny)
268 }
269}
270
271impl std::fmt::Display for Diagnostic {
272 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
273 write!(
274 f,
275 "{} {}: {} ({}) at {}",
276 self.level.as_str(),
277 self.id.code(),
278 self.message,
279 self.id.as_str(),
280 self.loc
281 )
282 }
283}
284
285#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
287pub struct LintsTable {
288 #[serde(flatten)]
289 pub levels: BTreeMap<String, LintLevel>,
290}
291
292impl LintsTable {
293 pub fn into_config(self) -> Result<LintConfig, String> {
298 LintConfig::default().with_overrides(&self.levels)
299 }
300}
301
302#[cfg(test)]
303mod tests {
304 use super::*;
305
306 #[test]
307 fn default_levels() {
308 let config = LintConfig::default();
309 assert_eq!(config.level(LintId::Unwrap), LintLevel::Deny);
310 assert_eq!(config.level(LintId::FormatSpec), LintLevel::Warn);
311 }
312
313 #[test]
314 fn overrides_allow_unwrap() {
315 let mut table = BTreeMap::new();
316 table.insert("unwrap".to_string(), LintLevel::Allow);
317 let config = LintConfig::default().with_overrides(&table).unwrap();
318 assert!(config.is_allowed(LintId::Unwrap));
319 assert!(!config.is_allowed(LintId::Expect));
320 }
321
322 #[test]
323 fn rejects_unknown_lint_name() {
324 let mut table = BTreeMap::new();
325 table.insert("not_a_lint".to_string(), LintLevel::Allow);
326 let err = LintConfig::default().with_overrides(&table).unwrap_err();
327 assert!(err.contains("not_a_lint"));
328 }
329
330 #[test]
331 fn lint_codes_are_stable() {
332 assert_eq!(LintId::Unwrap.code(), "E0001");
333 assert_eq!(LintId::Expect.code(), "E0002");
334 assert_eq!(LintId::FormatSpec.code(), "E0003");
335 assert_eq!(LintId::VariableIndex.code(), "E0004");
336 assert_eq!(LintId::IdentificationCtor.code(), "E0005");
337 assert_eq!(LintId::OptionIf.code(), "E0006");
338 assert_eq!(LintId::AmbiguousTry.code(), "E0007");
339 assert_eq!(LintId::AmbiguousMethod.code(), "E0008");
340 assert_eq!(LintId::SkippedMod.code(), "E0009");
341 }
342}