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