use std::collections::BTreeSet;
use heck::{ToShoutySnakeCase, ToSnakeCase, ToUpperCamelCase};
use once_cell::sync::Lazy;
use regex::Regex;
use crate::compiled::{CompiledDsdl, DsdlKind, Message, MessageKind};
use crate::TypeKey;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct Warning(WarningKind);
impl std::fmt::Display for Warning {
#[inline]
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(&self.0, f)
}
}
#[allow(clippy::enum_variant_names)]
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
enum WarningKind {
PackageCase {
name: String,
suggestion: String,
},
FieldCase {
key: TypeKey,
name: String,
suggestion: String,
},
ConstantCase {
key: TypeKey,
name: String,
suggestion: String,
},
TypeNameCase {
key: TypeKey,
suggestion: String,
},
}
impl std::fmt::Display for WarningKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
WarningKind::PackageCase {
name,
suggestion: alternative,
} => {
write!(
f,
"Package \"{}\" should have a snake_case name like \"{}\"",
name, alternative
)
}
WarningKind::FieldCase {
key: ty,
name,
suggestion: alternative,
} => {
write!(
f,
"In type {}, the field or variant \"{}\" should have a snake_case name like \"{}\"",
ty, name, alternative
)
}
WarningKind::ConstantCase {
key: ty,
name,
suggestion: alternative,
} => {
write!(f, "In type {}, the constant \"{}\" should have a SHOUTING_SNAKE_CASE name like \"{}\"", ty, name, alternative)
}
WarningKind::TypeNameCase {
key: ty,
suggestion: alternative,
} => {
write!(
f,
"The type {} should have a CamelCase name like \"{}\"",
ty, alternative
)
}
}
}
}
#[derive(Debug, Clone)]
pub struct Warnings {
warnings: BTreeSet<Warning>,
}
impl Warnings {
pub(crate) fn new() -> Self {
Warnings {
warnings: BTreeSet::new(),
}
}
pub fn is_empty(&self) -> bool {
self.warnings.is_empty()
}
fn insert(&mut self, kind: WarningKind) {
self.warnings.insert(Warning(kind));
}
pub(crate) fn check_pre_compile(&mut self, key: &TypeKey) {
for part in key.name().path() {
if !is_permissive_snake_case(part) {
let suggestion = part.to_snake_case();
self.insert(WarningKind::PackageCase {
name: part.to_owned(),
suggestion,
})
}
}
let actual_name = key.name().name();
if !is_permissive_upper_camel_case(actual_name) {
let suggestion = actual_name.to_upper_camel_case();
self.insert(WarningKind::TypeNameCase {
key: key.clone(),
suggestion,
})
}
}
pub(crate) fn check_post_compile(&mut self, key: &TypeKey, dsdl: &CompiledDsdl) {
match &dsdl.kind {
DsdlKind::Message(message) => self.check_message(key, message),
DsdlKind::Service { request, response } => {
self.check_message(key, request);
self.check_message(key, response);
}
}
}
fn check_message(&mut self, key: &TypeKey, message: &Message) {
match &message.kind {
MessageKind::Struct(struct_data) => {
for field in &struct_data.fields {
if let Some(name) = field.name() {
self.check_field_name(key, name);
}
}
}
MessageKind::Union(union_data) => {
for variant in &union_data.variants {
self.check_field_name(key, &variant.name);
}
}
}
for (name, _) in &message.constants {
let suggestion = name.to_shouty_snake_case();
if name != &suggestion {
self.insert(WarningKind::ConstantCase {
key: key.to_owned(),
name: name.to_owned(),
suggestion,
})
}
}
}
fn check_field_name(&mut self, key: &TypeKey, name: &str) {
let snake_case = is_permissive_snake_case(name);
if !snake_case {
let suggested = name.to_snake_case();
self.insert(WarningKind::FieldCase {
key: key.to_owned(),
name: name.to_owned(),
suggestion: suggested,
})
}
}
pub fn iter(&self) -> Iter<'_> {
Iter(self.warnings.iter())
}
}
fn is_permissive_snake_case(s: &str) -> bool {
s.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
}
fn is_permissive_upper_camel_case(s: &str) -> bool {
static PATTERN: Lazy<Regex> = Lazy::new(|| Regex::new("^_?[a-zA-Z0-9]*_?$").unwrap());
PATTERN.is_match(s)
}
#[derive(Debug, Clone)]
pub struct Iter<'a>(std::collections::btree_set::Iter<'a, Warning>);
impl<'a> Iterator for Iter<'a> {
type Item = &'a Warning;
#[inline]
fn next(&mut self) -> Option<Self::Item> {
self.0.next()
}
#[inline]
fn size_hint(&self) -> (usize, Option<usize>) {
self.0.size_hint()
}
}
impl ExactSizeIterator for Iter<'_> {
#[inline]
fn len(&self) -> usize {
self.0.len()
}
}
#[derive(Debug)]
pub struct IntoIter(std::collections::btree_set::IntoIter<Warning>);
impl Iterator for IntoIter {
type Item = Warning;
#[inline]
fn next(&mut self) -> Option<Self::Item> {
self.0.next()
}
#[inline]
fn size_hint(&self) -> (usize, Option<usize>) {
self.0.size_hint()
}
}
impl ExactSizeIterator for IntoIter {
#[inline]
fn len(&self) -> usize {
self.0.len()
}
}
impl IntoIterator for Warnings {
type Item = Warning;
type IntoIter = IntoIter;
fn into_iter(self) -> Self::IntoIter {
IntoIter(self.warnings.into_iter())
}
}
impl<'a> IntoIterator for &'a Warnings {
type Item = &'a Warning;
type IntoIter = Iter<'a>;
fn into_iter(self) -> Self::IntoIter {
self.iter()
}
}
#[cfg(test)]
mod test {
use super::is_permissive_upper_camel_case;
#[test]
fn test_upper_camel_case() {
assert!(is_permissive_upper_camel_case(""));
assert!(is_permissive_upper_camel_case("_"));
assert!(is_permissive_upper_camel_case("_Struct_"));
assert!(is_permissive_upper_camel_case("Struct_"));
}
}