use std::path::{Path, PathBuf};
use mf2_model::{ErrorClass, Message};
use crate::config::Config;
use crate::error::Result;
use crate::lint::{Level, Lint};
use crate::loader::{Loaded, Record, for_path};
use crate::report::{Report, Sink};
#[derive(Debug)]
pub struct LocaleSource {
pub tag: String,
pub path: PathBuf,
pub loaded: Loaded,
}
impl LocaleSource {
pub fn file(&self, record: &Record) -> &crate::loader::SourceFile {
self.loaded.file(record)
}
pub fn position(&self, record: &Record, start: u32, end: u32) -> mf2_resource::Position {
let span = record.source_span(start, end);
self.file(record).position(span.start)
}
}
pub fn load(
locales_dir: &Path,
tags: &[String],
config: &Config,
report: &mut Report,
) -> Result<Vec<LocaleSource>> {
let mut out = Vec::with_capacity(tags.len());
for tag in tags {
let dir = locales_dir.join(tag);
let path = if dir.is_dir() {
dir
} else {
locales_dir.join(format!("{tag}.json"))
};
let loader = for_path(&path);
let loaded = loader.load(&path)?;
let source = LocaleSource {
tag: tag.clone(),
path,
loaded,
};
check_container(&source, config, report);
out.push(source);
}
Ok(out)
}
fn check_container(source: &LocaleSource, config: &Config, report: &mut Report) {
let mut sink = Sink::new(report, &source.tag);
for problem in &source.loaded.problems {
let file = &source.loaded.files[problem.file];
sink.add(
Level::Error,
None,
&file.path,
file.position(problem.span.start),
None,
problem.message,
);
}
let level = config.level(Lint::LocaleMismatch);
for (file, declared) in source.loaded.files.iter().zip(&source.loaded.declared) {
if let Some(declared) = declared
&& *declared != source.tag
{
sink.add(
level,
Some(Lint::LocaleMismatch),
&file.path,
file.position(0),
None,
format!(
"this file declares @locale {declared:?} but sits under {:?}",
source.tag
),
);
}
}
}
pub fn parse<'a>(source: &'a LocaleSource, report: &mut Report) -> Vec<Option<Message<'a>>> {
let mut sink = Sink::new(report, &source.tag);
let mut models = Vec::with_capacity(source.loaded.records.len());
for record in &source.loaded.records {
let parsed = mf2_syntax::parse_model(&record.source);
for d in &parsed.diagnostics {
let (start, end) = d.span.map_or((0, 0), |s| (s.start, s.end));
let at = source.position(record, start, end);
sink.add_invalid(
&source.file(record).path,
at,
Some(&record.id),
d.kind,
describe(d),
);
}
models.push(if parsed.diagnostics.is_empty() {
parsed.message
} else {
None
});
}
models
}
fn describe(d: &mf2_model::Diagnostic) -> String {
let what = match d.kind.class() {
ErrorClass::Syntax => "syntax error",
ErrorClass::DataModel => "data model error",
ErrorClass::Resolution => "resolution error",
ErrorClass::MessageFunction => "message function error",
_ => "error",
};
match mf2_syntax::code::describe(d.code) {
Some(detail) => format!("{what}: {detail}"),
None => what.to_string(),
}
}
pub fn by_id<'a>(
source: &'a LocaleSource,
config: &Config,
report: &mut Report,
) -> std::collections::BTreeMap<&'a str, usize> {
let mut sink = Sink::new(report, &source.tag);
let mut out = std::collections::BTreeMap::new();
for (i, record) in source.loaded.records.iter().enumerate() {
if let Some(first) = out.insert(record.id.as_str(), i) {
let file = &source.loaded.files[record.file];
let earlier = &source.loaded.files[source.loaded.records[first].file];
sink.add(
config.level(Lint::DuplicateId),
Some(Lint::DuplicateId),
&file.path,
file.position(record.id_span.start),
Some(&record.id),
format!("this id is already defined in {}", earlier.path.display()),
);
}
}
out
}