forked from rubocop/rubocop-rails
/
lexically_scoped_action_filter.rb
177 lines (165 loc) · 4.84 KB
/
lexically_scoped_action_filter.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
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
# frozen_string_literal: true
module RuboCop
module Cop
module Rails
# This cop checks that methods specified in the filter's `only` or
# `except` options are defined within the same class or module.
#
# You can technically specify methods of superclass or methods added by
# mixins on the filter, but these can confuse developers. If you specify
# methods that are defined in other classes or modules, you should
# define the filter in that class or module.
#
# If you rely on behaviour defined in the superclass actions, you must
# remember to invoke `super` in the subclass actions.
#
# @example
# # bad
# class LoginController < ApplicationController
# before_action :require_login, only: %i[index settings logout]
#
# def index
# end
# end
#
# # good
# class LoginController < ApplicationController
# before_action :require_login, only: %i[index settings logout]
#
# def index
# end
#
# def settings
# end
#
# def logout
# end
# end
#
# @example
# # bad
# module FooMixin
# extend ActiveSupport::Concern
#
# included do
# before_action proc { authenticate }, only: :foo
# end
# end
#
# # good
# module FooMixin
# extend ActiveSupport::Concern
#
# included do
# before_action proc { authenticate }, only: :foo
# end
#
# def foo
# # something
# end
# end
#
# @example
# class ContentController < ApplicationController
# def update
# @content.update(content_attributes)
# end
# end
#
# class ArticlesController < ContentController
# before_action :load_article, only: [:update]
#
# # the cop requires this method, but it relies on behaviour defined
# # in the superclass, so needs to invoke `super`
# def update
# super
# end
#
# private
#
# def load_article
# @content = Article.find(params[:article_id])
# end
# end
class LexicallyScopedActionFilter < Cop
MSG = '%<action>s not explicitly defined on the %<type>s.'
RESTRICT_ON_SEND = %i[
after_action
append_after_action
append_around_action
append_before_action
around_action
before_action
prepend_after_action
prepend_around_action
prepend_before_action
skip_after_action
skip_around_action
skip_before_action
skip_action_callback
].freeze
FILTERS = RESTRICT_ON_SEND.map { |method_name| ":#{method_name}" }
def_node_matcher :only_or_except_filter_methods, <<~PATTERN
(send
nil?
{#{FILTERS.join(' ')}}
_
(hash
(pair
(sym {:only :except})
$_)))
PATTERN
def on_send(node)
methods_node = only_or_except_filter_methods(node)
return unless methods_node
parent = node.each_ancestor(:class, :module).first
return unless parent
block = parent.each_child_node(:begin).first
return unless block
defined_methods = block.each_child_node(:def).map(&:method_name)
methods = array_values(methods_node).reject do |method|
defined_methods.include?(method)
end
message = message(methods, parent)
add_offense(node, message: message) unless methods.empty?
end
private
# @param node [RuboCop::AST::Node]
# @return [Array<Symbol>]
def array_values(node) # rubocop:disable Metrics/MethodLength
case node.type
when :str
[node.str_content.to_sym]
when :sym
[node.value]
when :array
node.values.map do |v|
case v.type
when :str
v.str_content.to_sym
when :sym
v.value
end
end.compact
else
[]
end
end
# @param methods [Array<String>]
# @param parent [RuboCop::AST::Node]
# @return [String]
def message(methods, parent)
if methods.size == 1
format(MSG,
action: "`#{methods[0]}` is",
type: parent.type)
else
format(MSG,
action: "`#{methods.join('`, `')}` are",
type: parent.type)
end
end
end
end
end
end