1use std::cmp::Ordering;
12
13#[derive(Clone, Debug, Eq, PartialEq)]
15pub struct Warning {
16 pub kind: WarningKind,
18 pub name: String,
20 pub message: Option<String>,
22 pub warn_at: Option<String>,
24 pub remove_at: Option<String>,
26 pub replacement: Option<String>,
28}
29
30#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
34#[non_exhaustive]
35pub enum WarningKind {
36 DeprecatedFlag,
38 DeprecatedCommand,
40 DeprecatedEnv,
42 #[default]
44 Other,
45}
46
47impl Warning {
48 pub fn flag(
50 name: impl Into<String>,
51 message: Option<String>,
52 warn_at: Option<String>,
53 remove_at: Option<String>,
54 ) -> Self {
55 Self {
56 kind: WarningKind::DeprecatedFlag,
57 name: name.into(),
58 message,
59 warn_at,
60 remove_at,
61 replacement: None,
62 }
63 }
64
65 pub fn command(
67 name: impl Into<String>,
68 message: Option<String>,
69 warn_at: Option<String>,
70 remove_at: Option<String>,
71 ) -> Self {
72 Self {
73 kind: WarningKind::DeprecatedCommand,
74 name: name.into(),
75 message,
76 warn_at,
77 remove_at,
78 replacement: None,
79 }
80 }
81
82 pub fn env(name: impl Into<String>, replacement: Option<String>) -> Self {
84 Self {
85 kind: WarningKind::DeprecatedEnv,
86 name: name.into(),
87 message: None,
88 warn_at: None,
89 remove_at: None,
90 replacement,
91 }
92 }
93
94 pub fn render(&self) -> String {
96 let subject = match self.kind {
97 WarningKind::DeprecatedCommand => format!("the {} command", self.name),
98 _ => self.name.clone(),
99 };
100 let mut out = format!("warning: {subject} is deprecated");
101 if let Some(at) = &self.remove_at {
102 out.push_str(&format!(", removed at {at}"));
103 }
104 match (&self.message, &self.replacement) {
105 (Some(message), _) => out.push_str(&format!(": {message}")),
106 (None, Some(replacement)) => out.push_str(&format!(": use {replacement}")),
107 (None, None) => {}
108 }
109 out.push('\n');
110 out
111 }
112}
113
114pub fn version_reaches(current: Option<&str>, warn_at: Option<&str>) -> bool {
120 let (Some(warn_at), Some(current)) = (warn_at, current) else {
121 return true;
122 };
123 !matches!(compare(current, warn_at), Some(Ordering::Less))
124}
125
126pub fn compare(a: &str, b: &str) -> Option<Ordering> {
132 let (a_core, a_pre) = split(a);
133 let (b_core, b_pre) = split(b);
134 let mut a_segments = a_core.split('.');
135 let mut b_segments = b_core.split('.');
136 loop {
137 let (a_next, b_next) = (a_segments.next(), b_segments.next());
138 if a_next.is_none() && b_next.is_none() {
139 break;
140 }
141 match segment(a_next)?.cmp(&segment(b_next)?) {
142 Ordering::Equal => continue,
143 ordering => return Some(ordering),
144 }
145 }
146 Some(match (a_pre, b_pre) {
147 (None, None) => Ordering::Equal,
148 (Some(_), None) => Ordering::Less,
149 (None, Some(_)) => Ordering::Greater,
150 (Some(a), Some(b)) => a.cmp(b),
151 })
152}
153
154fn split(version: &str) -> (&str, Option<&str>) {
155 let version = version.split('+').next().unwrap_or(version);
156 match version.split_once('-') {
157 Some((core, pre)) => (core, Some(pre)),
158 None => (version, None),
159 }
160}
161
162fn segment(segment: Option<&str>) -> Option<u64> {
163 match segment {
164 None => Some(0),
165 Some(text) => text.parse().ok(),
166 }
167}
168
169pub fn retain_reached(warnings: &mut Vec<Warning>, version: Option<&str>) {
171 warnings.retain(|warning| version_reaches(version, warning.warn_at.as_deref()));
172}
173
174pub fn render(warnings: &[Warning]) -> String {
176 warnings.iter().map(Warning::render).collect()
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182
183 #[test]
184 fn the_gate_matches_the_rule() {
185 assert!(version_reaches(Some("1.0.0"), None));
186 assert!(version_reaches(None, Some("2.0.0")));
187 assert!(!version_reaches(Some("1.0.0"), Some("2.0.0")));
188 assert!(version_reaches(Some("2.0.0"), Some("2.0.0")));
189 assert!(!version_reaches(Some("2.0.0-rc.1"), Some("2.0.0")));
190 assert!(version_reaches(Some("nightly"), Some("2.0.0")));
191 assert_eq!(compare("2026.12", "2026.12.0"), Some(Ordering::Equal));
192 assert_eq!(compare("1.0.0+abc", "1.0.0+def"), Some(Ordering::Equal));
193 assert_eq!(compare("nightly", "1.0.0"), None);
194 }
195
196 #[test]
197 fn a_warning_says_what_to_do_about_it() {
198 assert_eq!(
199 Warning::flag(
200 "--old",
201 Some("use --new".into()),
202 None,
203 Some("2.0.0".into())
204 )
205 .render(),
206 "warning: --old is deprecated, removed at 2.0.0: use --new\n",
207 );
208 assert_eq!(
209 Warning::command("old", None, None, None).render(),
210 "warning: the old command is deprecated\n",
211 );
212 assert_eq!(
213 Warning::env("OLD_TOKEN", Some("APP_TOKEN".into())).render(),
214 "warning: OLD_TOKEN is deprecated: use APP_TOKEN\n",
215 );
216 }
217}