diff --git a/spec/ameba/rule/lint/not_nil_spec.cr b/spec/ameba/rule/lint/not_nil_spec.cr new file mode 100644 index 00000000..f7f50045 --- /dev/null +++ b/spec/ameba/rule/lint/not_nil_spec.cr @@ -0,0 +1,42 @@ +require "../../../spec_helper" + +module Ameba::Rule::Lint + subject = NotNil.new + + describe NotNil do + it "passes for valid cases" do + expect_no_issues subject, <<-CRYSTAL + (1..3).first?.not_nil!(:foo) + not_nil! + CRYSTAL + end + + it "reports if there is a `not_nil!` call" do + expect_issue subject, <<-CRYSTAL + (1..3).first?.not_nil! + # ^^^^^^^^ error: Avoid using `not_nil!` + CRYSTAL + end + + context "macro" do + it "doesn't report in macro scope" do + expect_no_issues subject, <<-CRYSTAL + {{ [1, 2, 3].first.not_nil! }} + CRYSTAL + end + end + + it "reports rule, pos and message" do + s = Source.new %( + (1..3).first?.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:15" + issue.end_location.to_s.should eq "source.cr:1:22" + issue.message.should eq "Avoid using `not_nil!`" + end + end +end diff --git a/src/ameba/rule/lint/not_nil.cr b/src/ameba/rule/lint/not_nil.cr new file mode 100644 index 00000000..46f41c58 --- /dev/null +++ b/src/ameba/rule/lint/not_nil.cr @@ -0,0 +1,57 @@ +module Ameba::Rule::Lint + # This rule is used to identify usages of `not_nil!` calls. + # + # For example, this is considered a code smell: + # + # ``` + # names = %w[Alice Bob] + # alice = names.find { |name| name == "Alice" }.not_nil! + # ``` + # + # And can be written as this: + # + # ``` + # names = %w[Alice Bob] + # alice = names.find { |name| name == "Alice" } + # + # if alice + # # ... + # end + # ``` + # + # YAML configuration example: + # + # ``` + # Lint/NotNil: + # Enabled: true + # ``` + class NotNil < Base + include AST::Util + + properties do + description "Identifies usage of `not_nil!` calls" + end + + NOT_NIL_NAME = "not_nil!" + MSG = "Avoid using `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 + return unless node.obj && node.args.empty? + + return unless name_location = node.name_location + return unless end_location = name_end_location(node) + + issue_for name_location, end_location, MSG + end + end +end