/
repeated_attribute_discount.rb
143 lines (123 loc) · 4.78 KB
/
repeated_attribute_discount.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
# frozen_string_literal: true
module RuboCop
module Cop
module Metrics
module Utils
# @api private
#
# Identifies repetitions `{c}send` calls with no arguments:
#
# foo.bar
# foo.bar # => repeated
# foo.bar.baz.qux # => inner send repeated
# foo.bar.baz.other # => both inner send repeated
# foo.bar(2) # => not repeated
#
# It also invalidates sequences if a receiver is reassigned:
#
# xx.foo.bar
# xx.foo.baz # => inner send repeated
# self.xx = any # => invalidates everything so far
# xx.foo.baz # => no repetition
# self.xx.foo.baz # => all repeated
#
module RepeatedAttributeDiscount
extend NodePattern::Macros
include RuboCop::AST::Sexp
# Plug into the calculator
def initialize(node, discount_repeated_attributes: false)
super(node)
return unless discount_repeated_attributes
self_attributes = {} # Share hash for `(send nil? :foo)` and `(send (self) :foo)`
@known_attributes = { s(:self) => self_attributes, nil => self_attributes }
# example after running `obj = foo.bar; obj.baz.qux`
# { nil => {foo: {bar: {}}},
# s(self) => same hash ^,
# s(:lvar, :obj) => {baz: {qux: {}}}
# }
end
def discount_repeated_attributes?
defined?(@known_attributes)
end
def evaluate_branch_nodes(node)
return if discount_repeated_attributes? && discount_repeated_attribute?(node)
super
end
def calculate_node(node)
update_repeated_attribute(node) if discount_repeated_attributes?
super
end
private
# @!method attribute_call?(node)
def_node_matcher :attribute_call?, <<~PATTERN
( {csend send} _receiver _method # and no parameters
)
PATTERN
def discount_repeated_attribute?(send_node)
return false unless attribute_call?(send_node)
repeated = true
find_attributes(send_node) do |hash, lookup|
return false if hash.nil?
repeated = false
hash[lookup] = {}
end
repeated
end
def update_repeated_attribute(node)
return unless (receiver, method = setter_to_getter(node))
calls = find_attributes(receiver) { return }
if method # e.g. `self.foo = 42`
calls.delete(method)
else # e.g. `var = 42`
calls.clear
end
end
# @!method root_node?(node)
def_node_matcher :root_node?, <<~PATTERN
{ nil? | self # e.g. receiver of `my_method` or `self.my_attr`
| lvar | ivar | cvar | gvar # e.g. receiver of `var.my_method`
| const } # e.g. receiver of `MyConst.foo.bar`
PATTERN
# Returns the "known_attributes" for the `node` by walking the receiver tree
# If at any step the subdirectory does not exist, it is yielded with the
# associated key (method_name)
# If the node is not a series of `(c)send` calls with no arguments,
# then `nil` is yielded
def find_attributes(node, &block)
if attribute_call?(node)
calls = find_attributes(node.receiver, &block)
value = node.method_name
elsif root_node?(node)
calls = @known_attributes
value = node
else
return yield nil
end
calls.fetch(value) { yield [calls, value] }
end
VAR_SETTER_TO_GETTER = {
lvasgn: :lvar,
ivasgn: :ivar,
cvasgn: :cvar,
gvasgn: :gvar
}.freeze
# @returns `[receiver, method | nil]` for the given setter `node`
# or `nil` if it is not a setter.
def setter_to_getter(node)
if (type = VAR_SETTER_TO_GETTER[node.type])
# (lvasgn :my_var (int 42)) => [(lvar my_var), nil]
[s(type, node.children.first), nil]
elsif node.shorthand_asgn? # (or-asgn (send _receiver :foo) _value)
# (or-asgn (send _receiver :foo) _value) => [_receiver, :foo]
node.children.first.children
elsif node.respond_to?(:setter_method?) && node.setter_method?
# (send _receiver :foo= (int 42) ) => [_receiver, :foo]
method_name = node.method_name[0...-1].to_sym
[node.receiver, method_name]
end
end
end
end
end
end
end