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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +2 -0
- data/Gemfile +7 -0
- data/LICENSE.md +9 -0
- data/README.md +68 -0
- data/config/default.yml +39 -0
- data/config/rubocop-airbnb.yml +96 -0
- data/config/rubocop-bundler.yml +8 -0
- data/config/rubocop-gemspec.yml +9 -0
- data/config/rubocop-layout.yml +514 -0
- data/config/rubocop-lint.yml +315 -0
- data/config/rubocop-metrics.yml +47 -0
- data/config/rubocop-naming.yml +68 -0
- data/config/rubocop-performance.yml +143 -0
- data/config/rubocop-rails.yml +193 -0
- data/config/rubocop-rspec.yml +281 -0
- data/config/rubocop-security.yml +13 -0
- data/config/rubocop-style.yml +953 -0
- data/lib/rubocop-airbnb.rb +11 -0
- data/lib/rubocop/airbnb.rb +16 -0
- data/lib/rubocop/airbnb/inflections.rb +14 -0
- data/lib/rubocop/airbnb/inject.rb +20 -0
- data/lib/rubocop/airbnb/rails_autoloading.rb +55 -0
- data/lib/rubocop/airbnb/version.rb +8 -0
- data/lib/rubocop/cop/airbnb/class_name.rb +47 -0
- data/lib/rubocop/cop/airbnb/class_or_module_declared_in_wrong_file.rb +138 -0
- data/lib/rubocop/cop/airbnb/const_assigned_in_wrong_file.rb +74 -0
- data/lib/rubocop/cop/airbnb/continuation_slash.rb +25 -0
- data/lib/rubocop/cop/airbnb/default_scope.rb +20 -0
- data/lib/rubocop/cop/airbnb/factory_attr_references_class.rb +74 -0
- data/lib/rubocop/cop/airbnb/factory_class_use_string.rb +39 -0
- data/lib/rubocop/cop/airbnb/mass_assignment_accessible_modifier.rb +18 -0
- data/lib/rubocop/cop/airbnb/module_method_in_wrong_file.rb +104 -0
- data/lib/rubocop/cop/airbnb/no_timeout.rb +19 -0
- data/lib/rubocop/cop/airbnb/opt_arg_parameters.rb +38 -0
- data/lib/rubocop/cop/airbnb/phrase_bundle_keys.rb +67 -0
- data/lib/rubocop/cop/airbnb/risky_activerecord_invocation.rb +63 -0
- data/lib/rubocop/cop/airbnb/rspec_describe_or_context_under_namespace.rb +114 -0
- data/lib/rubocop/cop/airbnb/rspec_environment_modification.rb +58 -0
- data/lib/rubocop/cop/airbnb/simple_modifier_conditional.rb +23 -0
- data/lib/rubocop/cop/airbnb/spec_constant_assignment.rb +55 -0
- data/lib/rubocop/cop/airbnb/unsafe_yaml_marshal.rb +47 -0
- data/rubocop-airbnb.gemspec +32 -0
- data/spec/rubocop/cop/airbnb/class_name_spec.rb +78 -0
- data/spec/rubocop/cop/airbnb/class_or_module_declared_in_wrong_file_spec.rb +174 -0
- data/spec/rubocop/cop/airbnb/const_assigned_in_wrong_file_spec.rb +178 -0
- data/spec/rubocop/cop/airbnb/continuation_slash_spec.rb +162 -0
- data/spec/rubocop/cop/airbnb/default_scope_spec.rb +38 -0
- data/spec/rubocop/cop/airbnb/factory_attr_references_class_spec.rb +160 -0
- data/spec/rubocop/cop/airbnb/factory_class_use_string_spec.rb +26 -0
- data/spec/rubocop/cop/airbnb/mass_assignment_accessible_modifier_spec.rb +28 -0
- data/spec/rubocop/cop/airbnb/module_method_in_wrong_file_spec.rb +181 -0
- data/spec/rubocop/cop/airbnb/no_timeout_spec.rb +30 -0
- data/spec/rubocop/cop/airbnb/opt_arg_parameter_spec.rb +103 -0
- data/spec/rubocop/cop/airbnb/phrase_bundle_keys_spec.rb +74 -0
- data/spec/rubocop/cop/airbnb/risky_activerecord_invocation_spec.rb +54 -0
- data/spec/rubocop/cop/airbnb/rspec_describe_or_context_under_namespace_spec.rb +284 -0
- data/spec/rubocop/cop/airbnb/rspec_environment_modification_spec.rb +64 -0
- data/spec/rubocop/cop/airbnb/simple_modifier_conditional_spec.rb +122 -0
- data/spec/rubocop/cop/airbnb/spec_constant_assignment_spec.rb +80 -0
- data/spec/rubocop/cop/airbnb/unsafe_yaml_marshal_spec.rb +50 -0
- data/spec/spec_helper.rb +35 -0
- metadata +150 -0
@@ -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,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
|