diff --git a/linter/src/ast/atom.rs b/linter/src/ast/atom.rs new file mode 100644 index 0000000..de8ea35 --- /dev/null +++ b/linter/src/ast/atom.rs @@ -0,0 +1,22 @@ +use huia_parser::Atom; +use rule::LinterRule; +use rules::atom_should_be_snake_case::AtomShouldBeSnakeCase; +use visitor::Visitor; + +impl<'a> Visitor<'a> for Atom<'a> { + fn children(&self) -> Vec> { + vec![] + } +} + +#[cfg(test)] +mod test { + use super::*; + use huia_parser::Parse; + + #[test] + fn has_no_children() { + let atom = Atom::parse(":MartyMcFly").unwrap(); + assert!(atom.children().is_empty()); + } +} diff --git a/linter/src/ast/atom_should_be_snake_case.rs b/linter/src/ast/atom_should_be_snake_case.rs deleted file mode 100644 index 8b393d3..0000000 --- a/linter/src/ast/atom_should_be_snake_case.rs +++ /dev/null @@ -1,54 +0,0 @@ -use heck::SnakeCase; -use huia_parser::ast::Atom; -use rule::LinterRule; -use severity::LinterSeverity; - -pub struct AtomShouldBeSnakeCase(); - -impl<'a> LinterRule> for AtomShouldBeSnakeCase { - fn is_valid(atom: Atom<'a>) -> bool { - atom.name().to_snake_case() == atom.name() - } - - fn message(_atom: Atom<'a>) -> String { - "Atom should be snake case".to_string() - } - - fn description(_atom: Atom<'a>) -> Option { - None - } - - fn suggestion(atom: Atom<'a>) -> Option { - let original = atom.name(); - let replacement = atom.name().to_snake_case(); - let message = format!("Replace \":{}\" with \":{}\"", original, replacement); - Some(message) - } - - fn severity(_atom: Atom<'a>) -> LinterSeverity { - LinterSeverity::Warn - } -} - -#[cfg(test)] -mod test { - use super::*; - use huia_parser::grammar::{parse_str, Rule}; - - #[test] - fn camel_case_atom() { - let pair = parse_str(":martyMcFly", Rule::atom); - let atom = Atom::from(pair); - assert!(!AtomShouldBeSnakeCase::is_valid(atom.clone())); - assert_eq!( - AtomShouldBeSnakeCase::message(atom.clone()), - "Atom should be snake case" - ); - assert!(AtomShouldBeSnakeCase::description(atom.clone()).is_none()); - assert_eq!( - AtomShouldBeSnakeCase::suggestion(atom.clone()).unwrap(), - "Replace \":martyMcFly\" with \":marty_mc_fly\"" - ); - assert!(AtomShouldBeSnakeCase::severity(atom).is_warn()); - } -} diff --git a/linter/src/ast/mod.rs b/linter/src/ast/mod.rs index 9ae0e2b..307db02 100644 --- a/linter/src/ast/mod.rs +++ b/linter/src/ast/mod.rs @@ -1 +1 @@ -mod atom_should_be_snake_case; +mod atom; diff --git a/linter/src/lib.rs b/linter/src/lib.rs index 0752ba6..1f5c2fe 100644 --- a/linter/src/lib.rs +++ b/linter/src/lib.rs @@ -5,10 +5,6 @@ mod ast; mod error; mod result; mod rule; +mod rules; mod severity; - -use result::LinterResult; - -pub trait Lintable { - fn lint(&self) -> LinterResult; -} +mod visitor; diff --git a/linter/src/rule.rs b/linter/src/rule.rs index c78eff2..d6efa2b 100644 --- a/linter/src/rule.rs +++ b/linter/src/rule.rs @@ -1,9 +1,13 @@ use severity::LinterSeverity; -pub trait LinterRule { - fn is_valid(node: T) -> bool; - fn message(node: T) -> String; - fn description(node: T) -> Option; - fn suggestion(node: T) -> Option; - fn severity(node: T) -> LinterSeverity; +pub trait LinterRule { + fn is_valid(&self) -> bool; + fn message(&self) -> String; + fn description(&self) -> Option; + fn suggestion(&self) -> Option; + fn severity(&self) -> LinterSeverity; + + fn is_invalid(&self) -> bool { + !self.is_valid() + } } diff --git a/linter/src/rules/atom_should_be_snake_case.rs b/linter/src/rules/atom_should_be_snake_case.rs new file mode 100644 index 0000000..7cb21ea --- /dev/null +++ b/linter/src/rules/atom_should_be_snake_case.rs @@ -0,0 +1,75 @@ +use heck::SnakeCase; +use huia_parser::Atom; +use rule::LinterRule; +use severity::LinterSeverity; + +#[derive(Debug, Clone)] +pub struct AtomShouldBeSnakeCase<'a>(&'a Atom<'a>); + +impl<'a> LinterRule for AtomShouldBeSnakeCase<'a> { + fn is_valid(&self) -> bool { + self.0.name().to_snake_case() == self.0.name() + } + + fn message(&self) -> String { + "Atom should be snake_case".to_string() + } + + fn description(&self) -> Option { + None + } + + fn suggestion(&self) -> Option { + let original = self.0.name(); + let replacement = original.to_snake_case(); + let message = format!("Replace \":{}\" with \":{}\"", original, replacement); + Some(message) + } + + fn severity(&self) -> LinterSeverity { + LinterSeverity::Warn + } +} + +impl<'a> From<&'a Atom<'a>> for AtomShouldBeSnakeCase<'a> { + fn from(atom: &'a Atom<'a>) -> Self { + AtomShouldBeSnakeCase(atom) + } +} + +#[cfg(test)] +mod test { + use super::*; + use huia_parser::Parse; + + #[test] + fn camel_case_atom() { + let atom = Atom::parse(":MartyMcFly").unwrap(); + let rule = AtomShouldBeSnakeCase::from(&atom); + assert!(!rule.is_valid()); + assert_eq!(rule.message(), "Atom should be snake_case"); + assert_eq!( + rule.suggestion().unwrap(), + "Replace \":MartyMcFly\" with \":marty_mc_fly\"" + ); + assert!(rule.severity().is_warn()); + } + + #[test] + fn snake_case_atom() { + let atom = Atom::parse(":marty_mcfly").unwrap(); + let rule = AtomShouldBeSnakeCase::from(&atom); + assert!(rule.is_valid()); + } + + #[test] + fn screaming_snake_case_atom() { + let atom = Atom::parse(":MARTY_MCFLY").unwrap(); + let rule = AtomShouldBeSnakeCase::from(&atom); + assert!(!rule.is_valid()); + assert_eq!( + rule.suggestion().unwrap(), + "Replace \":MARTY_MCFLY\" with \":marty_mcfly\"" + ); + } +} diff --git a/linter/src/rules/mod.rs b/linter/src/rules/mod.rs new file mode 100644 index 0000000..646301b --- /dev/null +++ b/linter/src/rules/mod.rs @@ -0,0 +1,2 @@ +pub mod atom_should_be_snake_case; +pub mod type_name_should_be_camel_case; diff --git a/linter/src/rules/type_name_should_be_camel_case.rs b/linter/src/rules/type_name_should_be_camel_case.rs new file mode 100644 index 0000000..155b7b3 --- /dev/null +++ b/linter/src/rules/type_name_should_be_camel_case.rs @@ -0,0 +1,64 @@ +use heck::CamelCase; +use huia_parser::TypeName; +use rule::LinterRule; +use severity::LinterSeverity; + +pub struct TypeNameShouldBeCamelCase<'a>(&'a TypeName<'a>); + +impl<'a> LinterRule for TypeNameShouldBeCamelCase<'a> { + fn is_valid(&self) -> bool { + self.0.name().to_camel_case() == self.0.name() + } + + fn message(&self) -> String { + "Type name should be CamelCase".to_string() + } + + fn description(&self) -> Option { + None + } + + fn suggestion(&self) -> Option { + let original = self.0.name(); + let replacement = original.to_camel_case(); + let message = format!("Replace \"{}\" with \"{}\"", original, replacement); + Some(message) + } + + fn severity(&self) -> LinterSeverity { + LinterSeverity::Warn + } +} + +impl<'a> From<&'a TypeName<'a>> for TypeNameShouldBeCamelCase<'a> { + fn from(type_name: &'a TypeName<'a>) -> Self { + TypeNameShouldBeCamelCase(type_name) + } +} + +#[cfg(test)] +mod test { + use super::*; + use huia_parser::Parse; + + #[test] + fn screaming_snake_case() { + let type_name = TypeName::parse("MARTY_MC_FLY").unwrap(); + let rule = TypeNameShouldBeCamelCase::from(&type_name); + assert!(!rule.is_valid()); + assert_eq!(rule.message(), "Type name should be CamelCase"); + assert_eq!( + rule.suggestion().unwrap(), + "Replace \"MARTY_MC_FLY\" with \"MartyMcFly\"" + ); + assert!(rule.severity().is_warn()); + } + + #[test] + fn camel_case_type_name() { + let type_name = TypeName::parse("MartyMcFly").unwrap(); + let rule = TypeNameShouldBeCamelCase::from(&type_name); + assert!(rule.is_valid()); + } + +} diff --git a/linter/src/visitor.rs b/linter/src/visitor.rs new file mode 100644 index 0000000..f3c95d7 --- /dev/null +++ b/linter/src/visitor.rs @@ -0,0 +1,3 @@ +pub trait Visitor<'a> { + fn children(&self) -> Vec>; +}