-
-
Notifications
You must be signed in to change notification settings - Fork 3k
/
missing_super.rb
159 lines (141 loc) · 4.85 KB
/
missing_super.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
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# frozen_string_literal: true
module RuboCop
module Cop
module Lint
# Checks for the presence of constructors and lifecycle callbacks
# without calls to `super`.
#
# This cop does not consider `method_missing` (and `respond_to_missing?`)
# because in some cases it makes sense to overtake what is considered a
# missing method. In other cases, the theoretical ideal handling could be
# challenging or verbose for no actual gain.
#
# Autocorrection is not supported because the position of `super` cannot be
# determined automatically.
#
# `Object` and `BasicObject` are allowed by this cop because of their
# stateless nature. However, sometimes you might want to allow other parent
# classes from this cop, for example in the case of an abstract class that is
# not meant to be called with `super`. In those cases, you can use the
# `AllowedParentClasses` option to specify which classes should be allowed
# *in addition to* `Object` and `BasicObject`.
#
# @example
# # bad
# class Employee < Person
# def initialize(name, salary)
# @salary = salary
# end
# end
#
# # good
# class Employee < Person
# def initialize(name, salary)
# super(name)
# @salary = salary
# end
# end
#
# # bad
# Employee = Class.new(Person) do
# def initialize(name, salary)
# @salary = salary
# end
# end
#
# # good
# Employee = Class.new(Person) do
# def initialize(name, salary)
# super(name)
# @salary = salary
# end
# end
#
# # bad
# class Parent
# def self.inherited(base)
# do_something
# end
# end
#
# # good
# class Parent
# def self.inherited(base)
# super
# do_something
# end
# end
#
# # good
# class ClassWithNoParent
# def initialize
# do_something
# end
# end
#
# @example AllowedParentClasses: [MyAbstractClass]
# # good
# class MyConcreteClass < MyAbstractClass
# def initialize
# do_something
# end
# end
#
class MissingSuper < Base
CONSTRUCTOR_MSG = 'Call `super` to initialize state of the parent class.'
CALLBACK_MSG = 'Call `super` to invoke callback defined in the parent class.'
STATELESS_CLASSES = %w[BasicObject Object].freeze
CLASS_LIFECYCLE_CALLBACKS = %i[inherited].freeze
METHOD_LIFECYCLE_CALLBACKS = %i[method_added method_removed method_undefined
singleton_method_added singleton_method_removed
singleton_method_undefined].freeze
CALLBACKS = (CLASS_LIFECYCLE_CALLBACKS + METHOD_LIFECYCLE_CALLBACKS).to_set.freeze
# @!method class_new_block(node)
def_node_matcher :class_new_block, <<~RUBY
({block numblock}
(send
(const {nil? cbase} :Class) :new $_) ...)
RUBY
def on_def(node)
return unless offender?(node)
if node.method?(:initialize) && inside_class_with_stateful_parent?(node)
add_offense(node, message: CONSTRUCTOR_MSG)
elsif callback_method_def?(node)
add_offense(node, message: CALLBACK_MSG)
end
end
def on_defs(node)
return if !callback_method_def?(node) || contains_super?(node)
add_offense(node, message: CALLBACK_MSG)
end
private
def offender?(node)
(node.method?(:initialize) || callback_method_def?(node)) && !contains_super?(node)
end
def callback_method_def?(node)
return unless CALLBACKS.include?(node.method_name)
node.each_ancestor(:class, :sclass, :module).first
end
def contains_super?(node)
node.each_descendant(:super, :zsuper).any?
end
def inside_class_with_stateful_parent?(node)
if (block_node = node.each_ancestor(:block, :numblock).first)
return false unless (super_class = class_new_block(block_node))
!allowed_class?(super_class)
elsif (class_node = node.each_ancestor(:class).first)
class_node.parent_class && !allowed_class?(class_node.parent_class)
else
false
end
end
def allowed_class?(node)
allowed_classes.include?(node.const_name)
end
def allowed_classes
@allowed_classes ||= STATELESS_CLASSES + cop_config.fetch('AllowedParentClasses', [])
end
end
end
end
end