rubocop-gusto 10.0.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 (40) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +4 -0
  3. data/LICENSE +21 -0
  4. data/README.md +53 -0
  5. data/config/default.yml +781 -0
  6. data/config/rails.yml +122 -0
  7. data/exe/gusto-rubocop +12 -0
  8. data/exe/rubocop-gusto +9 -0
  9. data/lib/rubocop/cop/gusto/bootsnap_load_file.rb +57 -0
  10. data/lib/rubocop/cop/gusto/datadog_constant.rb +16 -0
  11. data/lib/rubocop/cop/gusto/execute_migration.rb +16 -0
  12. data/lib/rubocop/cop/gusto/factory_classes_or_modules.rb +19 -0
  13. data/lib/rubocop/cop/gusto/min_by_max_by.rb +45 -0
  14. data/lib/rubocop/cop/gusto/no_metaprogramming.rb +131 -0
  15. data/lib/rubocop/cop/gusto/no_rescue_error_message_checking.rb +66 -0
  16. data/lib/rubocop/cop/gusto/no_send.rb +32 -0
  17. data/lib/rubocop/cop/gusto/object_in.rb +36 -0
  18. data/lib/rubocop/cop/gusto/paperclip_or_attachable.rb +17 -0
  19. data/lib/rubocop/cop/gusto/perform_class_method.rb +73 -0
  20. data/lib/rubocop/cop/gusto/polymorphic_type_validation.rb +89 -0
  21. data/lib/rubocop/cop/gusto/prefer_process_last_status.rb +35 -0
  22. data/lib/rubocop/cop/gusto/rabl_extends.rb +43 -0
  23. data/lib/rubocop/cop/gusto/rails_env.rb +72 -0
  24. data/lib/rubocop/cop/gusto/rake_constants.rb +68 -0
  25. data/lib/rubocop/cop/gusto/regexp_bypass.rb +90 -0
  26. data/lib/rubocop/cop/gusto/sidekiq_params.rb +21 -0
  27. data/lib/rubocop/cop/gusto/toplevel_constants.rb +55 -0
  28. data/lib/rubocop/cop/gusto/use_paint_not_colorize.rb +240 -0
  29. data/lib/rubocop/cop/gusto/vcr_recordings.rb +49 -0
  30. data/lib/rubocop/cop/internal_affairs/assignment_first.rb +56 -0
  31. data/lib/rubocop/cop/internal_affairs/require_restrict_on_send.rb +62 -0
  32. data/lib/rubocop/gusto/cli.rb +22 -0
  33. data/lib/rubocop/gusto/config_yml.rb +135 -0
  34. data/lib/rubocop/gusto/init.rb +59 -0
  35. data/lib/rubocop/gusto/plugin.rb +29 -0
  36. data/lib/rubocop/gusto/templates/rubocop.yml +25 -0
  37. data/lib/rubocop/gusto/version.rb +7 -0
  38. data/lib/rubocop/gusto.rb +9 -0
  39. data/lib/rubocop-gusto.rb +13 -0
  40. metadata +178 -0
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This cop enforces that polymorphic relations have a corresponding validation
4
+ # for their type field with an inclusion validation. This is required in order for Tapioca
5
+ # to generate correct Sorbet types
6
+ module RuboCop
7
+ module Cop
8
+ module Gusto
9
+ class PolymorphicTypeValidation < Base
10
+ RESTRICT_ON_SEND = %i(belongs_to validates polymorphic_methods_for).freeze
11
+
12
+ MSG = <<~MESSAGE
13
+ Polymorphic relations must validate their corresponding type field with "validates .. inclusion: { in: .. }", or using polymorphic_methods_for
14
+
15
+ Example:
16
+ # bad
17
+ belongs_to :subscription_detail, polymorphic: true
18
+
19
+ # good
20
+ VALID_TYPES = T.let([LidiSubscriptionDetail.polymorphic_name, LegacyActiveBenefits::VoluntaryLifeSubscriptionDetail.polymorphic_name].freeze, T::Array[String])
21
+ belongs_to :subscription_detail, polymorphic: true
22
+ validates :subscription_detail_type, presence: true, inclusion: { in: VALID_TYPES }
23
+
24
+ # also good (in ZP/HI, at least)
25
+ include PolymorphicCallable
26
+ VALID_TYPES = T.let([LidiSubscriptionDetail.polymorphic_name, LegacyActiveBenefits::VoluntaryLifeSubscriptionDetail.polymorphic_name].freeze, T::Array[String])
27
+ belongs_to :subscription_detail, polymorphic: true
28
+ polymorphic_methods_for :subscription_detail, VALID_TYPES
29
+ MESSAGE
30
+
31
+ ALLOW_BLANK_MSG = 'Polymorphic type validations cannot use allow_blank: true'
32
+
33
+ # @!method polymorphic_relation?(node)
34
+ def_node_matcher :polymorphic_relation?, <<~PATTERN
35
+ (send nil? :belongs_to _ (hash <(pair (sym :polymorphic) (true)) ...>))
36
+ PATTERN
37
+
38
+ # @!method type_validation?(node)
39
+ def_node_matcher :type_validation?, <<~PATTERN
40
+ (send nil? :validates (sym _) (hash <#inclusion_in? ...>))
41
+ PATTERN
42
+
43
+ # @!method inclusion_in?(node)
44
+ def_node_matcher :inclusion_in?, <<~PATTERN
45
+ (pair (sym :inclusion) (hash <(pair (sym :in) _) ...>))
46
+ PATTERN
47
+
48
+ # @!method polymorphic_methods_for?(node)
49
+ def_node_matcher :polymorphic_methods_for?, <<~PATTERN
50
+ (send nil? :polymorphic_methods_for (sym _) _)
51
+ PATTERN
52
+
53
+ # @!method allow_blank?(node)
54
+ def_node_matcher :allow_blank?, <<~PATTERN
55
+ (pair (sym :allow_blank) (true))
56
+ PATTERN
57
+
58
+ def on_send(node)
59
+ return unless polymorphic_relation?(node)
60
+
61
+ relation_name = node.first_argument.value
62
+ type_field = :"#{relation_name}_type"
63
+
64
+ # Look for either a validation of the type field or polymorphic_methods_for
65
+ has_validation = false
66
+ has_allow_blank = false
67
+
68
+ node.parent.each_node(:send) do |validation_node|
69
+ if type_validation?(validation_node) && validation_node.first_argument.value == type_field
70
+ has_validation = true
71
+ # Check for allow_blank in the validation options
72
+ validation_node.arguments[1].each_node(:pair) do |pair_node|
73
+ has_allow_blank = true if allow_blank?(pair_node)
74
+ end
75
+ elsif polymorphic_methods_for?(validation_node) && validation_node.first_argument.value == relation_name
76
+ has_validation = true
77
+ end
78
+ end
79
+
80
+ if has_allow_blank
81
+ add_offense(node, message: ALLOW_BLANK_MSG)
82
+ elsif !has_validation
83
+ add_offense(node)
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Gusto
6
+ # Don't use the `$?` or `$CHILD_STATUS` global variables. Instead, use `Process.last_status`
7
+ #
8
+ # @example
9
+ # # bad
10
+ # $?.exitstatus
11
+ # $CHILD_STATUS.success?
12
+ #
13
+ # # good
14
+ # Process.last_status.exit_status
15
+ # Process.last_status.success?
16
+ #
17
+ class PreferProcessLastStatus < Base
18
+ extend AutoCorrector
19
+
20
+ MSG = 'Prefer using `Process.last_status` instead of the global variables: `$?` and `$CHILD_STATUS`.'
21
+ OFFENDERS = Set[:$?, :$CHILD_STATUS].freeze
22
+
23
+ def on_gvar(node)
24
+ return unless OFFENDERS.include?(node.node_parts.first)
25
+
26
+ add_offense(node) { |corrector| autocorrect(corrector, node) }
27
+ end
28
+
29
+ def autocorrect(corrector, node)
30
+ corrector.replace(node, 'Process.last_status')
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Gusto
6
+ # Disallows the use of `extends` in Rabl templates due to poor caching performance.
7
+ # Inline the templating to generate your JSON instead.
8
+ #
9
+ # @example
10
+ # # bad
11
+ # extends 'path/to/template'
12
+ #
13
+ # # bad - but not covered by this rule
14
+ # partial 'path/to/template'
15
+ #
16
+ # # good - inline your templating
17
+ # node 'some_node'
18
+ # attributes :foo, :bar
19
+ # child(:baz) { attributes :qux }
20
+ #
21
+ class RablExtends < Base
22
+ MSG = 'Avoid using Rabl extends as it has poor caching performance. Inline your JSON instead.'
23
+ RABL_EXTENSION = '.rabl'
24
+ RESTRICT_ON_SEND = %i(extends).freeze
25
+
26
+ # @!method rabl_extends?(node)
27
+ def_node_matcher :rabl_extends?, <<~PATTERN
28
+ (send nil? :extends (str _) ...)
29
+ PATTERN
30
+
31
+ def on_send(node)
32
+ return unless rabl_extends?(node)
33
+
34
+ add_offense(node)
35
+ end
36
+
37
+ def relevant_file?(file)
38
+ file.end_with?(RABL_EXTENSION)
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Gusto
6
+ # NOTE: Being pushed upstream here: https://github.com/rubocop/rubocop-rails/pull/1375
7
+ # Checks for usage of `Rails.env` which can be replaced with Feature Flags
8
+ #
9
+ # Although `local?` is a form of an environment-specific check, it is allowed because
10
+ # it cannot be used to control overall environment rollout, but it can be helpful to
11
+ # distinguish or protect code that is explicitly written to only ever execute in a
12
+ # dev or test environment. `local?` is also a form of a feature flag.
13
+ #
14
+ # @example
15
+ #
16
+ # # bad
17
+ # Rails.env.production? || Rails.env.demo?
18
+ #
19
+ # # good
20
+ # if FeatureFlag.enabled?(:new_feature)
21
+ # # new feature code
22
+ # end
23
+ #
24
+ # # good
25
+ # raise unless Rails.env.local?
26
+ #
27
+ # # good
28
+ # abort ("The Rails environment is running in production mode!") unless Rails.env.local?
29
+ #
30
+ class RailsEnv < Base
31
+ # This allow list is derived from:
32
+ # (Rails.env.methods - Object.instance_methods).select { |m| m.to_s.end_with?('?') }
33
+ # and then removing the environment specific methods like development?, test?, production?
34
+ ALLOWED_LIST = Set.new(
35
+ %i(
36
+ unicode_normalized?
37
+ exclude?
38
+ empty?
39
+ acts_like_string?
40
+ include?
41
+ is_utf8?
42
+ casecmp?
43
+ match?
44
+ starts_with?
45
+ ends_with?
46
+ start_with?
47
+ end_with?
48
+ valid_encoding?
49
+ ascii_only?
50
+ between?
51
+ local?
52
+ )
53
+ ).freeze
54
+ MSG = 'Use Feature Flags or config instead of `Rails.env`.'
55
+ PROHIBITED_CLASS = 'Rails'
56
+ RESTRICT_ON_SEND = %i(env).freeze
57
+
58
+ def on_send(node)
59
+ return unless node.receiver&.const_name == PROHIBITED_CLASS
60
+
61
+ return unless (parent = node.parent)
62
+ return unless parent.send_type?
63
+ return unless parent.predicate_method?
64
+
65
+ return if ALLOWED_LIST.include?(parent.method_name)
66
+
67
+ add_offense(parent)
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Gusto
6
+ # Detects constants in a rake file because they are defined at the top level.
7
+ # It is confusing because the scope looks like it would be in the task or namespace,
8
+ # but actually it is defined at the top level.
9
+ #
10
+ # @example
11
+ # # bad
12
+ # task :foo do
13
+ # class C
14
+ # end
15
+ # end
16
+ #
17
+ # # bad
18
+ # namespace :foo do
19
+ # module M
20
+ # end
21
+ # end
22
+ #
23
+ # # good - It is also defined at the top level,
24
+ # # but it looks like intended behavior.
25
+ # class C
26
+ # end
27
+ # task :foo do
28
+ # end
29
+ #
30
+ class RakeConstants < Base
31
+ MSG = 'Do not define a constant in rake file, because they are sometimes `load`ed, instead of `require`d which can lead to warnings about redefining constants'
32
+
33
+ # @!method task_or_namespace?(node)
34
+ def_node_matcher :task_or_namespace?, <<-PATTERN
35
+ (block
36
+ (send _ {:task :namespace} ...)
37
+ args
38
+ _
39
+ )
40
+ PATTERN
41
+
42
+ def on_casgn(node)
43
+ return unless in_task_or_namespace?(node)
44
+
45
+ add_offense(node)
46
+ end
47
+
48
+ def on_class(node)
49
+ return unless in_task_or_namespace?(node)
50
+
51
+ add_offense(node)
52
+ end
53
+
54
+ def on_module(node)
55
+ return unless in_task_or_namespace?(node)
56
+
57
+ add_offense(node)
58
+ end
59
+
60
+ private
61
+
62
+ def in_task_or_namespace?(node)
63
+ node.each_ancestor(:block).any? { |ancestor| task_or_namespace?(ancestor) }
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Gusto
6
+ # Ensures that regular expressions use `\A` and `\z` anchors instead
7
+ # of `^` and `$` when matching the start or end of a string. This is
8
+ # critical for security validations as `^` and `$` will match the start/end
9
+ # of any line in a string, not just the start/end of the entire string.
10
+ #
11
+ # @example
12
+ # # bad - validating only a single line of input which could be split across multiple lines
13
+ # /^foo/
14
+ # /foo$/
15
+ # /^foo$/
16
+ #
17
+ # # good
18
+ # /\Afoo/
19
+ # /foo\z/
20
+ # /\Afoo\z/
21
+ #
22
+ # # good - multiline mode is allowed
23
+ # /^foo/m
24
+ # /foo$/m
25
+ #
26
+ # # okay - anchors in the middle of the pattern are not flagged
27
+ # /foo^bar/
28
+ # /foo$bar/
29
+ #
30
+ # @safety
31
+ # We choose to consider this cop safe even though the code is not equivalent.
32
+ # Replacing `^` and `$` with `\A` and `\z` will make the regex more strict,
33
+ # which is the intended behavior for securely validating input.
34
+ #
35
+ # @see https://ruby-doc.org/core/Regexp.html
36
+ # @see https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
37
+ #
38
+ class RegexpBypass < Base
39
+ extend AutoCorrector
40
+
41
+ MSG = 'Regular expressions matching a single line should use \A instead of ^ and \z instead of $'
42
+ PROHIBITED_ANCHOR = '^'
43
+ PROHIBITED_END_ANCHOR = '$'
44
+
45
+ def on_regexp(node)
46
+ return if node.children.find(&:regopt_type?)&.source&.include?('m')
47
+
48
+ first_child = node.children.first
49
+ return unless first_child && !first_child.regopt_type?
50
+
51
+ captureless_source = first_child.source.delete('()') # Remove parentheses to check actual content
52
+ return unless captureless_source.start_with?(PROHIBITED_ANCHOR) || captureless_source.end_with?(PROHIBITED_END_ANCHOR)
53
+
54
+ add_offense(first_child) do |corrector|
55
+ source_buffer = first_child.source_range.source_buffer
56
+ actual_source = first_child.source
57
+
58
+ if captureless_source.start_with?(PROHIBITED_ANCHOR)
59
+ # Find the position of ^ within the source, accounting for parentheses
60
+ caret_pos = actual_source.index(PROHIBITED_ANCHOR)
61
+ start_pos = first_child.source_range.begin_pos + caret_pos
62
+ corrector.replace(
63
+ Parser::Source::Range.new(
64
+ source_buffer,
65
+ start_pos,
66
+ start_pos + 1
67
+ ),
68
+ '\A'
69
+ )
70
+ end
71
+
72
+ if captureless_source.end_with?(PROHIBITED_END_ANCHOR)
73
+ # Find the position of $ within the source, accounting for parentheses
74
+ dollar_pos = actual_source.rindex(PROHIBITED_END_ANCHOR)
75
+ end_pos = first_child.source_range.begin_pos + dollar_pos + 1
76
+ corrector.replace(
77
+ Parser::Source::Range.new(
78
+ source_buffer,
79
+ end_pos - 1,
80
+ end_pos
81
+ ),
82
+ '\z'
83
+ )
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Gusto
6
+ class SidekiqParams < Base
7
+ MSG = 'Sidekiq perform methods cannot take keyword arguments'
8
+ PROHIBITED_ARG_TYPES = Set.new(%i(kwoptarg kwarg)).freeze
9
+
10
+ def on_def(node)
11
+ return unless node.method?(:perform)
12
+ return if node.arguments.empty?
13
+
14
+ node.arguments.each_child_node do |arg|
15
+ add_offense(node) if PROHIBITED_ARG_TYPES.include?(arg.type)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Gusto
6
+ # Checks that no top-level constants (excluding classes and modules)
7
+ # are defined. This rule exists to prevent accidental pollution of the
8
+ # global namespace as well as cases where application code has
9
+ # accidentally depended on test code.
10
+ #
11
+ # By default, this check is limited to files in `app/`, `lib/`, and
12
+ # `spec/` directories, except in the root of `lib/` and in support files
13
+ # in `spec/support/`.
14
+ #
15
+ # @example when in a checked directory
16
+ # # bad
17
+ # FOO = 'bar' # lib/foo/bar.rb
18
+ #
19
+ # # bad
20
+ # FOO = 'bar' # app/models/foo.rb
21
+ #
22
+ # # bad
23
+ # FOO = 'bar' # spec/foo.rb
24
+ #
25
+ # # good
26
+ # FOO = 'bar' # spec/spec_helper.rb
27
+ #
28
+ # # good
29
+ # class MyClass # lib/foo/bar.rb
30
+ # FOO = 'bar'
31
+ # end
32
+ #
33
+ # @example when in a `spec/support/` file
34
+ # # good
35
+ # FOO = 'bar' # spec/support/foo/bar.rb
36
+ #
37
+ # @example when in a `config/` file
38
+ # # good
39
+ # FOO = 'bar' # config/initializers/foo.rb
40
+ #
41
+ class ToplevelConstants < Base
42
+ MSG = 'Top-level constants should be defined in an initializer. See https://github.com/Gusto/rubocop-gusto/blob/main/lib/rubocop/cop/gusto/toplevel_constants.rb'
43
+
44
+ def on_casgn(node)
45
+ # Allow nested constants
46
+ return unless node.parent.nil? || node.ancestors.all?(&:begin_type?)
47
+ # Allow one-liners like `MyClass::MY_CONSTANT = 10`
48
+ return unless node.children.first.nil?
49
+
50
+ add_offense(node)
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end