use std::path::{Path, PathBuf};
use indexmap::IndexMap;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum SourceKind {
Root,
Ancestor,
Extends {
by: PathBuf,
},
Explicit,
Package,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct ConfigSource {
pub path: PathBuf,
pub kind: SourceKind,
}
impl ConfigSource {
#[must_use]
pub const fn is_discovered(&self) -> bool {
matches!(
self.kind,
SourceKind::Root | SourceKind::Ancestor | SourceKind::Explicit
)
}
}
#[must_use]
pub fn join_key(prefix: &str, key: &str) -> String {
let segment = if key.is_empty() || key.contains(['.', '[', ']', '"']) {
format!("\"{}\"", key.replace('\\', "\\\\").replace('"', "\\\""))
} else {
key.to_owned()
};
if prefix.is_empty() {
segment
} else {
format!("{prefix}.{segment}")
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Op {
Set,
Append,
Prepend,
Reset,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Assignment {
pub source: usize,
pub line: usize,
pub column: usize,
pub op: Op,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[non_exhaustive]
pub struct Origins {
entries: IndexMap<String, Vec<Assignment>>,
}
impl Origins {
#[must_use]
pub fn get(&self, key: &str) -> &[Assignment] {
match self.entries.get(key) {
Some(list) => list.as_slice(),
None => &[],
}
}
#[must_use]
pub fn last(&self, key: &str) -> Option<&Assignment> {
self.get(key).last()
}
pub fn iter(&self) -> impl Iterator<Item = (&str, &[Assignment])> {
self.entries.iter().map(|(key, list)| (key.as_str(), list.as_slice()))
}
#[must_use]
pub fn len(&self) -> usize {
self.entries.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub(crate) fn push(&mut self, key: String, assignment: Assignment) {
self.entries.entry(key).or_default().push(assignment);
}
}
#[derive(Debug, Clone, PartialEq, Default)]
#[non_exhaustive]
pub struct ResolvedConfig {
pub config: crate::Config,
pub sources: Vec<ConfigSource>,
pub origins: Origins,
}
impl ResolvedConfig {
#[must_use]
pub fn nearest_file(&self) -> Option<&Path> {
self.sources
.iter()
.rev()
.find(|source| source.is_discovered())
.map(|source| source.path.as_path())
}
}
#[cfg(test)]
mod tests {
use super::join_key;
#[test]
fn keys_with_separators_are_quoted() {
assert_eq!(join_key("", "lint"), "lint");
assert_eq!(join_key("js.plugins", "vitest"), "js.plugins.vitest");
assert_eq!(join_key("js.plugins", "a.b"), "js.plugins.\"a.b\"");
assert_eq!(join_key("js.plugins", "a[0]"), "js.plugins.\"a[0]\"");
assert_eq!(join_key("x", "q\"r"), "x.\"q\\\"r\"");
}
}