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