/
has_many_or_has_one_dependent.rb
143 lines (114 loc) · 3.96 KB
/
has_many_or_has_one_dependent.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 Rails
# Looks for `has_many` or `has_one` associations that don't
# specify a `:dependent` option.
#
# It doesn't register an offense if `:through` or `dependent: nil`
# is specified, or if the model is read-only.
#
# @example
# # bad
# class User < ActiveRecord::Base
# has_many :comments
# has_one :avatar
# end
#
# # good
# class User < ActiveRecord::Base
# has_many :comments, dependent: :restrict_with_exception
# has_one :avatar, dependent: :destroy
# has_many :articles, dependent: nil
# has_many :patients, through: :appointments
# end
#
# class User < ActiveRecord::Base
# has_many :comments
# has_one :avatar
#
# def readonly?
# true
# end
# end
class HasManyOrHasOneDependent < Base
MSG = 'Specify a `:dependent` option.'
RESTRICT_ON_SEND = %i[has_many has_one].freeze
def_node_search :active_resource_class?, <<~PATTERN
(const (const nil? :ActiveResource) :Base)
PATTERN
def_node_matcher :association_without_options?, <<~PATTERN
(send nil? {:has_many :has_one} _)
PATTERN
def_node_matcher :association_with_options?, <<~PATTERN
(send nil? {:has_many :has_one} ... (hash $...))
PATTERN
def_node_matcher :dependent_option?, <<~PATTERN
(pair (sym :dependent) {!nil (nil)})
PATTERN
def_node_matcher :present_option?, <<~PATTERN
(pair (sym :through) !nil)
PATTERN
def_node_matcher :with_options_block, <<~PATTERN
(block
(send nil? :with_options
(hash $...))
(args) ...)
PATTERN
def_node_matcher :association_extension_block?, <<~PATTERN
(block
(send nil? :has_many _)
(args) ...)
PATTERN
def_node_matcher :readonly?, <<~PATTERN
(def :readonly?
(args)
(true))
PATTERN
def on_send(node)
return if active_resource?(node.parent) || readonly_model?(node)
return if !association_without_options?(node) && valid_options?(association_with_options?(node))
return if valid_options_in_with_options_block?(node)
add_offense(node.loc.selector)
end
private
def readonly_model?(node)
return false unless (parent = node.parent)
parent.each_descendant(:def).any? { |def_node| readonly?(def_node) }
end
def valid_options_in_with_options_block?(node)
return true unless node.parent
n = node.parent.begin_type? || association_extension_block?(node.parent) ? node.parent.parent : node.parent
contain_valid_options_in_with_options_block?(n)
end
def contain_valid_options_in_with_options_block?(node)
if (options = with_options_block(node))
return true if valid_options?(options)
return false unless node.parent
return true if contain_valid_options_in_with_options_block?(node.parent.parent)
end
false
end
def valid_options?(options)
return false if options.nil?
options = extract_option_if_kwsplat(options)
return true unless options
return true if options.any? do |o|
dependent_option?(o) || present_option?(o)
end
false
end
def extract_option_if_kwsplat(options)
if options.first.kwsplat_type? && options.first.children.first.hash_type?
return options.first.children.first.pairs
end
options
end
def active_resource?(node)
return false if node.nil?
active_resource_class?(node)
end
end
end
end
end