diff --git a/spec/ameba/rules/type_names_spec.cr b/spec/ameba/rules/type_names_spec.cr new file mode 100644 index 00000000..04da8ad9 --- /dev/null +++ b/spec/ameba/rules/type_names_spec.cr @@ -0,0 +1,60 @@ +require "../../spec_helper" + +module Ameba + subject = Rules::TypeNames.new + + private def it_reports_name(content, expected) + it "reports type name #{expected}" do + s = Source.new content + Rules::TypeNames.new.catch(s).should_not be_valid + s.errors.first.message.should contain expected + end + end + + describe Rules::TypeNames do + it "passes if type names are camelcased" do + s = Source.new %( + class ParseError < Exception + end + + module HTTP + class RequestHandler + end + end + + alias NumericValue = Float32 | Float64 | Int32 | Int64 + + lib LibYAML + end + + struct TagDirective + end + + enum Time::DayOfWeek + end + ) + subject.catch(s).should be_valid + end + + it_reports_name "class My_class; end", "MyClass" + it_reports_name "module HTT_p; end", "HTTP" + it_reports_name "alias Numeric_value = Int32", "NumericValue" + it_reports_name "lib Lib_YAML; end", "LibYAML" + it_reports_name "struct Tag_directive; end", "TagDirective" + it_reports_name "enum Time_enum::Day_of_week; end", "TimeEnum::DayOfWeek" + + it "reports rule, pos and message" do + s = Source.new %( + class My_class + end + ) + subject.catch(s).should_not be_valid + error = s.errors.first + error.rule.should_not be_nil + error.pos.should eq 2 + error.message.should eq( + "Type name should be camelcased: MyClass, but it was My_class" + ) + end + end +end diff --git a/src/ameba/ast/traverse.cr b/src/ameba/ast/traverse.cr index be71e3ab..f711f3db 100644 --- a/src/ameba/ast/traverse.cr +++ b/src/ameba/ast/traverse.cr @@ -2,29 +2,36 @@ require "compiler/crystal/syntax/*" module Ameba::AST NODE_VISITORS = [ + Alias, Call, Case, + ClassDef, Def, + EnumDef, If, + LibDef, + ModuleDef, StringInterpolation, Unless, ] + abstract class Visitor < Crystal::Visitor + @rule : Rule + @source : Source + + def initialize(@rule, @source) + parser = Crystal::Parser.new(@source.content) + parser.filename = @source.path + parser.parse.accept self + end + + def visit(node : Crystal::ASTNode) + true + end + end + {% for name in NODE_VISITORS %} - class {{name}}Visitor < Crystal::Visitor - @rule : Rule - @source : Source - - def initialize(@rule, @source) - parser = Crystal::Parser.new(@source.content) - parser.filename = @source.path - parser.parse.accept self - end - - def visit(node : Crystal::ASTNode) - true - end - + class {{name}}Visitor < Visitor def visit(node : Crystal::{{name}}) @rule.test @source, node end diff --git a/src/ameba/rules/type_names.cr b/src/ameba/rules/type_names.cr new file mode 100644 index 00000000..e29df1dd --- /dev/null +++ b/src/ameba/rules/type_names.cr @@ -0,0 +1,88 @@ +module Ameba::Rules + # A rule that enforces type names in camelcase manner. + # + # For example, these are considered valid: + # + # ``` + # class ParseError < Exception + # end + # + # module HTTP + # class RequestHandler + # end + # end + # + # alias NumericValue = Float32 | Float64 | Int32 | Int64 + # + # lib LibYAML + # end + # + # struct TagDirective + # end + # + # enum Time::DayOfWeek + # end + # ``` + # + # And these are invalid type names + # + # ``` + # class My_class + # end + # + # module HTT_p + # end + # + # alias Numeric_value = Int32 + # + # lib Lib_YAML + # end + # + # struct Tag_directive + # end + # + # enum Time_enum::Day_of_week + # end + # ``` + # + struct TypeNames < Rule + def test(source) + [ + AST::ClassDefVisitor, + AST::EnumDefVisitor, + AST::ModuleDefVisitor, + AST::AliasVisitor, + AST::LibDefVisitor, + ].each(&.new self, source) + end + + private def check_node(source, node) + name = node.name.to_s + expected = name.camelcase + return if expected == name + + source.error self, node.location.try &.line_number, + "Type name should be camelcased: #{expected}, but it was #{name}" + end + + def test(source, node : Crystal::ClassDef) + check_node(source, node) + end + + def test(source, node : Crystal::Alias) + check_node(source, node) + end + + def test(source, node : Crystal::LibDef) + check_node(source, node) + end + + def test(source, node : Crystal::EnumDef) + check_node(source, node) + end + + def test(source, node : Crystal::ModuleDef) + check_node(source, node) + end + end +end