1use std::cmp::{Reverse, max_by_key};
2use std::fmt::Display;
3
4use cargo_util_schemas::manifest::RustVersion;
5use cargo_util_schemas::manifest::TomlLintLevel;
6use cargo_util_schemas::manifest::TomlToolLints;
7use cargo_util_terminal::report::Level;
8
9use crate::core::{Feature, Features};
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<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: &TomlToolLints,
33 pkg_rust_version: Option<&RustVersion>,
34 unstable_features: &Features,
35 ) -> (LintLevel, LintLevelSource) {
36 if self
39 .feature_gate
40 .is_some_and(|f| !unstable_features.is_enabled(f))
41 {
42 return (LintLevel::Allow, LintLevelSource::Default);
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 return (LintLevel::Allow, LintLevelSource::Default);
49 }
50 }
51
52 let lint_level_priority =
53 level_priority(self.name, self.primary_group.default_level, pkg_lints);
54
55 let group_level_priority = level_priority(
56 self.primary_group.name,
57 self.primary_group.default_level,
58 pkg_lints,
59 );
60
61 let (_, (l, s, _)) = max_by_key(
62 (self.name, lint_level_priority),
63 (self.primary_group.name, group_level_priority),
64 |(n, (l, s, p))| {
65 (
66 l == &LintLevel::Forbid,
67 *s != LintLevelSource::Default,
68 *p,
69 Reverse(*n),
70 )
71 },
72 );
73 (l, s)
74 }
75
76 pub fn emitted_source(&self, lint_level: LintLevel, source: LintLevelSource) -> String {
77 format!("`cargo::{}` is set to `{lint_level}` {source}", self.name,)
78 }
79}
80
81#[derive(Copy, Clone, Debug, PartialEq)]
82pub enum LintLevel {
83 Allow,
84 Warn,
85 Deny,
86 Forbid,
87}
88
89impl Display for LintLevel {
90 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91 match self {
92 LintLevel::Allow => write!(f, "allow"),
93 LintLevel::Warn => write!(f, "warn"),
94 LintLevel::Deny => write!(f, "deny"),
95 LintLevel::Forbid => write!(f, "forbid"),
96 }
97 }
98}
99
100impl LintLevel {
101 pub fn is_warn(&self) -> bool {
102 self == &LintLevel::Warn
103 }
104
105 pub fn is_error(&self) -> bool {
106 self == &LintLevel::Forbid || self == &LintLevel::Deny
107 }
108
109 pub fn to_diagnostic_level(self) -> Level<'static> {
110 match self {
111 LintLevel::Allow => unreachable!("allow does not map to a diagnostic level"),
112 LintLevel::Warn => Level::WARNING,
113 LintLevel::Deny => Level::ERROR,
114 LintLevel::Forbid => Level::ERROR,
115 }
116 }
117
118 pub fn force(self) -> bool {
119 match self {
120 Self::Allow => false,
121 Self::Warn => true,
122 Self::Deny => true,
123 Self::Forbid => true,
124 }
125 }
126}
127
128impl From<TomlLintLevel> for LintLevel {
129 fn from(toml_lint_level: TomlLintLevel) -> LintLevel {
130 match toml_lint_level {
131 TomlLintLevel::Allow => LintLevel::Allow,
132 TomlLintLevel::Warn => LintLevel::Warn,
133 TomlLintLevel::Deny => LintLevel::Deny,
134 TomlLintLevel::Forbid => LintLevel::Forbid,
135 }
136 }
137}
138
139#[derive(Copy, Clone, Debug, PartialEq, Eq)]
140pub enum LintLevelSource {
141 Default,
142 Package,
143}
144
145impl Display for LintLevelSource {
146 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
147 match self {
148 LintLevelSource::Default => write!(f, "by default"),
149 LintLevelSource::Package => write!(f, "in `[lints]`"),
150 }
151 }
152}
153
154impl LintLevelSource {
155 pub(crate) fn is_user_specified(&self) -> bool {
156 match self {
157 LintLevelSource::Default => false,
158 LintLevelSource::Package => true,
159 }
160 }
161}
162
163pub(crate) fn level_priority(
164 name: &str,
165 default_level: LintLevel,
166 pkg_lints: &TomlToolLints,
167) -> (LintLevel, LintLevelSource, i8) {
168 if let Some(defined_level) = pkg_lints.get(name) {
169 (
170 defined_level.level().into(),
171 LintLevelSource::Package,
172 defined_level.priority(),
173 )
174 } else {
175 (default_level, LintLevelSource::Default, 0)
176 }
177}
178
179#[derive(Clone, Debug)]
180pub struct LintGroup {
181 pub name: &'static str,
182 pub default_level: LintLevel,
183 pub desc: &'static str,
184 pub feature_gate: Option<&'static Feature>,
185 pub hidden: bool,
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191
192 const STYLE: LintGroup = LintGroup {
193 name: "style",
194 desc: "code that should be written in a more idiomatic way",
195 default_level: LintLevel::Warn,
196 feature_gate: None,
197 hidden: false,
198 };
199
200 fn test_lint(name: &'static str, group: &'static LintGroup) -> Lint {
201 Lint {
202 name,
203 desc: "test lint",
204 primary_group: group,
205 msrv: None,
206 feature_gate: None,
207 docs: None,
208 }
209 }
210
211 #[test]
212 fn lint_level_prefers_user_specified_over_default() {
213 let lint = test_lint("unused_dependencies", &STYLE);
214
215 let mut pkg_lints = TomlToolLints::new();
216 pkg_lints.insert(
217 "unused_dependencies".to_string(),
218 cargo_util_schemas::manifest::TomlLint::Level(TomlLintLevel::Deny),
219 );
220 let features = Features::default();
221
222 let (level, source) = lint.level(&pkg_lints, None, &features);
223 assert_eq!(level, LintLevel::Deny);
224 assert_eq!(source, LintLevelSource::Package);
225 }
226
227 #[test]
228 fn lint_level_group_overrides_default() {
229 let lint = test_lint("non_kebab_case_bins", &STYLE);
230
231 let mut pkg_lints = TomlToolLints::new();
232 pkg_lints.insert(
233 "style".to_string(),
234 cargo_util_schemas::manifest::TomlLint::Level(TomlLintLevel::Deny),
235 );
236 let features = Features::default();
237
238 let (level, source) = lint.level(&pkg_lints, None, &features);
239 assert_eq!(level, LintLevel::Deny);
240 assert_eq!(source, LintLevelSource::Package);
241 }
242}