command.rs 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349
  1. use crate::error::Error;
  2. use crate::ignore_block::IgnoreBlocks;
  3. use crate::token::Tokenizer;
  4. use regex::Regex;
  5. pub mod assign;
  6. pub mod close;
  7. pub mod glacier;
  8. pub mod nominate;
  9. pub mod note;
  10. pub mod ping;
  11. pub mod prioritize;
  12. pub mod relabel;
  13. pub mod second;
  14. pub mod shortcut;
  15. #[derive(Debug, PartialEq)]
  16. pub enum Command<'a> {
  17. Relabel(Result<relabel::RelabelCommand, Error<'a>>),
  18. Assign(Result<assign::AssignCommand, Error<'a>>),
  19. Ping(Result<ping::PingCommand, Error<'a>>),
  20. Nominate(Result<nominate::NominateCommand, Error<'a>>),
  21. Prioritize(Result<prioritize::PrioritizeCommand, Error<'a>>),
  22. Second(Result<second::SecondCommand, Error<'a>>),
  23. Glacier(Result<glacier::GlacierCommand, Error<'a>>),
  24. Shortcut(Result<shortcut::ShortcutCommand, Error<'a>>),
  25. Close(Result<close::CloseCommand, Error<'a>>),
  26. Note(Result<note::NoteCommand, Error<'a>>),
  27. }
  28. #[derive(Debug)]
  29. pub struct Input<'a> {
  30. all: &'a str,
  31. parsed: usize,
  32. ignore: IgnoreBlocks,
  33. /// A pattern for finding the start of a command based on the name of the
  34. /// configured bots.
  35. bot_re: Regex,
  36. }
  37. fn parse_single_command<'a, T, F, M>(
  38. parse: F,
  39. mapper: M,
  40. tokenizer: &Tokenizer<'a>,
  41. ) -> Option<(Tokenizer<'a>, Command<'a>)>
  42. where
  43. F: FnOnce(&mut Tokenizer<'a>) -> Result<Option<T>, Error<'a>>,
  44. M: FnOnce(Result<T, Error<'a>>) -> Command<'a>,
  45. T: std::fmt::Debug,
  46. {
  47. let mut tok = tokenizer.clone();
  48. let res = parse(&mut tok);
  49. log::info!("parsed {:?} command: {:?}", std::any::type_name::<T>(), res);
  50. match res {
  51. Ok(None) => None,
  52. Ok(Some(v)) => Some((tok, mapper(Ok(v)))),
  53. Err(err) => Some((tok, mapper(Err(err)))),
  54. }
  55. }
  56. impl<'a> Input<'a> {
  57. pub fn new(input: &'a str, bot: Vec<&'a str>) -> Input<'a> {
  58. let bots: Vec<_> = bot.iter().map(|bot| format!(r"(?:@{bot}\b)")).collect();
  59. let bot_re = Regex::new(&format!(
  60. r#"(?i)(?P<review>\br\?)|{bots}"#,
  61. bots = bots.join("|")
  62. ))
  63. .unwrap();
  64. Input {
  65. all: input,
  66. parsed: 0,
  67. ignore: IgnoreBlocks::new(input),
  68. bot_re,
  69. }
  70. }
  71. fn parse_command(&mut self) -> Option<Command<'a>> {
  72. let tok = Tokenizer::new(&self.all[self.parsed..]);
  73. log::info!("identified potential command");
  74. let mut success = vec![];
  75. let original_tokenizer = tok.clone();
  76. success.extend(parse_single_command(
  77. relabel::RelabelCommand::parse,
  78. Command::Relabel,
  79. &original_tokenizer,
  80. ));
  81. success.extend(parse_single_command(
  82. assign::AssignCommand::parse,
  83. Command::Assign,
  84. &original_tokenizer,
  85. ));
  86. success.extend(parse_single_command(
  87. note::NoteCommand::parse,
  88. Command::Note,
  89. &original_tokenizer,
  90. ));
  91. success.extend(parse_single_command(
  92. ping::PingCommand::parse,
  93. Command::Ping,
  94. &original_tokenizer,
  95. ));
  96. success.extend(parse_single_command(
  97. nominate::NominateCommand::parse,
  98. Command::Nominate,
  99. &original_tokenizer,
  100. ));
  101. success.extend(parse_single_command(
  102. prioritize::PrioritizeCommand::parse,
  103. Command::Prioritize,
  104. &original_tokenizer,
  105. ));
  106. success.extend(parse_single_command(
  107. second::SecondCommand::parse,
  108. Command::Second,
  109. &original_tokenizer,
  110. ));
  111. success.extend(parse_single_command(
  112. glacier::GlacierCommand::parse,
  113. Command::Glacier,
  114. &original_tokenizer,
  115. ));
  116. success.extend(parse_single_command(
  117. shortcut::ShortcutCommand::parse,
  118. Command::Shortcut,
  119. &original_tokenizer,
  120. ));
  121. success.extend(parse_single_command(
  122. close::CloseCommand::parse,
  123. Command::Close,
  124. &original_tokenizer,
  125. ));
  126. if success.len() > 1 {
  127. panic!(
  128. "succeeded parsing {:?} to multiple commands: {:?}",
  129. &self.all[self.parsed..],
  130. success
  131. );
  132. }
  133. let (mut tok, c) = success.pop()?;
  134. // if we errored out while parsing the command do not move the input forwards
  135. if c.is_ok() {
  136. self.parsed += tok.position();
  137. }
  138. Some(c)
  139. }
  140. /// Parses command for `r?`
  141. fn parse_review(&mut self) -> Option<Command<'a>> {
  142. let tok = Tokenizer::new(&self.all[self.parsed..]);
  143. match parse_single_command(assign::AssignCommand::parse_review, Command::Assign, &tok) {
  144. Some((mut tok, command)) => {
  145. self.parsed += tok.position();
  146. Some(command)
  147. }
  148. None => {
  149. log::warn!("expected r? parser to return something: {:?}", self.all);
  150. None
  151. }
  152. }
  153. }
  154. }
  155. impl<'a> Iterator for Input<'a> {
  156. type Item = Command<'a>;
  157. fn next(&mut self) -> Option<Command<'a>> {
  158. loop {
  159. let caps = self.bot_re.captures(&self.all[self.parsed..])?;
  160. let m = caps.get(0).unwrap();
  161. if self
  162. .ignore
  163. .overlaps_ignore((self.parsed + m.start())..(self.parsed + m.end()))
  164. .is_some()
  165. {
  166. log::info!("command overlaps ignored block; ignore: {:?}", self.ignore);
  167. self.parsed += m.end();
  168. continue;
  169. }
  170. self.parsed += m.end();
  171. if caps.name("review").is_some() {
  172. if let Some(command) = self.parse_review() {
  173. return Some(command);
  174. }
  175. } else if let Some(command) = self.parse_command() {
  176. return Some(command);
  177. }
  178. }
  179. }
  180. }
  181. impl<'a> Command<'a> {
  182. pub fn is_ok(&self) -> bool {
  183. match self {
  184. Command::Relabel(r) => r.is_ok(),
  185. Command::Assign(r) => r.is_ok(),
  186. Command::Ping(r) => r.is_ok(),
  187. Command::Nominate(r) => r.is_ok(),
  188. Command::Prioritize(r) => r.is_ok(),
  189. Command::Second(r) => r.is_ok(),
  190. Command::Glacier(r) => r.is_ok(),
  191. Command::Shortcut(r) => r.is_ok(),
  192. Command::Close(r) => r.is_ok(),
  193. Command::Note(r) => r.is_ok(),
  194. }
  195. }
  196. pub fn is_err(&self) -> bool {
  197. !self.is_ok()
  198. }
  199. }
  200. #[test]
  201. fn errors_outside_command_are_fine() {
  202. let input = "haha\" unterminated quotes @bot labels +bug. Terminating after the command";
  203. let mut input = Input::new(input, vec!["bot"]);
  204. assert!(input.next().unwrap().is_ok());
  205. }
  206. #[test]
  207. fn code_1() {
  208. let input = "`@bot modify label: +bug.`";
  209. let mut input = Input::new(input, vec!["bot"]);
  210. assert!(input.next().is_none());
  211. }
  212. #[test]
  213. fn code_2() {
  214. let input = "```
  215. @bot modify labels: +bug.
  216. ```";
  217. let mut input = Input::new(input, vec!["bot"]);
  218. assert!(input.next().is_none());
  219. }
  220. #[test]
  221. fn resumes_after_code() {
  222. // Handles a command after an ignored block.
  223. let input = "```
  224. @bot modify labels: +bug.
  225. ```
  226. @bot claim
  227. ";
  228. let mut input = Input::new(input, vec!["bot"]);
  229. assert!(matches!(input.next(), Some(Command::Assign(Ok(_)))));
  230. assert_eq!(input.next(), None);
  231. }
  232. #[test]
  233. fn edit_1() {
  234. let input_old = "@bot modify labels: +bug.";
  235. let mut input_old = Input::new(input_old, vec!["bot"]);
  236. let input_new = "Adding labels: @bot modify label +bug. some other text";
  237. let mut input_new = Input::new(input_new, vec!["bot"]);
  238. assert_eq!(input_old.next(), input_new.next());
  239. }
  240. #[test]
  241. fn edit_2() {
  242. let input_old = "@bot label bug.";
  243. let mut input_old = Input::new(input_old, vec!["bot"]);
  244. let input_new = "@bot modify labels to: +bug.";
  245. let mut input_new = Input::new(input_new, vec!["bot"]);
  246. assert_eq!(input_old.next(), input_new.next());
  247. }
  248. #[test]
  249. fn move_input_along() {
  250. let input = "@bot labels: +bug. Afterwards, delete the world.";
  251. let mut input = Input::new(input, vec!["bot"]);
  252. assert!(input.next().unwrap().is_ok());
  253. assert_eq!(&input.all[input.parsed..], " Afterwards, delete the world.");
  254. }
  255. #[test]
  256. fn move_input_along_1() {
  257. let input = "@bot modify labels\": +bug. Afterwards, delete the world.";
  258. let mut input = Input::new(input, vec!["bot"]);
  259. assert!(input.next().unwrap().is_err());
  260. // don't move input along if parsing the command fails
  261. assert_eq!(&input.all[..input.parsed], "@bot");
  262. }
  263. #[test]
  264. fn multiname() {
  265. let input = "@rustbot label to: +bug. Afterwards, delete the world. @triagebot prioritize";
  266. let mut input = Input::new(input, vec!["triagebot", "rustbot"]);
  267. assert!(dbg!(input.next().unwrap()).is_ok());
  268. assert_eq!(
  269. &input.all[input.parsed..],
  270. " Afterwards, delete the world. @triagebot prioritize"
  271. );
  272. assert!(input.next().unwrap().is_ok());
  273. assert!(input.next().is_none());
  274. }
  275. #[test]
  276. fn review_commands() {
  277. for (input, name) in [
  278. ("r? @octocat", "octocat"),
  279. ("r? octocat", "octocat"),
  280. ("R? @octocat", "octocat"),
  281. ("can I r? someone?", "someone"),
  282. ("Please r? @octocat can you review?", "octocat"),
  283. ("r? rust-lang/compiler", "rust-lang/compiler"),
  284. ("r? @D--a--s-h", "D--a--s-h"),
  285. ] {
  286. let mut input = Input::new(input, vec!["bot"]);
  287. assert_eq!(
  288. input.next(),
  289. Some(Command::Assign(Ok(assign::AssignCommand::ReviewName {
  290. name: name.to_string()
  291. })))
  292. );
  293. assert_eq!(input.next(), None);
  294. }
  295. }
  296. #[test]
  297. fn review_errors() {
  298. use std::error::Error;
  299. for input in ["r?", "r? @", "r? @ user", "r?:user", "r?! @foo", "r?\nline"] {
  300. let mut input = Input::new(input, vec!["bot"]);
  301. let err = match input.next() {
  302. Some(Command::Assign(Err(err))) => err,
  303. c => panic!("unexpected {:?}", c),
  304. };
  305. assert_eq!(
  306. err.source().unwrap().downcast_ref(),
  307. Some(&assign::ParseError::NoUser)
  308. );
  309. assert_eq!(input.next(), None);
  310. }
  311. }
  312. #[test]
  313. fn review_ignored() {
  314. // Checks for things that shouldn't be detected.
  315. for input in ["r", "reviewer? abc", "r foo"] {
  316. let mut input = Input::new(input, vec!["bot"]);
  317. assert_eq!(input.next(), None);
  318. }
  319. }