1use std::fmt;
4
5use crate::{formatdoc, writedoc};
6
7#[derive(Clone, Copy, Debug, Eq, PartialEq)]
9pub enum WarningKind {
10 Repeated,
12 Contradictory,
14 Redundant,
16}
17
18#[derive(Clone, Debug, Eq, PartialEq)]
20pub struct FlagWarning {
21 pub kind: WarningKind,
23 pub names: Vec<String>,
25}
26
27impl FlagWarning {
28 #[must_use]
30 pub fn line(&self, bin: &str) -> String {
31 formatdoc!("{bin}: warning: {self}")
32 }
33}
34
35impl fmt::Display for FlagWarning {
36 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37 let names = match self.kind {
38 WarningKind::Repeated => self.names.join("/"),
39 WarningKind::Contradictory | WarningKind::Redundant => self.names.join(" and "),
40 };
41 match self.kind {
42 WarningKind::Repeated => {
43 writedoc!(f, "{names} repeated; last value wins")
44 }
45 WarningKind::Contradictory => {
46 writedoc!(f, "{names} both set; last wins")
47 }
48 WarningKind::Redundant => {
49 writedoc!(f, "{names} are the same flag")
50 }
51 }
52 }
53}
54
55#[must_use]
57pub fn chassis_warnings<'a, I>(args: I) -> Vec<FlagWarning>
58where
59 I: IntoIterator<Item = &'a str>,
60{
61 let seen = collect(args);
62 let mut out = Vec::new();
63 push_repeat(&mut out, &seen.color, "--color");
64 push_repeat(&mut out, &seen.format, "--format");
65 push_repeat(&mut out, &seen.quiet, "--quiet");
66 push_repeat(&mut out, &seen.color_off, "--no-color");
67 if !seen.color.is_empty() && !seen.color_off.is_empty() {
68 out.push(FlagWarning {
69 kind: WarningKind::Contradictory,
70 names: vec!["--color".into(), "--no-color".into()],
71 });
72 }
73 push_dry(&mut out, &seen.dry);
74 out
75}
76
77#[must_use]
79pub fn warn_opposites<'a, I>(args: I, yes: &[&str], no: &[&str]) -> Vec<FlagWarning>
80where
81 I: IntoIterator<Item = &'a str>,
82{
83 let mut saw_yes = false;
84 let mut saw_no = false;
85 for arg in args {
86 if arg == "--" {
87 break;
88 }
89 if matches_any(arg, yes) {
90 saw_yes = true;
91 }
92 if matches_any(arg, no) {
93 saw_no = true;
94 }
95 }
96 if saw_yes && saw_no {
97 vec![FlagWarning {
98 kind: WarningKind::Contradictory,
99 names: vec![yes[0].to_owned(), no[0].to_owned()],
100 }]
101 } else {
102 Vec::new()
103 }
104}
105
106pub fn emit_warnings<'a, I>(bin: &str, warnings: I)
108where
109 I: IntoIterator<Item = &'a FlagWarning>,
110{
111 for warning in warnings {
112 eprintln!("{}", warning.line(bin));
113 }
114}
115
116#[derive(Default)]
117struct Seen {
118 color: Vec<String>,
119 color_off: Vec<String>,
120 format: Vec<String>,
121 quiet: Vec<String>,
122 dry: Vec<String>,
123}
124
125fn collect<'a, I>(args: I) -> Seen
126where
127 I: IntoIterator<Item = &'a str>,
128{
129 let mut seen = Seen::default();
130 let mut args = args.into_iter().peekable();
131 while let Some(arg) = args.next() {
132 if arg == "--" {
133 break;
134 }
135 if arg == "--no-color" {
136 seen.color_off.push(arg.to_owned());
137 continue;
138 }
139 if matches_any(arg, &["-q", "--quiet"]) {
140 seen.quiet.push("--quiet".into());
141 continue;
142 }
143 if matches_any(arg, &["-n", "--dry-run", "--preview"]) {
144 seen.dry.push(arg.to_owned());
145 continue;
146 }
147 if take_value(&mut args, arg, &["-c", "--color"]) {
148 seen.color.push("--color".into());
149 continue;
150 }
151 if take_value(&mut args, arg, &["-f", "--format"]) {
152 seen.format.push("--format".into());
153 }
154 }
155 seen
156}
157
158fn take_value<'a, I>(args: &mut std::iter::Peekable<I>, arg: &str, names: &[&str]) -> bool
159where
160 I: Iterator<Item = &'a str>,
161{
162 if names.contains(&arg) {
163 if matches!(args.peek(), Some(next) if !next.starts_with('-')) {
164 args.next();
165 }
166 return true;
167 }
168 names.iter().any(|name| {
169 name.starts_with("--")
170 && arg
171 .strip_prefix(name)
172 .is_some_and(|rest| rest.starts_with('='))
173 })
174}
175
176fn matches_any(arg: &str, names: &[&str]) -> bool {
177 names.contains(&arg)
178}
179
180fn push_repeat(out: &mut Vec<FlagWarning>, hits: &[String], name: &str) {
181 if hits.len() > 1 {
182 out.push(repeat(name));
183 }
184}
185
186fn push_dry(out: &mut Vec<FlagWarning>, hits: &[String]) {
187 if hits.len() < 2 {
188 return;
189 }
190 let aliases = hits.iter().any(|hit| hit == "--preview")
191 && hits.iter().any(|hit| hit == "--dry-run" || hit == "-n");
192 if aliases {
193 out.push(FlagWarning {
194 kind: WarningKind::Redundant,
195 names: vec!["--dry-run".into(), "--preview".into()],
196 });
197 } else {
198 out.push(repeat("--dry-run"));
199 }
200}
201
202fn repeat(name: &str) -> FlagWarning {
203 FlagWarning {
204 kind: WarningKind::Repeated,
205 names: vec![name.to_owned()],
206 }
207}
208
209#[cfg(test)]
210mod tests {
211 use super::{FlagWarning, WarningKind, chassis_warnings, warn_opposites};
212
213 fn kinds(args: &[&str]) -> Vec<WarningKind> {
214 chassis_warnings(args.iter().copied())
215 .into_iter()
216 .map(|warning| warning.kind)
217 .collect()
218 }
219
220 #[test]
221 fn equals_form_is_an_occurrence() {
222 assert_eq!(
223 kinds(&["--format=json", "--format=pretty"]),
224 [WarningKind::Repeated]
225 );
226 assert_eq!(
227 kinds(&["--color=always", "--color=never"]),
228 [WarningKind::Repeated]
229 );
230 }
231
232 #[test]
233 fn peek_does_not_eat_the_following_flag() {
234 let hits = kinds(&["--color", "--no-color"]);
235 assert!(hits.contains(&WarningKind::Contradictory), "{hits:?}");
236 }
237
238 #[test]
239 fn stops_at_double_dash() {
240 assert_eq!(
241 kinds(&["--format", "json", "--", "--format", "pretty"]),
242 Vec::<WarningKind>::new()
243 );
244 assert_eq!(
245 warn_opposites(["--pr", "--", "--no-pr"], &["--pr"], &["--no-pr"]),
246 Vec::<FlagWarning>::new()
247 );
248 }
249
250 #[test]
251 fn repeated_no_color() {
252 assert_eq!(
253 kinds(&["--no-color", "--no-color"]),
254 [WarningKind::Repeated]
255 );
256 }
257
258 #[test]
259 fn short_and_long_color_is_repeated() {
260 assert_eq!(
261 kinds(&["-c", "always", "--color", "never"]),
262 [WarningKind::Repeated]
263 );
264 }
265}