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