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

gripmock / grpctestify-rust / 24582789334

17 Apr 2026 07:23PM UTC coverage: 76.58% (+0.1%) from 76.438%
24582789334

push

github

web-flow
Merge pull request #39 from gripmock/refactoring-v3

refactoring part3

579 of 719 new or added lines in 11 files covered. (80.53%)

5 existing lines in 3 files now uncovered.

17585 of 22963 relevant lines covered (76.58%)

2430.68 hits per line

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

83.11
/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 {
44✔
22
        self.end - self.start
44✔
23
    }
44✔
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 {
1,225✔
61
        Self { kind, span }
1,225✔
62
    }
1,225✔
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> {
184✔
68
    let mut out = Vec::new();
184✔
69
    let cs: Vec<char> = source.chars().collect();
184✔
70
    let mut i = 0;
184✔
71

72
    while i < cs.len() {
1,725✔
73
        match cs[i] {
1,541✔
74
            ' ' | '\t' | '\n' | '\r' => {
315✔
75
                i += 1;
315✔
76
            }
315✔
77
            '@' => {
129✔
78
                let s = i;
129✔
79
                i += 1;
129✔
80
                out.push(Token::new(TokenKind::At, Span { start: s, end: i }));
129✔
81
            }
129✔
82
            '(' => {
130✔
83
                let s = i;
130✔
84
                i += 1;
130✔
85
                out.push(Token::new(TokenKind::LParen, Span { start: s, end: i }));
130✔
86
            }
130✔
87
            ')' => {
130✔
88
                let s = i;
130✔
89
                i += 1;
130✔
90
                out.push(Token::new(TokenKind::RParen, Span { start: s, end: i }));
130✔
91
            }
130✔
92
            '[' => {
3✔
93
                let s = i;
3✔
94
                i += 1;
3✔
95
                out.push(Token::new(TokenKind::LBracket, Span { start: s, end: i }));
3✔
96
            }
3✔
97
            ']' => {
3✔
98
                let s = i;
3✔
99
                i += 1;
3✔
100
                out.push(Token::new(TokenKind::RBracket, Span { start: s, end: i }));
3✔
101
            }
3✔
102
            '{' => {
103
                if i + 1 < cs.len() && cs[i + 1] == '{' {
2✔
104
                    let s = i;
1✔
105
                    i += 2;
1✔
106
                    out.push(Token::new(TokenKind::VarDelim, Span { start: s, end: i }));
1✔
107
                } else {
1✔
108
                    let s = i;
1✔
109
                    i += 1;
1✔
110
                    out.push(Token::new(TokenKind::LBrace, Span { start: s, end: i }));
1✔
111
                }
1✔
112
            }
113
            '}' => {
114
                if i + 1 < cs.len() && cs[i + 1] == '}' {
2✔
115
                    let s = i;
1✔
116
                    i += 2;
1✔
117
                    out.push(Token::new(TokenKind::VarDelim, Span { start: s, end: i }));
1✔
118
                } else {
1✔
119
                    let s = i;
1✔
120
                    i += 1;
1✔
121
                    out.push(Token::new(TokenKind::RBrace, Span { start: s, end: i }));
1✔
122
                }
1✔
123
            }
124
            '.' => {
181✔
125
                let s = i;
181✔
126
                i += 1;
181✔
127
                out.push(Token::new(TokenKind::Dot, Span { start: s, end: i }));
181✔
128
            }
181✔
129
            ',' => {
8✔
130
                let s = i;
8✔
131
                i += 1;
8✔
132
                out.push(Token::new(TokenKind::Comma, Span { start: s, end: i }));
8✔
133
            }
8✔
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] == '=' {
12✔
141
                    let s = i;
2✔
142
                    i += 2;
2✔
143
                    out.push(Token::new(
2✔
144
                        TokenKind::Op("!=".into()),
2✔
145
                        Span { start: s, end: i },
2✔
146
                    ));
2✔
147
                } else {
10✔
148
                    let s = i;
10✔
149
                    i += 1;
10✔
150
                    out.push(Token::new(TokenKind::Bang, Span { start: s, end: i }));
10✔
151
                }
10✔
152
            }
153
            '=' if i + 1 < cs.len() && cs[i + 1] == '=' => {
74✔
154
                let s = i;
74✔
155
                i += 2;
74✔
156
                out.push(Token::new(
74✔
157
                    TokenKind::Op("==".into()),
74✔
158
                    Span { start: s, end: i },
74✔
159
                ));
74✔
160
            }
74✔
NEW
161
            '=' => i += 1,
×
162
            '>' => {
163
                if i + 1 < cs.len() && cs[i + 1] == '=' {
4✔
164
                    let s = i;
2✔
165
                    i += 2;
2✔
166
                    out.push(Token::new(
2✔
167
                        TokenKind::Op(">=".into()),
2✔
168
                        Span { start: s, end: i },
2✔
169
                    ));
2✔
170
                } else {
2✔
171
                    let s = i;
2✔
172
                    i += 1;
2✔
173
                    out.push(Token::new(
2✔
174
                        TokenKind::Op(">".into()),
2✔
175
                        Span { start: s, end: i },
2✔
176
                    ));
2✔
177
                }
2✔
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;
50✔
198
                i += 1;
50✔
199
                let mut v = String::new();
50✔
200
                while i < cs.len() && cs[i] != '"' {
376✔
201
                    if cs[i] == '\\' && i + 1 < cs.len() {
326✔
202
                        i += 1;
×
203
                        v.push(cs[i]);
×
204
                    } else {
326✔
205
                        v.push(cs[i]);
326✔
206
                    }
326✔
207
                    i += 1;
326✔
208
                }
209
                if i < cs.len() {
50✔
210
                    i += 1;
50✔
211
                }
50✔
212
                out.push(Token::new(
50✔
213
                    TokenKind::StringLit(v),
50✔
214
                    Span { start: s, end: i },
50✔
215
                ));
216
            }
217
            c if c.is_ascii_digit() => {
488✔
218
                let s = i;
58✔
219
                let mut v = String::new();
58✔
220
                while i < cs.len() && (cs[i].is_ascii_digit() || cs[i] == '.') {
157✔
221
                    v.push(cs[i]);
99✔
222
                    i += 1;
99✔
223
                }
99✔
224
                out.push(Token::new(
58✔
225
                    TokenKind::NumberLit(v),
58✔
226
                    Span { start: s, end: i },
58✔
227
                ));
228
            }
229
            c if c.is_alphabetic() || c == '_' => {
430✔
230
                let s = i;
421✔
231
                let mut v = String::new();
421✔
232
                while i < cs.len() && (cs[i].is_alphanumeric() || cs[i] == '_') {
2,055✔
233
                    v.push(cs[i]);
1,634✔
234
                    i += 1;
1,634✔
235
                }
1,634✔
236
                let kind = match v.as_str() {
421✔
237
                    "contains" | "matches" | "startsWith" | "endsWith" | "startswith"
421✔
238
                    | "endswith" => TokenKind::Op(v.clone()),
408✔
239
                    _ => TokenKind::Ident(v.clone()),
408✔
240
                };
241
                out.push(Token::new(kind, Span { start: s, end: i }));
421✔
242
            }
243
            '/' => {
244
                let s = i;
8✔
245
                let mut j = i + 1;
8✔
246
                let mut is_regex = true;
8✔
247
                let mut pattern = String::new();
8✔
248
                let mut escaped = false;
8✔
249
                let mut in_cc = false;
8✔
250
                while j < cs.len() {
51✔
251
                    if escaped {
49✔
252
                        pattern.push(cs[j]);
1✔
253
                        escaped = false;
1✔
254
                        j += 1;
1✔
255
                    } else if cs[j] == '\\' {
48✔
256
                        pattern.push(cs[j]);
1✔
257
                        escaped = true;
1✔
258
                        j += 1;
1✔
259
                    } else if cs[j] == '[' {
47✔
260
                        in_cc = true;
×
261
                        pattern.push(cs[j]);
×
262
                        j += 1;
×
263
                    } else if cs[j] == ']' {
47✔
264
                        in_cc = false;
×
265
                        pattern.push(cs[j]);
×
266
                        j += 1;
×
267
                    } else if cs[j] == '/' && !in_cc {
47✔
268
                        j += 1;
6✔
269
                        let mut flags = String::new();
6✔
270
                        while j < cs.len()
11✔
271
                            && cs[j].is_ascii_alphabetic()
11✔
272
                            && "gimsuy".contains(cs[j])
5✔
273
                        {
5✔
274
                            flags.push(cs[j]);
5✔
275
                            j += 1;
5✔
276
                        }
5✔
277
                        if j < cs.len()
6✔
278
                            && (cs[j].is_alphanumeric() || cs[j] == '_')
6✔
279
                            && !flags.is_empty()
×
280
                        {
×
281
                            is_regex = false;
×
282
                        }
6✔
283
                        break;
6✔
284
                    } else if cs[j] == '\n' || cs[j] == '\r' || cs[j] == ' ' || cs[j] == '\t' {
41✔
285
                        is_regex = false;
×
286
                        break;
×
287
                    } else {
41✔
288
                        pattern.push(cs[j]);
41✔
289
                        j += 1;
41✔
290
                    }
41✔
291
                }
292
                if is_regex && !pattern.is_empty() && j > i + 1 {
8✔
293
                    out.push(Token::new(
8✔
294
                        TokenKind::RegExpLit {
8✔
295
                            pattern,
8✔
296
                            flags: String::new(),
8✔
297
                        },
8✔
298
                        Span { start: s, end: j },
8✔
299
                    ));
8✔
300
                    i = j;
8✔
301
                } else {
8✔
302
                    i += 1;
×
303
                    out.push(Token::new(TokenKind::Slash, Span { start: s, end: i }));
×
304
                }
×
305
            }
306
            _ => {
1✔
307
                i += 1;
1✔
308
            }
1✔
309
        }
310
    }
311
    out
184✔
312
}
184✔
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)> {
×
338
    let mut result = Vec::new();
×
339
    let mut i = 0;
×
340
    while i + 1 < tokens.len() {
×
341
        if let TokenKind::At = tokens[i].kind
×
342
            && let TokenKind::Ident(name) = &tokens[i + 1].kind
×
343
        {
×
344
            result.push((name.as_str(), tokens[i].span));
×
345
        }
×
346
        i += 1;
×
347
    }
348
    result
×
349
}
×
350

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

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

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

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

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

400
    #[test]
401
    fn test_spans_correct() {
1✔
402
        let tokens = tokenize_assertion(".x == 0");
1✔
403
        // "==" should be at position 3
404
        if let Some(t) = tokens
1✔
405
            .iter()
1✔
406
            .find(|t| matches!(&t.kind, TokenKind::Op(s) if s == "=="))
1✔
407
        {
408
            assert_eq!(t.span.start, 3);
1✔
409
            assert_eq!(t.span.end, 5);
1✔
410
        } else {
411
            panic!("No == token found");
×
412
        }
413
    }
1✔
414
}
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