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,58 @@
1
+ module RuboCop
2
+ module Cop
3
+ module Airbnb
4
+ # This cop checks how the Rails environment is modified in specs. If an individual method on
5
+ # Rails.env is modified multiple environment related branchs could be run down. Rather than
6
+ # modifying a single path or setting Rails.env in a way that could bleed into other specs,
7
+ # use `stub_env`
8
+ #
9
+ # @example
10
+ # # bad
11
+ #
12
+ # # spec/foo/bar_spec.rb
13
+ # before(:each) do
14
+ # allow(Rails.env).to receive(:production).and_return(true)
15
+ # end
16
+ #
17
+ # before(:each) do
18
+ # expect(Rails.env).to receive(:production).and_return(true)
19
+ # end
20
+ #
21
+ # before(:each) do
22
+ # Rails.env = :production
23
+ # end
24
+ #
25
+ # # good
26
+ #
27
+ # # spec/foo/bar_spec.rb do
28
+ # before(:each) do
29
+ # stub_env(:production)
30
+ # end
31
+ class RspecEnvironmentModification < Cop
32
+ def_node_matcher :allow_or_expect_rails_env, <<-PATTERN
33
+ (send (send nil? {:expect :allow} (send (const nil? :Rails) :env)) :to ...)
34
+ PATTERN
35
+
36
+ def_node_matcher :stub_rails_env, <<-PATTERN
37
+ (send (send (const nil? :Rails) :env) :stub _)
38
+ PATTERN
39
+
40
+ def_node_matcher :rails_env_assignment, '(send (const nil? :Rails) :env= ...)'
41
+
42
+ MESSAGE = "Do not stub or set Rails.env in specs. Use the `stub_env` method instead".freeze
43
+
44
+ def on_send(node)
45
+ path = node.source_range.source_buffer.name
46
+ return unless is_spec_file?(path)
47
+ if rails_env_assignment(node) || allow_or_expect_rails_env(node) || stub_rails_env(node)
48
+ add_offense(node, message: MESSAGE)
49
+ end
50
+ end
51
+
52
+ def is_spec_file?(path)
53
+ path.end_with?('_spec.rb')
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,23 @@
1
+ module RuboCop
2
+ module Cop
3
+ module Airbnb
4
+ # Cop to tackle prevent more complicated modifier if/unless statements
5
+ # https://github.com/airbnb/ruby#only-simple-if-unless
6
+ class SimpleModifierConditional < Cop
7
+ MSG = 'Modifier if/unless usage is okay when the body is simple, ' \
8
+ 'the condition is simple, and the whole thing fits on one line. ' \
9
+ 'Otherwise, avoid modifier if/unless.'.freeze
10
+
11
+ def_node_matcher :multiple_conditionals?, '(if ({and or :^} ...) ...)'
12
+
13
+ def on_if(node)
14
+ return unless node.modifier_form?
15
+
16
+ if multiple_conditionals?(node) || node.multiline?
17
+ add_offense(node)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,55 @@
1
+ require 'rubocop-rspec'
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Airbnb
6
+ # This cop checks for constant assignment inside of specs
7
+ #
8
+ # @example
9
+ # # bad
10
+ # describe Something do
11
+ # PAYLOAD = [1, 2, 3]
12
+ # end
13
+ #
14
+ # # good
15
+ # describe Something do
16
+ # let(:payload) { [1, 2, 3] }
17
+ # end
18
+ #
19
+ # # bad
20
+ # describe Something do
21
+ # MyClass::PAYLOAD = [1, 2, 3]
22
+ # end
23
+ #
24
+ # # good
25
+ # describe Something do
26
+ # before { stub_const('MyClass::PAYLOAD', [1, 2, 3])
27
+ # end
28
+ class SpecConstantAssignment < Cop
29
+ include RuboCop::RSpec::TopLevelDescribe
30
+ MESSAGE = "Defining constants inside of specs can cause spurious behavior. " \
31
+ "It is almost always preferable to use `let` statements, "\
32
+ "anonymous class/module definitions, or stub_const".freeze
33
+
34
+ def on_casgn(node)
35
+ return unless in_spec_file?(node)
36
+ parent_module_name = node.parent_module_name
37
+ if node.parent_module_name && parent_module_name != 'Object'
38
+ return
39
+ end
40
+ add_offense(node, message: MESSAGE)
41
+ end
42
+
43
+ private
44
+
45
+ def in_spec_file?(node)
46
+ filename = node.location.expression.source_buffer.name
47
+
48
+ # For tests, the input is a string
49
+ return true if filename == "(string)"
50
+ filename.include?("/spec/")
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,47 @@
1
+ module RuboCop
2
+ module Cop
3
+ module Airbnb
4
+ # Disallow use of YAML/Marshal methods that can trigger RCE on untrusted input
5
+ class UnsafeYamlMarshal < Cop
6
+ MSG = 'Using unsafe YAML parsing methods on untrusted input can lead ' \
7
+ 'to remote code execution. Use `safe_load`, `parse`, `parse_file`, or ' \
8
+ '`parse_stream` instead'.freeze
9
+
10
+ def on_send(node)
11
+ receiver, method_name, *_args = *node
12
+
13
+ return if receiver.nil?
14
+ return unless receiver.const_type?
15
+
16
+ check_yaml(node, receiver, method_name, *_args)
17
+ check_marshal(node, receiver, method_name, *_args)
18
+ rescue => e
19
+ puts e
20
+ puts e.backtrace
21
+ raise
22
+ end
23
+
24
+ def check_yaml(node, receiver, method_name, *_args)
25
+ return unless ['YAML', 'Psych'].include?(receiver.const_name)
26
+ return unless [:load, :load_documents, :load_file, :load_stream].include?(method_name)
27
+
28
+ message = "Using `#{receiver.const_name}.#{method_name}` on untrusted input can lead " \
29
+ "to remote code execution. Use `safe_load`, `parse`, `parse_file`, or " \
30
+ "`parse_stream` instead"
31
+
32
+ add_offense(node, message: message)
33
+ end
34
+
35
+ def check_marshal(node, receiver, method_name, *_args)
36
+ return unless receiver.const_name == 'Marshal'
37
+ return unless method_name == :load
38
+
39
+ message = 'Using `Marshal.load` on untrusted input can lead to remote code execution. ' \
40
+ 'Restructure your code to not use Marshal'
41
+
42
+ add_offense(node, message: message)
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,32 @@
1
+
2
+ $LOAD_PATH.unshift File.expand_path('../lib', __FILE__)
3
+ require 'rubocop/airbnb/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'rubocop-airbnb'
7
+ spec.summary = 'Custom code style checking for Airbnb.'
8
+ spec.description = <<-EOF
9
+ A plugin for RuboCop code style enforcing & linting tool. It includes Rubocop configuration
10
+ used at Airbnb and a few custom rules that have cause internal issues at Airbnb but are not
11
+ supported by core Rubocop.
12
+ EOF
13
+ spec.authors = ['Airbnb Engineering']
14
+ spec.email = ['rubocop@airbnb.com']
15
+ spec.homepage = 'https://github.com/airbnb/ruby'
16
+ spec.license = 'MIT'
17
+ spec.version = RuboCop::Airbnb::VERSION
18
+ spec.platform = Gem::Platform::RUBY
19
+ spec.required_ruby_version = '>= 2.1'
20
+
21
+ spec.require_paths = ['lib']
22
+ spec.files = Dir[
23
+ '{config,lib,spec}/**/*',
24
+ '*.md',
25
+ '*.gemspec',
26
+ 'Gemfile',
27
+ ]
28
+
29
+ spec.add_dependency('rubocop', '0.52.1')
30
+ spec.add_dependency('rubocop-rspec', '1.22.1')
31
+ spec.add_development_dependency('rspec', '~> 3.5')
32
+ end
@@ -0,0 +1,78 @@
1
+ describe RuboCop::Cop::Airbnb::ClassName do
2
+ subject(:cop) { described_class.new }
3
+
4
+ describe "belongs_to" do
5
+ it 'rejects with Model.name' do
6
+ source = [
7
+ 'class Coupon',
8
+ ' belongs_to :user, :class_name => User.name',
9
+ 'end',
10
+ ].join("\n")
11
+ inspect_source(source)
12
+
13
+ expect(cop.offenses.size).to eq(1)
14
+ expect(cop.offenses.map(&:line).sort).to eq([2])
15
+ end
16
+
17
+ it 'passes with "Model"' do
18
+ source = [
19
+ 'class Coupon',
20
+ ' belongs_to :user, :class_name => "User"',
21
+ 'end',
22
+ ].join("\n")
23
+ inspect_source(source)
24
+
25
+ expect(cop.offenses).to be_empty
26
+ end
27
+ end
28
+
29
+ describe "has_many" do
30
+ it 'rejects with Model.name' do
31
+ source = [
32
+ 'class Coupon',
33
+ ' has_many :reservations, :class_name => Reservation2.name',
34
+ 'end',
35
+ ].join("\n")
36
+ inspect_source(source)
37
+
38
+ expect(cop.offenses.size).to eq(1)
39
+ expect(cop.offenses.map(&:line)).to eq([2])
40
+ end
41
+
42
+ it 'passes with "Model"' do
43
+ source = [
44
+ 'class Coupon',
45
+ ' has_many :reservations, :class_name => "Reservation2"',
46
+ 'end',
47
+ ].join("\n")
48
+ inspect_source(source)
49
+
50
+ expect(cop.offenses).to be_empty
51
+ end
52
+ end
53
+
54
+ describe "has_one" do
55
+ it 'rejects with Model.name' do
56
+ source = [
57
+ 'class Coupon',
58
+ ' has_one :loss, :class_name => Payments::Loss.name',
59
+ 'end',
60
+ ].join("\n")
61
+ inspect_source(source)
62
+
63
+ expect(cop.offenses.size).to eq(1)
64
+ expect(cop.offenses.map(&:line)).to eq([2])
65
+ end
66
+
67
+ it 'passes with "Model"' do
68
+ source = [
69
+ 'class Coupon',
70
+ ' has_one :loss, :class_name => "Payments::Loss"',
71
+ 'end',
72
+ ].join("\n")
73
+ inspect_source(source)
74
+
75
+ expect(cop.offenses).to be_empty
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,174 @@
1
+ describe RuboCop::Cop::Airbnb::ClassOrModuleDeclaredInWrongFile do
2
+ subject(:cop) { described_class.new(config) }
3
+
4
+ let(:config) do
5
+ RuboCop::Config.new(
6
+ {
7
+ "Rails" => {
8
+ "Enabled" => true,
9
+ },
10
+ }
11
+ )
12
+ end
13
+
14
+ # Put source in a directory under /tmp because this cop cares about the filename
15
+ # but not the parent dir name.
16
+ let(:tmpdir) { Dir.mktmpdir }
17
+ let(:models_dir) do
18
+ gemfile = File.new("#{tmpdir}/Gemfile", "w")
19
+ gemfile.close
20
+ FileUtils.mkdir_p("#{tmpdir}/app/models").first
21
+ end
22
+
23
+ after do
24
+ FileUtils.rm_rf tmpdir
25
+ end
26
+
27
+ it 'rejects if class declaration is in a file with non-matching name' do
28
+ source = [
29
+ 'module Foo',
30
+ ' module Bar',
31
+ ' class Baz',
32
+ ' end',
33
+ ' end',
34
+ 'end',
35
+ ].join("\n")
36
+
37
+ File.open "#{models_dir}/qux.rb", "w" do |file|
38
+ inspect_source(source, file)
39
+ end
40
+
41
+ expect(cop.offenses.size).to eq(3)
42
+ expect(cop.offenses.map(&:line).sort).to eq([1, 2, 3])
43
+ expect(cop.offenses.first.message).to include('Module Foo should be defined in foo.rb.')
44
+ end
45
+
46
+ it 'rejects if class declaration is in a file with matching name but wrong parent dir' do
47
+ source = [
48
+ 'module Foo',
49
+ ' module Bar',
50
+ ' class Baz',
51
+ ' end',
52
+ ' end',
53
+ 'end',
54
+ ].join("\n")
55
+
56
+ File.open "#{models_dir}/baz.rb", "w" do |file|
57
+ inspect_source(source, file)
58
+ end
59
+
60
+ expect(cop.offenses.size).to eq(3)
61
+ expect(cop.offenses.map(&:line).sort).to eq([1, 2, 3])
62
+ expect(cop.offenses.last.message).to include('Class Baz should be defined in foo/bar/baz.rb.')
63
+ end
64
+
65
+ it 'accepts if class declaration is in a file with matching name and right parent dir' do
66
+ source = [
67
+ 'module Foo',
68
+ ' module Bar',
69
+ ' class Baz',
70
+ ' end',
71
+ ' end',
72
+ 'end',
73
+ ].join("\n")
74
+
75
+ FileUtils.mkdir_p "#{models_dir}/foo/bar"
76
+ File.open "#{models_dir}/foo/bar/baz.rb", "w" do |file|
77
+ inspect_source(source, file)
78
+ end
79
+
80
+ expect(cop.offenses).to be_empty
81
+ end
82
+
83
+ it 'rejects if class declaration is in wrong dir and parent module uses ::' do
84
+ source = [
85
+ 'module Foo::Bar',
86
+ ' class Baz',
87
+ ' end',
88
+ 'end',
89
+ ].join("\n")
90
+
91
+ FileUtils.mkdir_p "#{models_dir}/bar"
92
+ File.open "#{models_dir}/bar/baz.rb", "w" do |file|
93
+ inspect_source(source, file)
94
+ end
95
+
96
+ expect(cop.offenses.map(&:line).sort).to eq([1, 2])
97
+ expect(cop.offenses.last.message).to include('Class Baz should be defined in foo/bar/baz.rb.')
98
+ end
99
+
100
+ it 'accepts if class declaration is in a file with matching name and parent module uses ::' do
101
+ source = [
102
+ 'module Foo::Bar',
103
+ ' class Baz',
104
+ ' end',
105
+ 'end',
106
+ ].join("\n")
107
+
108
+ FileUtils.mkdir_p "#{models_dir}/foo/bar"
109
+ File.open "#{models_dir}/foo/bar/baz.rb", "w" do |file|
110
+ inspect_source(source, file)
111
+ end
112
+
113
+ expect(cop.offenses).to be_empty
114
+ end
115
+
116
+ it 'accepts class declaration where the containing class uses an acronym' do
117
+ source = [
118
+ 'module CSVFoo',
119
+ ' class Baz',
120
+ ' end',
121
+ 'end',
122
+ ].join("\n")
123
+
124
+ FileUtils.mkdir_p "#{models_dir}/csv_foo"
125
+ File.open "#{models_dir}/csv_foo/baz.rb", "w" do |file|
126
+ inspect_source(source, file)
127
+ end
128
+
129
+ expect(cop.offenses).to be_empty
130
+ end
131
+
132
+ it 'ignores class/module declaration in a rake task' do
133
+ source = [
134
+ 'class Baz',
135
+ 'end',
136
+ ].join("\n")
137
+
138
+ File.open "#{models_dir}/foo.rake", "w" do |file|
139
+ inspect_source(source, file)
140
+ end
141
+
142
+ expect(cop.offenses).to be_empty
143
+ end
144
+
145
+ it 'suggests moving error classes into the file that defines the owning scope' do
146
+ source = [
147
+ 'module Foo',
148
+ ' class BarError < StandardError; end',
149
+ 'end',
150
+ ].join("\n")
151
+
152
+ File.open "#{models_dir}/bar.rb", "w" do |file|
153
+ inspect_source(source, file)
154
+ end
155
+
156
+ expect(cop.offenses.map(&:line)).to include(2)
157
+ expect(cop.offenses.map(&:message)).to include(%r{Class BarError should be defined in foo\.rb.})
158
+ end
159
+
160
+ it 'recognizes error class based on the superclass name' do
161
+ source = [
162
+ 'module Foo',
163
+ ' class Bar < StandardError; end',
164
+ 'end',
165
+ ].join("\n")
166
+
167
+ File.open "#{models_dir}/bar.rb", "w" do |file|
168
+ inspect_source(source, file)
169
+ end
170
+
171
+ expect(cop.offenses.map(&:line)).to include(2)
172
+ expect(cop.offenses.map(&:message)).to include(%r{Class Bar should be defined in foo\.rb.})
173
+ end
174
+ end