architecture_component!(RuleDeclaration);
mod documentation;
mod options;
mod taxonomy;
pub use self::documentation::{Example, Reference, RuleDoc};
pub use self::options::{
CountOption, DeclaredOptions, EnforcementMode, FilterListDefaults, LanguageDefaults, ListKind,
ListOption, OptionProblem, OptionSpec, OptionsDeclaration, RuleOptions, RuleOptionsError,
RuleOverrides,
};
pub use self::taxonomy::{Classification, Consensus, ImpactedQuality, Precision, Topic};
use crate::diagnostic::{Diagnostic, Language, RuleName, SourceLocation, ViolationTemplate};
#[must_use]
pub fn is_kebab_case(name: &str) -> bool {
!name.is_empty()
&& !name.starts_with('-')
&& !name.ends_with('-')
&& name.chars().all(|character| {
character.is_ascii_lowercase() || character.is_ascii_digit() || character == '-'
})
}
pub fn closest_match(
label: &str,
candidates: impl Iterator<Item = &'static str>,
) -> Option<&'static str> {
let normalized = label.to_ascii_lowercase();
let tolerance = (normalized.chars().count() / 3).max(1);
candidates
.map(|known| (edit_distance(&normalized, known), known))
.filter(|&(distance, _)| distance <= tolerance)
.min()
.map(|(_, known)| known)
}
fn edit_distance(left: &str, right: &str) -> usize {
let right: Vec<char> = right.chars().collect();
let mut previous: Vec<usize> = (0..=right.len()).collect();
for (row, left_char) in left.chars().enumerate() {
let mut current = vec![row + 1];
for (column, &right_char) in right.iter().enumerate() {
let substitution = previous[column] + usize::from(left_char != right_char);
current.push(
substitution
.min(previous[column + 1] + 1)
.min(current[column] + 1),
);
}
previous = current;
}
previous[right.len()]
}
#[derive(Debug, Clone, Copy)]
pub struct Declaration<Options: OptionsDeclaration = ()> {
pub name: RuleName,
pub template: &'static ViolationTemplate,
pub languages: &'static [Language],
pub options: RuleOptions<Options>,
pub classification: Classification,
pub doc: RuleDoc,
}
impl<Options: OptionsDeclaration> Declaration<Options> {
#[must_use]
pub fn declared(&self) -> DeclaredRule {
DeclaredRule {
name: self.name,
template: self.template,
languages: self.languages,
options: self.options.declared(),
classification: self.classification,
doc: self.doc,
}
}
#[must_use]
pub fn render_diagnostic(
&self,
params: &[(&str, &str)],
location: SourceLocation,
) -> Diagnostic {
Diagnostic::new(self.name, self.template.render(params), location)
}
#[must_use]
pub fn render_diagnostic_for_lang(
&self,
language: Language,
params: &[(&str, &str)],
location: SourceLocation,
) -> Diagnostic {
Diagnostic::new(
self.name,
self.template.render_for_lang(language, params),
location,
)
}
}
#[derive(Debug, Clone)]
pub struct DeclaredRule {
pub name: RuleName,
pub template: &'static ViolationTemplate,
pub languages: &'static [Language],
pub options: DeclaredOptions,
pub classification: Classification,
pub doc: RuleDoc,
}
#[cfg(test)]
mod tests {
use super::*;
#[rstest::rstest]
#[case::missing_letter("alpa", Some("alpha"))]
#[case::case_folded("ALPHA", Some("alpha"))]
#[case::nearest_wins("alphabe", Some("alphabet"))]
#[case::at_tolerance("alphaxy", Some("alpha"))]
#[case::beyond_tolerance("alphaxyz", None)]
fn closest_match_suggests_typos_only(#[case] label: &str, #[case] expected: Option<&str>) {
let candidates = ["alpha", "alphabet", "omega"];
assert_eq!(closest_match(label, candidates.into_iter()), expected);
}
#[test]
fn edit_distance_is_levenshtein() {
assert_eq!(edit_distance("kitten", "sitting"), 3);
}
}