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