rubocop-airbnb 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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