rubocop-highlands 1.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 (65) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +2 -0
  3. data/Gemfile +7 -0
  4. data/LICENSE.md +9 -0
  5. data/README.md +68 -0
  6. data/config/default.yml +39 -0
  7. data/config/rubocop-bundler.yml +8 -0
  8. data/config/rubocop-gemspec.yml +9 -0
  9. data/config/rubocop-highlands.yml +100 -0
  10. data/config/rubocop-layout.yml +560 -0
  11. data/config/rubocop-lint.yml +301 -0
  12. data/config/rubocop-metrics.yml +47 -0
  13. data/config/rubocop-naming.yml +85 -0
  14. data/config/rubocop-performance.yml +141 -0
  15. data/config/rubocop-rails.yml +208 -0
  16. data/config/rubocop-rspec.yml +331 -0
  17. data/config/rubocop-security.yml +17 -0
  18. data/config/rubocop-style.yml +983 -0
  19. data/lib/rubocop-highlands.rb +11 -0
  20. data/lib/rubocop/cop/highlands/class_name.rb +47 -0
  21. data/lib/rubocop/cop/highlands/class_or_module_declared_in_wrong_file.rb +138 -0
  22. data/lib/rubocop/cop/highlands/const_assigned_in_wrong_file.rb +74 -0
  23. data/lib/rubocop/cop/highlands/continuation_slash.rb +25 -0
  24. data/lib/rubocop/cop/highlands/default_scope.rb +20 -0
  25. data/lib/rubocop/cop/highlands/factory_attr_references_class.rb +74 -0
  26. data/lib/rubocop/cop/highlands/factory_class_use_string.rb +39 -0
  27. data/lib/rubocop/cop/highlands/mass_assignment_accessible_modifier.rb +18 -0
  28. data/lib/rubocop/cop/highlands/module_method_in_wrong_file.rb +104 -0
  29. data/lib/rubocop/cop/highlands/no_timeout.rb +19 -0
  30. data/lib/rubocop/cop/highlands/opt_arg_parameters.rb +38 -0
  31. data/lib/rubocop/cop/highlands/phrase_bundle_keys.rb +67 -0
  32. data/lib/rubocop/cop/highlands/risky_activerecord_invocation.rb +63 -0
  33. data/lib/rubocop/cop/highlands/rspec_describe_or_context_under_namespace.rb +114 -0
  34. data/lib/rubocop/cop/highlands/rspec_environment_modification.rb +58 -0
  35. data/lib/rubocop/cop/highlands/simple_modifier_conditional.rb +23 -0
  36. data/lib/rubocop/cop/highlands/simple_unless.rb +19 -0
  37. data/lib/rubocop/cop/highlands/spec_constant_assignment.rb +55 -0
  38. data/lib/rubocop/cop/highlands/unsafe_yaml_marshal.rb +47 -0
  39. data/lib/rubocop/highlands.rb +16 -0
  40. data/lib/rubocop/highlands/inflections.rb +14 -0
  41. data/lib/rubocop/highlands/inject.rb +20 -0
  42. data/lib/rubocop/highlands/rails_autoloading.rb +55 -0
  43. data/lib/rubocop/highlands/version.rb +8 -0
  44. data/rubocop-highlands.gemspec +31 -0
  45. data/spec/rubocop/cop/highlands/class_name_spec.rb +78 -0
  46. data/spec/rubocop/cop/highlands/class_or_module_declared_in_wrong_file_spec.rb +174 -0
  47. data/spec/rubocop/cop/highlands/const_assigned_in_wrong_file_spec.rb +178 -0
  48. data/spec/rubocop/cop/highlands/continuation_slash_spec.rb +162 -0
  49. data/spec/rubocop/cop/highlands/default_scope_spec.rb +38 -0
  50. data/spec/rubocop/cop/highlands/factory_attr_references_class_spec.rb +160 -0
  51. data/spec/rubocop/cop/highlands/factory_class_use_string_spec.rb +26 -0
  52. data/spec/rubocop/cop/highlands/mass_assignment_accessible_modifier_spec.rb +28 -0
  53. data/spec/rubocop/cop/highlands/module_method_in_wrong_file_spec.rb +181 -0
  54. data/spec/rubocop/cop/highlands/no_timeout_spec.rb +30 -0
  55. data/spec/rubocop/cop/highlands/opt_arg_parameter_spec.rb +103 -0
  56. data/spec/rubocop/cop/highlands/phrase_bundle_keys_spec.rb +74 -0
  57. data/spec/rubocop/cop/highlands/risky_activerecord_invocation_spec.rb +54 -0
  58. data/spec/rubocop/cop/highlands/rspec_describe_or_context_under_namespace_spec.rb +284 -0
  59. data/spec/rubocop/cop/highlands/rspec_environment_modification_spec.rb +64 -0
  60. data/spec/rubocop/cop/highlands/simple_modifier_conditional_spec.rb +122 -0
  61. data/spec/rubocop/cop/highlands/simple_unless_spec.rb +36 -0
  62. data/spec/rubocop/cop/highlands/spec_constant_assignment_spec.rb +80 -0
  63. data/spec/rubocop/cop/highlands/unsafe_yaml_marshal_spec.rb +50 -0
  64. data/spec/spec_helper.rb +35 -0
  65. metadata +151 -0
@@ -0,0 +1,11 @@
1
+ require 'pathname'
2
+ require 'yaml'
3
+
4
+ # Load original rubocop gem
5
+ require 'rubocop'
6
+
7
+ require 'rubocop/highlands'
8
+ require 'rubocop/highlands/inject'
9
+ require 'rubocop/highlands/version'
10
+
11
+ RuboCop::Highlands::Inject.defaults!
@@ -0,0 +1,47 @@
1
+ module RuboCop
2
+ module Cop
3
+ module Highlands
4
+ # Cop to prevent cross-model references, which result in a cascade of autoloads. E.g.,
5
+ # belongs_to :user, :class_name => User.name
6
+ class ClassName < Cop
7
+ MSG = 'Use "Model" instead of Model.name at class scope to avoid cross-model references. ' \
8
+ 'They cause a long cascade of autoloading, slowing down app startup and slowing down ' \
9
+ 'reloading of zeus after changing a model.'.freeze
10
+
11
+ # Is this a has_many, has_one, or belongs_to with a :class_name arg? Make sure the
12
+ # class name is a hardcoded string. If not, add an offense and return true.
13
+ def on_send(node)
14
+ association_statement =
15
+ node.command?(:has_many) ||
16
+ node.command?(:has_one) ||
17
+ node.command?(:belongs_to)
18
+
19
+ return unless association_statement
20
+
21
+ class_pair = class_name_node(node)
22
+
23
+ if class_pair && !string_class_name?(class_pair)
24
+ add_offense(class_pair)
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ # Return the descendant node that is a hash pair (:key => value) whose key
31
+ # is :class_name.
32
+ def class_name_node(node)
33
+ node.descendants.detect do |e|
34
+ e.is_a?(Parser::AST::Node) &&
35
+ e.pair_type? &&
36
+ e.children[0].children[0] == :class_name
37
+ end
38
+ end
39
+
40
+ # Given a hash pair :class_name => value, is the value a hardcoded string?
41
+ def string_class_name?(class_pair)
42
+ class_pair.children[1].str_type?
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,138 @@
1
+ require_relative '../../highlands/inflections'
2
+ require_relative '../../highlands/rails_autoloading'
3
+
4
+ module RuboCop
5
+ module Cop
6
+ module Highlands
7
+ # This cop checks for a class or module declared in a file that does not match its name.
8
+ # The Rails autoloader can't find such a constant, but sometimes
9
+ # people "get lucky" if the file happened to be loaded before the method was defined.
10
+ #
11
+ # @example
12
+ # # bad
13
+ #
14
+ # # foo/bar.rb
15
+ # module Foo
16
+ # class Goop
17
+ # end
18
+ #
19
+ # module Moo
20
+ # end
21
+ # end
22
+ #
23
+ # # good
24
+ #
25
+ # # foo.rb
26
+ #
27
+ # # foo/goop.rb
28
+ # module Foo
29
+ # class Goop
30
+ # end
31
+ # end
32
+ #
33
+ # # foo/moo.rb
34
+ # module Foo
35
+ # module Moo
36
+ # end
37
+ # end
38
+ #
39
+ # Note that autoloading works fine if classes are defined in the file that defines
40
+ # the module. This is common usage for things like error classes, so we'll allow it.
41
+ # Nested classes are also allowed:
42
+ #
43
+ # @example
44
+ # # good
45
+ #
46
+ # # foo.rb
47
+ # module Foo
48
+ # class Bar < StandardError
49
+ # end
50
+ # end
51
+ #
52
+ # # good
53
+ #
54
+ # # foo.rb
55
+ # class Foo
56
+ # class Bar # nested class
57
+ # end
58
+ # end
59
+ class ClassOrModuleDeclaredInWrongFile < Cop
60
+ include Inflections
61
+ include RailsAutoloading
62
+
63
+ # class Foo or module Foo in the wrong file
64
+ CLASS_OR_MODULE_MSG =
65
+ "In order for Rails autoloading to be able to find and load this file when " \
66
+ "someone references this class/module, move its definition to a file that matches " \
67
+ "its name. %s %s should be defined in %s.".freeze
68
+ # class FooError < StandardError, in the wrong file
69
+ ERROR_CLASS_MSG =
70
+ "In order for Rails autoloading to be able to find and load this file when " \
71
+ "someone references this class, move its definition to a file that defines " \
72
+ "the owning module. Class %s should be defined in %s.".freeze
73
+
74
+ # module M
75
+ def on_module(node)
76
+ on_class_or_module(node)
77
+ end
78
+
79
+ # class C
80
+ def on_class(node)
81
+ on_class_or_module(node)
82
+ end
83
+
84
+ private
85
+
86
+ def on_class_or_module(node)
87
+ path = node.source_range.source_buffer.name
88
+ return unless run_rails_autoloading_cops?(path)
89
+
90
+ const_name = node.loc.name.source
91
+ parent_module_name = normalize_module_name(node.parent_module_name)
92
+ fully_qualified_const_name = full_const_name(parent_module_name, const_name)
93
+ expected_dir = underscore(fully_qualified_const_name)
94
+ allowable_paths = allowable_paths_for(expected_dir, allow_dir: true)
95
+ if allowable_paths.none? { |allowable_path| path =~ allowable_path }
96
+ add_error(const_name, fully_qualified_const_name, node)
97
+ end
98
+ rescue => e
99
+ puts e.backtrace
100
+ raise
101
+ end
102
+
103
+ def add_error(const_name, fully_qualified_const_name, node)
104
+ class_or_module = node.type.to_s.capitalize
105
+ error_class = error_class?(node, class_or_module, const_name)
106
+ if error_class
107
+ parent_module_names = split_modules(node.parent_module_name)
108
+ else
109
+ parent_module_names = split_modules(fully_qualified_const_name)
110
+ end
111
+ expected_file = "#{parent_module_names.map { |name| underscore(name) }.join("/")}.rb"
112
+ if error_class
113
+ add_offense(node, message: ERROR_CLASS_MSG % [const_name, expected_file])
114
+ else
115
+ add_offense(
116
+ node,
117
+ message: CLASS_OR_MODULE_MSG % [class_or_module, const_name, expected_file]
118
+ )
119
+ end
120
+ end
121
+
122
+ # Does this node define an Error class? (Classname or base class includes the word
123
+ # "Error" or "Exception".)
124
+ def error_class?(node, class_or_module, const_name)
125
+ return false unless class_or_module == "Class"
126
+ _, base_class, *_ = *node
127
+ return unless base_class
128
+ base_class_name = base_class.children[1].to_s
129
+ if const_name =~ /Error|Exception/ || base_class_name =~ /Error|Exception/
130
+ return true
131
+ end
132
+
133
+ false
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,74 @@
1
+ require_relative '../../highlands/inflections'
2
+ require_relative '../../highlands/rails_autoloading'
3
+
4
+ module RuboCop
5
+ module Cop
6
+ module Highlands
7
+ # This cop checks for a constant assigned in a file that does not match its owning scope.
8
+ # The Rails autoloader can't find such a constant, but sometimes
9
+ # people "get lucky" if the file happened to be loaded before the method was defined.
10
+ #
11
+ # @example
12
+ # # bad
13
+ #
14
+ # # foo/bar.rb
15
+ # module Foo
16
+ # BAZ = 42
17
+ # end
18
+ #
19
+ # # good
20
+ #
21
+ # # foo.rb
22
+ # module Foo
23
+ # BAZ = 42
24
+ # end
25
+ class ConstAssignedInWrongFile < Cop
26
+ include Inflections
27
+ include RailsAutoloading
28
+
29
+ # FOO = 42
30
+ ASSIGNMENT_MSG =
31
+ "In order for Rails autoloading to be able to find and load this file when " \
32
+ "someone references this const, move the const assignment to a file that defines " \
33
+ "the owning module. Const %s should be defined in %s.".freeze
34
+ # FOO = 42 at global scope
35
+ GLOBAL_ASSIGNMENT =
36
+ "In order for Rails autoloading to be able to find and load this file when " \
37
+ "someone references this const, move the const assignment to a file that defines " \
38
+ "the owning module. Const %s should be moved into a namespace or defined in %s.".freeze
39
+
40
+ # FOO = 42
41
+ def on_casgn(node)
42
+ path = node.source_range.source_buffer.name
43
+ return unless run_rails_autoloading_cops?(path)
44
+ return unless node.parent_module_name
45
+
46
+ # Ignore assignments like Foo::Bar = 42
47
+ return if node.children[0]
48
+
49
+ const_name = node.children[1]
50
+ parent_module_name = normalize_module_name(node.parent_module_name)
51
+ fully_qualified_const_name = full_const_name(parent_module_name, const_name)
52
+ expected_dir = underscore(fully_qualified_const_name)
53
+ allowable_paths = allowable_paths_for(expected_dir)
54
+ if allowable_paths.none? { |allowable_path| path =~ allowable_path }
55
+ add_error(const_name, node)
56
+ end
57
+ end
58
+
59
+ private
60
+
61
+ def add_error(const_name, node)
62
+ parent_module_names = split_modules(node.parent_module_name)
63
+ expected_file = "#{parent_module_names.map { |name| underscore(name) }.join("/")}.rb"
64
+ if expected_file == ".rb" # global namespace
65
+ expected_file = "#{underscore(const_name)}.rb"
66
+ add_offense(node, message: GLOBAL_ASSIGNMENT % [const_name, expected_file])
67
+ else
68
+ add_offense(node, message: ASSIGNMENT_MSG % [const_name, expected_file])
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,25 @@
1
+ module RuboCop
2
+ module Cop
3
+ module Highlands
4
+ class ContinuationSlash < Cop
5
+ MSG = 'Slash continuation should be reserved for closed string continuation. ' \
6
+ 'Many times it is used to get around other existing rules.'.freeze
7
+
8
+ def enforce_violation(node)
9
+ return if node.source.match(/["']\s*\\\n/)
10
+ return unless node.source.match(/\\\n/)
11
+ add_offense(node, message: message)
12
+ end
13
+
14
+ alias on_send enforce_violation
15
+ alias on_if enforce_violation
16
+
17
+ Util::ASGN_NODES.each do |type|
18
+ define_method("on_#{type}") do |node|
19
+ enforce_violation(node)
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,20 @@
1
+ module RuboCop
2
+ module Cop
3
+ module Highlands
4
+ # Cop to help prevent the scorge of Default Scopes from ActiveRecord.
5
+ # Once in place they are almost impossible to remove.
6
+ class DefaultScope < Cop
7
+ MSG = 'Avoid `default_scope`. Default scopes make it difficult to '\
8
+ 'refactor data access patterns since the scope becomes part '\
9
+ 'of every query unless explicitly excluded, even when it is '\
10
+ 'unnecessary or incidental to the desired logic.'.freeze
11
+
12
+ def on_send(node)
13
+ return unless node.command?(:default_scope)
14
+
15
+ add_offense(node)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,74 @@
1
+ module RuboCop
2
+ module Cop
3
+ module Highlands
4
+ # Cop to enforce "attr { CONST }" instead of "attr CONST" in factories,
5
+ # because the latter forces autoload, which slows down spec startup time and
6
+ # Zeus reload time after touching a model.
7
+ class FactoryAttrReferencesClass < Cop
8
+ MSG = "Instead of attr_name MyClass::MY_CONST, use attr_name { MyClass::MY_CONST }. " \
9
+ "This enables faster spec startup time and Zeus reload time.".freeze
10
+
11
+ def_node_search :factory_attributes, <<-PATTERN
12
+ (block (send nil {:factory :trait} ...) _ { (begin $...) $(send ...) } )
13
+ PATTERN
14
+
15
+ # Look for "attr CONST" expressions in factories or traits. In RuboCop, this is
16
+ # a `send` node, sending the attr method.
17
+ def on_send(node)
18
+ return unless in_factory_file?(node)
19
+ return unless in_factory_or_trait?(node)
20
+
21
+ add_const_offenses(node)
22
+ end
23
+
24
+ private
25
+
26
+ def in_factory_file?(node)
27
+ filename = node.location.expression.source_buffer.name
28
+
29
+ # For tests, the input is a string
30
+ filename.include?("spec/factories/") || filename == "(string)"
31
+ end
32
+
33
+ # Is this node in a factory or trait, but not inside a nested block in a factory or trait?
34
+ def in_factory_or_trait?(node)
35
+ return false unless node
36
+
37
+ # Bail out if this IS the factory or trait node.
38
+ return false unless factory_attributes(node)
39
+ return false unless node.parent
40
+
41
+ # Is this node in a block that was passed to the factory or trait method?
42
+ if node.parent.is_a?(RuboCop::AST::Node) && node.parent.block_type?
43
+ send_node = node.parent.children.first
44
+ return false unless send_node
45
+ return false unless send_node.send_type?
46
+
47
+ # Const is referenced in the block passed to a factory or trait.
48
+ return true if send_node.command?(:factory)
49
+ return true if send_node.command?(:trait)
50
+
51
+ # Const is a block that's nested deeper inside a factory or trait. This is what we want
52
+ # developers to do.
53
+ return false
54
+ end
55
+
56
+ in_factory_or_trait?(node.parent)
57
+ end
58
+
59
+ def add_const_offenses(node)
60
+ # Add an offense for any const reference
61
+ node.each_child_node(:const) do |const_node|
62
+ add_offense(const_node)
63
+ end
64
+
65
+ # Recurse into arrays, hashes, and method calls such as ConstName[:symbol],
66
+ # adding offenses for any const reference inside them.
67
+ node.each_child_node(:array, :hash, :pair, :send) do |array_node|
68
+ add_const_offenses(array_node)
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,39 @@
1
+ module RuboCop
2
+ module Cop
3
+ module Highlands
4
+ # Cop to tell developers to use :class => "MyClass" instead of :class => MyClass,
5
+ # because the latter slows down reloading zeus.
6
+ class FactoryClassUseString < Cop
7
+ MSG = 'Instead of :class => MyClass, use :class => "MyClass". ' \
8
+ "This enables faster spec startup time and faster Zeus reload time.".freeze
9
+
10
+ def on_send(node)
11
+ return unless node.command?(:factory)
12
+
13
+ class_pair = class_node(node)
14
+
15
+ if class_pair && !string_class_name?(class_pair)
16
+ add_offense(class_pair)
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ # Return the descendant node that is a hash pair (:key => value) whose key
23
+ # is :class.
24
+ def class_node(node)
25
+ node.descendants.detect do |e|
26
+ e.is_a?(Parser::AST::Node) &&
27
+ e.pair_type? &&
28
+ e.children[0].children[0] == :class
29
+ end
30
+ end
31
+
32
+ # Given a hash pair :class_name => value, is the value a hardcoded string?
33
+ def string_class_name?(class_pair)
34
+ class_pair.children[1].str_type?
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,18 @@
1
+ module RuboCop
2
+ module Cop
3
+ module Highlands
4
+ # Modifying Mass assignment restrictions eliminates the entire point of disabling
5
+ # mass assignment. It's a lazy, potentially dangerous approach that should be discouraged.
6
+ class MassAssignmentAccessibleModifier < Cop
7
+ MSG = 'Do no override and objects mass assignment restrictions.'.freeze
8
+
9
+ def on_send(node)
10
+ _receiver, method_name, *_args = *node
11
+
12
+ return unless method_name == :accessible=
13
+ add_offense(node, message: MSG)
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end