use std::collections::{BTreeMap, BTreeSet};
use super::defaults::{self, Kind, OPTIONS};
use super::lexicon::{self, Edits, Lexicon};
use super::{RuleId, Severity};
use crate::codec::{LINT, Problem, ReadError, SpecPath};
use crate::identity::Key;
use crate::yaml::{self, Entry, Node, Position, Value};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Similarity {
pub min_tokens: usize,
pub threshold: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum Setting {
Lexicon(Lexicon),
Count(usize),
Millionths(u32),
Names(Vec<String>),
}
impl Setting {
fn default_of(kind: &Kind) -> Self {
match kind {
Kind::Lexicon(terms) => {
Self::Lexicon(lexicon::effective(terms, None).expect("a valid default"))
}
Kind::Count { default, .. } => Self::Count(*default),
Kind::Millionths(default) => Self::Millionths(*default),
Kind::Names => Self::Names(Vec::new()),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LintConfig {
rules: BTreeMap<RuleId, (bool, Severity)>,
options: BTreeMap<(RuleId, &'static str), Setting>,
suppressions: BTreeSet<(RuleId, Key)>,
}
impl Default for LintConfig {
fn default() -> Self {
Self {
rules: RuleId::ALL
.iter()
.map(|rule| (*rule, (rule.enabled_by_default(), rule.default_severity())))
.collect(),
options: OPTIONS
.iter()
.map(|(rule, key, kind)| ((*rule, *key), Setting::default_of(kind)))
.collect(),
suppressions: BTreeSet::new(),
}
}
}
impl LintConfig {
pub fn read(bytes: Option<&[u8]>) -> Result<Self, Vec<ReadError>> {
let path = SpecPath::new(LINT);
let mut config = Self::default();
let Some(bytes) = bytes else {
return Ok(config);
};
let root = yaml::read(bytes).map_err(|error| vec![ReadError::yaml(&path, error)])?;
let mut reader = Reader {
path,
errors: Vec::new(),
};
if let Some(root) = root {
reader.file(&root, &mut config);
}
if reader.errors.is_empty() {
Ok(config)
} else {
Err(reader.errors)
}
}
#[must_use]
pub fn enabled(&self, rule: RuleId) -> bool {
self.rules[&rule].0
}
#[must_use]
pub fn severity(&self, rule: RuleId) -> Severity {
self.rules[&rule].1
}
#[must_use]
pub fn suppresses(&self, rule: RuleId, keys: &[Key]) -> bool {
keys.iter()
.any(|key| self.suppressions.contains(&(rule, *key)))
}
#[must_use]
pub fn lexicon(&self, rule: RuleId) -> &Lexicon {
self.lexicon_at(rule, "lexicon")
}
#[must_use]
pub fn modals(&self) -> &Lexicon {
self.lexicon_at(RuleId::Lq005, "modalLexicon")
}
#[must_use]
pub fn conjunctions(&self) -> &Lexicon {
self.lexicon_at(RuleId::Lq005, "conjunctionLexicon")
}
#[must_use]
pub fn conjunction_threshold(&self) -> usize {
self.count(RuleId::Lq005, "conjunctionThreshold")
}
#[must_use]
pub fn similarity(&self) -> Similarity {
let Setting::Millionths(threshold) = self.options[&(RuleId::Lq008, "threshold")] else {
unreachable!("`threshold` is a decimal");
};
Similarity {
min_tokens: self.count(RuleId::Lq008, "minTokens"),
threshold,
}
}
#[must_use]
pub fn max_criteria(&self) -> usize {
self.count(RuleId::V2006, "maxCriteria")
}
#[must_use]
pub fn max_body_words(&self) -> usize {
self.count(RuleId::V2006, "maxBodyWords")
}
#[must_use]
pub fn proper_names(&self) -> &[String] {
let Setting::Names(names) = &self.options[&(RuleId::V2008, "properNames")] else {
unreachable!("`properNames` is a list of names");
};
names
}
fn lexicon_at(&self, rule: RuleId, key: &'static str) -> &Lexicon {
let Setting::Lexicon(lexicon) = &self.options[&(rule, key)] else {
unreachable!("`{key}` is a lexicon");
};
lexicon
}
fn count(&self, rule: RuleId, key: &'static str) -> usize {
let Setting::Count(count) = self.options[&(rule, key)] else {
unreachable!("`{key}` is a count");
};
count
}
}
pub(super) fn millionths_text(millionths: u32) -> String {
let text = format!("{}.{:06}", millionths / 1_000_000, millionths % 1_000_000);
text.trim_end_matches('0').trim_end_matches('.').to_owned()
}
fn parse_millionths(text: &str) -> Option<u32> {
let (whole, fraction) = text.split_once('.').unwrap_or((text, ""));
let digits = |part: &str| part.chars().all(|c| c.is_ascii_digit());
if whole.is_empty() || !digits(whole) || !digits(fraction) || fraction.len() > 6 {
return None;
}
let whole: u32 = whole.parse().ok()?;
let fraction: u32 = format!("{fraction:0<6}").parse().ok()?;
let value = whole.checked_mul(1_000_000)?.checked_add(fraction)?;
(1..=1_000_000).contains(&value).then_some(value)
}
struct Reader {
path: SpecPath,
errors: Vec<ReadError>,
}
impl Reader {
fn fail(&mut self, position: Position, problem: Problem) {
self.errors
.push(ReadError::at(&self.path, Some(position), problem));
}
fn entries<'a>(&mut self, node: &'a Node) -> &'a [Entry] {
if let Value::Mapping(entries) = &node.value {
return entries;
}
self.fail(node.position, Problem::Expected("a mapping"));
&[]
}
fn items<'a>(&mut self, node: &'a Node) -> &'a [Node] {
match &node.value {
Value::Sequence(items) => items,
Value::Empty => &[],
_ => {
self.fail(node.position, Problem::Expected("a sequence"));
&[]
}
}
}
fn scalar<'a>(&mut self, node: &'a Node) -> Option<&'a str> {
if let Value::Scalar(scalar) = &node.value {
return Some(&scalar.text);
}
self.fail(node.position, Problem::Expected("a scalar"));
None
}
fn file(&mut self, root: &Node, config: &mut LintConfig) {
for entry in self.entries(root) {
match entry.key.as_str() {
"rules" => self.rules(&entry.value, config),
"suppressions" => self.suppressions(&entry.value, config),
other => self.fail(entry.key_position, Problem::UnknownField(other.to_owned())),
}
}
}
fn rules(&mut self, node: &Node, config: &mut LintConfig) {
for item in self.items(node) {
self.rule(item, config);
}
}
fn suppressions(&mut self, node: &Node, config: &mut LintConfig) {
for item in self.items(node) {
self.suppression(item, config);
}
}
fn rule(&mut self, item: &Node, config: &mut LintConfig) {
let entries = self.entries(item);
let Some(named) = entries.iter().find(|entry| entry.key == "rule") else {
if !entries.is_empty() {
self.fail(item.position, Problem::MissingField("rule"));
}
return;
};
let Some(rule) = self.parsed(&named.value, "a rule the Format defines", RuleId::parse)
else {
return;
};
for option in entries.iter().filter(|entry| entry.key != "rule") {
self.option(rule, option, config);
}
}
fn option(&mut self, rule: RuleId, option: &Entry, config: &mut LintConfig) {
let value = &option.value;
match option.key.as_str() {
"enabled" => {
if let Some(enabled) = self.parsed(value, "`true` or `false`", |text| match text {
"true" => Some(true),
"false" => Some(false),
_ => None,
}) {
config.rules.get_mut(&rule).expect("every rule").0 = enabled;
}
}
"severity" => {
if let Some(severity) = self.parsed(value, "warning, info or hint", Severity::parse)
{
config.rules.get_mut(&rule).expect("every rule").1 = severity;
}
}
key => {
let Some((key, kind)) = defaults::option(rule, key) else {
let unknown = Problem::UnknownField(key.to_owned());
return self.fail(option.key_position, unknown);
};
if let Some(setting) = self.setting(value, kind) {
config.options.insert((rule, key), setting);
}
}
}
}
fn parsed<T>(
&mut self,
node: &Node,
expected: &'static str,
parse: impl Fn(&str) -> Option<T>,
) -> Option<T> {
let text = self.scalar(node)?;
let parsed = parse(text);
if parsed.is_none() {
let value = text.to_owned();
self.fail(node.position, Problem::Invalid { value, expected });
}
parsed
}
fn setting(&mut self, node: &Node, kind: &Kind) -> Option<Setting> {
match kind {
Kind::Lexicon(defaults) => self.lexicon(node, defaults).map(Setting::Lexicon),
Kind::Count {
least, expected, ..
} => {
let count = |text: &str| text.parse().ok().filter(|count| count >= least);
self.parsed(node, expected, count).map(Setting::Count)
}
Kind::Millionths(_) => {
let expected = "a decimal above 0 and at most 1, with at most six places";
self.parsed(node, expected, parse_millionths)
.map(Setting::Millionths)
}
Kind::Names => self.names(node).map(Setting::Names),
}
}
fn names(&mut self, node: &Node) -> Option<Vec<String>> {
let errors = self.errors.len();
let names: Vec<String> = self
.items(node)
.iter()
.filter_map(|item| {
self.parsed(item, "a name", |text| {
(!text.trim().is_empty()).then(|| text.to_owned())
})
})
.collect();
(self.errors.len() == errors).then_some(names)
}
fn lexicon(&mut self, node: &Node, defaults: &[&str]) -> Option<Lexicon> {
let mut edits = Edits::default();
for entry in self.entries(node) {
let mut phrases = Vec::new();
for item in self.items(&entry.value) {
phrases.extend(self.scalar(item).map(str::to_owned));
}
match entry.key.as_str() {
"replace" => edits.replace = Some(phrases),
"extend" => edits.extend = phrases,
"remove" => edits.remove = phrases,
other => self.fail(entry.key_position, Problem::UnknownField(other.to_owned())),
}
}
lexicon::effective(defaults, Some(&edits))
.map_err(|problem| self.fail(node.position, Problem::Shape(problem)))
.ok()
}
fn suppression(&mut self, item: &Node, config: &mut LintConfig) {
let entries = self.entries(item);
let field = |name: &str| entries.iter().find(|entry| entry.key == name);
for entry in entries {
if !matches!(entry.key.as_str(), "rule" | "key" | "reason") {
self.fail(entry.key_position, Problem::UnknownField(entry.key.clone()));
}
}
let mut required = |name: &'static str| {
let found = field(name);
if found.is_none() {
self.errors.push(ReadError::at(
&self.path,
Some(item.position),
Problem::MissingField(name),
));
}
found
};
let (rule, key, reason) = (required("rule"), required("key"), required("reason"));
let rule = rule
.and_then(|rule| self.parsed(&rule.value, "a rule the Format defines", RuleId::parse));
let key =
key.and_then(|key| self.parsed(&key.value, "a key", |text| Key::parse(text).ok()));
let reason = reason.and_then(|reason| {
self.parsed(&reason.value, "a reason", |text| {
(!text.trim().is_empty()).then_some(())
})
});
if let (Some(rule), Some(key), Some(())) = (rule, key, reason) {
config.suppressions.insert((rule, key));
}
}
}