use std::collections::BTreeMap;
use serde::Deserialize;
use crate::span::SourceLoc;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum LintId {
Unwrap,
Expect,
FormatSpec,
VariableIndex,
IdentificationCtor,
OptionIf,
AmbiguousTry,
AmbiguousMethod,
SkippedMod,
}
impl LintId {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Unwrap => "unwrap",
Self::Expect => "expect",
Self::FormatSpec => "format_spec",
Self::VariableIndex => "variable_index",
Self::IdentificationCtor => "identification_ctor",
Self::OptionIf => "option_if",
Self::AmbiguousTry => "ambiguous_try",
Self::AmbiguousMethod => "ambiguous_method",
Self::SkippedMod => "skipped_mod",
}
}
#[must_use]
pub const fn code(self) -> &'static str {
match self {
Self::Unwrap => "E0001",
Self::Expect => "E0002",
Self::FormatSpec => "E0003",
Self::VariableIndex => "E0004",
Self::IdentificationCtor => "E0005",
Self::OptionIf => "E0006",
Self::AmbiguousTry => "E0007",
Self::AmbiguousMethod => "E0008",
Self::SkippedMod => "E0009",
}
}
#[must_use]
pub const fn default_level(self) -> LintLevel {
match self {
Self::FormatSpec => LintLevel::Warn,
Self::Unwrap
| Self::Expect
| Self::VariableIndex
| Self::IdentificationCtor
| Self::OptionIf
| Self::AmbiguousTry
| Self::AmbiguousMethod
| Self::SkippedMod => LintLevel::Deny,
}
}
#[must_use]
pub const fn help(self) -> &'static str {
match self {
Self::Unwrap => "use `if let Some(x) = ...` (or set `[lints] unwrap = \"allow\"`)",
Self::Expect => "use `if let Some(x) = ...` (or set `[lints] expect = \"allow\"`)",
#[allow(clippy::literal_string_with_formatting_args)]
Self::FormatSpec => "only `{}`, `{:?}`, and `{:#?}` are supported when lowering",
Self::VariableIndex => {
"literal indices are shifted `n -> n+1`; pass a 1-based index or use a literal"
}
Self::IdentificationCtor => {
"pass a payload with `.into()` instead, e.g. `force.into()` or `\"enemy\".into()`"
}
Self::OptionIf => {
"use `if let Some(x) = opt` or `opt.is_some()` (Lua truthiness skips `Some(false)`)"
}
Self::AmbiguousTry => {
"annotate as `Result` / `Option`, or convert with `.ok_or(...)?` for Options"
}
Self::AmbiguousMethod => {
"bind with an explicit `Result` / `Option` type so the correct helper is chosen"
}
Self::SkippedMod => {
"add `#[factorio_rs::export]` on the inline mod, or move items to a file module"
}
}
}
#[must_use]
pub const fn all() -> &'static [Self] {
&[
Self::Unwrap,
Self::Expect,
Self::FormatSpec,
Self::VariableIndex,
Self::IdentificationCtor,
Self::OptionIf,
Self::AmbiguousTry,
Self::AmbiguousMethod,
Self::SkippedMod,
]
}
#[must_use]
pub fn from_config_str(name: &str) -> Option<Self> {
Self::all().iter().copied().find(|id| id.as_str() == name)
}
}
impl std::fmt::Display for LintId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.code())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum LintLevel {
Allow,
Warn,
#[default]
Deny,
}
impl LintLevel {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Allow => "allow",
Self::Warn => "warn",
Self::Deny => "deny",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LintConfig {
levels: BTreeMap<LintId, LintLevel>,
}
impl Default for LintConfig {
fn default() -> Self {
Self {
levels: LintId::all()
.iter()
.map(|id| (*id, id.default_level()))
.collect(),
}
}
}
impl LintConfig {
#[must_use]
pub fn allow_all() -> Self {
Self {
levels: LintId::all()
.iter()
.map(|id| (*id, LintLevel::Allow))
.collect(),
}
}
pub fn with_overrides(
mut self,
overrides: &BTreeMap<String, LintLevel>,
) -> Result<Self, String> {
for (name, level) in overrides {
let Some(id) = LintId::from_config_str(name) else {
let known = LintId::all()
.iter()
.copied()
.map(LintId::as_str)
.collect::<Vec<_>>()
.join(", ");
return Err(format!("unknown lint `{name}` (known: {known})"));
};
self.levels.insert(id, *level);
}
Ok(self)
}
#[must_use]
pub fn level(&self, id: LintId) -> LintLevel {
self.levels
.get(&id)
.copied()
.unwrap_or_else(|| id.default_level())
}
#[must_use]
pub fn is_allowed(&self, id: LintId) -> bool {
matches!(self.level(id), LintLevel::Allow)
}
#[must_use]
pub fn allowing(mut self, id: LintId) -> Self {
self.levels.insert(id, LintLevel::Allow);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Diagnostic {
pub id: LintId,
pub level: LintLevel,
pub message: String,
pub loc: SourceLoc,
}
impl Diagnostic {
#[must_use]
pub fn new(
id: LintId,
level: LintLevel,
message: impl Into<String>,
loc: impl Into<SourceLoc>,
) -> Self {
Self {
id,
level,
message: message.into(),
loc: loc.into(),
}
}
#[must_use]
pub const fn is_error(&self) -> bool {
matches!(self.level, LintLevel::Deny)
}
}
impl std::fmt::Display for Diagnostic {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{} {}: {} ({}) at {}",
self.level.as_str(),
self.id.code(),
self.message,
self.id.as_str(),
self.loc
)
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
pub struct LintsTable {
#[serde(flatten)]
pub levels: BTreeMap<String, LintLevel>,
}
impl LintsTable {
pub fn into_config(self) -> Result<LintConfig, String> {
LintConfig::default().with_overrides(&self.levels)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_levels() {
let config = LintConfig::default();
assert_eq!(config.level(LintId::Unwrap), LintLevel::Deny);
assert_eq!(config.level(LintId::FormatSpec), LintLevel::Warn);
}
#[test]
fn overrides_allow_unwrap() {
let mut table = BTreeMap::new();
table.insert("unwrap".to_string(), LintLevel::Allow);
let config = LintConfig::default().with_overrides(&table).unwrap();
assert!(config.is_allowed(LintId::Unwrap));
assert!(!config.is_allowed(LintId::Expect));
}
#[test]
fn rejects_unknown_lint_name() {
let mut table = BTreeMap::new();
table.insert("not_a_lint".to_string(), LintLevel::Allow);
let err = LintConfig::default().with_overrides(&table).unwrap_err();
assert!(err.contains("not_a_lint"));
}
#[test]
fn lint_codes_are_stable() {
assert_eq!(LintId::Unwrap.code(), "E0001");
assert_eq!(LintId::Expect.code(), "E0002");
assert_eq!(LintId::FormatSpec.code(), "E0003");
assert_eq!(LintId::VariableIndex.code(), "E0004");
assert_eq!(LintId::IdentificationCtor.code(), "E0005");
assert_eq!(LintId::OptionIf.code(), "E0006");
assert_eq!(LintId::AmbiguousTry.code(), "E0007");
assert_eq!(LintId::AmbiguousMethod.code(), "E0008");
assert_eq!(LintId::SkippedMod.code(), "E0009");
}
}