• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

gripmock / grpctestify-rust / 24861076244

23 Apr 2026 10:01PM UTC coverage: 77.725% (+0.8%) from 76.897%
24861076244

Pull #42

github

web-flow
Merge ed18c97c2 into 59e77d08a
Pull Request #42: bump rustc & command grpcurl (exp)

1062 of 1251 new or added lines in 25 files covered. (84.89%)

95 existing lines in 7 files now uncovered.

18856 of 24260 relevant lines covered (77.72%)

40973.18 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

83.67
/src/parser/tokenizer.rs
1
//! Public tokenizer for GCTF assertion expressions.
2
//!
3
//! Pipeline: `text → tokenize_assertion() → Vec<Token>` where each `Token`
4
//! has a `Span { start, end }` with exact byte positions in the source.
5
//!
6
//! This is used by:
7
//! - **LSP semantic tokens** — highlight operators, keywords, plugins, regexes
8
//! - **Optimizer** — safe string-literal-aware rule matching (4.3)
9
//! - **Semantics** — type-checking operators against TypeInfo
10

11
use serde::{Deserialize, Serialize};
12

13
/// Byte range in source text.
14
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
15
pub struct Span {
16
    pub start: usize,
17
    pub end: usize,
18
}
19

20
impl Span {
21
    pub fn len(&self) -> usize {
52✔
22
        self.end - self.start
52✔
23
    }
52✔
24
    pub fn is_empty(&self) -> bool {
×
25
        self.start == self.end
×
26
    }
×
27
}
28

29
/// Token kinds produced by the assertion tokenizer.
30
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
31
pub enum TokenKind {
32
    Ident(String),
33
    StringLit(String),
34
    NumberLit(String),
35
    Op(String),
36
    RegExpLit { pattern: String, flags: String },
37
    At,
38
    LParen,
39
    RParen,
40
    LBracket,
41
    RBracket,
42
    LBrace,
43
    RBrace,
44
    Dot,
45
    Comma,
46
    Bang,
47
    Pipe,
48
    Slash,
49
    VarDelim,
50
}
51

52
/// A token with its source position.
53
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
54
pub struct Token {
55
    pub kind: TokenKind,
56
    pub span: Span,
57
}
58

59
impl Token {
60
    pub fn new(kind: TokenKind, span: Span) -> Self {
24,504,677✔
61
        Self { kind, span }
24,504,677✔
62
    }
24,504,677✔
63
}
64

65
/// Tokenize an assertion expression string into a list of tokens with
66
/// exact byte positions.
67
pub fn tokenize_assertion(source: &str) -> Vec<Token> {
1,300,784✔
68
    let mut out = Vec::with_capacity(source.len() / 2);
1,300,784✔
69
    let cs: Vec<char> = source.chars().collect();
1,300,784✔
70
    let mut i = 0;
1,300,784✔
71

72
    while i < cs.len() {
39,606,879✔
73
        match cs[i] {
38,306,095✔
74
            ' ' | '\t' | '\n' | '\r' => {
13,801,415✔
75
                i += 1;
13,801,415✔
76
            }
13,801,415✔
77
            '@' => {
800,467✔
78
                let s = i;
800,467✔
79
                i += 1;
800,467✔
80
                out.push(Token::new(TokenKind::At, Span { start: s, end: i }));
800,467✔
81
            }
800,467✔
82
            '(' => {
1,600,501✔
83
                let s = i;
1,600,501✔
84
                i += 1;
1,600,501✔
85
                out.push(Token::new(TokenKind::LParen, Span { start: s, end: i }));
1,600,501✔
86
            }
1,600,501✔
87
            ')' => {
1,600,501✔
88
                let s = i;
1,600,501✔
89
                i += 1;
1,600,501✔
90
                out.push(Token::new(TokenKind::RParen, Span { start: s, end: i }));
1,600,501✔
91
            }
1,600,501✔
92
            '[' => {
7✔
93
                let s = i;
7✔
94
                i += 1;
7✔
95
                out.push(Token::new(TokenKind::LBracket, Span { start: s, end: i }));
7✔
96
            }
7✔
97
            ']' => {
7✔
98
                let s = i;
7✔
99
                i += 1;
7✔
100
                out.push(Token::new(TokenKind::RBracket, Span { start: s, end: i }));
7✔
101
            }
7✔
102
            '{' => {
103
                if i + 1 < cs.len() && cs[i + 1] == '{' {
400,024✔
104
                    let s = i;
400,021✔
105
                    i += 2;
400,021✔
106
                    out.push(Token::new(TokenKind::VarDelim, Span { start: s, end: i }));
400,021✔
107
                } else {
400,021✔
108
                    let s = i;
3✔
109
                    i += 1;
3✔
110
                    out.push(Token::new(TokenKind::LBrace, Span { start: s, end: i }));
3✔
111
                }
3✔
112
            }
113
            '}' => {
114
                if i + 1 < cs.len() && cs[i + 1] == '}' {
400,024✔
115
                    let s = i;
400,021✔
116
                    i += 2;
400,021✔
117
                    out.push(Token::new(TokenKind::VarDelim, Span { start: s, end: i }));
400,021✔
118
                } else {
400,021✔
119
                    let s = i;
3✔
120
                    i += 1;
3✔
121
                    out.push(Token::new(TokenKind::RBrace, Span { start: s, end: i }));
3✔
122
                }
3✔
123
            }
124
            '.' => {
3,800,421✔
125
                let s = i;
3,800,421✔
126
                i += 1;
3,800,421✔
127
                out.push(Token::new(TokenKind::Dot, Span { start: s, end: i }));
3,800,421✔
128
            }
3,800,421✔
129
            ',' => {
400,010✔
130
                let s = i;
400,010✔
131
                i += 1;
400,010✔
132
                out.push(Token::new(TokenKind::Comma, Span { start: s, end: i }));
400,010✔
133
            }
400,010✔
134
            '|' => {
7✔
135
                let s = i;
7✔
136
                i += 1;
7✔
137
                out.push(Token::new(TokenKind::Pipe, Span { start: s, end: i }));
7✔
138
            }
7✔
139
            '!' => {
140
                if i + 1 < cs.len() && cs[i + 1] == '=' {
142✔
141
                    let s = i;
60✔
142
                    i += 2;
60✔
143
                    out.push(Token::new(
60✔
144
                        TokenKind::Op("!=".into()),
60✔
145
                        Span { start: s, end: i },
60✔
146
                    ));
60✔
147
                } else {
82✔
148
                    let s = i;
82✔
149
                    i += 1;
82✔
150
                    out.push(Token::new(TokenKind::Bang, Span { start: s, end: i }));
82✔
151
                }
82✔
152
            }
153
            '=' if i + 1 < cs.len() && cs[i + 1] == '=' => {
2,600,253✔
154
                let s = i;
2,600,253✔
155
                i += 2;
2,600,253✔
156
                out.push(Token::new(
2,600,253✔
157
                    TokenKind::Op("==".into()),
2,600,253✔
158
                    Span { start: s, end: i },
2,600,253✔
159
                ));
2,600,253✔
160
            }
2,600,253✔
161
            '=' => i += 1,
×
162
            '>' => {
163
                if i + 1 < cs.len() && cs[i + 1] == '=' {
800,049✔
164
                    let s = i;
400,016✔
165
                    i += 2;
400,016✔
166
                    out.push(Token::new(
400,016✔
167
                        TokenKind::Op(">=".into()),
400,016✔
168
                        Span { start: s, end: i },
400,016✔
169
                    ));
400,016✔
170
                } else {
400,033✔
171
                    let s = i;
400,033✔
172
                    i += 1;
400,033✔
173
                    out.push(Token::new(
400,033✔
174
                        TokenKind::Op(">".into()),
400,033✔
175
                        Span { start: s, end: i },
400,033✔
176
                    ));
400,033✔
177
                }
400,033✔
178
            }
179
            '<' => {
180
                if i + 1 < cs.len() && cs[i + 1] == '=' {
3✔
181
                    let s = i;
1✔
182
                    i += 2;
1✔
183
                    out.push(Token::new(
1✔
184
                        TokenKind::Op("<=".into()),
1✔
185
                        Span { start: s, end: i },
1✔
186
                    ));
1✔
187
                } else {
2✔
188
                    let s = i;
2✔
189
                    i += 1;
2✔
190
                    out.push(Token::new(
2✔
191
                        TokenKind::Op("<".into()),
2✔
192
                        Span { start: s, end: i },
2✔
193
                    ));
2✔
194
                }
2✔
195
            }
196
            '"' => {
197
                let s = i;
400,457✔
198
                i += 1;
400,457✔
199
                let mut v = String::new();
400,457✔
200
                while i < cs.len() && cs[i] != '"' {
2,401,828✔
201
                    if cs[i] == '\\' && i + 1 < cs.len() {
2,001,371✔
202
                        i += 1;
×
203
                        v.push(cs[i]);
×
204
                    } else {
2,001,371✔
205
                        v.push(cs[i]);
2,001,371✔
206
                    }
2,001,371✔
207
                    i += 1;
2,001,371✔
208
                }
209
                if i < cs.len() {
400,457✔
210
                    i += 1;
400,457✔
211
                }
400,457✔
212
                out.push(Token::new(
400,457✔
213
                    TokenKind::StringLit(v),
400,457✔
214
                    Span { start: s, end: i },
400,457✔
215
                ));
216
            }
217
            c if c.is_ascii_digit() => {
11,701,807✔
218
                let s = i;
1,700,239✔
219
                let mut v = String::new();
1,700,239✔
220
                while i < cs.len() && (cs[i].is_ascii_digit() || cs[i] == '.') {
4,300,702✔
221
                    v.push(cs[i]);
2,600,463✔
222
                    i += 1;
2,600,463✔
223
                }
2,600,463✔
224
                out.push(Token::new(
1,700,239✔
225
                    TokenKind::NumberLit(v),
1,700,239✔
226
                    Span { start: s, end: i },
1,700,239✔
227
                ));
228
            }
229
            c if c.is_alphabetic() || c == '_' => {
10,001,568✔
230
                let s = i;
9,601,555✔
231
                let mut v = String::new();
9,601,555✔
232
                while i < cs.len() && (cs[i].is_alphanumeric() || cs[i] == '_') {
50,309,674✔
233
                    v.push(cs[i]);
40,708,119✔
234
                    i += 1;
40,708,119✔
235
                }
40,708,119✔
236
                let kind = match v.as_str() {
9,601,555✔
237
                    "contains" | "matches" | "startsWith" | "endsWith" | "startswith"
9,601,555✔
238
                    | "endswith" => TokenKind::Op(v),
9,601,518✔
239
                    _ => TokenKind::Ident(v),
9,601,518✔
240
                };
241
                out.push(Token::new(kind, Span { start: s, end: i }));
9,601,555✔
242
            }
243
            '/' => {
244
                let s = i;
400,010✔
245
                let mut j = i + 1;
400,010✔
246
                let mut is_regex = true;
400,010✔
247
                let mut pattern = String::new();
400,010✔
248
                let mut escaped = false;
400,010✔
249
                let mut in_cc = false;
400,010✔
250
                while j < cs.len() {
2,400,064✔
251
                    if escaped {
2,400,061✔
252
                        pattern.push(cs[j]);
1✔
253
                        escaped = false;
1✔
254
                        j += 1;
1✔
255
                    } else if cs[j] == '\\' {
2,400,060✔
256
                        pattern.push(cs[j]);
1✔
257
                        escaped = true;
1✔
258
                        j += 1;
1✔
259
                    } else if cs[j] == '[' {
2,400,059✔
260
                        in_cc = true;
×
261
                        pattern.push(cs[j]);
×
262
                        j += 1;
×
263
                    } else if cs[j] == ']' {
2,400,059✔
264
                        in_cc = false;
×
265
                        pattern.push(cs[j]);
×
266
                        j += 1;
×
267
                    } else if cs[j] == '/' && !in_cc {
2,400,059✔
268
                        j += 1;
400,007✔
269
                        let mut flags = String::new();
400,007✔
270
                        while j < cs.len()
800,012✔
271
                            && cs[j].is_ascii_alphabetic()
800,011✔
272
                            && "gimsuy".contains(cs[j])
400,005✔
273
                        {
400,005✔
274
                            flags.push(cs[j]);
400,005✔
275
                            j += 1;
400,005✔
276
                        }
400,005✔
277
                        if j < cs.len()
400,007✔
278
                            && (cs[j].is_alphanumeric() || cs[j] == '_')
400,006✔
279
                            && !flags.is_empty()
×
280
                        {
×
281
                            is_regex = false;
×
282
                        }
400,007✔
283
                        break;
400,007✔
284
                    } else if cs[j] == '\n' || cs[j] == '\r' || cs[j] == ' ' || cs[j] == '\t' {
2,000,052✔
285
                        is_regex = false;
×
286
                        break;
×
287
                    } else {
2,000,052✔
288
                        pattern.push(cs[j]);
2,000,052✔
289
                        j += 1;
2,000,052✔
290
                    }
2,000,052✔
291
                }
292
                if is_regex && !pattern.is_empty() && j > i + 1 {
400,010✔
293
                    out.push(Token::new(
400,010✔
294
                        TokenKind::RegExpLit {
400,010✔
295
                            pattern,
400,010✔
296
                            flags: String::new(),
400,010✔
297
                        },
400,010✔
298
                        Span { start: s, end: j },
400,010✔
299
                    ));
400,010✔
300
                    i = j;
400,010✔
301
                } else {
400,010✔
302
                    i += 1;
×
303
                    out.push(Token::new(TokenKind::Slash, Span { start: s, end: i }));
×
304
                }
×
305
            }
306
            _ => {
3✔
307
                i += 1;
3✔
308
            }
3✔
309
        }
310
    }
311
    out
1,300,784✔
312
}
1,300,784✔
313

314
/// Collect all identifier tokens.
315
pub fn collect_identifiers(tokens: &[Token]) -> Vec<&str> {
×
316
    tokens
×
317
        .iter()
×
318
        .filter_map(|t| match &t.kind {
×
319
            TokenKind::Ident(s) => Some(s.as_str()),
×
320
            _ => None,
×
321
        })
×
322
        .collect()
×
323
}
×
324

325
/// Collect all operator tokens.
326
pub fn collect_operators(tokens: &[Token]) -> Vec<&str> {
×
327
    tokens
×
328
        .iter()
×
329
        .filter_map(|t| match &t.kind {
×
330
            TokenKind::Op(s) => Some(s.as_str()),
×
331
            _ => None,
×
332
        })
×
333
        .collect()
×
334
}
×
335

336
/// Collect plugin call names with spans.
337
pub fn collect_plugin_calls(tokens: &[Token]) -> Vec<(&str, Span)> {
×
NEW
338
    let mut result = Vec::with_capacity(tokens.len() / 4);
×
NEW
339
    for [at, ident] in tokens.array_windows::<2>() {
×
NEW
340
        if let TokenKind::At = at.kind
×
NEW
341
            && let TokenKind::Ident(name) = &ident.kind
×
342
        {
×
NEW
343
            result.push((name.as_str(), at.span));
×
344
        }
×
345
    }
UNCOV
346
    result
×
347
}
×
348

349
#[cfg(test)]
350
mod tests {
351
    use super::*;
352

353
    #[test]
354
    fn test_tokenize_simple() {
1✔
355
        let tokens = tokenize_assertion(".id == 123");
1✔
356
        assert!(!tokens.is_empty());
1✔
357
        assert!(
1✔
358
            tokens
1✔
359
                .iter()
1✔
360
                .any(|t| matches!(&t.kind, TokenKind::Op(s) if s == "=="))
1✔
361
        );
362
        assert!(
1✔
363
            tokens
1✔
364
                .iter()
1✔
365
                .any(|t| matches!(&t.kind, TokenKind::NumberLit(s) if s == "123"))
1✔
366
        );
367
    }
1✔
368

369
    #[test]
370
    fn test_tokenize_plugin_call() {
1✔
371
        let tokens = tokenize_assertion("@len(.items) == 0");
1✔
372
        assert!(tokens.iter().any(|t| matches!(t.kind, TokenKind::At)));
1✔
373
        assert!(
1✔
374
            tokens
1✔
375
                .iter()
1✔
376
                .any(|t| matches!(&t.kind, TokenKind::Ident(s) if s == "len"))
1✔
377
        );
378
    }
1✔
379

380
    #[test]
381
    fn test_tokenize_regex() {
1✔
382
        let tokens = tokenize_assertion("@regex(.x, /hello/i) == true");
1✔
383
        assert!(tokens.iter().any(
1✔
384
            |t| matches!(&t.kind, TokenKind::RegExpLit { pattern, .. } if pattern == "hello")
1✔
385
        ));
386
    }
1✔
387

388
    #[test]
389
    fn test_tokenize_string_literal() {
1✔
390
        let tokens = tokenize_assertion(".name == \"hello world\"");
1✔
391
        assert!(
1✔
392
            tokens
1✔
393
                .iter()
1✔
394
                .any(|t| matches!(&t.kind, TokenKind::StringLit(s) if s == "hello world"))
1✔
395
        );
396
    }
1✔
397

398
    #[test]
399
    fn test_spans_correct() {
1✔
400
        let tokens = tokenize_assertion(".x == 0");
1✔
401
        // "==" should be at position 3
402
        if let Some(t) = tokens
1✔
403
            .iter()
1✔
404
            .find(|t| matches!(&t.kind, TokenKind::Op(s) if s == "=="))
1✔
405
        {
406
            assert_eq!(t.span.start, 3);
1✔
407
            assert_eq!(t.span.end, 5);
1✔
408
        } else {
409
            panic!("No == token found");
×
410
        }
411
    }
1✔
412
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc