Skip to main content

twine_core/config/
diagnostics.rs

1use std::path::{Path, PathBuf};
2
3use thiserror::Error;
4use toml::de::{DeTable, DeValue};
5
6#[derive(Clone, Debug, Eq, Error, PartialEq)]
7#[error("{file}:{line}: {key}: {problem}", file = .file.display())]
8pub struct ConfigDiagnostic {
9    pub file: PathBuf,
10    pub line: usize,
11    pub key: String,
12    pub problem: ConfigProblem,
13}
14
15#[derive(Clone, Debug, Eq, Error, PartialEq)]
16pub enum ConfigProblem {
17    #[error("invalid TOML syntax; using defaults")]
18    InvalidToml,
19    #[error("invalid setting value or type; using defaults")]
20    InvalidValue,
21    #[error("configuration import cycle; using defaults")]
22    ImportCycle,
23    #[error("configuration import limit exceeded; using defaults")]
24    ImportLimit,
25    #[error("unknown key; ignored")]
26    UnknownKey,
27    #[error("{0}")]
28    File(String),
29}
30
31impl ConfigDiagnostic {
32    pub(super) fn at(
33        path: &Path,
34        source: &str,
35        offset: usize,
36        key: String,
37        problem: ConfigProblem,
38    ) -> Self {
39        Self {
40            file: path.to_owned(),
41            line: source.as_bytes()[..offset.min(source.len())]
42                .split(|&byte| byte == b'\n')
43                .count(),
44            key,
45            problem,
46        }
47    }
48}
49
50pub(super) struct Location {
51    pub segments: Vec<String>,
52    pub start: usize,
53    end: usize,
54}
55
56pub(super) fn locations(table: &DeTable<'_>) -> Vec<Location> {
57    let mut locations = Vec::new();
58    collect_locations(table, &[], &mut locations);
59    locations
60}
61
62fn collect_locations(table: &DeTable<'_>, prefix: &[String], locations: &mut Vec<Location>) {
63    for (key, value) in table {
64        let mut path = prefix.to_vec();
65        path.push(key.get_ref().to_string());
66        locations.push(Location {
67            segments: path.clone(),
68            start: key.span().start,
69            end: value.span().end,
70        });
71        if let DeValue::Table(table) = value.get_ref() {
72            collect_locations(table, &path, locations);
73        }
74    }
75}
76
77pub(super) fn key_at(locations: &[Location], source: &str, error: &toml::de::Error) -> String {
78    let span = error.span().unwrap_or(0..0);
79    let offset = span.start;
80    // Duplicate assignments are omitted from the recovered tree. The parser's error span
81    // identifies the key; parse that token and recover its parent only when unambiguous.
82    if error.message().starts_with("duplicate key")
83        && let Some(token) = source.get(span)
84        && let Ok(key) = DeTable::parse(&format!("{token} = 0"))
85        && let Some(duplicate) = self::locations(key.get_ref()).last()
86    {
87        let mut originals = locations.iter().filter(|location| {
88            location.start < offset && location.segments.ends_with(&duplicate.segments)
89        });
90        if let Some(original) = originals.next()
91            && originals.next().is_none()
92        {
93            return display_key(&original.segments);
94        }
95        return display_key(&duplicate.segments);
96    }
97    // Recovery retains spans for many malformed values. Prefer the innermost containing key;
98    // an unrecognizable document/header has no trustworthy key to report.
99    locations
100        .iter()
101        .filter(|location| location.start <= offset && offset <= location.end)
102        .max_by_key(|location| location.start)
103        .map_or_else(
104            || "<document>".to_owned(),
105            |location| display_key(&location.segments),
106        )
107}
108
109pub(super) fn ignored_segments(path: &serde_ignored::Path<'_>) -> Vec<String> {
110    match path {
111        serde_ignored::Path::Root => Vec::new(),
112        serde_ignored::Path::Map { parent, key } => {
113            let mut segments = ignored_segments(parent);
114            segments.push(key.clone());
115            segments
116        }
117        serde_ignored::Path::Seq { parent, index } => {
118            let mut segments = ignored_segments(parent);
119            segments.push(index.to_string());
120            segments
121        }
122        serde_ignored::Path::Some { parent }
123        | serde_ignored::Path::NewtypeStruct { parent }
124        | serde_ignored::Path::NewtypeVariant { parent } => ignored_segments(parent),
125    }
126}
127
128pub(super) fn display_key(segments: &[String]) -> String {
129    segments
130        .iter()
131        .map(|segment| {
132            if !segment.is_empty()
133                && segment
134                    .bytes()
135                    .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-'))
136            {
137                segment.clone()
138            } else {
139                format!("{segment:?}")
140            }
141        })
142        .collect::<Vec<_>>()
143        .join(".")
144}