gitlab-styles 9.2.0 → 10.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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