Skip to main content

mir_extractor/dataflow/
field.rs

1/// Field-sensitive taint analysis module
2///
3/// This module provides data structures and algorithms for tracking taint
4/// at the granularity of individual struct fields, enabling more precise
5/// analysis and reducing false positives.
6use std::collections::HashMap;
7
8/// Represents a path to a specific field within a struct hierarchy
9///
10/// Examples:
11/// - `_1.0` → base_var="_1", indices=[0]
12/// - `_1.1.2` → base_var="_1", indices=[1, 2]
13/// - `_3` → base_var="_3", indices=[] (whole variable)
14#[derive(Debug, Clone, PartialEq, Eq, Hash)]
15pub struct FieldPath {
16    /// Base variable name (e.g., "_1", "_3", "_10")
17    pub base_var: String,
18
19    /// Field indices from root to leaf
20    /// Empty vec means the entire variable (not a specific field)
21    pub indices: Vec<usize>,
22}
23
24impl FieldPath {
25    /// Create a new field path
26    pub fn new(base_var: String, indices: Vec<usize>) -> Self {
27        FieldPath { base_var, indices }
28    }
29
30    /// Create a field path for an entire variable (no field access)
31    pub fn whole_var(base_var: String) -> Self {
32        FieldPath {
33            base_var,
34            indices: Vec::new(),
35        }
36    }
37
38    /// Create a field path from a base and single field index
39    pub fn single_field(base_var: String, index: usize) -> Self {
40        FieldPath {
41            base_var,
42            indices: vec![index],
43        }
44    }
45
46    /// Convert to canonical string representation
47    /// Examples: "_1" , "_1.0", "_1.1.2"
48    pub fn to_string(&self) -> String {
49        if self.indices.is_empty() {
50            self.base_var.clone()
51        } else {
52            let indices_str = self
53                .indices
54                .iter()
55                .map(|i| i.to_string())
56                .collect::<Vec<_>>()
57                .join(".");
58            format!("{}.{}", self.base_var, indices_str)
59        }
60    }
61
62    /// Check if this path is a prefix of another path
63    ///
64    /// Example: `_1.1` is a prefix of `_1.1.2`
65    /// Example: `_1` is a prefix of `_1.0`
66    /// Example: `_1.2` is NOT a prefix of `_1.1.2`
67    pub fn is_prefix_of(&self, other: &FieldPath) -> bool {
68        if self.base_var != other.base_var {
69            return false;
70        }
71
72        if self.indices.len() > other.indices.len() {
73            return false;
74        }
75
76        self.indices
77            .iter()
78            .zip(other.indices.iter())
79            .all(|(a, b)| a == b)
80    }
81
82    /// Check if this is a whole variable (no field indices)
83    pub fn is_whole_var(&self) -> bool {
84        self.indices.is_empty()
85    }
86
87    /// Get parent field path (remove last index)
88    /// Example: `_1.1.2` → `Some(_1.1)`
89    /// Example: `_1.0` → `Some(_1)`
90    /// Example: `_1` → `None`
91    pub fn parent(&self) -> Option<FieldPath> {
92        if self.indices.is_empty() {
93            None
94        } else {
95            let mut parent_indices = self.indices.clone();
96            parent_indices.pop();
97            Some(FieldPath {
98                base_var: self.base_var.clone(),
99                indices: parent_indices,
100            })
101        }
102    }
103}
104
105/// Taint state for a field or variable
106#[derive(Debug, Clone, PartialEq)]
107pub enum FieldTaint {
108    /// Field is clean (not tainted)
109    Clean,
110
111    /// Field is tainted from a source
112    Tainted {
113        source_type: String,
114        source_location: String,
115    },
116
117    /// Field was sanitized
118    Sanitized { sanitizer: String },
119
120    /// Unknown state (not yet analyzed)
121    Unknown,
122}
123
124impl FieldTaint {
125    /// Check if taint state is tainted
126    pub fn is_tainted(&self) -> bool {
127        matches!(self, FieldTaint::Tainted { .. })
128    }
129
130    /// Check if taint state is clean
131    pub fn is_clean(&self) -> bool {
132        matches!(self, FieldTaint::Clean)
133    }
134
135    /// Check if taint state is sanitized
136    pub fn is_sanitized(&self) -> bool {
137        matches!(self, FieldTaint::Sanitized { .. })
138    }
139}
140
141/// Maps field paths to their taint states
142///
143/// This data structure tracks taint at the field level, enabling precise
144/// analysis where only specific fields of a struct are tainted.
145#[derive(Debug, Clone)]
146pub struct FieldTaintMap {
147    /// Maps field paths to taint states
148    fields: HashMap<FieldPath, FieldTaint>,
149}
150
151impl FieldTaintMap {
152    /// Create a new empty field taint map
153    pub fn new() -> Self {
154        FieldTaintMap {
155            fields: HashMap::new(),
156        }
157    }
158
159    /// Set taint state for a specific field
160    pub fn set_field_taint(&mut self, path: FieldPath, taint: FieldTaint) {
161        self.fields.insert(path, taint);
162    }
163
164    /// Get taint state for a specific field
165    ///
166    /// If the exact field is not in the map, checks parent fields.
167    /// Returns Unknown if no information is available.
168    pub fn get_field_taint(&self, path: &FieldPath) -> FieldTaint {
169        // Check exact match first
170        if let Some(taint) = self.fields.get(path) {
171            return taint.clone();
172        }
173
174        // Check parent fields (if child is unknown but parent is tainted, child is too)
175        let mut current = path.clone();
176        while let Some(parent) = current.parent() {
177            if let Some(taint) = self.fields.get(&parent) {
178                return taint.clone();
179            }
180            current = parent;
181        }
182
183        // No information available
184        FieldTaint::Unknown
185    }
186
187    /// Set taint for an entire variable (all fields)
188    ///
189    /// This is used when a whole struct is assigned a tainted value.
190    /// In conservative analysis, this taints all known fields of the struct.
191    pub fn set_var_taint(&mut self, base_var: &str, taint: FieldTaint) {
192        // Set taint for the base variable
193        let base_path = FieldPath::whole_var(base_var.to_string());
194        self.fields.insert(base_path, taint.clone());
195
196        // Also propagate to all known fields of this variable
197        let fields_to_update: Vec<FieldPath> = self
198            .fields
199            .keys()
200            .filter(|path| path.base_var == base_var && !path.is_whole_var())
201            .cloned()
202            .collect();
203
204        for field_path in fields_to_update {
205            self.fields.insert(field_path, taint.clone());
206        }
207    }
208
209    /// Get all fields for a given base variable
210    pub fn get_fields_for_var(&self, base_var: &str) -> Vec<(FieldPath, FieldTaint)> {
211        self.fields
212            .iter()
213            .filter(|(path, _)| path.base_var == base_var)
214            .map(|(path, taint)| (path.clone(), taint.clone()))
215            .collect()
216    }
217
218    /// Check if any field of a variable is tainted
219    pub fn has_tainted_field(&self, base_var: &str) -> bool {
220        self.fields
221            .iter()
222            .any(|(path, taint)| path.base_var == base_var && taint.is_tainted())
223    }
224
225    /// Merge another FieldTaintMap into this one
226    ///
227    /// Used when combining taint states from different paths.
228    /// If a field appears in both maps, the more specific taint wins:
229    /// Tainted > Sanitized > Clean > Unknown
230    pub fn merge(&mut self, other: &FieldTaintMap) {
231        for (path, other_taint) in &other.fields {
232            let current_taint = self.get_field_taint(path);
233
234            // Merge logic: tainted wins over everything
235            let merged_taint = match (&current_taint, other_taint) {
236                (FieldTaint::Tainted { .. }, _) => current_taint,
237                (_, FieldTaint::Tainted { .. }) => other_taint.clone(),
238                (FieldTaint::Sanitized { .. }, _) => current_taint,
239                (_, FieldTaint::Sanitized { .. }) => other_taint.clone(),
240                (FieldTaint::Clean, _) => current_taint,
241                _ => other_taint.clone(),
242            };
243
244            self.fields.insert(path.clone(), merged_taint);
245        }
246    }
247
248    /// Clear all taint information
249    pub fn clear(&mut self) {
250        self.fields.clear();
251    }
252
253    /// Get number of tracked fields
254    pub fn len(&self) -> usize {
255        self.fields.len()
256    }
257
258    /// Check if map is empty
259    pub fn is_empty(&self) -> bool {
260        self.fields.is_empty()
261    }
262}
263
264impl Default for FieldTaintMap {
265    fn default() -> Self {
266        Self::new()
267    }
268}
269
270/// Parse field access patterns from MIR expressions
271pub mod parser {
272    use super::*;
273
274    /// Parse a field access from a MIR expression
275    ///
276    /// Handles patterns:
277    /// - `(_1.0: Type)` → FieldPath { base_var: "_1", indices: [0] }
278    /// - `(_1.1: Type)` → FieldPath { base_var: "_1", indices: [1] }
279    /// - `((_1.1: Type).2: Type2)` → FieldPath { base_var: "_1", indices: [1, 2] }
280    /// - `((_2 as Ready).0: String)` → FieldPath { base_var: "_2", indices: [0] }
281    ///
282    /// Returns None if the expression is not a field access.
283    pub fn parse_field_access(expr: &str) -> Option<FieldPath> {
284        let expr = expr.trim();
285
286        // Check if this looks like a field access (requires a dot)
287        if !expr.contains('.') {
288            return None;
289        }
290
291        // If it contains parentheses, try MIR-style parsing
292        if expr.contains('(') {
293            // Try parsing as nested field first
294            if let Some(path) = parse_nested_field_access(expr) {
295                return Some(path);
296            }
297
298            // Try parsing as downcast field access
299            if let Some(path) = parse_downcast_field_access(expr) {
300                return Some(path);
301            }
302
303            // Try parsing as simple MIR field access
304            if let Some(path) = parse_simple_field_access(expr) {
305                return Some(path);
306            }
307        }
308
309        // Try parsing simple dot notation: _VAR.INDEX (e.g., _3.0, _1.2.3)
310        parse_dot_notation(expr)
311    }
312
313    /// Parse simple dot notation: _VAR.INDEX (without MIR type annotations)
314    fn parse_dot_notation(expr: &str) -> Option<FieldPath> {
315        let expr = expr.trim();
316
317        // Pattern: _VAR.INDEX or _VAR.INDEX.INDEX2
318        // Examples: _3.0, _1.2.3
319
320        if !expr.starts_with('_') {
321            return None;
322        }
323
324        let parts: Vec<&str> = expr.split('.').collect();
325        if parts.len() < 2 {
326            return None;
327        }
328
329        let base_var = parts[0].trim().to_string();
330
331        // Validate base_var is a proper variable name (_N)
332        if !base_var.starts_with('_') {
333            return None;
334        }
335        let digits_only = base_var[1..].chars().all(|c| c.is_ascii_digit());
336        if !digits_only || base_var.len() < 2 {
337            return None;
338        }
339
340        // Parse indices
341        let mut indices = Vec::new();
342        for part in &parts[1..] {
343            if let Ok(index) = part.trim().parse::<usize>() {
344                indices.push(index);
345            } else {
346                // Not a valid numeric index, not a simple field access
347                return None;
348            }
349        }
350
351        if indices.is_empty() {
352            None
353        } else {
354            Some(FieldPath::new(base_var, indices))
355        }
356    }
357
358    /// Parse downcast field access: ((_VAR as Variant).INDEX: TYPE)
359    fn parse_downcast_field_access(expr: &str) -> Option<FieldPath> {
360        let expr = expr.trim();
361
362        // Pattern: ((_VAR as Variant).INDEX: TYPE)
363        // Example: ((_2 as Ready).0: std::string::String)
364
365        if !expr.starts_with("((") {
366            return None;
367        }
368
369        // Find " as "
370        let as_pos = expr.find(" as ")?;
371
372        // Extract base var: "_2"
373        // Skip "((" (2 chars)
374        if as_pos <= 2 {
375            return None;
376        }
377        let base_var_raw = expr[2..as_pos].trim();
378
379        // Handle dereference (*_25) or parens
380        let mut clean_base = base_var_raw;
381        while clean_base.starts_with('(') && clean_base.ends_with(')') {
382            clean_base = &clean_base[1..clean_base.len() - 1].trim();
383        }
384
385        let base_var = if clean_base.starts_with('*') {
386            clean_base[1..].trim().to_string()
387        } else {
388            clean_base.to_string()
389        };
390
391        if !base_var.starts_with('_') {
392            return None;
393        }
394
395        // Find closing paren of downcast followed by dot
396        // We search from as_pos
397        let downcast_end = expr[as_pos..].find(").")?;
398        let absolute_downcast_end = as_pos + downcast_end;
399
400        // Extract index part: ".0: String)"
401        // The dot is at absolute_downcast_end + 1
402        let remaining = &expr[absolute_downcast_end + 1..];
403
404        // Should start with dot
405        if !remaining.starts_with('.') {
406            return None;
407        }
408
409        // Find colon
410        let colon_pos = remaining.find(':')?;
411
412        // Extract index: "0"
413        // Skip dot (1 char)
414        let index_str = remaining[1..colon_pos].trim();
415
416        if let Ok(index) = index_str.parse::<usize>() {
417            // For downcasts, we treat it as accessing field 0 of the base variable
418            // This is an approximation, but sufficient for taint tracking
419            // If _2 is tainted, then ((_2 as Ready).0) is tainted
420            return Some(FieldPath::new(base_var, vec![index]));
421        }
422
423        None
424    }
425
426    /// Parse a simple field access: (_VAR.INDEX: TYPE)
427    fn parse_simple_field_access(expr: &str) -> Option<FieldPath> {
428        let expr = expr.trim();
429
430        // Pattern: (_VAR.INDEX: TYPE)
431        // Example: (_1.0: std::string::String)
432
433        if !expr.starts_with('(') {
434            return None;
435        }
436
437        // Find the colon that separates field from type
438        let colon_pos = expr.find(':')?;
439
440        // Extract the field part: "_VAR.INDEX"
441        let field_part = &expr[1..colon_pos].trim();
442
443        // Split by dot to get base and indices
444        let parts: Vec<&str> = field_part.split('.').collect();
445
446        if parts.len() < 2 {
447            return None;
448        }
449
450        let base_var = parts[0].trim().to_string();
451
452        // Parse indices
453        let mut indices = Vec::new();
454        for part in &parts[1..] {
455            if let Ok(index) = part.trim().parse::<usize>() {
456                indices.push(index);
457            } else {
458                // Not a valid index
459                return None;
460            }
461        }
462
463        if indices.is_empty() {
464            None
465        } else {
466            Some(FieldPath::new(base_var, indices))
467        }
468    }
469
470    /// Parse nested field access: ((_VAR.OUTER: TYPE).INNER: TYPE2)
471    fn parse_nested_field_access(expr: &str) -> Option<FieldPath> {
472        let expr = expr.trim();
473
474        // Pattern: ((_VAR.INDEX: TYPE).INDEX2: TYPE2)
475        // Example: ((_1.1: Credentials).2: std::string::String)
476
477        if !expr.starts_with("((") {
478            return None;
479        }
480
481        // Find the matching closing paren for the inner expression
482        let mut depth = 0;
483        let mut inner_end = 0;
484
485        for (i, ch) in expr.char_indices() {
486            match ch {
487                '(' => depth += 1,
488                ')' => {
489                    depth -= 1;
490                    if depth == 1 {
491                        // Found the end of inner expression
492                        inner_end = i;
493                        break;
494                    }
495                }
496                _ => {}
497            }
498        }
499
500        if inner_end == 0 {
501            return None;
502        }
503
504        // Parse the inner expression first
505        let inner_expr = &expr[1..inner_end + 1]; // (_1.1: Type)
506        let mut base_path = parse_simple_field_access(inner_expr)?;
507
508        // Now parse the outer index
509        let remaining = &expr[inner_end + 1..];
510
511        // Pattern should be: .INDEX: TYPE)
512        if !remaining.starts_with('.') {
513            return None;
514        }
515
516        // Find the colon
517        let colon_pos = remaining.find(':')?;
518        let index_str = &remaining[1..colon_pos].trim();
519
520        if let Ok(index) = index_str.parse::<usize>() {
521            base_path.indices.push(index);
522            Some(base_path)
523        } else {
524            None
525        }
526    }
527
528    /// Check if an expression contains a field access
529    pub fn contains_field_access(expr: &str) -> bool {
530        // Check for characteristic elements of MIR field access
531        // (_1.0: Type) or ((_1 as Variant).0: Type) or dereferenced (*_1.0)
532        let has_mir_style = expr.contains(':')
533            && expr.contains('.')
534            && (expr.contains("(_") || expr.contains("(*_"));
535        if has_mir_style {
536            return true;
537        }
538
539        // Also check for simple dot notation: _VAR.INDEX
540        // e.g., _3.0, _1.2
541        if expr.starts_with('_') && expr.contains('.') {
542            let parts: Vec<&str> = expr.split('.').collect();
543            if parts.len() >= 2 {
544                // Check if at least one part after the base is a numeric index
545                return parts[1..].iter().any(|p| p.trim().parse::<usize>().is_ok());
546            }
547        }
548
549        false
550    }
551
552    /// Extract the base variable from an expression
553    ///
554    /// Examples:
555    /// - `(_1.0: Type)` → Some("_1")
556    /// - `_2` → Some("_2")
557    /// - `move _3` → Some("_3")
558    pub fn extract_base_var(expr: &str) -> Option<String> {
559        let expr = expr.trim();
560
561        // Handle field access
562        if let Some(path) = parse_field_access(expr) {
563            return Some(path.base_var);
564        }
565
566        // Handle simple variable
567        if expr.starts_with('_') {
568            // Extract until non-digit
569            let var: String = expr
570                .chars()
571                .take_while(|c| *c == '_' || c.is_ascii_digit())
572                .collect();
573            if !var.is_empty() {
574                return Some(var);
575            }
576        }
577
578        // Handle prefixed variables (move _1, copy _2, &_3)
579        // Check longer prefixes first to avoid incorrect matches
580        for prefix in &["&mut ", "move ", "copy ", "&"] {
581            if expr.starts_with(prefix) {
582                let rest = &expr[prefix.len()..];
583                return extract_base_var(rest);
584            }
585        }
586
587        None
588    }
589
590    /// Extract all field paths from an expression
591    ///
592    /// This handles complex expressions that may reference multiple fields.
593    pub fn extract_all_field_paths(expr: &str) -> Vec<FieldPath> {
594        let mut paths = Vec::new();
595        let expr = expr.trim();
596
597        // Look for field access patterns
598        let mut search_pos = 0;
599
600        while let Some(paren_start) = expr[search_pos..].find("(_") {
601            let actual_pos = search_pos + paren_start;
602
603            // Try to extract field access from this position
604            let remaining = &expr[actual_pos..];
605
606            // Find the extent of this field access
607            if let Some(colon_pos) = remaining.find(':') {
608                // Try to find the closing paren
609                if let Some(close_paren) = remaining[colon_pos..].find(')') {
610                    let field_expr = &remaining[..colon_pos + close_paren + 1];
611
612                    if let Some(path) = parse_field_access(field_expr) {
613                        paths.push(path);
614                    }
615                }
616            }
617
618            search_pos = actual_pos + 1;
619        }
620
621        paths
622    }
623}
624
625#[cfg(test)]
626mod tests {
627    use super::*;
628
629    #[test]
630    fn test_field_path_creation() {
631        let path = FieldPath::new("_1".to_string(), vec![0, 1, 2]);
632        assert_eq!(path.base_var, "_1");
633        assert_eq!(path.indices, vec![0, 1, 2]);
634    }
635
636    #[test]
637    fn test_field_path_to_string() {
638        let path1 = FieldPath::whole_var("_1".to_string());
639        assert_eq!(path1.to_string(), "_1");
640
641        let path2 = FieldPath::single_field("_1".to_string(), 0);
642        assert_eq!(path2.to_string(), "_1.0");
643
644        let path3 = FieldPath::new("_1".to_string(), vec![1, 2]);
645        assert_eq!(path3.to_string(), "_1.1.2");
646    }
647
648    #[test]
649    fn test_field_path_is_prefix() {
650        let path1 = FieldPath::whole_var("_1".to_string());
651        let path2 = FieldPath::single_field("_1".to_string(), 0);
652        let path3 = FieldPath::new("_1".to_string(), vec![0, 1]);
653        let path4 = FieldPath::new("_1".to_string(), vec![1, 2]);
654
655        assert!(path1.is_prefix_of(&path2));
656        assert!(path1.is_prefix_of(&path3));
657        assert!(path2.is_prefix_of(&path3));
658        assert!(!path2.is_prefix_of(&path4));
659        assert!(!path3.is_prefix_of(&path2));
660    }
661
662    #[test]
663    fn test_field_path_parent() {
664        let path1 = FieldPath::new("_1".to_string(), vec![1, 2]);
665        let parent1 = path1.parent().unwrap();
666        assert_eq!(parent1.to_string(), "_1.1");
667
668        let parent2 = parent1.parent().unwrap();
669        assert_eq!(parent2.to_string(), "_1");
670
671        assert!(parent2.parent().is_none());
672    }
673
674    #[test]
675    fn test_field_taint_map_basic() {
676        let mut map = FieldTaintMap::new();
677
678        let path = FieldPath::single_field("_1".to_string(), 0);
679        map.set_field_taint(
680            path.clone(),
681            FieldTaint::Tainted {
682                source_type: "environment".to_string(),
683                source_location: "env::args".to_string(),
684            },
685        );
686
687        let taint = map.get_field_taint(&path);
688        assert!(taint.is_tainted());
689    }
690
691    #[test]
692    fn test_field_taint_map_inheritance() {
693        let mut map = FieldTaintMap::new();
694
695        // Set parent field as tainted
696        let parent = FieldPath::single_field("_1".to_string(), 1);
697        map.set_field_taint(
698            parent,
699            FieldTaint::Tainted {
700                source_type: "environment".to_string(),
701                source_location: "test".to_string(),
702            },
703        );
704
705        // Child field should inherit taint
706        let child = FieldPath::new("_1".to_string(), vec![1, 2]);
707        let taint = map.get_field_taint(&child);
708        assert!(taint.is_tainted());
709    }
710
711    #[test]
712    fn test_set_var_taint() {
713        let mut map = FieldTaintMap::new();
714
715        // Add some fields first
716        map.set_field_taint(
717            FieldPath::single_field("_1".to_string(), 0),
718            FieldTaint::Clean,
719        );
720        map.set_field_taint(
721            FieldPath::single_field("_1".to_string(), 1),
722            FieldTaint::Clean,
723        );
724
725        // Taint entire variable
726        map.set_var_taint(
727            "_1",
728            FieldTaint::Tainted {
729                source_type: "test".to_string(),
730                source_location: "test".to_string(),
731            },
732        );
733
734        // All fields should be tainted
735        assert!(map
736            .get_field_taint(&FieldPath::single_field("_1".to_string(), 0))
737            .is_tainted());
738        assert!(map
739            .get_field_taint(&FieldPath::single_field("_1".to_string(), 1))
740            .is_tainted());
741    }
742
743    #[test]
744    fn test_merge() {
745        let mut map1 = FieldTaintMap::new();
746        map1.set_field_taint(
747            FieldPath::single_field("_1".to_string(), 0),
748            FieldTaint::Tainted {
749                source_type: "test".to_string(),
750                source_location: "test".to_string(),
751            },
752        );
753
754        let mut map2 = FieldTaintMap::new();
755        map2.set_field_taint(
756            FieldPath::single_field("_1".to_string(), 1),
757            FieldTaint::Tainted {
758                source_type: "test2".to_string(),
759                source_location: "test2".to_string(),
760            },
761        );
762
763        map1.merge(&map2);
764
765        assert!(map1
766            .get_field_taint(&FieldPath::single_field("_1".to_string(), 0))
767            .is_tainted());
768        assert!(map1
769            .get_field_taint(&FieldPath::single_field("_1".to_string(), 1))
770            .is_tainted());
771    }
772
773    // Parser tests
774
775    #[test]
776    fn test_parse_simple_field_access() {
777        use parser::parse_field_access;
778
779        let path = parse_field_access("(_1.0: std::string::String)").unwrap();
780        assert_eq!(path.base_var, "_1");
781        assert_eq!(path.indices, vec![0]);
782
783        let path = parse_field_access("(_1.1: std::string::String)").unwrap();
784        assert_eq!(path.base_var, "_1");
785        assert_eq!(path.indices, vec![1]);
786
787        let path = parse_field_access("(_3.2: u32)").unwrap();
788        assert_eq!(path.base_var, "_3");
789        assert_eq!(path.indices, vec![2]);
790    }
791
792    #[test]
793    fn test_parse_nested_field_access() {
794        use parser::parse_field_access;
795
796        let path = parse_field_access("((_1.1: Credentials).0: std::string::String)").unwrap();
797        assert_eq!(path.base_var, "_1");
798        assert_eq!(path.indices, vec![1, 0]);
799
800        let path = parse_field_access("((_1.1: Credentials).2: std::string::String)").unwrap();
801        assert_eq!(path.base_var, "_1");
802        assert_eq!(path.indices, vec![1, 2]);
803    }
804
805    #[test]
806    fn test_parse_field_access_invalid() {
807        use parser::parse_field_access;
808
809        // Not a field access
810        assert!(parse_field_access("_1").is_none());
811        assert!(parse_field_access("move _2").is_none());
812        assert!(parse_field_access("copy _3").is_none());
813
814        // No indices
815        assert!(parse_field_access("(_1: Type)").is_none());
816    }
817
818    #[test]
819    fn test_contains_field_access() {
820        use parser::contains_field_access;
821
822        assert!(contains_field_access("(_1.0: String)"));
823        assert!(contains_field_access("((_1.1: Type).2: Type2)"));
824        assert!(!contains_field_access("_1"));
825        assert!(!contains_field_access("move _2"));
826    }
827
828    #[test]
829    fn test_extract_base_var() {
830        use parser::extract_base_var;
831
832        assert_eq!(extract_base_var("(_1.0: Type)").unwrap(), "_1");
833        assert_eq!(extract_base_var("_2").unwrap(), "_2");
834        assert_eq!(extract_base_var("move _3").unwrap(), "_3");
835        assert_eq!(extract_base_var("copy _4").unwrap(), "_4");
836        assert_eq!(extract_base_var("&_5").unwrap(), "_5");
837        assert_eq!(extract_base_var("&mut _6").unwrap(), "_6");
838    }
839
840    #[test]
841    fn test_extract_all_field_paths() {
842        use parser::extract_all_field_paths;
843
844        let paths = extract_all_field_paths("(_1.0: String) = move (_2.1: String)");
845        assert_eq!(paths.len(), 2);
846        assert_eq!(paths[0].to_string(), "_1.0");
847        assert_eq!(paths[1].to_string(), "_2.1");
848
849        let paths = extract_all_field_paths("_3 = Command::arg(copy _4, copy (_5.2: String))");
850        assert_eq!(paths.len(), 1);
851        assert_eq!(paths[0].to_string(), "_5.2");
852    }
853}