1use std::collections::BTreeMap;
4
5use serde::Deserialize;
6
7use crate::meta::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,
23 OptionIf,
25 AmbiguousTry,
27 AmbiguousMethod,
29 SkippedMod,
31 ResultIf,
33 ErrNil,
35 OptionTry,
37 IntegerDiv,
39 StructRest,
41 NumericCast,
43 TodoMacro,
45 StorageIndex,
47}
48
49impl LintId {
50 #[must_use]
52 pub const fn as_str(self) -> &'static str {
53 match self {
54 Self::Unwrap => "unwrap",
55 Self::Expect => "expect",
56 Self::FormatSpec => "format_spec",
57 Self::VariableIndex => "variable_index",
58 Self::IdentificationCtor => "identification_ctor",
59 Self::OptionIf => "option_if",
60 Self::AmbiguousTry => "ambiguous_try",
61 Self::AmbiguousMethod => "ambiguous_method",
62 Self::SkippedMod => "skipped_mod",
63 Self::ResultIf => "result_if",
64 Self::ErrNil => "err_nil",
65 Self::OptionTry => "option_try",
66 Self::IntegerDiv => "integer_div",
67 Self::StructRest => "struct_rest",
68 Self::NumericCast => "numeric_cast",
69 Self::TodoMacro => "todo_macro",
70 Self::StorageIndex => "storage_index",
71 }
72 }
73
74 #[must_use]
76 pub const fn code(self) -> &'static str {
77 match self {
78 Self::Unwrap => "E0001",
79 Self::Expect => "E0002",
80 Self::FormatSpec => "E0003",
81 Self::VariableIndex => "E0004",
82 Self::IdentificationCtor => "E0005",
83 Self::OptionIf => "E0006",
84 Self::AmbiguousTry => "E0007",
85 Self::AmbiguousMethod => "E0008",
86 Self::SkippedMod => "E0009",
87 Self::ResultIf => "E0010",
88 Self::ErrNil => "E0011",
89 Self::OptionTry => "E0012",
90 Self::IntegerDiv => "E0013",
91 Self::StructRest => "E0014",
92 Self::NumericCast => "E0015",
93 Self::TodoMacro => "E0016",
94 Self::StorageIndex => "E0017",
95 }
96 }
97
98 #[must_use]
105 pub const fn default_level(self) -> LintLevel {
106 match self {
107 Self::FormatSpec | Self::IntegerDiv | Self::NumericCast | Self::StorageIndex => {
108 LintLevel::Warn
109 }
110 Self::Unwrap
111 | Self::Expect
112 | Self::VariableIndex
113 | Self::OptionIf
114 | Self::AmbiguousTry
115 | Self::AmbiguousMethod
116 | Self::SkippedMod
117 | Self::ResultIf
118 | Self::ErrNil
119 | Self::OptionTry
120 | Self::StructRest
121 | Self::TodoMacro => LintLevel::Deny,
122 Self::IdentificationCtor => LintLevel::Allow,
124 }
125 }
126
127 #[must_use]
129 pub const fn help(self) -> &'static str {
130 match self {
131 Self::Unwrap => "use `if let Some(x) = ...` (or set `[lints] unwrap = \"allow\"`)",
132 Self::Expect => "use `if let Some(x) = ...` (or set `[lints] expect = \"allow\"`)",
133 #[allow(clippy::literal_string_with_formatting_args)]
134 Self::FormatSpec => "only `{}`, `{:?}`, and `{:#?}` are supported when lowering",
135 Self::VariableIndex => {
136 "literal indices are shifted `n -> n+1`; pass a 1-based index or use a literal"
137 }
138 Self::IdentificationCtor => {
139 "Identification constructors (`ForceID::Name(...)`) now lower to the payload; this lint is unused"
140 }
141 Self::OptionIf => {
142 "use `if let Some(x) = opt` or `opt.is_some()` (Lua truthiness skips `Some(false)`)"
143 }
144 Self::AmbiguousTry => {
145 "annotate as `Result` / `Option`, or convert with `.ok_or(...)?` for Options"
146 }
147 Self::AmbiguousMethod => {
148 "bind with an explicit `Result` / `Option` type so the correct helper is chosen"
149 }
150 Self::SkippedMod => {
151 "add `#[factorio_rs::export]` on the inline mod, or move items to a file module"
152 }
153 Self::ResultIf => {
154 "use `if let Ok(x) = result` or `result.is_ok()` (a Result table is always truthy in Lua)"
155 }
156 Self::ErrNil => {
157 "use a non-nil error payload (`String`, number, table); `Err(nil)` looks like Ok"
158 }
159 Self::OptionTry => {
160 "bind `let x: Option<_> = api(...); x?` or use `.ok_or(...)?`; for Result calls bind `let r: Result<...> = ...; r?`"
161 }
162 Self::IntegerDiv => {
163 "Lua `/` is float division; use a float operand (`n / 2.0`) or set `[lints] integer_div = \"allow\"`"
164 }
165 Self::StructRest => {
166 "only `..Default::default()` is ignored on purpose; copy fields explicitly or use that form"
167 }
168 Self::NumericCast => {
169 "Lua has no integer cast; remove `as` or use explicit math if you need clamping"
170 }
171 Self::TodoMacro => {
172 "use `panic!(\"...\")` (lowers to `error(...)`) or finish the code path"
173 }
174 Self::StorageIndex => {
175 "use `storage.get::<T>(\"key\")` / `storage.set(\"key\", value)` for typed access"
176 }
177 }
178 }
179
180 #[must_use]
182 pub const fn all() -> &'static [Self] {
183 &[
184 Self::Unwrap,
185 Self::Expect,
186 Self::FormatSpec,
187 Self::VariableIndex,
188 Self::IdentificationCtor,
189 Self::OptionIf,
190 Self::AmbiguousTry,
191 Self::AmbiguousMethod,
192 Self::SkippedMod,
193 Self::ResultIf,
194 Self::ErrNil,
195 Self::OptionTry,
196 Self::IntegerDiv,
197 Self::StructRest,
198 Self::NumericCast,
199 Self::TodoMacro,
200 Self::StorageIndex,
201 ]
202 }
203
204 #[must_use]
206 pub fn from_config_str(name: &str) -> Option<Self> {
207 Self::all().iter().copied().find(|id| id.as_str() == name)
208 }
209}
210
211impl std::fmt::Display for LintId {
212 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
213 f.write_str(self.code())
214 }
215}
216
217#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)]
219#[serde(rename_all = "snake_case")]
220pub enum LintLevel {
221 Allow,
223 Warn,
225 #[default]
227 Deny,
228}
229
230impl LintLevel {
231 #[must_use]
232 pub const fn as_str(self) -> &'static str {
233 match self {
234 Self::Allow => "allow",
235 Self::Warn => "warn",
236 Self::Deny => "deny",
237 }
238 }
239}
240
241#[derive(Debug, Clone, PartialEq, Eq)]
243pub struct LintConfig {
244 levels: BTreeMap<LintId, LintLevel>,
245}
246
247impl Default for LintConfig {
248 fn default() -> Self {
249 Self {
250 levels: LintId::all()
251 .iter()
252 .map(|id| (*id, id.default_level()))
253 .collect(),
254 }
255 }
256}
257
258impl LintConfig {
259 #[must_use]
261 pub fn allow_all() -> Self {
262 Self {
263 levels: LintId::all()
264 .iter()
265 .map(|id| (*id, LintLevel::Allow))
266 .collect(),
267 }
268 }
269
270 pub fn with_overrides(
275 mut self,
276 overrides: &BTreeMap<String, LintLevel>,
277 ) -> Result<Self, String> {
278 for (name, level) in overrides {
279 let Some(id) = LintId::from_config_str(name) else {
280 let known = LintId::all()
281 .iter()
282 .copied()
283 .map(LintId::as_str)
284 .collect::<Vec<_>>()
285 .join(", ");
286 return Err(format!("unknown lint `{name}` (known: {known})"));
287 };
288 self.levels.insert(id, *level);
289 }
290 Ok(self)
291 }
292
293 #[must_use]
294 pub fn level(&self, id: LintId) -> LintLevel {
295 self.levels
296 .get(&id)
297 .copied()
298 .unwrap_or_else(|| id.default_level())
299 }
300
301 #[must_use]
302 pub fn is_allowed(&self, id: LintId) -> bool {
303 matches!(self.level(id), LintLevel::Allow)
304 }
305
306 #[must_use]
308 pub fn allowing(mut self, id: LintId) -> Self {
309 self.levels.insert(id, LintLevel::Allow);
310 self
311 }
312}
313
314#[derive(Debug, Clone, PartialEq, Eq)]
316pub struct Diagnostic {
317 pub id: LintId,
318 pub level: LintLevel,
319 pub message: String,
320 pub loc: SourceLoc,
321}
322
323impl Diagnostic {
324 #[must_use]
325 pub fn new(
326 id: LintId,
327 level: LintLevel,
328 message: impl Into<String>,
329 loc: impl Into<SourceLoc>,
330 ) -> Self {
331 Self {
332 id,
333 level,
334 message: message.into(),
335 loc: loc.into(),
336 }
337 }
338
339 #[must_use]
340 pub const fn is_error(&self) -> bool {
341 matches!(self.level, LintLevel::Deny)
342 }
343}
344
345impl std::fmt::Display for Diagnostic {
346 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
347 write!(
348 f,
349 "{} {}: {} ({}) at {}",
350 self.level.as_str(),
351 self.id.code(),
352 self.message,
353 self.id.as_str(),
354 self.loc
355 )
356 }
357}
358
359#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
361pub struct LintsTable {
362 #[serde(flatten)]
363 pub levels: BTreeMap<String, LintLevel>,
364}
365
366impl LintsTable {
367 pub fn into_config(self) -> Result<LintConfig, String> {
372 LintConfig::default().with_overrides(&self.levels)
373 }
374}
375
376#[cfg(test)]
377mod tests {
378 use super::*;
379
380 #[test]
381 fn default_levels() {
382 let config = LintConfig::default();
383 assert_eq!(config.level(LintId::Unwrap), LintLevel::Deny);
384 assert_eq!(config.level(LintId::FormatSpec), LintLevel::Warn);
385 assert_eq!(config.level(LintId::ResultIf), LintLevel::Deny);
386 assert_eq!(config.level(LintId::ErrNil), LintLevel::Deny);
387 assert_eq!(config.level(LintId::OptionTry), LintLevel::Deny);
388 assert_eq!(config.level(LintId::IntegerDiv), LintLevel::Warn);
389 assert_eq!(config.level(LintId::StructRest), LintLevel::Deny);
390 assert_eq!(config.level(LintId::NumericCast), LintLevel::Warn);
391 assert_eq!(config.level(LintId::TodoMacro), LintLevel::Deny);
392 assert_eq!(config.level(LintId::StorageIndex), LintLevel::Warn);
393 assert_eq!(config.level(LintId::IdentificationCtor), LintLevel::Allow);
394 }
395
396 #[test]
397 fn overrides_allow_unwrap() {
398 let mut table = BTreeMap::new();
399 table.insert("unwrap".to_string(), LintLevel::Allow);
400 let config = LintConfig::default().with_overrides(&table).unwrap();
401 assert!(config.is_allowed(LintId::Unwrap));
402 assert!(!config.is_allowed(LintId::Expect));
403 }
404
405 #[test]
406 fn rejects_unknown_lint_name() {
407 let mut table = BTreeMap::new();
408 table.insert("not_a_lint".to_string(), LintLevel::Allow);
409 let err = LintConfig::default().with_overrides(&table).unwrap_err();
410 assert!(err.contains("not_a_lint"));
411 }
412
413 #[test]
414 fn lint_codes_are_stable() {
415 assert_eq!(LintId::Unwrap.code(), "E0001");
416 assert_eq!(LintId::Expect.code(), "E0002");
417 assert_eq!(LintId::FormatSpec.code(), "E0003");
418 assert_eq!(LintId::VariableIndex.code(), "E0004");
419 assert_eq!(LintId::IdentificationCtor.code(), "E0005");
420 assert_eq!(LintId::OptionIf.code(), "E0006");
421 assert_eq!(LintId::AmbiguousTry.code(), "E0007");
422 assert_eq!(LintId::AmbiguousMethod.code(), "E0008");
423 assert_eq!(LintId::SkippedMod.code(), "E0009");
424 assert_eq!(LintId::ResultIf.code(), "E0010");
425 assert_eq!(LintId::ErrNil.code(), "E0011");
426 assert_eq!(LintId::OptionTry.code(), "E0012");
427 assert_eq!(LintId::IntegerDiv.code(), "E0013");
428 assert_eq!(LintId::StructRest.code(), "E0014");
429 assert_eq!(LintId::NumericCast.code(), "E0015");
430 assert_eq!(LintId::TodoMacro.code(), "E0016");
431 assert_eq!(LintId::StorageIndex.code(), "E0017");
432 }
433}