Skip to main content

use_ecmascript/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7/// ECMAScript edition numbers for commonly named targets.
8#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
9#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
10pub enum EcmaScriptEdition {
11    Edition5,
12    Edition6,
13    Edition7,
14    Edition8,
15    Edition9,
16    Edition10,
17    Edition11,
18    Edition12,
19    Edition13,
20    Edition14,
21    Edition15,
22}
23
24impl EcmaScriptEdition {
25    /// Returns the numeric ECMAScript edition.
26    #[must_use]
27    pub const fn number(self) -> u8 {
28        match self {
29            Self::Edition5 => 5,
30            Self::Edition6 => 6,
31            Self::Edition7 => 7,
32            Self::Edition8 => 8,
33            Self::Edition9 => 9,
34            Self::Edition10 => 10,
35            Self::Edition11 => 11,
36            Self::Edition12 => 12,
37            Self::Edition13 => 13,
38            Self::Edition14 => 14,
39            Self::Edition15 => 15,
40        }
41    }
42}
43
44/// Calendar year for annual ECMAScript editions.
45#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
46#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
47pub struct EcmaScriptYear(u16);
48
49impl EcmaScriptYear {
50    /// Creates a supported ECMAScript edition year.
51    ///
52    /// # Errors
53    ///
54    /// Returns [`EcmaScriptParseError::UnsupportedYear`] when `year` is outside ES2015..=ES2024.
55    pub const fn new(year: u16) -> Result<Self, EcmaScriptParseError> {
56        if year >= 2015 && year <= 2024 {
57            Ok(Self(year))
58        } else {
59            Err(EcmaScriptParseError::UnsupportedYear)
60        }
61    }
62
63    /// Returns the calendar year.
64    #[must_use]
65    pub const fn get(self) -> u16 {
66        self.0
67    }
68}
69
70impl fmt::Display for EcmaScriptYear {
71    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
72        write!(formatter, "{}", self.0)
73    }
74}
75
76impl FromStr for EcmaScriptYear {
77    type Err = EcmaScriptParseError;
78
79    fn from_str(input: &str) -> Result<Self, Self::Err> {
80        let trimmed = input.trim();
81        if trimmed.is_empty() {
82            return Err(EcmaScriptParseError::Empty);
83        }
84
85        let year = trimmed
86            .parse::<u16>()
87            .map_err(|_error| EcmaScriptParseError::UnknownTarget)?;
88        Self::new(year)
89    }
90}
91
92/// Common ECMAScript language target labels.
93#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
94#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
95pub enum EcmaScriptTarget {
96    Es5,
97    Es2015,
98    Es2016,
99    Es2017,
100    Es2018,
101    Es2019,
102    Es2020,
103    Es2021,
104    Es2022,
105    Es2023,
106    Es2024,
107    EsNext,
108}
109
110pub const ES5: EcmaScriptTarget = EcmaScriptTarget::Es5;
111pub const ES2015: EcmaScriptTarget = EcmaScriptTarget::Es2015;
112pub const ES2016: EcmaScriptTarget = EcmaScriptTarget::Es2016;
113pub const ES2017: EcmaScriptTarget = EcmaScriptTarget::Es2017;
114pub const ES2018: EcmaScriptTarget = EcmaScriptTarget::Es2018;
115pub const ES2019: EcmaScriptTarget = EcmaScriptTarget::Es2019;
116pub const ES2020: EcmaScriptTarget = EcmaScriptTarget::Es2020;
117pub const ES2021: EcmaScriptTarget = EcmaScriptTarget::Es2021;
118pub const ES2022: EcmaScriptTarget = EcmaScriptTarget::Es2022;
119pub const ES2023: EcmaScriptTarget = EcmaScriptTarget::Es2023;
120pub const ES2024: EcmaScriptTarget = EcmaScriptTarget::Es2024;
121pub const ESNEXT: EcmaScriptTarget = EcmaScriptTarget::EsNext;
122
123impl EcmaScriptTarget {
124    /// Returns the annual edition year when the target has one.
125    #[must_use]
126    pub const fn year(self) -> Option<EcmaScriptYear> {
127        match self {
128            Self::Es5 | Self::EsNext => None,
129            Self::Es2015 => Some(EcmaScriptYear(2015)),
130            Self::Es2016 => Some(EcmaScriptYear(2016)),
131            Self::Es2017 => Some(EcmaScriptYear(2017)),
132            Self::Es2018 => Some(EcmaScriptYear(2018)),
133            Self::Es2019 => Some(EcmaScriptYear(2019)),
134            Self::Es2020 => Some(EcmaScriptYear(2020)),
135            Self::Es2021 => Some(EcmaScriptYear(2021)),
136            Self::Es2022 => Some(EcmaScriptYear(2022)),
137            Self::Es2023 => Some(EcmaScriptYear(2023)),
138            Self::Es2024 => Some(EcmaScriptYear(2024)),
139        }
140    }
141
142    /// Returns the edition number when the target maps to a stable edition.
143    #[must_use]
144    pub const fn edition(self) -> Option<EcmaScriptEdition> {
145        match self {
146            Self::Es5 => Some(EcmaScriptEdition::Edition5),
147            Self::Es2015 => Some(EcmaScriptEdition::Edition6),
148            Self::Es2016 => Some(EcmaScriptEdition::Edition7),
149            Self::Es2017 => Some(EcmaScriptEdition::Edition8),
150            Self::Es2018 => Some(EcmaScriptEdition::Edition9),
151            Self::Es2019 => Some(EcmaScriptEdition::Edition10),
152            Self::Es2020 => Some(EcmaScriptEdition::Edition11),
153            Self::Es2021 => Some(EcmaScriptEdition::Edition12),
154            Self::Es2022 => Some(EcmaScriptEdition::Edition13),
155            Self::Es2023 => Some(EcmaScriptEdition::Edition14),
156            Self::Es2024 => Some(EcmaScriptEdition::Edition15),
157            Self::EsNext => None,
158        }
159    }
160}
161
162impl fmt::Display for EcmaScriptTarget {
163    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
164        formatter.write_str(match self {
165            Self::Es5 => "ES5",
166            Self::Es2015 => "ES2015",
167            Self::Es2016 => "ES2016",
168            Self::Es2017 => "ES2017",
169            Self::Es2018 => "ES2018",
170            Self::Es2019 => "ES2019",
171            Self::Es2020 => "ES2020",
172            Self::Es2021 => "ES2021",
173            Self::Es2022 => "ES2022",
174            Self::Es2023 => "ES2023",
175            Self::Es2024 => "ES2024",
176            Self::EsNext => "ESNext",
177        })
178    }
179}
180
181impl FromStr for EcmaScriptTarget {
182    type Err = EcmaScriptParseError;
183
184    fn from_str(input: &str) -> Result<Self, Self::Err> {
185        let trimmed = input.trim();
186        if trimmed.is_empty() {
187            return Err(EcmaScriptParseError::Empty);
188        }
189
190        let normalized = normalize_target(trimmed);
191        match normalized.as_str() {
192            "es5" | "ecmascript5" => Ok(Self::Es5),
193            "es6" | "es2015" | "ecmascript2015" => Ok(Self::Es2015),
194            "es7" | "es2016" | "ecmascript2016" => Ok(Self::Es2016),
195            "es8" | "es2017" | "ecmascript2017" => Ok(Self::Es2017),
196            "es9" | "es2018" | "ecmascript2018" => Ok(Self::Es2018),
197            "es10" | "es2019" | "ecmascript2019" => Ok(Self::Es2019),
198            "es11" | "es2020" | "ecmascript2020" => Ok(Self::Es2020),
199            "es12" | "es2021" | "ecmascript2021" => Ok(Self::Es2021),
200            "es13" | "es2022" | "ecmascript2022" => Ok(Self::Es2022),
201            "es14" | "es2023" | "ecmascript2023" => Ok(Self::Es2023),
202            "es15" | "es2024" | "ecmascript2024" => Ok(Self::Es2024),
203            "esnext" | "next" => Ok(Self::EsNext),
204            _ => Err(EcmaScriptParseError::UnknownTarget),
205        }
206    }
207}
208
209/// Error returned while parsing ECMAScript labels.
210#[derive(Clone, Copy, Debug, Eq, PartialEq)]
211pub enum EcmaScriptParseError {
212    Empty,
213    UnsupportedYear,
214    UnknownTarget,
215}
216
217impl fmt::Display for EcmaScriptParseError {
218    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
219        match self {
220            Self::Empty => formatter.write_str("ECMAScript target cannot be empty"),
221            Self::UnsupportedYear => formatter.write_str("unsupported ECMAScript edition year"),
222            Self::UnknownTarget => formatter.write_str("unknown ECMAScript target"),
223        }
224    }
225}
226
227impl Error for EcmaScriptParseError {}
228
229fn normalize_target(input: &str) -> String {
230    input
231        .chars()
232        .filter(|character| !matches!(character, '-' | '_' | ' '))
233        .flat_map(char::to_lowercase)
234        .collect()
235}
236
237#[cfg(test)]
238mod tests {
239    use super::{ES2020, ESNEXT, EcmaScriptParseError, EcmaScriptTarget, EcmaScriptYear};
240
241    #[test]
242    fn parses_common_targets() -> Result<(), EcmaScriptParseError> {
243        assert_eq!("es2020".parse::<EcmaScriptTarget>()?, ES2020);
244        assert_eq!("ES2020".parse::<EcmaScriptTarget>()?, ES2020);
245        assert_eq!("es-next".parse::<EcmaScriptTarget>()?, ESNEXT);
246        assert_eq!(ES2020.to_string(), "ES2020");
247        Ok(())
248    }
249
250    #[test]
251    fn validates_years() {
252        assert_eq!(EcmaScriptYear::new(2024).map(EcmaScriptYear::get), Ok(2024));
253        assert_eq!(
254            EcmaScriptYear::new(2014),
255            Err(EcmaScriptParseError::UnsupportedYear)
256        );
257    }
258}