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

gripmock / grpctestify-rust / 24368489201

13 Apr 2026 09:44PM UTC coverage: 76.048% (+0.6%) from 75.445%
24368489201

Pull #35

github

web-flow
Merge 745330ff7 into 4ba0f08f1
Pull Request #35: feat: meta section & refactoring

2754 of 3728 new or added lines in 47 files covered. (73.87%)

155 existing lines in 9 files now uncovered.

17097 of 22482 relevant lines covered (76.05%)

2480.3 hits per line

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

71.81
/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✔
NEW
24
    pub fn is_empty(&self) -> bool {
×
NEW
25
        self.start == self.end
×
NEW
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
    Slash,
48
    VarDelim,
49
}
50

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

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

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

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

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

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

333
/// Collect plugin call names with spans.
NEW
334
pub fn collect_plugin_calls(tokens: &[Token]) -> Vec<(&str, Span)> {
×
NEW
335
    let mut result = Vec::new();
×
NEW
336
    let mut i = 0;
×
NEW
337
    while i + 1 < tokens.len() {
×
NEW
338
        if let TokenKind::At = tokens[i].kind
×
NEW
339
            && let TokenKind::Ident(name) = &tokens[i + 1].kind
×
NEW
340
        {
×
NEW
341
            result.push((name.as_str(), tokens[i].span));
×
NEW
342
        }
×
NEW
343
        i += 1;
×
344
    }
NEW
345
    result
×
NEW
346
}
×
347

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

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

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

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

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

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