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,50 @@ 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+ ( "r? rust-lang/compiler" , "rust-lang/compiler" ) ,
311+ ( "r? @D--a--s-h" , "D--a--s-h" ) ,
312+ ] {
313+ let mut input = Input :: new ( input, vec ! [ "bot" ] ) ;
314+ assert_eq ! (
315+ input. next( ) ,
316+ Some ( Command :: Assign ( Ok ( assign:: AssignCommand :: ReviewName {
317+ name: name. to_string( )
318+ } ) ) )
319+ ) ;
320+ assert_eq ! ( input. next( ) , None ) ;
321+ }
322+ }
323+
324+ #[ test]
325+ fn review_errors ( ) {
326+ use std:: error:: Error ;
327+ for input in [ "r?" , "r? @" , "r? @ user" , "r?:user" , "r?! @foo" , "r?\n line" ] {
328+ let mut input = Input :: new ( input, vec ! [ "bot" ] ) ;
329+ let err = match input. next ( ) {
330+ Some ( Command :: Assign ( Err ( err) ) ) => err,
331+ c => panic ! ( "unexpected {:?}" , c) ,
332+ } ;
333+ assert_eq ! (
334+ err. source( ) . unwrap( ) . downcast_ref( ) ,
335+ Some ( & assign:: ParseError :: NoUser )
336+ ) ;
337+ assert_eq ! ( input. next( ) , None ) ;
338+ }
339+ }
340+
341+ #[ test]
342+ fn review_ignored ( ) {
343+ // Checks for things that shouldn't be detected.
344+ for input in [ "r" , "reviewer? abc" , "r foo" ] {
345+ let mut input = Input :: new ( input, vec ! [ "bot" ] ) ;
346+ assert_eq ! ( input. next( ) , None ) ;
347+ }
348+ }
0 commit comments