Skip to content

Commit a3d0606

Browse files
Merge pull request #1656 from ehuss/highfive
Support highfive functionality
2 parents 12ca4aa + 077cdf8 commit a3d0606

File tree

13 files changed

+1330
-194
lines changed

13 files changed

+1330
-194
lines changed

Cargo.lock

Lines changed: 7 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ cynic = { version = "0.14" }
4040
itertools = "0.10.2"
4141
tower = { version = "0.4.13", features = ["util", "limit", "buffer", "load-shed"] }
4242
github-graphql = { path = "github-graphql" }
43+
rand = "0.8.5"
44+
ignore = "0.4.18"
4345

4446
[dependencies.serde]
4547
version = "1"

parser/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ edition = "2021"
77
[dependencies]
88
pulldown-cmark = "0.7.0"
99
log = "0.4"
10+
regex = "1.6.0"

parser/src/command.rs

Lines changed: 111 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use crate::error::Error;
22
use crate::ignore_block::IgnoreBlocks;
3-
use crate::token::{Token, Tokenizer};
3+
use crate::token::Tokenizer;
4+
use regex::Regex;
45

56
pub mod assign;
67
pub mod close;
@@ -13,10 +14,6 @@ pub mod relabel;
1314
pub mod second;
1415
pub mod shortcut;
1516

16-
pub fn find_command_start(input: &str, bot: &str) -> Option<usize> {
17-
input.to_ascii_lowercase().find(&format!("@{}", bot))
18-
}
19-
2017
#[derive(Debug, PartialEq)]
2118
pub enum Command<'a> {
2219
Relabel(Result<relabel::RelabelCommand, Error<'a>>),
@@ -36,9 +33,9 @@ pub struct Input<'a> {
3633
all: &'a str,
3734
parsed: usize,
3835
ignore: IgnoreBlocks,
39-
40-
// A list of possible bot names.
41-
bot: Vec<&'a str>,
36+
/// A pattern for finding the start of a command based on the name of the
37+
/// configured bots.
38+
bot_re: Regex,
4239
}
4340

4441
fn parse_single_command<'a, T, F, M>(
@@ -63,25 +60,22 @@ where
6360

6461
impl<'a> Input<'a> {
6562
pub fn new(input: &'a str, bot: Vec<&'a str>) -> Input<'a> {
63+
let bots: Vec<_> = bot.iter().map(|bot| format!(r"(?:@{bot}\b)")).collect();
64+
let bot_re = Regex::new(&format!(
65+
r#"(?i)(?P<review>\br\?)|{bots}"#,
66+
bots = bots.join("|")
67+
))
68+
.unwrap();
6669
Input {
6770
all: input,
6871
parsed: 0,
6972
ignore: IgnoreBlocks::new(input),
70-
bot,
73+
bot_re,
7174
}
7275
}
7376

7477
fn parse_command(&mut self) -> Option<Command<'a>> {
75-
let mut tok = Tokenizer::new(&self.all[self.parsed..]);
76-
let name_length = if let Ok(Some(Token::Word(bot_name))) = tok.next_token() {
77-
assert!(self
78-
.bot
79-
.iter()
80-
.any(|name| bot_name.eq_ignore_ascii_case(&format!("@{}", name))));
81-
bot_name.len()
82-
} else {
83-
panic!("no bot name?")
84-
};
78+
let tok = Tokenizer::new(&self.all[self.parsed..]);
8579
log::info!("identified potential command");
8680

8781
let mut success = vec![];
@@ -147,41 +141,55 @@ impl<'a> Input<'a> {
147141
);
148142
}
149143

150-
if self
151-
.ignore
152-
.overlaps_ignore((self.parsed)..(self.parsed + tok.position()))
153-
.is_some()
154-
{
155-
log::info!("command overlaps ignored block; ignore: {:?}", self.ignore);
156-
return None;
157-
}
158-
159144
let (mut tok, c) = success.pop()?;
160145
// if we errored out while parsing the command do not move the input forwards
161-
self.parsed += if c.is_ok() {
162-
tok.position()
163-
} else {
164-
name_length
165-
};
146+
if c.is_ok() {
147+
self.parsed += tok.position();
148+
}
166149
Some(c)
167150
}
151+
152+
/// Parses command for `r?`
153+
fn parse_review(&mut self) -> Option<Command<'a>> {
154+
let tok = Tokenizer::new(&self.all[self.parsed..]);
155+
match parse_single_command(assign::AssignCommand::parse_review, Command::Assign, &tok) {
156+
Some((mut tok, command)) => {
157+
self.parsed += tok.position();
158+
Some(command)
159+
}
160+
None => {
161+
log::warn!("expected r? parser to return something: {:?}", self.all);
162+
None
163+
}
164+
}
165+
}
168166
}
169167

170168
impl<'a> Iterator for Input<'a> {
171169
type Item = Command<'a>;
172170

173171
fn next(&mut self) -> Option<Command<'a>> {
174172
loop {
175-
let start = self
176-
.bot
177-
.iter()
178-
.filter_map(|name| find_command_start(&self.all[self.parsed..], name))
179-
.min()?;
180-
self.parsed += start;
181-
if let Some(command) = self.parse_command() {
173+
let caps = self.bot_re.captures(&self.all[self.parsed..])?;
174+
let m = caps.get(0).unwrap();
175+
if self
176+
.ignore
177+
.overlaps_ignore((self.parsed + m.start())..(self.parsed + m.end()))
178+
.is_some()
179+
{
180+
log::info!("command overlaps ignored block; ignore: {:?}", self.ignore);
181+
self.parsed += m.end();
182+
continue;
183+
}
184+
185+
self.parsed += m.end();
186+
if caps.name("review").is_some() {
187+
if let Some(command) = self.parse_review() {
188+
return Some(command);
189+
}
190+
} else if let Some(command) = self.parse_command() {
182191
return Some(command);
183192
}
184-
self.parsed += self.bot.len() + 1;
185193
}
186194
}
187195
}
@@ -230,6 +238,20 @@ fn code_2() {
230238
assert!(input.next().is_none());
231239
}
232240

241+
#[test]
242+
fn resumes_after_code() {
243+
// Handles a command after an ignored block.
244+
let input = "```
245+
@bot modify labels: +bug.
246+
```
247+
248+
@bot claim
249+
";
250+
let mut input = Input::new(input, vec!["bot"]);
251+
assert!(matches!(input.next(), Some(Command::Assign(Ok(_)))));
252+
assert_eq!(input.next(), None);
253+
}
254+
233255
#[test]
234256
fn edit_1() {
235257
let input_old = "@bot modify labels: +bug.";
@@ -277,3 +299,51 @@ fn multiname() {
277299
assert!(input.next().unwrap().is_ok());
278300
assert!(input.next().is_none());
279301
}
302+
303+
#[test]
304+
fn review_commands() {
305+
for (input, name) in [
306+
("r? @octocat", "octocat"),
307+
("r? octocat", "octocat"),
308+
("R? @octocat", "octocat"),
309+
("can I r? someone?", "someone"),
310+
("Please r? @octocat can you review?", "octocat"),
311+
("r? rust-lang/compiler", "rust-lang/compiler"),
312+
("r? @D--a--s-h", "D--a--s-h"),
313+
] {
314+
let mut input = Input::new(input, vec!["bot"]);
315+
assert_eq!(
316+
input.next(),
317+
Some(Command::Assign(Ok(assign::AssignCommand::ReviewName {
318+
name: name.to_string()
319+
})))
320+
);
321+
assert_eq!(input.next(), None);
322+
}
323+
}
324+
325+
#[test]
326+
fn review_errors() {
327+
use std::error::Error;
328+
for input in ["r?", "r? @", "r? @ user", "r?:user", "r?! @foo", "r?\nline"] {
329+
let mut input = Input::new(input, vec!["bot"]);
330+
let err = match input.next() {
331+
Some(Command::Assign(Err(err))) => err,
332+
c => panic!("unexpected {:?}", c),
333+
};
334+
assert_eq!(
335+
err.source().unwrap().downcast_ref(),
336+
Some(&assign::ParseError::NoUser)
337+
);
338+
assert_eq!(input.next(), None);
339+
}
340+
}
341+
342+
#[test]
343+
fn review_ignored() {
344+
// Checks for things that shouldn't be detected.
345+
for input in ["r", "reviewer? abc", "r foo"] {
346+
let mut input = Input::new(input, vec!["bot"]);
347+
assert_eq!(input.next(), None);
348+
}
349+
}

0 commit comments

Comments
 (0)