use serde::Deserialize;
use std::collections::HashMap;
use std::path::Path;
#[derive(Debug, Clone, Deserialize)]
#[serde(default)]
pub struct Config {
pub max_line_length: usize,
pub use_tabs: bool,
pub max_function_length: usize,
pub max_file_length: usize,
pub max_parameters: usize,
pub max_returns: usize,
pub max_nesting_depth: usize,
pub max_local_variables: usize,
pub max_branches: usize,
pub max_class_variables: usize,
pub max_public_methods: usize,
pub max_inner_classes: usize,
pub exclude: Vec<String>,
pub rules: HashMap<String, RuleSeverityConfig>,
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum RuleSeverityConfig {
Off,
Warn,
Error,
}
impl Default for Config {
fn default() -> Self {
Self {
max_line_length: 100,
use_tabs: true,
max_function_length: 50,
max_file_length: 1000,
max_parameters: 5,
max_returns: 6,
max_nesting_depth: 4,
max_local_variables: 10,
max_branches: 8,
max_class_variables: 15,
max_public_methods: 20,
max_inner_classes: 5,
exclude: vec![".godot".to_string(), "addons".to_string()],
rules: HashMap::new(),
}
}
}
impl Config {
pub fn from_file(path: &Path) -> Result<Self, ConfigError> {
let content = std::fs::read_to_string(path)
.map_err(|e| ConfigError::ReadError(path.display().to_string(), e.to_string()))?;
let config: Config = toml::from_str(&content)
.map_err(|e| ConfigError::ParseError(path.display().to_string(), e.to_string()))?;
Ok(config)
}
pub fn find_and_load(start_dir: &Path) -> Self {
let config_names = ["gdstyle.toml", ".gdstyle.toml"];
let mut dir = start_dir.to_path_buf();
loop {
for name in &config_names {
let path = dir.join(name);
if path.exists() {
match Self::from_file(&path) {
Ok(config) => return config,
Err(e) => {
eprintln!("warning: failed to load {}: {}", path.display(), e);
}
}
}
}
if !dir.pop() {
break;
}
}
Self::default()
}
const OFF_BY_DEFAULT: &[&str] = &[
"quality/type-hint",
"quality/empty-function",
"quality/no-debug-print",
];
pub fn is_rule_enabled(&self, rule_name: &str) -> bool {
match self.rules.get(rule_name) {
Some(RuleSeverityConfig::Off) => false,
Some(_) => true,
None => !Self::OFF_BY_DEFAULT.contains(&rule_name),
}
}
}
#[derive(Debug)]
pub enum ConfigError {
ReadError(String, String),
ParseError(String, String),
}
impl std::fmt::Display for ConfigError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ConfigError::ReadError(path, err) => write!(f, "cannot read {}: {}", path, err),
ConfigError::ParseError(path, err) => write!(f, "cannot parse {}: {}", path, err),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_config() {
let config = Config::default();
assert_eq!(config.max_line_length, 100);
assert!(config.use_tabs);
assert_eq!(config.max_function_length, 50);
assert!(config.exclude.contains(&".godot".to_string()));
assert!(config.exclude.contains(&"addons".to_string()));
}
#[test]
fn parse_toml_config() {
let toml = r#"
max_line_length = 80
use_tabs = false
max_function_length = 30
exclude = [".godot", "addons", "generated"]
[rules]
"naming/class-name-pascal-case" = "error"
"format/max-line-length" = "warn"
"quality/max-function-length" = "off"
"#;
let config: Config = toml::from_str(toml).unwrap();
assert_eq!(config.max_line_length, 80);
assert!(!config.use_tabs);
assert_eq!(config.max_function_length, 30);
assert_eq!(config.exclude.len(), 3);
assert_eq!(
config.rules.get("quality/max-function-length"),
Some(&RuleSeverityConfig::Off)
);
}
#[test]
fn rule_enabled_check() {
let mut config = Config::default();
config.rules.insert(
"naming/class-name-pascal-case".to_string(),
RuleSeverityConfig::Off,
);
assert!(!config.is_rule_enabled("naming/class-name-pascal-case"));
assert!(config.is_rule_enabled("naming/function-name-snake-case"));
}
}