1use std::cmp::Reverse;
2use std::fmt::Display;
3
4use cargo_util_schemas::manifest;
5use cargo_util_terminal::report::Level;
6
7use crate::core::{Feature, Features};
8
9#[derive(Clone, Debug)]
10pub struct Lint {
11 pub name: &'static str,
12 pub desc: &'static str,
13 pub primary_group: &'static LintGroup,
14 pub msrv: Option<manifest::RustVersion>,
20 pub feature_gate: Option<&'static Feature>,
21 pub docs: Option<&'static str>,
25}
26
27impl Lint {
28 pub fn level(
29 &self,
30 pkg_lints: &manifest::TomlToolLints,
31 pkg_rust_version: Option<&manifest::RustVersion>,
32 unstable_features: &Features,
33 ) -> LintLevelProduct {
34 if self
37 .feature_gate
38 .is_some_and(|f| !unstable_features.is_enabled(f))
39 {
40 let level = LintLevel::Allow;
41 let source = LintLevelSource::Default;
42 return LintLevelProduct { level, source };
43 }
44
45 if let (Some(msrv), Some(pkg_rust_version)) = (&self.msrv, pkg_rust_version) {
46 let pkg_rust_version = pkg_rust_version.to_partial();
47 if !msrv.is_compatible_with(&pkg_rust_version) {
48 let level = LintLevel::Allow;
49 let source = LintLevelSource::Default;
50 return LintLevelProduct { level, source };
51 }
52 }
53
54 let lint_level_priority =
55 level_priority(self.name, self.primary_group.default_level, pkg_lints);
56
57 let group_level_priority = level_priority(
58 self.primary_group.name,
59 self.primary_group.default_level,
60 pkg_lints,
61 );
62
63 let default_group = if LintLevel::Warn <= self.primary_group.default_level {
64 let lint_level_priority =
65 level_priority("default", self.primary_group.default_level, pkg_lints);
66 Some(("default", lint_level_priority))
67 } else {
68 None
69 };
70
71 let (_, (level, source, _)) = [
72 (self.name, lint_level_priority),
73 (self.primary_group.name, group_level_priority),
74 ]
75 .into_iter()
76 .chain(default_group)
77 .max_by_key(|(n, (l, s, p))| {
78 (
79 l == &LintLevel::Forbid,
80 *s != LintLevelSource::Default,
81 *p,
82 Reverse(*n),
83 )
84 })
85 .unwrap();
86 LintLevelProduct { level, source }
87 }
88
89 pub fn emitted_source(&self, lint_level: LintLevel, source: LintLevelSource) -> String {
90 format!("`cargo::{}` is set to `{lint_level}` {source}", self.name,)
91 }
92}
93
94pub struct LintLevelProduct {
95 pub level: LintLevel,
96 pub source: LintLevelSource,
97}
98
99#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
100pub enum LintLevel {
101 Allow,
102 Warn,
103 Deny,
104 Forbid,
105}
106
107impl Display for LintLevel {
108 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109 match self {
110 LintLevel::Allow => write!(f, "allow"),
111 LintLevel::Warn => write!(f, "warn"),
112 LintLevel::Deny => write!(f, "deny"),
113 LintLevel::Forbid => write!(f, "forbid"),
114 }
115 }
116}
117
118impl LintLevel {
119 pub fn is_warn(&self) -> bool {
120 self == &LintLevel::Warn
121 }
122
123 pub fn is_error(&self) -> bool {
124 self == &LintLevel::Forbid || self == &LintLevel::Deny
125 }
126
127 pub fn to_diagnostic_level(self) -> Level<'static> {
128 match self {
129 LintLevel::Allow => unreachable!("allow does not map to a diagnostic level"),
130 LintLevel::Warn => Level::WARNING,
131 LintLevel::Deny => Level::ERROR,
132 LintLevel::Forbid => Level::ERROR,
133 }
134 }
135
136 pub fn force(self) -> bool {
137 match self {
138 Self::Allow => false,
139 Self::Warn => true,
140 Self::Deny => true,
141 Self::Forbid => true,
142 }
143 }
144}
145
146impl From<manifest::TomlLintLevel> for LintLevel {
147 fn from(toml_lint_level: manifest::TomlLintLevel) -> LintLevel {
148 match toml_lint_level {
149 manifest::TomlLintLevel::Allow => LintLevel::Allow,
150 manifest::TomlLintLevel::Warn => LintLevel::Warn,
151 manifest::TomlLintLevel::Deny => LintLevel::Deny,
152 manifest::TomlLintLevel::Forbid => LintLevel::Forbid,
153 }
154 }
155}
156
157#[derive(Copy, Clone, Debug, PartialEq, Eq)]
158pub enum LintLevelSource {
159 Default,
160 Package,
161}
162
163impl Display for LintLevelSource {
164 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
165 match self {
166 LintLevelSource::Default => write!(f, "by default"),
167 LintLevelSource::Package => write!(f, "in `[lints]`"),
168 }
169 }
170}
171
172impl LintLevelSource {
173 pub(crate) fn is_user_specified(&self) -> bool {
174 match self {
175 LintLevelSource::Default => false,
176 LintLevelSource::Package => true,
177 }
178 }
179}
180
181pub(crate) fn level_priority(
182 name: &str,
183 default_level: LintLevel,
184 pkg_lints: &manifest::TomlToolLints,
185) -> (LintLevel, LintLevelSource, i8) {
186 if let Some(defined_level) = pkg_lints.get(name) {
187 (
188 defined_level.level().into(),
189 LintLevelSource::Package,
190 defined_level.priority(),
191 )
192 } else {
193 (default_level, LintLevelSource::Default, 0)
194 }
195}
196
197#[derive(Clone, Debug)]
198pub struct LintGroup {
199 pub name: &'static str,
200 pub default_level: LintLevel,
201 pub desc: &'static str,
202 pub feature_gate: Option<&'static Feature>,
203 pub hidden: bool,
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209
210 const STYLE: LintGroup = LintGroup {
211 name: "style",
212 desc: "code that should be written in a more idiomatic way",
213 default_level: LintLevel::Warn,
214 feature_gate: None,
215 hidden: false,
216 };
217
218 fn test_lint(name: &'static str, group: &'static LintGroup) -> Lint {
219 Lint {
220 name,
221 desc: "test lint",
222 primary_group: group,
223 msrv: None,
224 feature_gate: None,
225 docs: None,
226 }
227 }
228
229 #[test]
230 fn lint_level_prefers_user_specified_over_default() {
231 let lint = test_lint("unused_dependencies", &STYLE);
232
233 let mut pkg_lints = manifest::TomlToolLints::new();
234 pkg_lints.insert(
235 "unused_dependencies".to_string(),
236 manifest::TomlLint::Level(manifest::TomlLintLevel::Deny),
237 );
238 let features = Features::default();
239
240 let LintLevelProduct { level, source } = lint.level(&pkg_lints, None, &features);
241 assert_eq!(level, LintLevel::Deny);
242 assert_eq!(source, LintLevelSource::Package);
243 }
244
245 #[test]
246 fn lint_level_group_overrides_default() {
247 let lint = test_lint("non_kebab_case_bins", &STYLE);
248
249 let mut pkg_lints = manifest::TomlToolLints::new();
250 pkg_lints.insert(
251 "style".to_string(),
252 manifest::TomlLint::Level(manifest::TomlLintLevel::Deny),
253 );
254 let features = Features::default();
255
256 let LintLevelProduct { level, source } = lint.level(&pkg_lints, None, &features);
257 assert_eq!(level, LintLevel::Deny);
258 assert_eq!(source, LintLevelSource::Package);
259 }
260
261 #[test]
262 fn default_group_overrides_default() {
263 let lint = test_lint("non_kebab_case_bins", &STYLE);
264
265 let mut pkg_lints = manifest::TomlToolLints::new();
266 pkg_lints.insert(
267 "default".to_string(),
268 manifest::TomlLint::Level(manifest::TomlLintLevel::Deny),
269 );
270 let features = Features::default();
271
272 let LintLevelProduct { level, source } = lint.level(&pkg_lints, None, &features);
273 assert_eq!(level, LintLevel::Deny);
274 assert_eq!(source, LintLevelSource::Package);
275 }
276
277 #[test]
278 fn default_before_primary() {
279 let lint = test_lint("non_kebab_case_bins", &STYLE);
280
281 let mut pkg_lints = manifest::TomlToolLints::new();
282 pkg_lints.insert(
283 "default".to_string(),
284 manifest::TomlLint::Level(manifest::TomlLintLevel::Deny),
285 );
286 pkg_lints.insert(
287 "style".to_string(),
288 manifest::TomlLint::Level(manifest::TomlLintLevel::Allow),
289 );
290 let features = Features::default();
291
292 let LintLevelProduct { level, source } = lint.level(&pkg_lints, None, &features);
293 assert_eq!(level, LintLevel::Deny);
294 assert_eq!(source, LintLevelSource::Package);
295 }
296
297 #[test]
298 fn default_after_primary() {
299 let lint = test_lint("non_kebab_case_bins", &STYLE);
300
301 let mut pkg_lints = manifest::TomlToolLints::new();
302 pkg_lints.insert(
303 "style".to_string(),
304 manifest::TomlLint::Level(manifest::TomlLintLevel::Allow),
305 );
306 pkg_lints.insert(
307 "default".to_string(),
308 manifest::TomlLint::Level(manifest::TomlLintLevel::Deny),
309 );
310 let features = Features::default();
311
312 let LintLevelProduct { level, source } = lint.level(&pkg_lints, None, &features);
313 assert_eq!(level, LintLevel::Deny);
314 assert_eq!(source, LintLevelSource::Package);
315 }
316
317 #[test]
318 fn default_higher_than_primary() {
319 let lint = test_lint("non_kebab_case_bins", &STYLE);
320
321 let mut pkg_lints = manifest::TomlToolLints::new();
322 pkg_lints.insert(
323 "default".to_string(),
324 manifest::TomlLint::Config(manifest::TomlLintConfig {
325 level: manifest::TomlLintLevel::Deny,
326 priority: 1,
327 config: Default::default(),
328 }),
329 );
330 pkg_lints.insert(
331 "style".to_string(),
332 manifest::TomlLint::Config(manifest::TomlLintConfig {
333 level: manifest::TomlLintLevel::Allow,
334 priority: -1,
335 config: Default::default(),
336 }),
337 );
338 let features = Features::default();
339
340 let LintLevelProduct { level, source } = lint.level(&pkg_lints, None, &features);
341 assert_eq!(level, LintLevel::Deny);
342 assert_eq!(source, LintLevelSource::Package);
343 }
344
345 #[test]
346 fn default_lower_than_primary() {
347 let lint = test_lint("non_kebab_case_bins", &STYLE);
348
349 let mut pkg_lints = manifest::TomlToolLints::new();
350 pkg_lints.insert(
351 "default".to_string(),
352 manifest::TomlLint::Config(manifest::TomlLintConfig {
353 level: manifest::TomlLintLevel::Deny,
354 priority: -1,
355 config: Default::default(),
356 }),
357 );
358 pkg_lints.insert(
359 "style".to_string(),
360 manifest::TomlLint::Config(manifest::TomlLintConfig {
361 level: manifest::TomlLintLevel::Allow,
362 priority: 1,
363 config: Default::default(),
364 }),
365 );
366 let features = Features::default();
367
368 let LintLevelProduct { level, source } = lint.level(&pkg_lints, None, &features);
369 assert_eq!(level, LintLevel::Allow);
370 assert_eq!(source, LintLevelSource::Package);
371 }
372}