twine_core/config/
diagnostics.rs1use 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 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 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}