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::features::cargo_docs_link;
10use crate::workspace::{Feature, Features};
11
12#[derive(Clone, Debug)]
13pub struct Lint {
14 pub name: &'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(
102 &self,
103 lint_level: LintLevel,
104 source: LintLevelSource,
105 gctx: &GlobalContext,
106 ) -> String {
107 let url = cargo_docs_link(&format!("reference/lints.html#{}", self.name));
108 let link = gctx.shell().err_hyperlink(&url);
109 let name = self.name;
110 format!("`{link}cargo::{name}{link:#}` is set to `{lint_level}` {source}")
111 }
112}
113
114pub struct LintLevelProduct {
115 pub level: LintLevel,
116 pub source: LintLevelSource,
117}
118
119#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
120pub enum LintLevel {
121 Allow,
122 Warn,
123 Deny,
124 Forbid,
125}
126
127impl Display for LintLevel {
128 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
129 match self {
130 LintLevel::Allow => write!(f, "allow"),
131 LintLevel::Warn => write!(f, "warn"),
132 LintLevel::Deny => write!(f, "deny"),
133 LintLevel::Forbid => write!(f, "forbid"),
134 }
135 }
136}
137
138impl LintLevel {
139 pub fn is_warn(&self) -> bool {
140 self == &LintLevel::Warn
141 }
142
143 pub fn is_error(&self) -> bool {
144 self == &LintLevel::Forbid || self == &LintLevel::Deny
145 }
146
147 pub fn to_diagnostic_level(self) -> Level<'static> {
148 match self {
149 LintLevel::Allow => unreachable!("allow does not map to a diagnostic level"),
150 LintLevel::Warn => Level::WARNING,
151 LintLevel::Deny => Level::ERROR,
152 LintLevel::Forbid => Level::ERROR,
153 }
154 }
155
156 pub fn force(self) -> bool {
157 match self {
158 Self::Allow => false,
159 Self::Warn => true,
160 Self::Deny => true,
161 Self::Forbid => true,
162 }
163 }
164}
165
166impl From<manifest::TomlLintLevel> for LintLevel {
167 fn from(toml_lint_level: manifest::TomlLintLevel) -> LintLevel {
168 match toml_lint_level {
169 manifest::TomlLintLevel::Allow => LintLevel::Allow,
170 manifest::TomlLintLevel::Warn => LintLevel::Warn,
171 manifest::TomlLintLevel::Deny => LintLevel::Deny,
172 manifest::TomlLintLevel::Forbid => LintLevel::Forbid,
173 }
174 }
175}
176
177#[derive(Copy, Clone, Debug, PartialEq, Eq)]
178pub enum LintLevelSource {
179 Default,
180 Package,
181}
182
183impl Display for LintLevelSource {
184 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
185 match self {
186 LintLevelSource::Default => write!(f, "by default"),
187 LintLevelSource::Package => write!(f, "in `[lints]`"),
188 }
189 }
190}
191
192impl LintLevelSource {
193 pub(crate) fn is_user_specified(&self) -> bool {
194 match self {
195 LintLevelSource::Default => false,
196 LintLevelSource::Package => true,
197 }
198 }
199}
200
201pub(crate) fn level_priority(
202 name: &str,
203 default_level: LintLevel,
204 pkg_lints: &manifest::TomlToolLints,
205) -> (LintLevel, LintLevelSource, i8) {
206 if let Some(defined_level) = pkg_lints.get(name) {
207 (
208 defined_level.level().into(),
209 LintLevelSource::Package,
210 defined_level.priority(),
211 )
212 } else {
213 (default_level, LintLevelSource::Default, 0)
214 }
215}
216
217#[derive(Clone, Debug)]
218pub struct LintGroup {
219 pub name: &'static str,
220 pub default_level: LintLevel,
221 pub desc: &'static str,
222 pub feature_gate: Option<&'static Feature>,
223 pub hidden: bool,
224}
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229
230 const STYLE: LintGroup = LintGroup {
231 name: "style",
232 desc: "code that should be written in a more idiomatic way",
233 default_level: LintLevel::Warn,
234 feature_gate: None,
235 hidden: false,
236 };
237
238 fn gctx() -> GlobalContext {
239 let cwd = std::env::current_dir().unwrap();
240 GlobalContext::new(
241 cargo_util_terminal::Shell::new(),
242 cwd.clone(),
243 home::cargo_home_with_cwd(&cwd).unwrap(),
244 )
245 }
246
247 fn test_lint(name: &'static str, group: &'static LintGroup) -> Lint {
248 Lint {
249 name,
250 primary_group: group,
251 msrv: None,
252 feature_gate: None,
253 docs: None,
254 }
255 }
256
257 #[test]
258 fn lint_level_prefers_user_specified_over_default() {
259 let lint = test_lint("unused_dependencies", &STYLE);
260
261 let mut pkg_lints = manifest::TomlToolLints::new();
262 pkg_lints.insert(
263 "unused_dependencies".to_string(),
264 manifest::TomlLint::Level(manifest::TomlLintLevel::Deny),
265 );
266 let features = Features::default();
267
268 let LintLevelProduct { level, source } = lint.level(&pkg_lints, None, &features, &gctx());
269 assert_eq!(level, LintLevel::Deny);
270 assert_eq!(source, LintLevelSource::Package);
271 }
272
273 #[test]
274 fn lint_level_group_overrides_default() {
275 let lint = test_lint("non_kebab_case_bins", &STYLE);
276
277 let mut pkg_lints = manifest::TomlToolLints::new();
278 pkg_lints.insert(
279 "style".to_string(),
280 manifest::TomlLint::Level(manifest::TomlLintLevel::Deny),
281 );
282 let features = Features::default();
283
284 let LintLevelProduct { level, source } = lint.level(&pkg_lints, None, &features, &gctx());
285 assert_eq!(level, LintLevel::Deny);
286 assert_eq!(source, LintLevelSource::Package);
287 }
288
289 #[test]
290 fn default_group_overrides_default() {
291 let lint = test_lint("non_kebab_case_bins", &STYLE);
292
293 let mut pkg_lints = manifest::TomlToolLints::new();
294 pkg_lints.insert(
295 "default".to_string(),
296 manifest::TomlLint::Level(manifest::TomlLintLevel::Deny),
297 );
298 let features = Features::default();
299
300 let LintLevelProduct { level, source } = lint.level(&pkg_lints, None, &features, &gctx());
301 assert_eq!(level, LintLevel::Deny);
302 assert_eq!(source, LintLevelSource::Package);
303 }
304
305 #[test]
306 fn default_before_primary() {
307 let lint = test_lint("non_kebab_case_bins", &STYLE);
308
309 let mut pkg_lints = manifest::TomlToolLints::new();
310 pkg_lints.insert(
311 "default".to_string(),
312 manifest::TomlLint::Level(manifest::TomlLintLevel::Deny),
313 );
314 pkg_lints.insert(
315 "style".to_string(),
316 manifest::TomlLint::Level(manifest::TomlLintLevel::Allow),
317 );
318 let features = Features::default();
319
320 let LintLevelProduct { level, source } = lint.level(&pkg_lints, None, &features, &gctx());
321 assert_eq!(level, LintLevel::Deny);
322 assert_eq!(source, LintLevelSource::Package);
323 }
324
325 #[test]
326 fn default_after_primary() {
327 let lint = test_lint("non_kebab_case_bins", &STYLE);
328
329 let mut pkg_lints = manifest::TomlToolLints::new();
330 pkg_lints.insert(
331 "style".to_string(),
332 manifest::TomlLint::Level(manifest::TomlLintLevel::Allow),
333 );
334 pkg_lints.insert(
335 "default".to_string(),
336 manifest::TomlLint::Level(manifest::TomlLintLevel::Deny),
337 );
338 let features = Features::default();
339
340 let LintLevelProduct { level, source } = lint.level(&pkg_lints, None, &features, &gctx());
341 assert_eq!(level, LintLevel::Deny);
342 assert_eq!(source, LintLevelSource::Package);
343 }
344
345 #[test]
346 fn default_higher_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, &gctx());
369 assert_eq!(level, LintLevel::Deny);
370 assert_eq!(source, LintLevelSource::Package);
371 }
372
373 #[test]
374 fn default_lower_than_primary() {
375 let lint = test_lint("non_kebab_case_bins", &STYLE);
376
377 let mut pkg_lints = manifest::TomlToolLints::new();
378 pkg_lints.insert(
379 "default".to_string(),
380 manifest::TomlLint::Config(manifest::TomlLintConfig {
381 level: manifest::TomlLintLevel::Deny,
382 priority: -1,
383 config: Default::default(),
384 }),
385 );
386 pkg_lints.insert(
387 "style".to_string(),
388 manifest::TomlLint::Config(manifest::TomlLintConfig {
389 level: manifest::TomlLintLevel::Allow,
390 priority: 1,
391 config: Default::default(),
392 }),
393 );
394 let features = Features::default();
395
396 let LintLevelProduct { level, source } = lint.level(&pkg_lints, None, &features, &gctx());
397 assert_eq!(level, LintLevel::Allow);
398 assert_eq!(source, LintLevelSource::Package);
399 }
400}