1use std::collections::HashMap;
5
6use serde_json::Value;
7
8use crate::{Error, Lint};
9
10#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
12#[non_exhaustive]
13pub enum Level {
14 Allow,
16 Warn,
18 Deny,
20}
21
22impl Level {
23 fn parse(text: &str) -> Option<Level> {
24 match text {
25 "allow" => Some(Level::Allow),
26 "warn" => Some(Level::Warn),
27 "deny" | "forbid" => Some(Level::Deny),
28 _ => None,
29 }
30 }
31
32 pub fn label(self) -> &'static str {
34 match self {
35 Level::Allow => "allowed",
36 Level::Warn => "warning",
37 Level::Deny => "error",
38 }
39 }
40}
41
42#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
44#[non_exhaustive]
45pub enum Language {
46 #[default]
48 English,
49 PortugueseBrazil,
51}
52
53#[derive(Clone, Debug)]
55pub struct Config {
56 levels: HashMap<Lint, Level>,
57 pub language: Language,
59 pub extra_verbs: Vec<String>,
61 pub max_file_lines: usize,
63}
64
65impl Default for Config {
66 fn default() -> Self {
67 Config {
68 levels: HashMap::new(),
69 language: Language::default(),
70 extra_verbs: Vec::new(),
71 max_file_lines: 600,
72 }
73 }
74}
75
76impl Config {
77 pub fn level(&self, lint: Lint) -> Level {
79 self.levels.get(&lint).copied().unwrap_or(Level::Deny)
80 }
81
82 pub fn set_level(&mut self, lint: Lint, level: Level) {
84 self.levels.insert(lint, level);
85 }
86
87 pub fn from_metadata(workspace: Option<&Value>, package: Option<&Value>) -> Result<Config, Error> {
90 let mut config = Config::default();
91 for table in [workspace, package].into_iter().flatten() {
92 config.merge(table)?;
93 }
94 Ok(config)
95 }
96
97 fn merge(&mut self, table: &Value) -> Result<(), Error> {
98 let Some(table) = table.as_object() else {
99 return Err(Error::Config("[metadata.ravenrs-lint] must be a table".into()));
100 };
101 for (key, value) in table {
102 match key.as_str() {
103 "language" => {
104 self.language = match value.as_str() {
105 Some("en") => Language::English,
106 Some("pt-BR" | "pt") => Language::PortugueseBrazil,
107 _ => {
108 return Err(Error::Config(format!(
109 "language must be \"en\" or \"pt-BR\", not {value}"
110 )));
111 }
112 }
113 }
114 "extra-verbs" => {
115 let words = value.as_array().and_then(|items| {
116 items.iter().map(|w| w.as_str().map(str::to_owned)).collect::<Option<Vec<_>>>()
117 });
118 self.extra_verbs =
119 words.ok_or_else(|| Error::Config("extra-verbs must be a list of words".into()))?;
120 }
121 "max-file-lines" => {
122 self.max_file_lines = value
123 .as_u64()
124 .and_then(|n| usize::try_from(n).ok())
125 .ok_or_else(|| Error::Config("max-file-lines must be a whole number".into()))?;
126 }
127 "lints" => self.merge_levels(value)?,
128 other => {
129 return Err(Error::Config(format!(
130 "unknown key `{other}`; the keys are language, extra-verbs, max-file-lines and lints"
131 )));
132 }
133 }
134 }
135 Ok(())
136 }
137
138 fn merge_levels(&mut self, value: &Value) -> Result<(), Error> {
139 let Some(levels) = value.as_object() else {
140 return Err(Error::Config("[metadata.ravenrs-lint.lints] must be a table".into()));
141 };
142 for (name, value) in levels {
143 let lint = Lint::from_name(name).ok_or_else(|| {
144 let known: Vec<&str> = Lint::ALL.iter().map(|l| l.name()).collect();
145 Error::Config(format!("unknown rule `{name}`; the rules are {}", known.join(", ")))
146 })?;
147 let (level, reason) = match value {
149 Value::String(level) => (level.as_str(), None),
150 Value::Object(table) => (
151 table.get("level").and_then(Value::as_str).unwrap_or_default(),
152 table.get("reason").and_then(Value::as_str).map(str::trim).filter(|r| !r.is_empty()),
153 ),
154 _ => ("", None),
155 };
156 let level = Level::parse(level).ok_or_else(|| {
157 Error::Config(format!(
158 "`{name}` must be \"deny\" or {{ level = \"warn\", reason = \"...\" }}"
159 ))
160 })?;
161 if level < Level::Deny && reason.is_none() {
162 return Err(Error::Config(format!(
163 "`{name}` is lowered to {level:?} without a reason. Fix the code instead; when lowering the \
164 rule is right, write {name} = {{ level = \"{}\", reason = \"why\" }}",
165 match level {
166 Level::Allow => "allow",
167 _ => "warn",
168 }
169 )));
170 }
171 self.levels.insert(lint, level);
172 }
173 Ok(())
174 }
175}