syncable_cli/analyzer/dclint/parser/
mod.rs

1//! YAML parser for Docker Compose files.
2//!
3//! Provides parsing of docker-compose.yaml files with position tracking
4//! for accurate error reporting.
5
6pub mod compose;
7
8pub use compose::{
9    ComposeFile, ParseError, Position, Service, ServiceBuild, ServicePort, ServiceVolume,
10    parse_compose, parse_compose_with_positions,
11};
12
13use yaml_rust2::{Yaml, YamlLoader};
14
15/// Parse a YAML string and return the document.
16pub fn parse_yaml(content: &str) -> Result<Yaml, ParseError> {
17    let docs =
18        YamlLoader::load_from_str(content).map_err(|e| ParseError::YamlError(e.to_string()))?;
19
20    docs.into_iter().next().ok_or(ParseError::EmptyDocument)
21}
22
23/// Find the line number for a given path in the source YAML.
24///
25/// This function searches the raw source for the key to determine its position.
26pub fn find_line_for_key(source: &str, path: &[&str]) -> Option<u32> {
27    if path.is_empty() {
28        return Some(1);
29    }
30
31    let lines: Vec<&str> = source.lines().collect();
32    let mut current_indent = 0;
33    let mut path_idx = 0;
34
35    for (line_num, line) in lines.iter().enumerate() {
36        if line.trim().is_empty() || line.trim().starts_with('#') {
37            continue;
38        }
39
40        let indent = line.len() - line.trim_start().len();
41        let trimmed = line.trim();
42
43        // Check if this line starts with the current path element as a key
44        let target_key = path[path_idx];
45        let key_pattern = format!("{}:", target_key);
46
47        if (trimmed.starts_with(&key_pattern) || trimmed == target_key)
48            && (path_idx == 0 || indent > current_indent)
49        {
50            path_idx += 1;
51            current_indent = indent;
52
53            if path_idx == path.len() {
54                return Some((line_num + 1) as u32); // 1-indexed
55            }
56        }
57    }
58
59    None
60}
61
62/// Find the line number for a service key.
63pub fn find_line_for_service(source: &str, service_name: &str) -> Option<u32> {
64    find_line_for_key(source, &["services", service_name])
65}
66
67/// Find the line number for a key within a service.
68pub fn find_line_for_service_key(source: &str, service_name: &str, key: &str) -> Option<u32> {
69    find_line_for_key(source, &["services", service_name, key])
70}
71
72/// Find the column for a value on a given line.
73pub fn find_column_for_value(source: &str, line: u32, key: &str) -> u32 {
74    let lines: Vec<&str> = source.lines().collect();
75    if let Some(line_content) = lines.get((line - 1) as usize) {
76        if let Some(pos) = line_content.find(':') {
77            // Column after the colon and any whitespace
78            let after_colon = &line_content[pos + 1..];
79            let leading_ws = after_colon.len() - after_colon.trim_start().len();
80            return (pos + 2 + leading_ws) as u32;
81        }
82        // If no colon, look for the key position
83        if let Some(pos) = line_content.find(key) {
84            return (pos + 1) as u32;
85        }
86    }
87    1
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    #[test]
95    fn test_find_line_for_key() {
96        let yaml = r#"
97services:
98  web:
99    image: nginx
100    ports:
101      - "80:80"
102  db:
103    image: postgres
104"#;
105        assert_eq!(find_line_for_key(yaml, &["services"]), Some(2));
106        assert_eq!(find_line_for_key(yaml, &["services", "web"]), Some(3));
107        assert_eq!(
108            find_line_for_key(yaml, &["services", "web", "image"]),
109            Some(4)
110        );
111        assert_eq!(find_line_for_key(yaml, &["services", "db"]), Some(7));
112    }
113
114    #[test]
115    fn test_find_line_for_service() {
116        let yaml = r#"
117services:
118  web:
119    image: nginx
120  db:
121    image: postgres
122"#;
123        assert_eq!(find_line_for_service(yaml, "web"), Some(3));
124        assert_eq!(find_line_for_service(yaml, "db"), Some(5));
125        assert_eq!(find_line_for_service(yaml, "nonexistent"), None);
126    }
127}