1#[non_exhaustive]
43#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
44pub enum CompatibilityMode {
45 #[default]
48 Default,
49 Strict,
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59pub enum ExplicitChoice {
60 Strict,
62 Default,
64}
65
66pub fn resolve(
75 explicit_flag: Option<ExplicitChoice>,
76 env_strict: Option<&str>,
77 argv0_basename: Option<&str>,
78) -> CompatibilityMode {
79 match explicit_flag {
80 Some(ExplicitChoice::Default) => return CompatibilityMode::Default,
81 Some(ExplicitChoice::Strict) => return CompatibilityMode::Strict,
82 None => {}
83 }
84
85 if let Some(value) = env_strict {
86 if env_var_is_truthy(value) {
87 return CompatibilityMode::Strict;
88 }
89 }
90
91 if let Some(name) = argv0_basename {
92 if name.eq_ignore_ascii_case("ts") {
93 return CompatibilityMode::Strict;
94 }
95 }
96
97 CompatibilityMode::Default
98}
99
100fn env_var_is_truthy(value: &str) -> bool {
104 matches!(
105 value.trim().to_ascii_lowercase().as_str(),
106 "1" | "true" | "yes" | "on"
107 )
108}
109
110pub fn argv0_basename(argv0: &std::ffi::OsStr) -> Option<String> {
115 use std::path::Path;
116 Path::new(argv0)
117 .file_stem()
118 .and_then(|s| s.to_str())
119 .map(|s| s.to_owned())
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125
126 #[test]
127 fn explicit_no_strict_wins_over_everything() {
128 let mode = resolve(Some(ExplicitChoice::Default), Some("1"), Some("ts"));
129 assert_eq!(mode, CompatibilityMode::Default);
130 }
131
132 #[test]
133 fn explicit_strict_wins_over_env_and_argv() {
134 let mode = resolve(Some(ExplicitChoice::Strict), Some("0"), Some("rusty-ts"));
135 assert_eq!(mode, CompatibilityMode::Strict);
136 }
137
138 #[test]
139 fn env_truthy_enables_strict() {
140 for truthy in ["1", "true", "TRUE", "yes", "Yes", "on", " 1 "] {
141 assert_eq!(
142 resolve(None, Some(truthy), Some("rusty-ts")),
143 CompatibilityMode::Strict,
144 "env {truthy:?} should enable Strict",
145 );
146 }
147 }
148
149 #[test]
150 fn env_falsy_or_unset_falls_through() {
151 for falsy in ["0", "false", "no", "off", "", " "] {
152 assert_eq!(
153 resolve(None, Some(falsy), Some("rusty-ts")),
154 CompatibilityMode::Default,
155 "env {falsy:?} should not enable Strict",
156 );
157 }
158 assert_eq!(
159 resolve(None, None, Some("rusty-ts")),
160 CompatibilityMode::Default,
161 );
162 }
163
164 #[test]
165 fn argv0_ts_enables_strict() {
166 assert_eq!(resolve(None, None, Some("ts")), CompatibilityMode::Strict);
167 }
168
169 #[test]
170 fn argv0_ts_case_insensitive() {
171 assert_eq!(resolve(None, None, Some("TS")), CompatibilityMode::Strict);
172 assert_eq!(resolve(None, None, Some("Ts")), CompatibilityMode::Strict);
173 }
174
175 #[test]
176 fn argv0_rusty_ts_stays_default() {
177 assert_eq!(
178 resolve(None, None, Some("rusty-ts")),
179 CompatibilityMode::Default,
180 );
181 }
182
183 #[test]
184 fn argv0_basename_strips_exe() {
185 use std::ffi::OsStr;
186 assert_eq!(argv0_basename(OsStr::new("ts.exe")).as_deref(), Some("ts"));
187 assert_eq!(argv0_basename(OsStr::new("ts")).as_deref(), Some("ts"));
188 assert_eq!(argv0_basename(OsStr::new("./ts")).as_deref(), Some("ts"),);
189 }
190
191 #[test]
192 fn argv0_basename_handles_path_components() {
193 use std::ffi::OsStr;
194 assert_eq!(
197 argv0_basename(OsStr::new("/usr/local/bin/ts")).as_deref(),
198 Some("ts"),
199 );
200 }
201
202 #[test]
204 fn precedence_table() {
205 type Row = (
207 Option<ExplicitChoice>,
208 Option<&'static str>,
209 Option<&'static str>,
210 CompatibilityMode,
211 );
212 let cases: &[Row] = &[
213 (None, None, None, CompatibilityMode::Default),
215 (None, None, Some("rusty-ts"), CompatibilityMode::Default),
216 (None, None, Some("ts"), CompatibilityMode::Strict),
218 (None, Some("1"), Some("rusty-ts"), CompatibilityMode::Strict),
220 (None, Some("true"), None, CompatibilityMode::Strict),
221 (None, Some("0"), Some("ts"), CompatibilityMode::Strict), (
224 None,
225 Some("0"),
226 Some("rusty-ts"),
227 CompatibilityMode::Default,
228 ),
229 (
231 Some(ExplicitChoice::Strict),
232 Some("0"),
233 Some("rusty-ts"),
234 CompatibilityMode::Strict,
235 ),
236 (
237 Some(ExplicitChoice::Strict),
238 None,
239 Some("ts"),
240 CompatibilityMode::Strict,
241 ),
242 (
244 Some(ExplicitChoice::Default),
245 Some("1"),
246 Some("ts"),
247 CompatibilityMode::Default,
248 ),
249 (
250 Some(ExplicitChoice::Default),
251 None,
252 Some("ts"),
253 CompatibilityMode::Default,
254 ),
255 ];
256
257 for (i, (explicit, env, argv, expected)) in cases.iter().enumerate() {
258 let actual = resolve(*explicit, *env, *argv);
259 assert_eq!(
260 actual, *expected,
261 "case {i}: explicit={explicit:?} env={env:?} argv={argv:?} expected {expected:?} got {actual:?}",
262 );
263 }
264 }
265}