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)]
#[non_exhaustive]
pub struct Assignment {
pub source: usize,
pub line: usize,
pub column: usize,
pub op: Op,
}
impl Assignment {
#[must_use]
pub const fn new(source: usize, line: usize, column: usize, op: Op) -> Self {
Self {
source,
line,
column,
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 fn retain_list(&mut self, list: &str, keep: impl Fn(usize) -> bool) {
let prefix = format!("{list}[");
let split = |key: &str| -> Option<(usize, String)> {
let (digits, tail) = key.strip_prefix(prefix.as_str())?.split_once(']')?;
Some((digits.parse().ok()?, tail.to_owned()))
};
let len = self
.entries
.keys()
.filter_map(|key| split(key))
.map(|(index, _)| index + 1)
.max();
let mut kept = 0;
let renumbered: Vec<Option<usize>> = (0..len.unwrap_or(0))
.map(|index| {
keep(index).then(|| {
kept += 1;
kept - 1
})
})
.collect();
self.entries = std::mem::take(&mut self.entries)
.into_iter()
.filter_map(|(key, assignments)| match split(&key) {
None => Some((key, assignments)),
Some((index, tail)) => renumbered[index].map(|new| (format!("{prefix}{new}]{tail}"), assignments)),
})
.collect();
}
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::{Assignment, Op, Origins, join_key};
#[test]
fn retaining_list_entries_numbers_the_kept_ones_from_zero() {
let mut origins = Origins::default();
for (key, source) in [
("ignore", 0),
("ignore[0]", 1),
("ignore[1]", 1),
("ignore[2]", 2),
("ignore[3]", 3),
("overrides[0].rules", 4),
("ignored", 5),
] {
origins.push(key.to_owned(), Assignment::new(source, 1, 1, Op::Append));
}
origins.retain_list("ignore", |index| index >= 2);
let keys: Vec<(&str, usize)> = origins.iter().map(|(key, list)| (key, list[0].source)).collect();
assert_eq!(
keys,
[
("ignore", 0),
("ignore[0]", 2),
("ignore[1]", 3),
("overrides[0].rules", 4),
("ignored", 5)
]
);
}
#[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\"");
}
}