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