relabel.rs 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292
  1. //! The labels command parser.
  2. //!
  3. //! This can parse arbitrary input, giving the list of labels added/removed.
  4. //!
  5. //! The grammar is as follows:
  6. //!
  7. //! ```text
  8. //! Command: `@bot modify labels:? to? <label-list>.` or `@bot label:? <label-list>.`
  9. //!
  10. //! <label-list>:
  11. //! - <label-delta>
  12. //! - <label-delta> and <label-list>
  13. //! - <label-delta>, <label-list>
  14. //! - <label-delta>, and <label-list>
  15. //!
  16. //! <label-delta>:
  17. //! - +<label>
  18. //! - -<label>
  19. //! this can start with a + or -, but then the only supported way of adding it
  20. //! is with the previous two variants of this (i.e., ++label and -+label).
  21. //! - <label>
  22. //!
  23. //! <label>: \S+
  24. //! ```
  25. use crate::error::Error;
  26. use crate::token::{Token, Tokenizer};
  27. #[cfg(test)]
  28. use std::error::Error as _;
  29. use std::fmt;
  30. #[derive(Debug, PartialEq, Eq)]
  31. pub struct RelabelCommand(pub Vec<LabelDelta>);
  32. #[derive(Debug, PartialEq, Eq)]
  33. pub enum LabelDelta {
  34. Add(Label),
  35. Remove(Label),
  36. }
  37. #[derive(Debug, PartialEq, Eq, Clone)]
  38. pub struct Label(String);
  39. #[derive(PartialEq, Eq, Debug)]
  40. pub enum ParseError {
  41. EmptyLabel,
  42. ExpectedLabelDelta,
  43. MisleadingTo,
  44. NoSeparator,
  45. }
  46. impl std::error::Error for ParseError {}
  47. impl fmt::Display for ParseError {
  48. fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
  49. match self {
  50. ParseError::EmptyLabel => write!(f, "empty label"),
  51. ParseError::ExpectedLabelDelta => write!(f, "a label delta"),
  52. ParseError::MisleadingTo => write!(f, "forbidden `to`, use `+to`"),
  53. ParseError::NoSeparator => write!(f, "must have `:` or `to` as label starter"),
  54. }
  55. }
  56. }
  57. impl Label {
  58. fn parse(input: &str) -> Result<Label, ParseError> {
  59. if input.is_empty() {
  60. Err(ParseError::EmptyLabel)
  61. } else {
  62. Ok(Label(input.into()))
  63. }
  64. }
  65. }
  66. impl std::ops::Deref for Label {
  67. type Target = String;
  68. fn deref(&self) -> &String {
  69. &self.0
  70. }
  71. }
  72. impl LabelDelta {
  73. fn parse<'a>(input: &mut Tokenizer<'a>) -> Result<LabelDelta, Error<'a>> {
  74. let delta = match input.peek_token()? {
  75. Some(Token::Word(delta)) => {
  76. input.next_token()?;
  77. delta
  78. }
  79. _ => {
  80. return Err(input.error(ParseError::ExpectedLabelDelta));
  81. }
  82. };
  83. if delta.starts_with('+') {
  84. Ok(LabelDelta::Add(
  85. Label::parse(&delta[1..]).map_err(|e| input.error(e))?,
  86. ))
  87. } else if delta.starts_with('-') {
  88. Ok(LabelDelta::Remove(
  89. Label::parse(&delta[1..]).map_err(|e| input.error(e))?,
  90. ))
  91. } else {
  92. Ok(LabelDelta::Add(
  93. Label::parse(delta).map_err(|e| input.error(e))?,
  94. ))
  95. }
  96. }
  97. pub fn label(&self) -> &Label {
  98. match self {
  99. LabelDelta::Add(l) => l,
  100. LabelDelta::Remove(l) => l,
  101. }
  102. }
  103. }
  104. #[test]
  105. fn delta_empty() {
  106. let mut tok = Tokenizer::new("+ testing");
  107. let err = LabelDelta::parse(&mut tok).unwrap_err();
  108. assert_eq!(
  109. err.source().unwrap().downcast_ref::<ParseError>(),
  110. Some(&ParseError::EmptyLabel)
  111. );
  112. assert_eq!(err.position(), 1);
  113. }
  114. impl RelabelCommand {
  115. pub fn parse<'a>(input: &mut Tokenizer<'a>) -> Result<Option<Self>, Error<'a>> {
  116. let mut toks = input.clone();
  117. if toks.eat_token(Token::Word("modify"))? {
  118. if toks.eat_token(Token::Word("labels"))? {
  119. if toks.eat_token(Token::Colon)? {
  120. // ate the colon
  121. } else if toks.eat_token(Token::Word("to"))? {
  122. // optionally eat the colon after to, e.g.:
  123. // @rustbot modify labels to: -S-waiting-on-author, +S-waiting-on-review
  124. toks.eat_token(Token::Colon)?;
  125. } else {
  126. // It's okay if there's no to or colon, we can just eat labels
  127. // afterwards.
  128. }
  129. // continue
  130. {} // FIXME(rustfmt#4506): this is needed to get rustfmt to indent the comment correctly
  131. } else {
  132. return Ok(None);
  133. }
  134. } else if toks.eat_token(Token::Word("label"))? {
  135. // optionally eat a colon
  136. toks.eat_token(Token::Colon)?;
  137. // continue
  138. {} // FIXME(rustfmt#4506): this is needed to get rustfmt to indent the comment correctly
  139. } else {
  140. return Ok(None);
  141. }
  142. if let Some(Token::Word("to")) = toks.peek_token()? {
  143. return Err(toks.error(ParseError::MisleadingTo));
  144. }
  145. // start parsing deltas
  146. let mut deltas = Vec::new();
  147. loop {
  148. deltas.push(LabelDelta::parse(&mut toks)?);
  149. // optional `, and` separator
  150. if let Some(Token::Comma) = toks.peek_token()? {
  151. toks.next_token()?;
  152. }
  153. if let Some(Token::Word("and")) = toks.peek_token()? {
  154. toks.next_token()?;
  155. }
  156. if let Some(Token::Semi) | Some(Token::Dot) | Some(Token::EndOfLine) =
  157. toks.peek_token()?
  158. {
  159. toks.next_token()?;
  160. *input = toks;
  161. return Ok(Some(RelabelCommand(deltas)));
  162. }
  163. }
  164. }
  165. }
  166. #[cfg(test)]
  167. fn parse<'a>(input: &'a str) -> Result<Option<Vec<LabelDelta>>, Error<'a>> {
  168. let mut toks = Tokenizer::new(input);
  169. Ok(RelabelCommand::parse(&mut toks)?.map(|c| c.0))
  170. }
  171. #[test]
  172. fn parse_simple() {
  173. assert_eq!(
  174. parse("modify labels: +T-compiler -T-lang bug."),
  175. Ok(Some(vec![
  176. LabelDelta::Add(Label("T-compiler".into())),
  177. LabelDelta::Remove(Label("T-lang".into())),
  178. LabelDelta::Add(Label("bug".into())),
  179. ]))
  180. );
  181. }
  182. #[test]
  183. fn parse_leading_to_label() {
  184. assert_eq!(
  185. parse("modify labels: to -T-lang")
  186. .unwrap_err()
  187. .source()
  188. .unwrap()
  189. .downcast_ref(),
  190. Some(&ParseError::MisleadingTo)
  191. );
  192. }
  193. #[test]
  194. fn parse_no_label_paragraph() {
  195. assert_eq!(
  196. parse("modify labels yep; Labels do in fact exist but this is not a label paragraph."),
  197. Ok(Some(vec![LabelDelta::Add(Label("yep".into())),]))
  198. );
  199. }
  200. #[test]
  201. fn parse_no_dot() {
  202. assert_eq!(
  203. parse("modify labels to +T-compiler -T-lang bug"),
  204. Ok(Some(vec![
  205. LabelDelta::Add(Label("T-compiler".into())),
  206. LabelDelta::Remove(Label("T-lang".into())),
  207. LabelDelta::Add(Label("bug".into())),
  208. ]))
  209. );
  210. }
  211. #[test]
  212. fn parse_to_colon() {
  213. assert_eq!(
  214. parse("modify labels to: +T-compiler -T-lang bug"),
  215. Ok(Some(vec![
  216. LabelDelta::Add(Label("T-compiler".into())),
  217. LabelDelta::Remove(Label("T-lang".into())),
  218. LabelDelta::Add(Label("bug".into())),
  219. ]))
  220. );
  221. }
  222. #[test]
  223. fn parse_shorter_command() {
  224. assert_eq!(
  225. parse("label +T-compiler -T-lang bug"),
  226. Ok(Some(vec![
  227. LabelDelta::Add(Label("T-compiler".into())),
  228. LabelDelta::Remove(Label("T-lang".into())),
  229. LabelDelta::Add(Label("bug".into())),
  230. ]))
  231. );
  232. }
  233. #[test]
  234. fn parse_shorter_command_with_colon() {
  235. assert_eq!(
  236. parse("label: +T-compiler -T-lang bug"),
  237. Ok(Some(vec![
  238. LabelDelta::Add(Label("T-compiler".into())),
  239. LabelDelta::Remove(Label("T-lang".into())),
  240. LabelDelta::Add(Label("bug".into())),
  241. ]))
  242. );
  243. }
  244. #[test]
  245. fn parse_shorter_command_with_to() {
  246. assert_eq!(
  247. parse("label to +T-compiler -T-lang bug")
  248. .unwrap_err()
  249. .source()
  250. .unwrap()
  251. .downcast_ref(),
  252. Some(&ParseError::MisleadingTo)
  253. );
  254. }
  255. #[test]
  256. fn parse_shorter_command_with_to_colon() {
  257. assert_eq!(
  258. parse("label to: +T-compiler -T-lang bug")
  259. .unwrap_err()
  260. .source()
  261. .unwrap()
  262. .downcast_ref(),
  263. Some(&ParseError::MisleadingTo)
  264. );
  265. }