gitlab-styles 9.2.0 → 10.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -3
  3. data/.gitlab/merge_request_templates/Release.md +18 -5
  4. data/.gitlab-ci.yml +17 -2
  5. data/.rubocop.yml +6 -1
  6. data/.rubocop_todo.yml +36 -0
  7. data/.tests_mapping.yml +10 -0
  8. data/Gemfile +0 -11
  9. data/Gemfile.lock +227 -0
  10. data/README.md +0 -1
  11. data/gitlab-styles.gemspec +15 -8
  12. data/lefthook.yml +11 -3
  13. data/lib/gitlab/styles/rubocop/migration_helpers.rb +1 -1
  14. data/lib/gitlab/styles/version.rb +1 -1
  15. data/lib/rubocop/cop/active_record_dependent.rb +0 -5
  16. data/lib/rubocop/cop/active_record_serialize.rb +0 -6
  17. data/lib/rubocop/cop/avoid_return_from_blocks.rb +4 -4
  18. data/lib/rubocop/cop/custom_error_class.rb +1 -1
  19. data/lib/rubocop/cop/gem_fetcher.rb +1 -1
  20. data/lib/rubocop/cop/gitlab_security/deep_munge.rb +36 -0
  21. data/lib/rubocop/cop/gitlab_security/json_serialization.rb +133 -0
  22. data/lib/rubocop/cop/gitlab_security/public_send.rb +47 -0
  23. data/lib/rubocop/cop/gitlab_security/redirect_to_params_update.rb +38 -0
  24. data/lib/rubocop/cop/gitlab_security/send_file_params.rb +40 -0
  25. data/lib/rubocop/cop/gitlab_security/sql_injection.rb +41 -0
  26. data/lib/rubocop/cop/gitlab_security/system_command_injection.rb +38 -0
  27. data/lib/rubocop/cop/in_batches.rb +0 -2
  28. data/lib/rubocop/cop/internal_affairs/missing_cop_department.rb +80 -0
  29. data/lib/rubocop/cop/internal_affairs/use_restrict_on_send.rb +99 -0
  30. data/lib/rubocop/cop/line_break_after_guard_clauses.rb +4 -6
  31. data/lib/rubocop/cop/line_break_around_conditional_block.rb +1 -1
  32. data/lib/rubocop/cop/migration/update_large_table.rb +1 -0
  33. data/lib/rubocop/cop/polymorphic_associations.rb +0 -5
  34. data/lib/rubocop/cop/rails/include_url_helper.rb +0 -2
  35. data/lib/rubocop/cop/redirect_with_status.rb +44 -30
  36. data/lib/rubocop/cop/rspec/empty_line_after_shared_example.rb +9 -2
  37. data/lib/rubocop/cop/rspec/example_starting_character.rb +1 -1
  38. data/lib/rubocop/cop/rspec/factory_bot/excessive_create_list.rb +52 -0
  39. data/lib/rubocop/cop/rspec/useless_dynamic_definition.rb +67 -0
  40. data/lib/rubocop/cop/rspec/verbose_include_metadata.rb +1 -1
  41. data/rubocop-capybara.yml +8 -0
  42. data/rubocop-default.yml +2 -4
  43. data/rubocop-gemspec.yml +6 -0
  44. data/rubocop-internal-affairs.yml +11 -0
  45. data/rubocop-layout.yml +2 -2
  46. data/rubocop-lint.yml +134 -5
  47. data/rubocop-naming.yml +5 -0
  48. data/rubocop-rails.yml +33 -1
  49. data/rubocop-rspec.yml +5 -5
  50. data/rubocop-security.yml +19 -1
  51. data/rubocop-style.yml +18 -3
  52. metadata +142 -29
  53. data/lib/gitlab/styles/rubocop/model_helpers.rb +0 -19
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module GitlabSecurity
6
+ # Checks for `to_json` / `as_json` without allowing via `only`.
7
+ #
8
+ # Either method called on an instance of a `Serializer` class will be
9
+ # ignored. Associations included via `include` are subject to the same
10
+ # rules.
11
+ #
12
+ # @example
13
+ #
14
+ # # bad
15
+ # render json: @user.to_json
16
+ # render json: @user.to_json(except: %i[password])
17
+ # render json: @user.to_json(
18
+ # only: %i[username],
19
+ # include: [:identities]
20
+ # )
21
+ #
22
+ # # acceptable
23
+ # render json: UserSerializer.new.to_json
24
+ #
25
+ # # good
26
+ # render json: @user.to_json(only: %i[name username])
27
+ # render json: @user.to_json(
28
+ # only: %i[username],
29
+ # include: { identities: { only: %i[provider] } }
30
+ # )
31
+ #
32
+ # See https://gitlab.com/gitlab-org/gitlab-ce/issues/29661
33
+ class JsonSerialization < RuboCop::Cop::Cop
34
+ MSG = "Don't use `%s` without specifying `only`"
35
+
36
+ # Check for `to_json` sent to any object that's not a Hash literal or
37
+ # Serializer instance
38
+ # @!method json_serialization?(node)
39
+ def_node_matcher :json_serialization?, <<~PATTERN
40
+ (send !{nil? hash #serializer?} ${:to_json :as_json} $...)
41
+ PATTERN
42
+
43
+ # Check if node is a `only: ...` pair
44
+ # @!method only_pair?(node)
45
+ def_node_matcher :only_pair?, <<~PATTERN
46
+ (pair (sym :only) ...)
47
+ PATTERN
48
+
49
+ # Check if node is a `include: {...}` pair
50
+ # @!method include_pair?(node)
51
+ def_node_matcher :include_pair?, <<~PATTERN
52
+ (pair (sym :include) (hash $...))
53
+ PATTERN
54
+
55
+ # Check for a `only: [...]` pair anywhere in the node
56
+ # @!method contains_only?(node)
57
+ def_node_search :contains_only?, <<~PATTERN
58
+ (pair (sym :only) (array ...))
59
+ PATTERN
60
+
61
+ # Check for `SomeConstant.new`
62
+ # @!method constant_init(node)
63
+ def_node_search :constant_init, <<~PATTERN
64
+ (send (const nil? $_) :new ...)
65
+ PATTERN
66
+
67
+ def on_send(node)
68
+ matched = json_serialization?(node)
69
+ return unless matched
70
+
71
+ @_has_top_level_only = false
72
+ @method = matched.first
73
+
74
+ if matched.last.nil? || matched.last.empty?
75
+ # Empty `to_json` call
76
+ add_offense(node, location: :selector, message: format_message)
77
+ else
78
+ check_arguments(node, matched)
79
+ end
80
+ end
81
+
82
+ private
83
+
84
+ def format_message
85
+ format(MSG, @method)
86
+ end
87
+
88
+ def serializer?(node)
89
+ constant_init(node).any? { |name| name.to_s.end_with?('Serializer') }
90
+ end
91
+
92
+ def check_arguments(node, matched)
93
+ options = matched.last.first
94
+
95
+ # If `to_json` was given an argument that isn't a Hash, we don't
96
+ # know what to do here, so just move along
97
+ return unless options.hash_type?
98
+
99
+ options.each_child_node do |child_node|
100
+ check_pair(child_node)
101
+ end
102
+
103
+ return unless requires_only?
104
+
105
+ # Add a top-level offense for the entire argument list, but only if
106
+ # we haven't yet added any offenses to the child Hash values (such
107
+ # as `include`)
108
+ add_offense(node.children.last, message: format_message)
109
+ end
110
+
111
+ def check_pair(pair)
112
+ if only_pair?(pair)
113
+ @_has_top_level_only = true
114
+ elsif include_pair?(pair)
115
+ includes = pair.value
116
+
117
+ includes.each_child_node do |child_node|
118
+ next if contains_only?(child_node)
119
+
120
+ add_offense(child_node, message: format_message)
121
+ end
122
+ end
123
+ end
124
+
125
+ def requires_only?
126
+ return false if @_has_top_level_only
127
+
128
+ offenses.count.zero?
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module GitlabSecurity
6
+ # Checks for the use of `public_send`, `send`, and `__send__` methods.
7
+ #
8
+ # If passed untrusted input these methods can be used to execute arbitrary
9
+ # methods on behalf of an attacker.
10
+ #
11
+ # @example
12
+ #
13
+ # # bad
14
+ # myobj.public_send("#{params[:foo]}")
15
+ #
16
+ # # good
17
+ # case params[:foo].to_s
18
+ # when 'choice1'
19
+ # items.choice1
20
+ # when 'choice2'
21
+ # items.choice2
22
+ # when 'choice3'
23
+ # items.choice3
24
+ # end
25
+ class PublicSend < RuboCop::Cop::Base
26
+ MSG = 'Avoid using `%s`.'
27
+
28
+ RESTRICT_ON_SEND = %i[send public_send __send__].freeze
29
+
30
+ # @!method send?(node)
31
+ def_node_matcher :send?, <<-PATTERN
32
+ ({csend | send} _ ${:send :public_send :__send__} ...)
33
+ PATTERN
34
+
35
+ def on_send(node)
36
+ send?(node) do |match|
37
+ next unless node.arguments?
38
+
39
+ add_offense(node.loc.selector, message: format(MSG, match))
40
+ end
41
+ end
42
+
43
+ alias_method :on_csend, :on_send
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module GitlabSecurity
6
+ # Check for use of redirect_to(params.update())
7
+ #
8
+ # Passing user params to the redirect_to method provides an open redirect
9
+ #
10
+ # @example
11
+ #
12
+ # # bad
13
+ # redirect_to(params.update(action: 'main'))
14
+ #
15
+ # # good
16
+ # redirect_to(allowed(params))
17
+ #
18
+ class RedirectToParamsUpdate < RuboCop::Cop::Base
19
+ MSG = 'Avoid using `redirect_to(params.%<name>s(...))`. ' \
20
+ 'Only pass allowed arguments into redirect_to() (e.g. not including `host`)'
21
+
22
+ # @!method redirect_to_params_update_node(node)
23
+ def_node_matcher :redirect_to_params_update_node, <<-PATTERN
24
+ (send nil? :redirect_to $(send (send nil? :params) ${:update :merge} ...))
25
+ PATTERN
26
+
27
+ def on_send(node)
28
+ selected, name = redirect_to_params_update_node(node)
29
+ return unless name
30
+
31
+ message = format(MSG, name: name)
32
+
33
+ add_offense(selected, message: message)
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module GitlabSecurity
6
+ # Check for use of send_file(..., params[], ...)
7
+ #
8
+ # Passing user params to the send_file() method allows directory traversal
9
+ #
10
+ # @example
11
+ #
12
+ # # bad
13
+ # send_file("/tmp/myproj/" + params[:filename])
14
+ #
15
+ # # good (verify directory)
16
+
17
+ # basename = File.expand_path("/tmp/myproj")
18
+ # filename = File.expand_path(File.join(basename, @file.public_filename))
19
+ # raise if basename != filename
20
+ # send_file filename, disposition: 'inline'
21
+ #
22
+ class SendFileParams < RuboCop::Cop::Base
23
+ MSG = 'Do not pass user provided params directly to send_file(), ' \
24
+ 'verify the path with file.expand_path() first.'
25
+
26
+ # @!method params_node?(node)
27
+ def_node_search :params_node?, <<-PATTERN
28
+ (send (send nil? :params) ... )
29
+ PATTERN
30
+
31
+ def on_send(node)
32
+ return unless node.command?(:send_file)
33
+ return unless node.arguments.any? { |e| params_node?(e) }
34
+
35
+ add_offense(node.loc.selector)
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module GitlabSecurity
6
+ # Check for use of where("name = '#{params[:name]}'")
7
+ #
8
+ # Passing user input to where() without parameterization can result in SQL Injection
9
+ #
10
+ # @example
11
+ #
12
+ # # bad
13
+ # u = User.where("name = '#{params[:name]}'")
14
+ #
15
+ # # good (parameters)
16
+ # u = User.where("name = ? AND id = ?", params[:name], params[:id])
17
+ # u = User.where(name: params[:name], id: params[:id])
18
+ #
19
+ class SqlInjection < RuboCop::Cop::Base
20
+ MSG = 'Parameterize all user-input passed to where(), do not directly embed user input in SQL queries.'
21
+
22
+ # @!method where_user_input?(node)
23
+ def_node_matcher :where_user_input?, <<-PATTERN
24
+ (send _ :where ...)
25
+ PATTERN
26
+
27
+ # @!method string_var_string?(node)
28
+ def_node_matcher :string_var_string?, <<-PATTERN
29
+ (dstr (str ...) (begin ...) (str ...) ...)
30
+ PATTERN
31
+
32
+ def on_send(node)
33
+ return unless where_user_input?(node)
34
+ return unless node.arguments.any? { |e| string_var_string?(e) }
35
+
36
+ add_offense(node.loc.selector)
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module GitlabSecurity
6
+ # Check for use of system("/bin/ls #{params[:file]}")
7
+ #
8
+ # Passing user input to system() without sanitization and parameterization can result in command injection
9
+ #
10
+ # @example
11
+ #
12
+ # # bad
13
+ # system("/bin/ls #{filename}")
14
+ #
15
+ # # good (parameters)
16
+ # system("/bin/ls", filename)
17
+ # # even better
18
+ # exec("/bin/ls", shell_escape(filename))
19
+ #
20
+ class SystemCommandInjection < RuboCop::Cop::Base
21
+ MSG = 'Do not include variables in the command name for system(). ' \
22
+ 'Use parameters "system(cmd, params)" or exec() instead.'
23
+
24
+ # @!method system_var?(node)
25
+ def_node_matcher :system_var?, <<-PATTERN
26
+ (dstr (str ...) (begin ...) ...)
27
+ PATTERN
28
+
29
+ def on_send(node)
30
+ return unless node.command?(:system)
31
+ return unless node.arguments.any? { |e| system_var?(e) }
32
+
33
+ add_offense(node.loc.selector)
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative '../../gitlab/styles/rubocop/model_helpers'
4
-
5
3
  module Rubocop
6
4
  module Cop
7
5
  # Cop that prevents the use of `in_batches`
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module InternalAffairs
6
+ # Enforces the use of explicit department names for cop rules.
7
+ #
8
+ # @example
9
+ # # bad
10
+ # module RuboCop
11
+ # module Cop
12
+ # class Implicit
13
+ # end
14
+ # end
15
+ # end
16
+ #
17
+ # module RuboCop
18
+ # module Cop
19
+ # module Cop
20
+ # class Explicit
21
+ # end
22
+ # end
23
+ # end
24
+ # end
25
+ #
26
+ # # good
27
+ # module RuboCop
28
+ # module Cop
29
+ # module Foo
30
+ # class Implicit
31
+ # end
32
+ # end
33
+ # end
34
+ # end
35
+ #
36
+ # module RuboCop
37
+ # module Cop
38
+ # module Foo
39
+ # class Explicit
40
+ # end
41
+ # end
42
+ # end
43
+ # end
44
+ class MissingCopDepartment < Base
45
+ MSG = 'Define a proper department. Using `Cop/` as department is discourged.'
46
+
47
+ COP_DEPARTMENT = 'Cop'
48
+
49
+ def on_class(node)
50
+ namespace = full_namespace(node)
51
+
52
+ # Skip top-level RuboCop::Cop
53
+ names = namespace.drop(2)
54
+
55
+ add_offense(node.loc.name) if names.size < 2 || names.first == COP_DEPARTMENT
56
+ end
57
+
58
+ private
59
+
60
+ def full_namespace(node)
61
+ (node_namespace(node) + parents_namespace(node)).reverse
62
+ end
63
+
64
+ def node_namespace(node)
65
+ name_parts(node).reverse
66
+ end
67
+
68
+ def parents_namespace(node)
69
+ node
70
+ .each_ancestor(:module, :class)
71
+ .flat_map { |node| name_parts(node) }
72
+ end
73
+
74
+ def name_parts(node)
75
+ node.identifier.source.split('::')
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module InternalAffairs
6
+ # Flags if `RESTRICT_ON_SEND` constant not defined and method name is
7
+ # checked programmatically in `on_send` methods.
8
+ #
9
+ # @example
10
+ # # bad
11
+ # def on_send(node)
12
+ # return unless method_name(node) == :foo
13
+ # return unless node.children[1] == :foo
14
+ # return unless METHOD_NAMES.include?(method_name(node))
15
+ #
16
+ # name = node.children[1]
17
+ # return unless name == :foo
18
+ # name2 = method_name(node)
19
+ # return unless name == :foo
20
+ #
21
+ # # more code
22
+ # end
23
+ #
24
+ # # good
25
+ # RESTRICT_ON_SEND = %i[foo].freeze
26
+ #
27
+ # def on_send(node)
28
+ # # more code
29
+ # end
30
+ #
31
+ # # ignored - not `on_send`
32
+ # def on_def(node)
33
+ # return unless method_name(node) == :foo
34
+ # end
35
+ #
36
+ # # ignored - `else` branch
37
+ # def on_send(node)
38
+ # if method_name(node) == :foo
39
+ # add_offense(node)
40
+ # else
41
+ # something_else
42
+ # end
43
+ # end
44
+ class UseRestrictOnSend < Base
45
+ MSG = 'Define constant `RESTRICT_ON_SEND` to speed up calls to `on_send`. ' \
46
+ 'The following line is then no longer necessary:'
47
+
48
+ # @!method method_name_plain(node)
49
+ def_node_matcher :method_name_plain, <<~PATTERN
50
+ {
51
+ (send _ :method_name _ ...) # method_name(node)
52
+ (send
53
+ (send _ :children) :[] (int 1) # node.children[1]
54
+ )
55
+ }
56
+ PATTERN
57
+
58
+ # @!method method_name_call(node)
59
+ def_node_matcher :method_name_call, <<~PATTERN
60
+ {
61
+ #method_name_plain
62
+ (lvar %1)
63
+ }
64
+ PATTERN
65
+
66
+ # @!method method_name_assignment(node)
67
+ def_node_search :method_name_assignment, <<~PATTERN
68
+ (lvasgn $_name #method_name_plain)
69
+ PATTERN
70
+
71
+ # @!method method_name_check(node)
72
+ def_node_search :method_name_check, <<~PATTERN
73
+ (if
74
+ ${
75
+ (send #method_name_call(%1) {:== :!=} _) # method_name(node) == foo
76
+ (send _ :include? #method_name_call(%1)) # a.include?(method_name(node))
77
+ }
78
+ {!nil? nil? | nil? !nil?} # has either `if` or `else` branch - not both
79
+ )
80
+ PATTERN
81
+
82
+ def on_def(node)
83
+ return unless node.method?(:on_send)
84
+ return if @restrict_on_send_set
85
+
86
+ local_assignments = method_name_assignment(node).to_set
87
+
88
+ method_name_check(node, local_assignments) do |call_node|
89
+ add_offense(call_node)
90
+ end
91
+ end
92
+
93
+ def on_casgn(node)
94
+ @restrict_on_send_set = true if node.name == :RESTRICT_ON_SEND
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
@@ -56,6 +56,8 @@ module Rubocop
56
56
  #
57
57
  # do_something_more
58
58
  class LineBreakAfterGuardClauses < RuboCop::Cop::Base
59
+ extend RuboCop::Cop::AutoCorrector
60
+
59
61
  MSG = 'Add a line break after guard clauses'
60
62
 
61
63
  # @!method guard_clause_node?(node)
@@ -68,12 +70,8 @@ module Rubocop
68
70
  return unless guard_clause?(node)
69
71
  return if next_line(node).blank? || clause_last_line?(next_line(node)) || guard_clause?(next_sibling(node))
70
72
 
71
- add_offense(node)
72
- end
73
-
74
- def autocorrect(node)
75
- lambda do |corrector|
76
- corrector.insert_after(node.loc.expression, "\n")
73
+ add_offense(node) do |corrector|
74
+ corrector.insert_after(node, "\n")
77
75
  end
78
76
  end
79
77
 
@@ -125,7 +125,7 @@ module Rubocop
125
125
  end
126
126
 
127
127
  def in_haml?(node)
128
- node.location.expression.source_buffer.name.end_with?('.haml.rb')
128
+ node.source_range.source_buffer.name.end_with?('.haml.rb')
129
129
  end
130
130
  end
131
131
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require_relative '../../../gitlab/styles/rubocop/migration_helpers'
3
4
 
4
5
  module Rubocop
@@ -1,17 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative '../../gitlab/styles/rubocop/model_helpers'
4
-
5
3
  module Rubocop
6
4
  module Cop
7
5
  # Cop that prevents the use of polymorphic associations
8
6
  class PolymorphicAssociations < RuboCop::Cop::Base
9
- include Gitlab::Styles::Rubocop::ModelHelpers
10
-
11
7
  MSG = 'Do not use polymorphic associations, use separate tables instead'
12
8
 
13
9
  def on_send(node)
14
- return unless in_model?(node)
15
10
  return unless node.children[1] == :belongs_to
16
11
 
17
12
  node.children.last.each_node(:pair) do |pair|
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative '../../../gitlab/styles/rubocop/model_helpers'
4
-
5
3
  module Rubocop
6
4
  module Cop
7
5
  module Rails
@@ -2,44 +2,58 @@
2
2
 
3
3
  module Rubocop
4
4
  module Cop
5
- # Prevents usage of 'redirect_to' in actions 'destroy' without specifying 'status'.
5
+ # Prevents usage of 'redirect_to' in actions 'destroy' and 'destroy_all'
6
+ # without specifying 'status'.
7
+ #
8
+ # @example
9
+ # # bad
10
+ #
11
+ # def destroy
12
+ # redirect_to root_path
13
+ # end
14
+ #
15
+ # def destroy_all
16
+ # redirect_to root_path, alert: 'Oh no!'
17
+ # end
18
+ #
19
+ # # good
20
+ #
21
+ # def destroy
22
+ # redirect_to root_path, status: 302
23
+ # end
24
+ #
25
+ # def destroy_all
26
+ # redirect_to root_path, alert: 'Oh no!', status: 302
27
+ # end
28
+ #
29
+ # def show
30
+ # redirect_to root_path
31
+ # end
32
+ #
6
33
  # See https://gitlab.com/gitlab-org/gitlab-ce/issues/31840
7
34
  class RedirectWithStatus < RuboCop::Cop::Base
8
- MSG = 'Do not use "redirect_to" without "status" in "destroy" action'
35
+ MSG = 'Do not use "redirect_to" without "status" in "%<name>s" action.'
9
36
 
10
- def on_def(node)
11
- return unless in_controller?(node)
12
- return unless destroy?(node) || destroy_all?(node)
37
+ RESTRICT_ON_SEND = %i[redirect_to].freeze
13
38
 
14
- node.each_descendant(:send) do |def_node|
15
- next unless redirect_to?(def_node)
39
+ ACTIONS = %i[destroy destroy_all].to_set.freeze
16
40
 
17
- methods = []
41
+ # @!method redirect_to_with_status?(node)
42
+ def_node_matcher :redirect_to_with_status?, <<~PATTERN
43
+ (send nil? :redirect_to ...
44
+ (hash <(pair (sym :status) _) ...>)
45
+ )
46
+ PATTERN
18
47
 
19
- def_node.children.last.each_node(:pair) do |pair|
20
- methods << pair.children.first.children.first
21
- end
48
+ def on_send(node)
49
+ return if redirect_to_with_status?(node)
22
50
 
23
- add_offense(def_node.loc.selector) unless methods.include?(:status)
24
- end
25
- end
26
-
27
- private
28
-
29
- def in_controller?(node)
30
- node.location.expression.source_buffer.name.end_with?('_controller.rb')
31
- end
51
+ node.each_ancestor(:def) do |def_node|
52
+ next unless ACTIONS.include?(def_node.method_name)
32
53
 
33
- def destroy?(node)
34
- node.children.first == :destroy
35
- end
36
-
37
- def destroy_all?(node)
38
- node.children.first == :destroy_all
39
- end
40
-
41
- def redirect_to?(node)
42
- node.children[1] == :redirect_to
54
+ message = format(MSG, name: def_node.method_name)
55
+ add_offense(node.loc.selector, message: message)
56
+ end
43
57
  end
44
58
  end
45
59
  end