diff --git a/spec/ameba/rule/lint/static_comparison_spec.cr b/spec/ameba/rule/lint/static_comparison_spec.cr new file mode 100644 index 00000000..d905cdc0 --- /dev/null +++ b/spec/ameba/rule/lint/static_comparison_spec.cr @@ -0,0 +1,59 @@ +require "../../../spec_helper" + +module Ameba::Rule::Lint + subject = StaticComparison.new + + describe StaticComparison do + it "passes for valid cases" do + expect_no_issues subject, <<-CRYSTAL + "foo" == foo + "foo" != foo + foo == "foo" + foo != "foo" + CRYSTAL + end + + it "reports if there is a static comparison evaluating to the same" do + expect_issue subject, <<-CRYSTAL + "foo" === "foo" + # ^^^^^^^^^^^^^ error: Comparison always evaluates to the same + CRYSTAL + end + + it "reports if there is a static comparison evaluating to true (2)" do + expect_issue subject, <<-CRYSTAL + "foo" == "foo" + # ^^^^^^^^^^^^ error: Comparison always evaluates to true + CRYSTAL + end + + it "reports if there is a static comparison evaluating to false" do + expect_issue subject, <<-CRYSTAL + "foo" != "foo" + # ^^^^^^^^^^^^ error: Comparison always evaluates to false + CRYSTAL + end + + context "macro" do + it "reports in macro scope" do + expect_issue subject, <<-CRYSTAL + {{ "foo" == "foo" }} + # ^^^^^^^^^^^^^^ error: Comparison always evaluates to true + CRYSTAL + end + end + + it "reports rule, pos and message" do + s = Source.new %( + "foo" == "foo" + ), "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:1" + issue.end_location.to_s.should eq "source.cr:1:14" + issue.message.should eq "Comparison always evaluates to true" + end + end +end diff --git a/src/ameba/rule/lint/static_comparison.cr b/src/ameba/rule/lint/static_comparison.cr new file mode 100644 index 00000000..e66ace5a --- /dev/null +++ b/src/ameba/rule/lint/static_comparison.cr @@ -0,0 +1,58 @@ +module Ameba::Rule::Lint + # This rule is used to identify static comparisons - + # the ones that will always have the same result. + # + # For example, this will be always false: + # + # ``` + # "foo" == 42 + # ``` + # + # YAML configuration example: + # + # ``` + # Lint/StaticComparison: + # Enabled: true + # ``` + class StaticComparison < Base + include AST::Util + + properties do + description "Identifies static comparisons" + end + + OP_NAMES = %w(=== == !=) + MSG = "Comparison always evaluates to %s" + + PRIMITIVES = { + Crystal::NilLiteral, + Crystal::BoolLiteral, + Crystal::NumberLiteral, + Crystal::CharLiteral, + Crystal::StringLiteral, + Crystal::SymbolLiteral, + Crystal::RangeLiteral, + Crystal::RegexLiteral, + Crystal::TupleLiteral, + Crystal::NamedTupleLiteral, + Crystal::ArrayLiteral, + Crystal::HashLiteral, + Crystal::ProcLiteral, + } + + def test(source, node : Crystal::Call) + return unless node.name.in?(OP_NAMES) + return unless (obj = node.obj) && (arg = node.args.first?) + return unless obj.class.in?(PRIMITIVES) && arg.class.in?(PRIMITIVES) + + what = + case node.name + when "===" then "the same" + when "==" then (obj.to_s == arg.to_s).to_s + when "!=" then (obj.to_s != arg.to_s).to_s + end + + issue_for node, MSG % what + end + end +end