use std::cell::RefCell;
use std::collections::BTreeMap;
use std::io::Cursor;
use std::path::Path;
use ec4rs::property::{Charset as EcCharset, EndOfLine, FinalNewline, TrimTrailingWs};
use ec4rs::rawvalue::RawValue;
use ec4rs::{ConfigParser, ParseError, Properties, PropertyKey, PropertyValue, Section};
use super::{Bounded, Budget};
use crate::paths::ProjectPath;
use crate::report::{Code, Diagnostic};
use crate::tree::ReadTree;
pub const FILE_NAME: &str = ".editorconfig";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Charset {
Utf8,
Utf8Bom,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LineEnding {
Lf,
CrLf,
Cr,
}
impl LineEnding {
#[must_use]
pub fn name(self) -> &'static str {
match self {
Self::Lf => "lf",
Self::CrLf => "crlf",
Self::Cr => "cr",
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct Effective {
pub charset: Option<Charset>,
pub end_of_line: Option<LineEnding>,
pub insert_final_newline: Option<bool>,
pub trim_trailing_whitespace: Option<bool>,
}
struct Parsed {
is_root: bool,
sections: Vec<Section>,
}
type Cached = Option<Result<std::rc::Rc<Parsed>, ()>>;
pub struct Resolver<'a> {
tree: &'a dyn ReadTree,
budget: &'a Budget,
cache: RefCell<BTreeMap<ProjectPath, Cached>>,
reported: RefCell<Vec<Diagnostic>>,
fatal: RefCell<Option<String>>,
}
impl<'a> Resolver<'a> {
#[must_use]
pub fn new(tree: &'a dyn ReadTree, budget: &'a Budget) -> Self {
Self {
tree,
budget,
cache: RefCell::new(BTreeMap::new()),
reported: RefCell::new(Vec::new()),
fatal: RefCell::new(None),
}
}
pub fn take_diagnostics(&self) -> Vec<Diagnostic> {
std::mem::take(&mut *self.reported.borrow_mut())
}
pub fn fatal(&self) -> Result<(), String> {
match self.fatal.borrow().as_ref() {
Some(message) => Err(message.clone()),
None => Ok(()),
}
}
pub fn properties(&self, path: &ProjectPath) -> Result<Effective, Vec<Diagnostic>> {
let mut directories = vec![ProjectPath::root()];
directories.extend(path.parent().ancestors());
let mut applicable: Vec<(ProjectPath, std::rc::Rc<Parsed>)> = Vec::new();
for directory in directories {
match self.parsed(&directory) {
None => {}
Some(Ok(parsed)) => {
if parsed.is_root {
applicable.clear();
}
applicable.push((directory, parsed));
}
Some(Err(())) => return Err(Vec::new()),
}
}
let mut properties = Properties::new();
for (directory, parsed) in &applicable {
let relative = path
.strip_prefix(directory)
.expect("the directory is an ancestor");
let relative = Path::new(relative.as_str());
for section in &parsed.sections {
if section.applies_to(relative) {
for (key, value) in section.props() {
properties.insert_raw_for_key(key, value.clone());
}
}
}
}
let mut problems = Vec::new();
let mut unsupported = |key: &str, raw: &RawValue| {
problems.push(Diagnostic::new(
Code::HygieneConfig,
path.as_str(),
format!(
"`{key} = {}` is not a value Bearout can enforce; remove the property, set it to `unset`, or exclude the file from the selection",
raw.into_str()
),
));
};
let charset = match typed::<EcCharset>(&properties) {
Ok(None) => None,
Ok(Some(EcCharset::Utf8)) => Some(Charset::Utf8),
Ok(Some(EcCharset::Utf8Bom)) => Some(Charset::Utf8Bom),
Ok(Some(_)) | Err(()) => {
unsupported(EcCharset::key(), properties.get_raw::<EcCharset>());
None
}
};
let end_of_line = match typed::<EndOfLine>(&properties) {
Ok(None) => None,
Ok(Some(EndOfLine::Lf)) => Some(LineEnding::Lf),
Ok(Some(EndOfLine::CrLf)) => Some(LineEnding::CrLf),
Ok(Some(EndOfLine::Cr)) => Some(LineEnding::Cr),
Err(()) => {
unsupported(EndOfLine::key(), properties.get_raw::<EndOfLine>());
None
}
};
let insert_final_newline = match typed::<FinalNewline>(&properties) {
Ok(None) => None,
Ok(Some(FinalNewline::Value(value))) => Some(value),
Err(()) => {
unsupported(FinalNewline::key(), properties.get_raw::<FinalNewline>());
None
}
};
let trim_trailing_whitespace = match typed::<TrimTrailingWs>(&properties) {
Ok(None) => None,
Ok(Some(TrimTrailingWs::Value(value))) => Some(value),
Err(()) => {
unsupported(
TrimTrailingWs::key(),
properties.get_raw::<TrimTrailingWs>(),
);
None
}
};
if !problems.is_empty() {
return Err(problems);
}
Ok(Effective {
charset,
end_of_line,
insert_final_newline,
trim_trailing_whitespace,
})
}
fn parsed(&self, directory: &ProjectPath) -> Cached {
if let Some(cached) = self.cache.borrow().get(directory) {
return cached.clone();
}
let file = directory.join(&ProjectPath::parse(FILE_NAME).expect("constant"));
let entry = if self.tree.is_file(&file) {
Some(match self.parse(&file) {
Ok(parsed) => Ok(std::rc::Rc::new(parsed)),
Err(diagnostic) => {
self.reported.borrow_mut().push(diagnostic);
Err(())
}
})
} else {
None
};
self.cache
.borrow_mut()
.insert(directory.clone(), entry.clone());
entry
}
fn parse(&self, file: &ProjectPath) -> Result<Parsed, Diagnostic> {
let report = |message: String, line: Option<u32>| {
Diagnostic::new(Code::HygieneConfig, file.as_str(), message).at_line(line)
};
match self.tree.symlink_component(file) {
Ok(None) => {}
Ok(Some(link)) => {
return Err(report(
format!(
"`{FILE_NAME}` is reached through the symbolic link `{link}`; configuration is never read through links"
),
None,
));
}
Err(error) => {
return Err(report(
format!("cannot inspect `{FILE_NAME}`: {error}"),
None,
));
}
}
let bytes = match self.budget.read(self.tree, file) {
Bounded::Within(bytes) => bytes,
Bounded::OverFileLimit => {
let limit = self.budget.limits().file_bytes;
return Err(report(
super::over_limit(self.tree, file, &format!("`{FILE_NAME}`"), limit),
None,
));
}
Bounded::Exhausted(message) => {
self.fatal.borrow_mut().get_or_insert(message);
return Err(report("hygiene input budget exhausted".to_owned(), None));
}
Bounded::Unreadable(error) => {
return Err(report(format!("cannot read `{FILE_NAME}`: {error}"), None));
}
};
let mut parser = ConfigParser::new_buffered(Cursor::new(bytes))
.map_err(|error| report(describe(&error), None))?;
let is_root = parser.is_root;
let mut sections = Vec::new();
loop {
let line = u32::try_from(parser.line_no()).ok();
match parser.next() {
None => break,
Some(Ok(section)) => sections.push(section),
Some(Err(error)) => return Err(report(describe(&error), line)),
}
}
Ok(Parsed { is_root, sections })
}
}
fn typed<T: PropertyKey + PropertyValue>(properties: &Properties) -> Result<Option<T>, ()> {
let raw = properties.get_raw::<T>().filter_unset();
if raw.is_unset() {
return Ok(None);
}
raw.parse::<T>().map(Some).map_err(|_| ())
}
fn describe(error: &ParseError) -> String {
match error {
ParseError::Eof => "unexpected end of file".to_owned(),
ParseError::Io(error) => format!("cannot read `{FILE_NAME}`: {error}"),
ParseError::InvalidLine => format!(
"`{FILE_NAME}` has a line that is neither a section header, a property, nor a comment"
),
ParseError::EmptyCharClass => {
format!("`{FILE_NAME}` has a section header with an empty character class")
}
}
}