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