use std::collections::{HashMap, HashSet};
use strum::{EnumIter, IntoEnumIterator as _, IntoStaticStr, VariantArray as _};
use super::closest_match;
use crate::diagnostic::Language;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FilterListDefaults {
pub base: &'static [&'static str],
pub extend: &'static [(Language, &'static [&'static str])],
pub remove: &'static [(Language, &'static [&'static str])],
}
impl FilterListDefaults {
#[must_use]
pub fn resolve_default_for_lang(&self, lang: Language) -> HashSet<String> {
let mut set: HashSet<String> = self.base.iter().map(|&item| item.to_string()).collect();
for &(target_lang, items) in self.extend {
if target_lang == lang {
set.extend(items.iter().map(|&item| item.to_string()));
}
}
for &(target_lang, items) in self.remove {
if target_lang == lang {
for &item in items {
set.remove(item);
}
}
}
set
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LanguageDefaults<T: Copy + 'static> {
pub base: T,
pub overrides: &'static [(Language, T)],
}
impl<T: Copy + 'static> LanguageDefaults<T> {
#[must_use]
pub const fn new(base: T, overrides: &'static [(Language, T)]) -> Self {
Self { base, overrides }
}
#[must_use]
pub fn resolve_default_for_lang(&self, lang: Language) -> T {
language_override(self.overrides, lang).unwrap_or(self.base)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumIter, IntoStaticStr)]
#[strum(serialize_all = "kebab-case")]
pub enum EnforcementMode {
Ban,
RequireExplanation,
}
impl EnforcementMode {
pub const KEY: &'static str = "enforcement-mode";
pub const DOC: &'static str = "`ban` flags every occurrence; `require-explanation` accepts \
an occurrence explained by a comment at the end of its line \
or in the comment lines directly above it. A comment at the \
end of any header line of the enclosing statement, or directly \
above that header (above the first decorator, if any), also \
counts and excuses every finding on that header, from any rule \
in this mode; the body of a block statement is not part of its \
header. The comment needs at least three words; docstrings, \
Rust doc comments (`///`, `//!`, `/** */`, `/*! */`), and bare \
tool directives such as `noqa` or `type: ignore` do not count, \
but a reason written after a directive does.";
pub const EXPLANATION_HINT: &'static str =
"Explain why in a comment at the end of the flagged line or directly above it.";
}
pub trait OptionsDeclaration: Copy + 'static {
type Resolved;
type Param<'a>;
fn as_param(resolved: &Self::Resolved) -> Self::Param<'_>;
fn specs(self) -> Vec<OptionSpec>;
fn resolve(self, language: Language, overrides: Option<&RuleOverrides>) -> Self::Resolved;
}
impl OptionsDeclaration for () {
type Resolved = ();
type Param<'a> = ();
fn as_param(&(): &()) {}
fn specs(self) -> Vec<OptionSpec> {
Vec::new()
}
fn resolve(self, _language: Language, _overrides: Option<&RuleOverrides>) {}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CountOption {
pub key: &'static str,
pub doc: &'static str,
pub default: LanguageDefaults<usize>,
}
impl OptionsDeclaration for CountOption {
type Resolved = usize;
type Param<'a> = usize;
fn as_param(&count: &usize) -> usize {
count
}
fn specs(self) -> Vec<OptionSpec> {
vec![OptionSpec::Count(self)]
}
fn resolve(self, language: Language, overrides: Option<&RuleOverrides>) -> usize {
layers(overrides, language)
.find_map(|values| values.counts.get(self.key).copied())
.unwrap_or_else(|| self.default.resolve_default_for_lang(language))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ListKind {
Deny,
Allow,
}
impl ListKind {
#[must_use]
pub const fn replace_key(self) -> &'static str {
match self {
Self::Deny => "banned",
Self::Allow => "allowed",
}
}
#[must_use]
pub const fn extend_key(self) -> &'static str {
match self {
Self::Deny => "extend-banned",
Self::Allow => "extend-allowed",
}
}
#[must_use]
pub const fn remove_key(self) -> &'static str {
match self {
Self::Deny => "remove-banned",
Self::Allow => "remove-allowed",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ListOption {
pub kind: ListKind,
pub doc: &'static str,
pub default: FilterListDefaults,
}
impl OptionsDeclaration for ListOption {
type Resolved = HashSet<String>;
type Param<'a> = &'a HashSet<String>;
fn as_param(items: &HashSet<String>) -> &HashSet<String> {
items
}
fn specs(self) -> Vec<OptionSpec> {
vec![OptionSpec::List(self)]
}
fn resolve(self, language: Language, overrides: Option<&RuleOverrides>) -> HashSet<String> {
let mut items = self.default.resolve_default_for_lang(language);
if let Some(overrides) = overrides {
overrides.global.list.apply_to(&mut items);
if let Some(language_values) = overrides.for_language(language) {
language_values.list.apply_to(&mut items);
}
}
items
}
}
impl<First: OptionsDeclaration, Second: OptionsDeclaration> OptionsDeclaration for (First, Second) {
type Resolved = (First::Resolved, Second::Resolved);
type Param<'a> = (First::Param<'a>, Second::Param<'a>);
fn as_param((first, second): &Self::Resolved) -> Self::Param<'_> {
(First::as_param(first), Second::as_param(second))
}
fn specs(self) -> Vec<OptionSpec> {
let (first, second) = self;
let mut specs = first.specs();
specs.extend(second.specs());
specs
}
fn resolve(self, language: Language, overrides: Option<&RuleOverrides>) -> Self::Resolved {
let (first, second) = self;
(
first.resolve(language, overrides),
second.resolve(language, overrides),
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OptionSpec {
Count(CountOption),
List(ListOption),
}
impl OptionSpec {
#[must_use]
pub fn keys(self) -> Vec<&'static str> {
match self {
Self::Count(option) => vec![option.key],
Self::List(option) => vec![
option.kind.replace_key(),
option.kind.extend_key(),
option.kind.remove_key(),
],
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RuleOptions<Options: OptionsDeclaration> {
pub enforcement_mode: Option<LanguageDefaults<EnforcementMode>>,
pub options: Options,
}
impl<Options: OptionsDeclaration> RuleOptions<Options> {
#[must_use]
pub const fn code_rule(options: Options) -> Self {
Self {
enforcement_mode: Some(LanguageDefaults::new(EnforcementMode::Ban, &[])),
options,
}
}
#[must_use]
pub fn declared(&self) -> DeclaredOptions {
DeclaredOptions {
enforcement_mode: self.enforcement_mode,
options: self.options.specs(),
}
}
#[must_use]
pub fn enforcement_mode(
&self,
language: Language,
overrides: Option<&RuleOverrides>,
) -> EnforcementMode {
resolve_enforcement_mode(self.enforcement_mode, language, overrides)
}
}
impl RuleOptions<()> {
#[must_use]
pub const fn none() -> Self {
Self {
enforcement_mode: None,
options: (),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DeclaredOptions {
pub enforcement_mode: Option<LanguageDefaults<EnforcementMode>>,
pub options: Vec<OptionSpec>,
}
impl DeclaredOptions {
#[must_use]
pub fn keys(&self) -> Vec<&'static str> {
self.enforcement_mode
.map(|_| EnforcementMode::KEY)
.into_iter()
.chain(self.options.iter().flat_map(|option| option.keys()))
.collect()
}
#[must_use]
pub fn enforcement_mode(
&self,
language: Language,
overrides: Option<&RuleOverrides>,
) -> EnforcementMode {
resolve_enforcement_mode(self.enforcement_mode, language, overrides)
}
#[must_use]
pub fn default_toml(
&self,
rule_name: crate::diagnostic::RuleName,
languages: &[Language],
) -> Option<String> {
let mut rule_table = toml::Table::new();
if let Some(default) = self.enforcement_mode {
rule_table.insert(
EnforcementMode::KEY.into(),
<&str>::from(default.base).into(),
);
}
for option in &self.options {
match option {
OptionSpec::Count(count) => {
rule_table.insert(count.key.into(), toml_count(count.default.base));
}
OptionSpec::List(list) => {
rule_table.insert(
list.kind.replace_key().into(),
list.default.base.to_vec().into(),
);
}
}
}
if rule_table.is_empty() {
return None;
}
for &language in languages {
let mut lang_table = toml::Table::new();
if let Some(default) = self.enforcement_mode
&& let Some(mode) = language_override(default.overrides, language)
{
lang_table.insert(EnforcementMode::KEY.into(), <&str>::from(mode).into());
}
for option in &self.options {
match option {
OptionSpec::Count(count) => {
if let Some(value) = language_override(count.default.overrides, language) {
lang_table.insert(count.key.into(), toml_count(value));
}
}
OptionSpec::List(list) => {
for (key, entries) in [
(list.kind.extend_key(), list.default.extend),
(list.kind.remove_key(), list.default.remove),
] {
if let Some(items) = language_override(entries, language) {
lang_table.insert(key.into(), items.to_vec().into());
}
}
}
}
}
if !lang_table.is_empty() {
rule_table.insert(language.as_str().into(), toml::Value::Table(lang_table));
}
}
let rules = toml::Table::from_iter([(rule_name.0.into(), toml::Value::Table(rule_table))]);
let root = toml::Table::from_iter([("rules".into(), toml::Value::Table(rules))]);
toml::to_string(&root).ok()
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct RuleOverrides {
global: OptionValues,
per_language: Vec<(Language, OptionValues)>,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum OptionProblem {
#[error("unknown key; {}", match (suggestion, expected.as_slice()) {
(Some(known), _) => format!("did you mean `{known}`?"),
(None, []) => "this rule takes no options".to_owned(),
(None, keys) => format!("expected one of `{}`", keys.join("`, `")),
})]
UnknownKey {
suggestion: Option<&'static str>,
expected: Vec<&'static str>,
},
#[error("expected {expected}, found {found}")]
WrongType {
expected: String,
found: String,
},
#[error("this rule does not analyze this language; it analyzes {analyzed}")]
UnsupportedLanguage {
analyzed: String,
},
#[error("this rule has no enforcement mode; it always reports every finding")]
EnforcementModeRejected,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("`{key_path}`: {problem}")]
pub struct RuleOptionsError {
pub key_path: String,
pub problem: OptionProblem,
}
impl RuleOverrides {
pub fn parse(
rule_name: &str,
declaration: &DeclaredOptions,
supported_languages: &[Language],
value: &toml::Value,
) -> Result<Self, RuleOptionsError> {
let rule_path = format!("rules.{rule_name}");
let mut overrides = Self::default();
for (key, value) in expect_table(value, &rule_path)? {
let key_path = format!("{rule_path}.{key}");
match language_named(key) {
Some(language) if supported_languages.contains(&language) => {
let mut values = OptionValues::default();
for (key, value) in expect_table(value, &key_path)? {
values.set(declaration, &[], &key_path, key, value)?;
}
overrides.per_language.push((language, values));
}
Some(_) => {
let analyzed: Vec<_> = supported_languages
.iter()
.map(|&language| language.as_str())
.collect();
return Err(RuleOptionsError {
key_path,
problem: OptionProblem::UnsupportedLanguage {
analyzed: analyzed.join(", "),
},
});
}
None => {
overrides.global.set(
declaration,
supported_languages,
&rule_path,
key,
value,
)?;
}
}
}
Ok(overrides)
}
fn for_language(&self, language: Language) -> Option<&OptionValues> {
self.per_language
.iter()
.find_map(|(candidate, values)| (*candidate == language).then_some(values))
}
}
fn toml_count(count: usize) -> toml::Value {
toml::Value::Integer(i64::try_from(count).unwrap_or(i64::MAX))
}
fn resolve_enforcement_mode(
default: Option<LanguageDefaults<EnforcementMode>>,
language: Language,
overrides: Option<&RuleOverrides>,
) -> EnforcementMode {
layers(overrides, language)
.find_map(|values| values.enforcement_mode)
.unwrap_or_else(|| {
default.map_or(EnforcementMode::Ban, |default| {
default.resolve_default_for_lang(language)
})
})
}
fn language_override<T: Copy>(entries: &[(Language, T)], language: Language) -> Option<T> {
entries
.iter()
.find_map(|&(target_lang, value)| (target_lang == language).then_some(value))
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
struct OptionValues {
enforcement_mode: Option<EnforcementMode>,
counts: HashMap<&'static str, usize>,
list: ListOverride,
}
impl OptionValues {
fn set(
&mut self,
declaration: &DeclaredOptions,
languages: &[Language],
table_path: &str,
key: &str,
value: &toml::Value,
) -> Result<(), RuleOptionsError> {
let key_path = format!("{table_path}.{key}");
let error = |problem| RuleOptionsError {
key_path: key_path.clone(),
problem,
};
if key == EnforcementMode::KEY {
if declaration.enforcement_mode.is_none() {
return Err(error(OptionProblem::EnforcementModeRejected));
}
self.enforcement_mode = Some(parse_enforcement_mode(value).map_err(error)?);
return Ok(());
}
for &option in &declaration.options {
match option {
OptionSpec::Count(count) if key == count.key => {
self.counts
.insert(count.key, parse_count(value).map_err(error)?);
return Ok(());
}
OptionSpec::List(list) if key == list.kind.replace_key() => {
self.list.replace = Some(parse_items(value).map_err(error)?);
return Ok(());
}
OptionSpec::List(list) if key == list.kind.extend_key() => {
self.list.extend = parse_items(value).map_err(error)?;
return Ok(());
}
OptionSpec::List(list) if key == list.kind.remove_key() => {
self.list.remove = parse_items(value).map_err(error)?;
return Ok(());
}
OptionSpec::Count(_) | OptionSpec::List(_) => {}
}
}
let mut expected = declaration.keys();
expected.extend(languages.iter().map(|&language| language.as_str()));
Err(error(OptionProblem::UnknownKey {
suggestion: closest_match(key, expected.iter().copied()),
expected,
}))
}
}
fn layers(
overrides: Option<&RuleOverrides>,
language: Language,
) -> impl Iterator<Item = &OptionValues> {
overrides
.and_then(move |overrides| overrides.for_language(language))
.into_iter()
.chain(overrides.map(|overrides| &overrides.global))
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
struct ListOverride {
replace: Option<HashSet<String>>,
extend: HashSet<String>,
remove: HashSet<String>,
}
impl ListOverride {
fn apply_to(&self, items: &mut HashSet<String>) {
if let Some(replacement) = &self.replace {
items.clone_from(replacement);
}
items.extend(self.extend.iter().cloned());
for item in &self.remove {
items.remove(item);
}
}
}
fn language_named(key: &str) -> Option<Language> {
Language::VARIANTS
.iter()
.copied()
.find(|&language| language.as_str() == key)
}
fn expect_table<'value>(
value: &'value toml::Value,
key_path: &str,
) -> Result<&'value toml::Table, RuleOptionsError> {
value.as_table().ok_or_else(|| RuleOptionsError {
key_path: key_path.to_owned(),
problem: wrong_type("a table", value),
})
}
fn parse_enforcement_mode(value: &toml::Value) -> Result<EnforcementMode, OptionProblem> {
EnforcementMode::iter()
.find(|&mode| value.as_str() == Some(mode.into()))
.ok_or_else(|| {
let labels: Vec<&str> = EnforcementMode::iter().map(Into::into).collect();
wrong_type(&format!("`{}`", labels.join("` or `")), value)
})
}
fn parse_count(value: &toml::Value) -> Result<usize, OptionProblem> {
value
.as_integer()
.and_then(|number| usize::try_from(number).ok())
.ok_or_else(|| wrong_type("a non-negative integer", value))
}
fn parse_items(value: &toml::Value) -> Result<HashSet<String>, OptionProblem> {
value
.as_array()
.and_then(|items| {
items
.iter()
.map(|item| item.as_str().map(str::to_owned))
.collect()
})
.ok_or_else(|| wrong_type("an array of strings", value))
}
fn wrong_type(expected: &str, value: &toml::Value) -> OptionProblem {
let found = match value {
toml::Value::String(text) => format!("\"{text}\""),
toml::Value::Integer(number) => number.to_string(),
other => other.type_str().to_owned(),
};
OptionProblem::WrongType {
expected: expected.to_owned(),
found,
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
const LIMIT: CountOption = CountOption {
key: "max-items",
doc: "Maximum items.",
default: LanguageDefaults::new(4, &[(Language::Rust, 6)]),
};
const DENY: ListOption = ListOption {
kind: ListKind::Deny,
doc: "Banned words.",
default: FilterListDefaults {
base: &["default_one", "common_ok"],
extend: &[],
remove: &[],
},
};
const ALLOW: ListOption = ListOption {
kind: ListKind::Allow,
doc: "Allowed words.",
default: FilterListDefaults {
base: &["default_base"],
extend: &[(Language::Rust, &["rust_extra"])],
remove: &[],
},
};
const WITH_COUNT: RuleOptions<CountOption> = RuleOptions {
enforcement_mode: Some(LanguageDefaults::new(
EnforcementMode::Ban,
&[(Language::Rust, EnforcementMode::RequireExplanation)],
)),
options: LIMIT,
};
const DENYING_RULE: RuleOptions<ListOption> = RuleOptions::code_rule(DENY);
const BOTH_LANGUAGES: &[Language] = &[Language::Python, Language::Rust];
fn parse(
declaration: &DeclaredOptions,
languages: &[Language],
toml_content: &str,
) -> Result<RuleOverrides, RuleOptionsError> {
let value = toml::Value::Table(toml::from_str(toml_content).unwrap());
RuleOverrides::parse("some-rule", declaration, languages, &value)
}
fn words(items: &[&str]) -> HashSet<String> {
items.iter().map(|&item| item.to_owned()).collect()
}
#[test]
fn test_filter_list_defaults_resolve() {
const DEFAULTS: FilterListDefaults = FilterListDefaults {
base: &["common", "shared", "temp"],
extend: &[(Language::Rust, &["rust_only"])],
remove: &[(Language::Rust, &["temp"])],
};
let python_defaults = DEFAULTS.resolve_default_for_lang(Language::Python);
let rust_defaults = DEFAULTS.resolve_default_for_lang(Language::Rust);
assert_eq!(
python_defaults,
HashSet::from(["common", "shared", "temp"].map(String::from))
);
assert_eq!(
rust_defaults,
HashSet::from(["common", "shared", "rust_only"].map(String::from))
);
}
#[rstest]
#[case::python_default("", Language::Python, 4)]
#[case::rust_default("", Language::Rust, 6)]
#[case::rule_table("max-items = 5", Language::Rust, 5)]
#[case::other_language_table(indoc::indoc! {r"
max-items = 5
[rust]
max-items = 10"}, Language::Python, 5)]
#[case::language_table(indoc::indoc! {r"
max-items = 5
[rust]
max-items = 10"}, Language::Rust, 10)]
fn count_prefers_language_then_rule_table_then_default(
#[case] toml_content: &str,
#[case] language: Language,
#[case] expected: usize,
) {
let overrides = parse(&WITH_COUNT.declared(), BOTH_LANGUAGES, toml_content).unwrap();
assert_eq!(LIMIT.resolve(language, Some(&overrides)), expected);
}
#[rstest]
#[case::python_default("", Language::Python, EnforcementMode::Ban)]
#[case::rust_default("", Language::Rust, EnforcementMode::RequireExplanation)]
#[case::rule_table("enforcement-mode = \"ban\"", Language::Rust, EnforcementMode::Ban)]
#[case::language_table(
indoc::indoc! {r#"
enforcement-mode = "ban"
[python]
enforcement-mode = "require-explanation""#},
Language::Python,
EnforcementMode::RequireExplanation
)]
fn enforcement_mode_prefers_language_then_rule_table_then_default(
#[case] toml_content: &str,
#[case] language: Language,
#[case] expected: EnforcementMode,
) {
let overrides = parse(&WITH_COUNT.declared(), BOTH_LANGUAGES, toml_content).unwrap();
assert_eq!(
WITH_COUNT.enforcement_mode(language, Some(&overrides)),
expected
);
}
#[rstest]
#[case::deny(
DENY,
indoc::indoc! {r#"
extend-banned = ["global_bad", "rust_ok"]
remove-banned = ["common_ok"]
[rust]
extend-banned = ["rust_bad"]
remove-banned = ["rust_ok"]
[python]
banned = ["py_only_bad", "py_dropped"]
extend-banned = ["py_extra"]
remove-banned = ["py_dropped"]
"#},
&["default_one", "global_bad", "rust_bad"],
&["py_only_bad", "py_extra"]
)]
#[case::allow(
ALLOW,
indoc::indoc! {r#"
extend-allowed = ["global_allowed", "rust_revoked"]
remove-allowed = ["default_base"]
[rust]
extend-allowed = ["rust_allowed", "default_base"]
remove-allowed = ["rust_revoked"]
[python]
allowed = ["py_only_allowed"]
"#},
&["default_base", "rust_extra", "global_allowed", "rust_allowed"],
&["py_only_allowed"]
)]
fn list_applies_global_table_then_language_table(
#[case] option: ListOption,
#[case] toml_content: &str,
#[case] rust_expected: &[&str],
#[case] python_expected: &[&str],
) {
let overrides = parse(
&RuleOptions::code_rule(option).declared(),
BOTH_LANGUAGES,
toml_content,
)
.unwrap();
let rust = option.resolve(Language::Rust, Some(&overrides));
let python = option.resolve(Language::Python, Some(&overrides));
assert_eq!(rust, words(rust_expected));
assert_eq!(python, words(python_expected));
}
#[test]
fn a_pair_resolves_each_member_and_lists_both_specs() {
let pair = (LIMIT, DENY);
let overrides = parse(
&RuleOptions::code_rule(pair).declared(),
BOTH_LANGUAGES,
"max-items = 9\nbanned = [\"only\"]",
)
.unwrap();
assert_eq!(
pair.specs(),
vec![OptionSpec::Count(LIMIT), OptionSpec::List(DENY)]
);
assert_eq!(
pair.resolve(Language::Python, Some(&overrides)),
(9, words(&["only"]))
);
}
#[test]
fn missing_overrides_resolve_to_defaults() {
assert_eq!(LIMIT.resolve(Language::Rust, None), 6);
assert_eq!(
WITH_COUNT.enforcement_mode(Language::Rust, None),
EnforcementMode::RequireExplanation
);
}
#[rstest]
#[case::typo(
WITH_COUNT.declared(),
BOTH_LANGUAGES,
"max-itemz = 5",
"`rules.some-rule.max-itemz`: unknown key; did you mean `max-items`?"
)]
#[case::unrelated_key(
DENYING_RULE.declared(),
&[Language::Python],
"colour = 1",
"`rules.some-rule.colour`: unknown key; expected one of `enforcement-mode`, `banned`, \
`extend-banned`, `remove-banned`, `python`"
)]
#[case::no_options(
RuleOptions::none().declared(),
&[],
"max = 1",
"`rules.some-rule.max`: unknown key; this rule takes no options"
)]
#[case::count_wrong_type(
WITH_COUNT.declared(),
BOTH_LANGUAGES,
"max-items = \"five\"",
"`rules.some-rule.max-items`: expected a non-negative integer, found \"five\""
)]
#[case::negative_count(
WITH_COUNT.declared(),
BOTH_LANGUAGES,
"max-items = -1",
"`rules.some-rule.max-items`: expected a non-negative integer, found -1"
)]
#[case::list_wrong_type(
DENYING_RULE.declared(),
BOTH_LANGUAGES,
"extend-banned = [\"ok\", 3]",
"`rules.some-rule.extend-banned`: expected an array of strings, found array"
)]
#[case::enforcement_mode_wrong_value(
WITH_COUNT.declared(),
BOTH_LANGUAGES,
"enforcement-mode = \"bann\"",
"`rules.some-rule.enforcement-mode`: expected `ban` or `require-explanation`, \
found \"bann\""
)]
#[case::language_not_a_table(
WITH_COUNT.declared(),
BOTH_LANGUAGES,
"rust = 1",
"`rules.some-rule.rust`: expected a table, found 1"
)]
#[case::unsupported_language(
WITH_COUNT.declared(),
&[Language::Python],
"[rust]\nmax-items = 1",
"`rules.some-rule.rust`: this rule does not analyze this language; it analyzes python"
)]
#[case::enforcement_mode_rejected(
RuleOptions::none().declared(),
BOTH_LANGUAGES,
"enforcement-mode = \"ban\"",
"`rules.some-rule.enforcement-mode`: this rule has no enforcement mode; it always \
reports every finding"
)]
fn invalid_options_are_rejected_with_their_key_path(
#[case] declaration: DeclaredOptions,
#[case] languages: &[Language],
#[case] toml_content: &str,
#[case] message: &str,
) {
let error = parse(&declaration, languages, toml_content).unwrap_err();
assert_eq!(error.to_string(), message);
}
#[test]
fn rule_value_must_be_a_table() {
let error = RuleOverrides::parse(
"some-rule",
&WITH_COUNT.declared(),
BOTH_LANGUAGES,
&toml::Value::Boolean(true),
)
.unwrap_err();
assert_eq!(
error.to_string(),
"`rules.some-rule`: expected a table, found boolean"
);
}
}