diff --git a/config/ameba.yml b/config/ameba.yml index 89cb381a..63416a42 100644 --- a/config/ameba.yml +++ b/config/ameba.yml @@ -18,6 +18,10 @@ EmptyExpression: # Disallows empty expressions. Enabled: true +HashDuplicatedKey: + # Disallows duplicated keys in hash literals. + Enabled: true + LargeNumbers: # A rule that disallows usage of large numbers without underscore. Enabled: true diff --git a/spec/ameba/rule/hash_duplicated_key_spec.cr b/spec/ameba/rule/hash_duplicated_key_spec.cr new file mode 100644 index 00000000..188cec6e --- /dev/null +++ b/spec/ameba/rule/hash_duplicated_key_spec.cr @@ -0,0 +1,41 @@ +require "../../spec_helper" + +module Ameba::Rule + describe HashDuplicatedKey do + subject = HashDuplicatedKey.new + + it "passes if there is no duplicated keys in a hash literals" do + s = Source.new %( + h = {"a" => 1, :a => 2, "b" => 3} + h = {"a" => 1, "b" => 2, "c" => {"a" => 3, "b" => 4}} + h = {} of String => String + ) + subject.catch(s).should be_valid + end + + it "fails if there is a duplicated key in a hash literal" do + s = Source.new %q( + h = {"a" => 1, "b" => 2, "a" => 3} + ) + subject.catch(s).should_not be_valid + end + + it "fails if there is a duplicated key in the inner hash literal" do + s = Source.new %q( + h = {"a" => 1, "b" => {"a" => 3, "b" => 4, "a" => 5}} + ) + subject.catch(s).should_not be_valid + end + + it "reports rule, location and message" do + s = Source.new %q( + h = {"a" => 1, "a" => 2} + ), "source.cr" + subject.catch(s).should_not be_valid + error = s.errors.first + error.rule.should_not be_nil + error.location.to_s.should eq "source.cr:2:13" + error.message.should eq "Duplicated keys in hash literal." + end + end +end diff --git a/src/ameba/ast/traverse.cr b/src/ameba/ast/traverse.cr index 13dac419..1332a1b1 100644 --- a/src/ameba/ast/traverse.cr +++ b/src/ameba/ast/traverse.cr @@ -14,6 +14,7 @@ module Ameba::AST EnumDef, ExceptionHandler, Expressions, + HashLiteral, If, InstanceVar, LibDef, diff --git a/src/ameba/rule/hash_duplicated_key.cr b/src/ameba/rule/hash_duplicated_key.cr new file mode 100644 index 00000000..52a59b48 --- /dev/null +++ b/src/ameba/rule/hash_duplicated_key.cr @@ -0,0 +1,40 @@ +module Ameba::Rule + # A rule that disallows duplicated keys in hash literals. + # + # This is considered invalid: + # + # ``` + # h = {"foo" => 1, "bar" => 2, "foo" => 3} + # ``` + # + # And it has to written as this instead: + # + # ``` + # h = {"foo" => 1, "bar" => 2} + # ``` + # + # YAML configuration example: + # + # ``` + # HashDuplicatedKey: + # Enabled: true + # ``` + # + struct HashDuplicatedKey < Base + def test(source) + AST::Visitor.new self, source + end + + def test(source, node : Crystal::HashLiteral) + return unless duplicated_keys?(node.entries) + + source.error self, node.location, "Duplicated keys in hash literal." + end + + private def duplicated_keys?(entries) + entries.map(&.key) + .group_by(&.itself) + .any? { |_, v| v.size > 1 } + end + end +end