diff --git a/spec/ameba/rule/lint/not_nil_after_no_bang_spec.cr b/spec/ameba/rule/lint/not_nil_after_no_bang_spec.cr new file mode 100644 index 00000000..521b79ef --- /dev/null +++ b/spec/ameba/rule/lint/not_nil_after_no_bang_spec.cr @@ -0,0 +1,75 @@ +require "../../../spec_helper" + +module Ameba::Rule::Lint + subject = NotNilAfterNoBang.new + + describe NotNilAfterNoBang do + it "passes for valid cases" do + expect_no_issues subject, <<-CRYSTAL + (1..3).index(1).not_nil!(:foo) + (1..3).index { |i| i > 2 }.not_nil!(:foo) + (1..3).find { |i| i > 2 }.not_nil!(:foo) + CRYSTAL + end + + it "reports if there is an `index` call followed by `not_nil!`" do + source = expect_issue subject, <<-CRYSTAL + (1..3).index(1).not_nil! + # ^^^^^^^^^^^^^^^^^ error: Use `index! {...}` instead of `index {...}.not_nil!` + CRYSTAL + + expect_correction source, <<-CRYSTAL + (1..3).index!(1) + CRYSTAL + end + + it "reports if there is an `index` call with block followed by `not_nil!`" do + source = expect_issue subject, <<-CRYSTAL + (1..3).index { |i| i > 2 }.not_nil! + # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: Use `index! {...}` instead of `index {...}.not_nil!` + CRYSTAL + + expect_correction source, <<-CRYSTAL + (1..3).index! { |i| i > 2 } + CRYSTAL + end + + it "reports if there is a `find` call with block followed by `not_nil!`" do + source = expect_issue subject, <<-CRYSTAL + (1..3).find { |i| i > 2 }.not_nil! + # ^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: Use `find! {...}` instead of `find {...}.not_nil!` + CRYSTAL + + expect_correction source, <<-CRYSTAL + (1..3).find! { |i| i > 2 } + CRYSTAL + end + + it "passes if there is a `find` call without block followed by `not_nil!`" do + expect_no_issues subject, <<-CRYSTAL + (1..3).find(1).not_nil! + CRYSTAL + end + + context "macro" do + it "doesn't report in macro scope" do + expect_no_issues subject, <<-CRYSTAL + {{ [1, 2, 3].index(1).not_nil! }} + CRYSTAL + end + end + + it "reports rule, pos and message" do + s = Source.new %( + (1..3).index(1).not_nil! + ), "source.cr" + subject.catch(s).should_not be_valid + issue = s.issues.first + + issue.rule.should_not be_nil + issue.location.to_s.should eq "source.cr:1:8" + issue.end_location.to_s.should eq "source.cr:1:24" + issue.message.should eq "Use `index! {...}` instead of `index {...}.not_nil!`" + end + end +end diff --git a/src/ameba/rule/lint/not_nil_after_no_bang.cr b/src/ameba/rule/lint/not_nil_after_no_bang.cr new file mode 100644 index 00000000..d222c43e --- /dev/null +++ b/src/ameba/rule/lint/not_nil_after_no_bang.cr @@ -0,0 +1,61 @@ +module Ameba::Rule::Lint + # This rule is used to identify usage of `index/find` calls followed by `not_nil!`. + # + # For example, this is considered a code smell: + # + # ``` + # %w[Alice Bob].find(&.match(/^A./)).not_nil! + # ``` + # + # And can be written as this: + # + # ``` + # %w[Alice Bob].find!(&.match(/^A./)) + # ``` + # + # YAML configuration example: + # + # ``` + # Lint/NotNilAfterNoBang: + # Enabled: true + # ``` + class NotNilAfterNoBang < Base + include AST::Util + + properties do + description "Identifies usage of `index/find` calls followed by `not_nil!`" + end + + BLOCK_CALL_NAMES = %w(index find) + CALL_NAMES = %w(index) + + NOT_NIL_NAME = "not_nil!" + MSG = "Use `%s! {...}` instead of `%s {...}.not_nil!`" + + def test(source) + AST::NodeVisitor.new self, source, skip: [ + Crystal::Macro, + Crystal::MacroExpression, + Crystal::MacroIf, + Crystal::MacroFor, + ] + end + + def test(source, node : Crystal::Call) + return unless node.name == NOT_NIL_NAME && node.args.empty? + return unless (obj = node.obj).is_a?(Crystal::Call) + return unless obj.name.in?(obj.block ? BLOCK_CALL_NAMES : CALL_NAMES) + + return unless name_location = obj.name_location + return unless name_location_end = name_end_location(obj) + return unless end_location = name_end_location(node) + + msg = MSG % {obj.name, obj.name} + + issue_for name_location, end_location, msg do |corrector| + corrector.insert_after(name_location_end, '!') + corrector.remove_trailing(node, ".not_nil!".size) + end + end + end +end