1
1
use crate :: error:: Error ;
2
2
use crate :: ignore_block:: IgnoreBlocks ;
3
- use crate :: token:: { Token , Tokenizer } ;
3
+ use crate :: token:: Tokenizer ;
4
+ use regex:: Regex ;
4
5
5
6
pub mod assign;
6
7
pub mod close;
@@ -13,10 +14,6 @@ pub mod relabel;
13
14
pub mod second;
14
15
pub mod shortcut;
15
16
16
- pub fn find_command_start ( input : & str , bot : & str ) -> Option < usize > {
17
- input. to_ascii_lowercase ( ) . find ( & format ! ( "@{}" , bot) )
18
- }
19
-
20
17
#[ derive( Debug , PartialEq ) ]
21
18
pub enum Command < ' a > {
22
19
Relabel ( Result < relabel:: RelabelCommand , Error < ' a > > ) ,
@@ -36,9 +33,9 @@ pub struct Input<'a> {
36
33
all : & ' a str ,
37
34
parsed : usize ,
38
35
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 ,
42
39
}
43
40
44
41
fn parse_single_command < ' a , T , F , M > (
@@ -63,25 +60,22 @@ where
63
60
64
61
impl < ' a > Input < ' a > {
65
62
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 ( ) ;
66
69
Input {
67
70
all : input,
68
71
parsed : 0 ,
69
72
ignore : IgnoreBlocks :: new ( input) ,
70
- bot ,
73
+ bot_re ,
71
74
}
72
75
}
73
76
74
77
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 ..] ) ;
85
79
log:: info!( "identified potential command" ) ;
86
80
87
81
let mut success = vec ! [ ] ;
@@ -147,41 +141,55 @@ impl<'a> Input<'a> {
147
141
) ;
148
142
}
149
143
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
-
159
144
let ( mut tok, c) = success. pop ( ) ?;
160
145
// 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
+ }
166
149
Some ( c)
167
150
}
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
+ }
168
166
}
169
167
170
168
impl < ' a > Iterator for Input < ' a > {
171
169
type Item = Command < ' a > ;
172
170
173
171
fn next ( & mut self ) -> Option < Command < ' a > > {
174
172
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 ( ) {
182
191
return Some ( command) ;
183
192
}
184
- self . parsed += self . bot . len ( ) + 1 ;
185
193
}
186
194
}
187
195
}
@@ -230,6 +238,20 @@ fn code_2() {
230
238
assert ! ( input. next( ) . is_none( ) ) ;
231
239
}
232
240
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
+
233
255
#[ test]
234
256
fn edit_1 ( ) {
235
257
let input_old = "@bot modify labels: +bug." ;
@@ -277,3 +299,51 @@ fn multiname() {
277
299
assert ! ( input. next( ) . unwrap( ) . is_ok( ) ) ;
278
300
assert ! ( input. next( ) . is_none( ) ) ;
279
301
}
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