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,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