11use crate :: error:: Error ;
22use crate :: ignore_block:: IgnoreBlocks ;
3- use crate :: token:: { Token , Tokenizer } ;
3+ use crate :: token:: Tokenizer ;
4+ use regex:: Regex ;
45
56pub mod assign;
67pub mod close;
@@ -13,10 +14,6 @@ pub mod relabel;
1314pub mod second;
1415pub 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 ) ]
2118pub 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
4441fn parse_single_command < ' a , T , F , M > (
@@ -63,25 +60,22 @@ where
6360
6461impl < ' 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
170168impl < ' 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]
234256fn 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?\n line" ] {
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