rubocop-airbnb 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 (63) 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-airbnb.yml +96 -0
  8. data/config/rubocop-bundler.yml +8 -0
  9. data/config/rubocop-gemspec.yml +9 -0
  10. data/config/rubocop-layout.yml +514 -0
  11. data/config/rubocop-lint.yml +315 -0
  12. data/config/rubocop-metrics.yml +47 -0
  13. data/config/rubocop-naming.yml +68 -0
  14. data/config/rubocop-performance.yml +143 -0
  15. data/config/rubocop-rails.yml +193 -0
  16. data/config/rubocop-rspec.yml +281 -0
  17. data/config/rubocop-security.yml +13 -0
  18. data/config/rubocop-style.yml +953 -0
  19. data/lib/rubocop-airbnb.rb +11 -0
  20. data/lib/rubocop/airbnb.rb +16 -0
  21. data/lib/rubocop/airbnb/inflections.rb +14 -0
  22. data/lib/rubocop/airbnb/inject.rb +20 -0
  23. data/lib/rubocop/airbnb/rails_autoloading.rb +55 -0
  24. data/lib/rubocop/airbnb/version.rb +8 -0
  25. data/lib/rubocop/cop/airbnb/class_name.rb +47 -0
  26. data/lib/rubocop/cop/airbnb/class_or_module_declared_in_wrong_file.rb +138 -0
  27. data/lib/rubocop/cop/airbnb/const_assigned_in_wrong_file.rb +74 -0
  28. data/lib/rubocop/cop/airbnb/continuation_slash.rb +25 -0
  29. data/lib/rubocop/cop/airbnb/default_scope.rb +20 -0
  30. data/lib/rubocop/cop/airbnb/factory_attr_references_class.rb +74 -0
  31. data/lib/rubocop/cop/airbnb/factory_class_use_string.rb +39 -0
  32. data/lib/rubocop/cop/airbnb/mass_assignment_accessible_modifier.rb +18 -0
  33. data/lib/rubocop/cop/airbnb/module_method_in_wrong_file.rb +104 -0
  34. data/lib/rubocop/cop/airbnb/no_timeout.rb +19 -0
  35. data/lib/rubocop/cop/airbnb/opt_arg_parameters.rb +38 -0
  36. data/lib/rubocop/cop/airbnb/phrase_bundle_keys.rb +67 -0
  37. data/lib/rubocop/cop/airbnb/risky_activerecord_invocation.rb +63 -0
  38. data/lib/rubocop/cop/airbnb/rspec_describe_or_context_under_namespace.rb +114 -0
  39. data/lib/rubocop/cop/airbnb/rspec_environment_modification.rb +58 -0
  40. data/lib/rubocop/cop/airbnb/simple_modifier_conditional.rb +23 -0
  41. data/lib/rubocop/cop/airbnb/spec_constant_assignment.rb +55 -0
  42. data/lib/rubocop/cop/airbnb/unsafe_yaml_marshal.rb +47 -0
  43. data/rubocop-airbnb.gemspec +32 -0
  44. data/spec/rubocop/cop/airbnb/class_name_spec.rb +78 -0
  45. data/spec/rubocop/cop/airbnb/class_or_module_declared_in_wrong_file_spec.rb +174 -0
  46. data/spec/rubocop/cop/airbnb/const_assigned_in_wrong_file_spec.rb +178 -0
  47. data/spec/rubocop/cop/airbnb/continuation_slash_spec.rb +162 -0
  48. data/spec/rubocop/cop/airbnb/default_scope_spec.rb +38 -0
  49. data/spec/rubocop/cop/airbnb/factory_attr_references_class_spec.rb +160 -0
  50. data/spec/rubocop/cop/airbnb/factory_class_use_string_spec.rb +26 -0
  51. data/spec/rubocop/cop/airbnb/mass_assignment_accessible_modifier_spec.rb +28 -0
  52. data/spec/rubocop/cop/airbnb/module_method_in_wrong_file_spec.rb +181 -0
  53. data/spec/rubocop/cop/airbnb/no_timeout_spec.rb +30 -0
  54. data/spec/rubocop/cop/airbnb/opt_arg_parameter_spec.rb +103 -0
  55. data/spec/rubocop/cop/airbnb/phrase_bundle_keys_spec.rb +74 -0
  56. data/spec/rubocop/cop/airbnb/risky_activerecord_invocation_spec.rb +54 -0
  57. data/spec/rubocop/cop/airbnb/rspec_describe_or_context_under_namespace_spec.rb +284 -0
  58. data/spec/rubocop/cop/airbnb/rspec_environment_modification_spec.rb +64 -0
  59. data/spec/rubocop/cop/airbnb/simple_modifier_conditional_spec.rb +122 -0
  60. data/spec/rubocop/cop/airbnb/spec_constant_assignment_spec.rb +80 -0
  61. data/spec/rubocop/cop/airbnb/unsafe_yaml_marshal_spec.rb +50 -0
  62. data/spec/spec_helper.rb +35 -0
  63. metadata +150 -0
@@ -0,0 +1,11 @@
1
+ require 'pathname'
2
+ require 'yaml'
3
+
4
+ # Load original rubocop gem
5
+ require 'rubocop'
6
+
7
+ require 'rubocop/airbnb'
8
+ require 'rubocop/airbnb/inject'
9
+ require 'rubocop/airbnb/version'
10
+
11
+ RuboCop::Airbnb::Inject.defaults!
@@ -0,0 +1,16 @@
1
+ require 'pathname'
2
+ require 'psych'
3
+
4
+ Dir.glob(File.expand_path('cop/**/*.rb', File.dirname(__FILE__))).map(&method(:require))
5
+
6
+ module RuboCop
7
+ # RuboCop Airbnb project namespace
8
+ module Airbnb
9
+ PROJECT_ROOT =
10
+ Pathname.new(__FILE__).parent.parent.parent.expand_path.freeze
11
+ CONFIG_DEFAULT = PROJECT_ROOT.join('config', 'default.yml').freeze
12
+ CONFIG = Psych.safe_load(CONFIG_DEFAULT.read).freeze
13
+
14
+ private_constant(*constants(false))
15
+ end
16
+ end
@@ -0,0 +1,14 @@
1
+ # String inflections copied over from ActiveSupport
2
+ module Inflections
3
+ # Convert Foo::BarBaz to foo/bar_baz.
4
+ # Copied from ActiveSupport.
5
+ def underscore(camel_cased_word)
6
+ word = camel_cased_word.to_s.dup
7
+ word.gsub!(/::/, '/')
8
+ word.gsub!(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
9
+ word.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
10
+ word.tr!("-", "_")
11
+ word.downcase!
12
+ word
13
+ end
14
+ end
@@ -0,0 +1,20 @@
1
+ # Straight up ripped from the custom Rspec rubocop
2
+ # https://github.com/nevir/rubocop-rspec/blob/master/lib/rubocop/rspec/inject.rb
3
+ require 'yaml'
4
+
5
+ module RuboCop
6
+ module Airbnb
7
+ # Because RuboCop doesn't yet support plugins, we have to monkey patch in a
8
+ # bit of our configuration.
9
+ module Inject
10
+ def self.defaults!
11
+ path = CONFIG_DEFAULT.to_s
12
+ hash = ConfigLoader.load_file(path).to_hash
13
+ config = Config.new(hash, path)
14
+ puts "configuration from #{path}" if ConfigLoader.debug?
15
+ config = ConfigLoader.merge_with_default(config, path)
16
+ ConfigLoader.instance_variable_set(:@default_configuration, config)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,55 @@
1
+ # These methods are useful for Rubocop rules related to Rails autoloading.
2
+ module RailsAutoloading
3
+ def run_rails_autoloading_cops?(path)
4
+ return false unless config["Rails".freeze]
5
+ return false unless config["Rails".freeze]["Enabled".freeze]
6
+
7
+ # Ignore rake tasks
8
+ return false unless path.end_with?(".rb")
9
+
10
+ true
11
+ end
12
+
13
+ # Given "foo/bar/baz", return:
14
+ # [
15
+ # %r{/foo.rb$},
16
+ # %r{/foo/bar.rb$},
17
+ # %r{/foo/bar/baz.rb$},
18
+ # %r{/foo/bar/baz/}, # <= only if allow_dir = true
19
+ # ]
20
+ def allowable_paths_for(expected_dir, options = {})
21
+ options = { allow_dir: false }.merge(options)
22
+ allowable_paths = []
23
+ next_slash = expected_dir.index("/")
24
+ while next_slash
25
+ allowable_paths << %r{/#{expected_dir[0...next_slash]}.rb$}
26
+ next_slash = expected_dir.index("/", next_slash + 1)
27
+ end
28
+ allowable_paths << %r{#{expected_dir}.rb$}
29
+ allowable_paths << %r{/#{expected_dir}/} if options[:allow_dir]
30
+ allowable_paths
31
+ end
32
+
33
+ def normalize_module_name(module_name)
34
+ return '' if module_name.nil?
35
+ normalized_name = module_name.gsub(/#<Class:|>/, "")
36
+ normalized_name = "" if normalized_name == "Object"
37
+ normalized_name
38
+ end
39
+
40
+ # module_name looks like one of these:
41
+ # Foo::Bar for an instance method
42
+ # #<Class:Foo::Bar> for a class method.
43
+ # For either case we return ["Foo", "Bar"]
44
+ def split_modules(module_name)
45
+ normalize_module_name(module_name).split("::")
46
+ end
47
+
48
+ def full_const_name(parent_module_name, const_name)
49
+ if parent_module_name == "".freeze
50
+ "#{const_name}"
51
+ else
52
+ "#{parent_module_name}::#{const_name}"
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Airbnb
5
+ # Version information for the the Airbnb RuboCop plugin.
6
+ VERSION = '1.0.0'.freeze
7
+ end
8
+ end
@@ -0,0 +1,47 @@
1
+ module RuboCop
2
+ module Cop
3
+ module Airbnb
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 '../../airbnb/inflections'
2
+ require_relative '../../airbnb/rails_autoloading'
3
+
4
+ module RuboCop
5
+ module Cop
6
+ module Airbnb
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 '../../airbnb/inflections'
2
+ require_relative '../../airbnb/rails_autoloading'
3
+
4
+ module RuboCop
5
+ module Cop
6
+ module Airbnb
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 Airbnb
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 Airbnb
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 Airbnb
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