diff --git a/spec/ameba/rule/naming/ascii_identifiers_spec.cr b/spec/ameba/rule/naming/ascii_identifiers_spec.cr new file mode 100644 index 00000000..ca4df7a3 --- /dev/null +++ b/spec/ameba/rule/naming/ascii_identifiers_spec.cr @@ -0,0 +1,94 @@ +require "../../../spec_helper" + +module Ameba::Rule::Naming + subject = AsciiIdentifiers.new + + describe AsciiIdentifiers do + it "reports classes with names containing non-ascii characters" do + expect_issue subject, <<-CRYSTAL + class BigAwesome🐺 + # ^^^^^^^^^^^ error: Identifier contains non-ascii characters + @🐺_name : String + # ^^^^^^^ error: Identifier contains non-ascii characters + end + CRYSTAL + end + + it "reports modules with names containing non-ascii characters" do + expect_issue subject, <<-CRYSTAL + module Bąk + # ^^^ error: Identifier contains non-ascii characters + @@bąk_name : String + # ^^^^^^^^^^ error: Identifier contains non-ascii characters + end + CRYSTAL + end + + it "reports enums with names containing non-ascii characters" do + expect_issue subject, <<-CRYSTAL + enum TypeOf🔥 + # ^^^^^^^ error: Identifier contains non-ascii characters + end + CRYSTAL + end + + it "reports defs with names containing non-ascii characters" do + expect_issue subject, <<-CRYSTAL + def łódź + # ^^^^ error: Identifier contains non-ascii characters + end + CRYSTAL + end + + it "reports defs with parameter names containing non-ascii characters" do + expect_issue subject, <<-CRYSTAL + def forest_adventure(include_🐺 = true, include_🐿 = true) + # ^ error: Identifier contains non-ascii characters + # ^ error: Identifier contains non-ascii characters + end + CRYSTAL + end + + it "reports argument names containing non-ascii characters" do + expect_issue subject, <<-CRYSTAL + %w[wensleydale cheddar brie].each { |🧀| nil } + # ^ error: Identifier contains non-ascii characters + CRYSTAL + end + + it "reports aliases with names containing non-ascii characters" do + expect_issue subject, <<-CRYSTAL + alias JSON🧀 = JSON::Any + # ^^^^^ error: Identifier contains non-ascii characters + CRYSTAL + end + + it "reports constants with names containing non-ascii characters" do + expect_issue subject, <<-CRYSTAL + I_LOVE_🍣 = true + # ^^^^^^ error: Identifier contains non-ascii characters + CRYSTAL + end + + it "reports assignments with variable names containing non-ascii characters" do + expect_issue subject, <<-CRYSTAL + space_👾 = true + # ^^^^^ error: Identifier contains non-ascii characters + CRYSTAL + end + + it "reports multiple assignments with variable names containing non-ascii characters" do + expect_issue subject, <<-CRYSTAL + foo, space_👾 = true, true + # ^^^^^^^ error: Identifier contains non-ascii characters + CRYSTAL + end + + it "passes for strings with non-ascii characters" do + expect_no_issues subject, <<-CRYSTAL + space = "👾" + space = :invader # 👾 + CRYSTAL + end + end +end diff --git a/src/ameba/rule/naming/ascii_identifiers.cr b/src/ameba/rule/naming/ascii_identifiers.cr new file mode 100644 index 00000000..4ee4f2c7 --- /dev/null +++ b/src/ameba/rule/naming/ascii_identifiers.cr @@ -0,0 +1,80 @@ +module Ameba::Rule::Naming + # A rule that reports non-ascii characters in identifiers. + # + # Favour this: + # + # ``` + # class BigAwesomeWolf + # end + # ``` + # + # Over this: + # + # ``` + # class BigAwesome🐺 + # end + # ``` + # + # YAML configuration example: + # + # ``` + # Naming/AsciiIdentifiers: + # Enabled: true + # ``` + class AsciiIdentifiers < Base + include AST::Util + + properties do + description "Disallows non-ascii characters in identifiers" + end + + MSG = "Identifier contains non-ascii characters" + + def test(source, node : Crystal::Assign) + if (target = node.target).is_a?(Crystal::Path) + check_issue(source, target, target) + end + end + + def test(source, node : Crystal::MultiAssign) + node.targets.each do |target| + check_issue(source, target, target) + end + end + + def test(source, node : Crystal::Def) + check_issue_with_location(source, node) + + node.args.each do |arg| + check_issue_with_location(source, arg) + end + end + + def test(source, node : Crystal::ClassVar | Crystal::InstanceVar | Crystal::Var | Crystal::Alias) + check_issue_with_location(source, node) + end + + def test(source, node : Crystal::ClassDef | Crystal::ModuleDef | Crystal::EnumDef | Crystal::LibDef) + check_issue(source, node.name, node.name) + end + + private def check_issue_with_location(source, node) + location = name_location(node) + end_location = name_end_location(node) + + if location && end_location + check_issue(source, location, end_location, node.name) + else + check_issue(source, node, node.name) + end + end + + private def check_issue(source, location, end_location, name) + issue_for location, end_location, MSG unless name.to_s.ascii_only? + end + + private def check_issue(source, node, name) + issue_for node, MSG unless name.to_s.ascii_only? + end + end +end