From d3b952f58a73574c7f45604eb4ace773193475e3 Mon Sep 17 00:00:00 2001 From: Sijawusz Pur Rahnama Date: Wed, 20 Jan 2021 03:17:00 +0100 Subject: [PATCH] Add Performance/ChainedCallsWithNoBang rule --- .../chained_calls_with_no_bang_spec.cr | 69 +++++++++++++++++ .../performance/chained_calls_with_no_bang.cr | 76 +++++++++++++++++++ 2 files changed, 145 insertions(+) create mode 100644 spec/ameba/rule/performance/chained_calls_with_no_bang_spec.cr create mode 100644 src/ameba/rule/performance/chained_calls_with_no_bang.cr diff --git a/spec/ameba/rule/performance/chained_calls_with_no_bang_spec.cr b/spec/ameba/rule/performance/chained_calls_with_no_bang_spec.cr new file mode 100644 index 00000000..4259b866 --- /dev/null +++ b/spec/ameba/rule/performance/chained_calls_with_no_bang_spec.cr @@ -0,0 +1,69 @@ +require "../../../spec_helper" + +module Ameba::Rule::Performance + subject = ChainedCallsWithNoBang.new + + describe ChainedCallsWithNoBang do + it "passes if there is no potential performance improvements" do + source = Source.new %( + (1..3).select { |e| e > 1 }.sort! + (1..3).select { |e| e > 1 }.sort_by!(&.itself) + (1..3).select { |e| e > 1 }.uniq! + (1..3).select { |e| e > 1 }.shuffle! + (1..3).select { |e| e > 1 }.reverse! + (1..3).select { |e| e > 1 }.rotate! + ) + subject.catch(source).should be_valid + end + + it "reports if there is select followed by reverse" do + source = Source.new %( + [1, 2, 3].select { |e| e > 1 }.reverse + ) + subject.catch(source).should_not be_valid + end + + it "reports if there is select followed by reverse followed by other call" do + source = Source.new %( + [1, 2, 3].select { |e| e > 2 }.reverse.size + ) + subject.catch(source).should_not be_valid + end + + context "properties" do + it "allows to configure `call_names`" do + source = Source.new %( + [1, 2, 3].select { |e| e > 2 }.reverse + ) + rule = ChainedCallsWithNoBang.new + rule.call_names = %w(uniq) + rule.catch(source).should be_valid + end + end + + it "reports rule, pos and message" do + source = Source.new path: "source.cr", code: <<-CODE + [1, 2, 3].select { |e| e > 1 }.reverse + CODE + + subject.catch(source).should_not be_valid + source.issues.size.should eq 1 + + issue = source.issues.first + issue.rule.should_not be_nil + issue.location.to_s.should eq "source.cr:1:32" + issue.end_location.to_s.should eq "source.cr:1:39" + + issue.message.should eq "Use bang method variant `reverse!` after chained `select` call" + end + + context "macro" do + it "doesn't report in macro scope" do + source = Source.new %( + {{ [1, 2, 3].select { |e| e > 2 }.reverse }} + ) + subject.catch(source).should be_valid + end + end + end +end diff --git a/src/ameba/rule/performance/chained_calls_with_no_bang.cr b/src/ameba/rule/performance/chained_calls_with_no_bang.cr new file mode 100644 index 00000000..8e145f7f --- /dev/null +++ b/src/ameba/rule/performance/chained_calls_with_no_bang.cr @@ -0,0 +1,76 @@ +module Ameba::Rule::Performance + # This rule is used to identify usage of chained calls not utilizing + # the bang method variants. + # + # For example, this is considered inefficient: + # + # ``` + # names = %w[Alice Bob] + # chars = names + # .flat_map(&.chars) + # .uniq + # .sort + # ``` + # + # And can be written as this: + # + # ``` + # names = %w[Alice Bob] + # chars = names + # .flat_map(&.chars) + # .uniq! + # .sort! + # ``` + # + # YAML configuration example: + # + # ``` + # Performance/ChainedCallsWithNoBang + # Enabled: true + # CallNames: + # - uniq + # - sort + # - sort_by + # - shuffle + # - reverse + # - rotate + # ``` + class ChainedCallsWithNoBang < Base + properties do + description "Identifies usage of chained calls not utilizing the bang method variants." + + # All of those have bang method variants returning `self` + # and are not modifying the receiver type (like `compact` does), + # thus are safe to switch to the bang variant. + call_names : Array(String) = %w(uniq sort sort_by shuffle reverse rotate) + end + + # All these methods are allocating a new object + ALLOCATING_METHOD_NAMES = %w( + keys values values_at map map_with_index flat_map compact_map + flatten compact select reject combinations permutations sample + transpose invert group_by chunks tally merge chars clone + captures named_captures + ) + + MSG = "Use bang method variant `%s!` after chained `%s` call" + + 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 (obj = node.obj).is_a?(Crystal::Call) + return unless node.name.in?(call_names) + return unless obj.name.in?(call_names) || obj.name.in?(ALLOCATING_METHOD_NAMES) + + issue_for node.name_location, node.name_end_location, + MSG % {node.name, obj.name} + end + end +end