rubocop-fourshark 0.2.1
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/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +20 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +129 -0
- data/Rakefile +33 -0
- data/config/default.yml +96 -0
- data/lib/rubocop/cop/factory_bot/association_in_factory.rb +67 -0
- data/lib/rubocop/cop/fourshark_cops.rb +14 -0
- data/lib/rubocop/cop/layout/multiline_statement_spacing.rb +55 -0
- data/lib/rubocop/cop/rails/bidirectional_association.rb +169 -0
- data/lib/rubocop/cop/rails/mandatory_inverse_of.rb +79 -0
- data/lib/rubocop/cop/rails/optional_belongs_to.rb +48 -0
- data/lib/rubocop/cop/rails/ordered_macros.rb +74 -0
- data/lib/rubocop/cop/rspec/conditional_in_let.rb +57 -0
- data/lib/rubocop/cop/rspec/factory_bot_in_before.rb +46 -0
- data/lib/rubocop/cop/rspec/inverse_of_matcher.rb +125 -0
- data/lib/rubocop/cop/rspec/overwritten_let.rb +91 -0
- data/lib/rubocop/cop/style/disallow_safe_navigation.rb +17 -0
- data/lib/rubocop/cop/style/disallow_try.rb +19 -0
- data/lib/rubocop/fourshark/plugin.rb +31 -0
- data/lib/rubocop/fourshark/version.rb +7 -0
- data/lib/rubocop/fourshark.rb +10 -0
- data/lib/rubocop-fourshark.rb +9 -0
- metadata +181 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rubocop'
|
|
4
|
+
|
|
5
|
+
module RuboCop
|
|
6
|
+
module Cop
|
|
7
|
+
module Rails
|
|
8
|
+
# Ensures that every ActiveRecord association
|
|
9
|
+
# (belongs_to, has_many, has_one) has an `inverse_of` option.
|
|
10
|
+
#
|
|
11
|
+
# Stricter than the stock `Rails/InverseOf`, which only flags associations
|
|
12
|
+
# where Active Record cannot auto-detect the inverse. This cop mandates
|
|
13
|
+
# `inverse_of` on every association. Disable the stock cop when enabling
|
|
14
|
+
# this one.
|
|
15
|
+
#
|
|
16
|
+
# @example
|
|
17
|
+
# # bad
|
|
18
|
+
# belongs_to :user
|
|
19
|
+
#
|
|
20
|
+
# # good
|
|
21
|
+
# belongs_to :user, inverse_of: :posts
|
|
22
|
+
#
|
|
23
|
+
class MandatoryInverseOf < ::RuboCop::Cop::Base
|
|
24
|
+
MSG = 'All associations must declare `inverse_of`.'
|
|
25
|
+
|
|
26
|
+
def self.default_configuration
|
|
27
|
+
super.merge(
|
|
28
|
+
'Include' => ['app/models/**/*.rb'],
|
|
29
|
+
'Exclude' => ['app/serializers/**/*.rb']
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def on_send(node)
|
|
34
|
+
return unless in_model_file?(processed_source.file_path)
|
|
35
|
+
return unless association?(node)
|
|
36
|
+
return if polymorphic_or_through?(node)
|
|
37
|
+
|
|
38
|
+
kwargs = node.last_argument
|
|
39
|
+
|
|
40
|
+
missing_inverse_of =
|
|
41
|
+
!kwargs || !kwargs.hash_type? || kwargs.keys.none? { |k| k.value == :inverse_of }
|
|
42
|
+
|
|
43
|
+
add_offense(node.loc.selector) if missing_inverse_of
|
|
44
|
+
rescue StandardError => e
|
|
45
|
+
source_name =
|
|
46
|
+
if processed_source && processed_source.buffer
|
|
47
|
+
processed_source.file_path
|
|
48
|
+
else
|
|
49
|
+
'unknown'
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
warn "Rails/MandatoryInverseOf failed on #{source_name}: #{e.message}"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def association?(node)
|
|
58
|
+
%i[belongs_to has_many has_one].include?(node.method_name)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def in_model_file?(path)
|
|
62
|
+
return false if path.nil?
|
|
63
|
+
|
|
64
|
+
path.start_with?(File.join(Dir.pwd, 'app/models'))
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Detects if the association has `polymorphic: true` or `through: ...`
|
|
68
|
+
def polymorphic_or_through?(node)
|
|
69
|
+
# Look for any hash arguments and check their keys
|
|
70
|
+
node.arguments.any? do |arg|
|
|
71
|
+
next false unless arg.hash_type?
|
|
72
|
+
|
|
73
|
+
arg.keys.any? { |k| %i[polymorphic through].include?(k.value) }
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rubocop'
|
|
4
|
+
|
|
5
|
+
module RuboCop
|
|
6
|
+
module Cop
|
|
7
|
+
module Rails
|
|
8
|
+
# Requires `belongs_to` to declare `optional: true`. The 4Shark convention
|
|
9
|
+
# is to skip Rails' automatic presence/existence validation (a SELECT per
|
|
10
|
+
# record) and validate presence manually with `validates :x_id, presence: true`.
|
|
11
|
+
#
|
|
12
|
+
# Scoped to `app/models` via the `Include` config.
|
|
13
|
+
#
|
|
14
|
+
# @example
|
|
15
|
+
# # bad
|
|
16
|
+
# belongs_to :user
|
|
17
|
+
#
|
|
18
|
+
# # good
|
|
19
|
+
# belongs_to :user, optional: true
|
|
20
|
+
#
|
|
21
|
+
class OptionalBelongsTo < ::RuboCop::Cop::Base
|
|
22
|
+
MSG = 'Declare `belongs_to` with `optional: true` and validate presence manually.'
|
|
23
|
+
|
|
24
|
+
def self.default_configuration
|
|
25
|
+
super.merge(
|
|
26
|
+
'Include' => ['app/models/**/*.rb']
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def on_send(node)
|
|
31
|
+
return unless node.method?(:belongs_to)
|
|
32
|
+
return if optional_true?(node)
|
|
33
|
+
|
|
34
|
+
add_offense(node.loc.selector)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def optional_true?(node)
|
|
40
|
+
kwargs = node.last_argument
|
|
41
|
+
return false if !kwargs || !kwargs.hash_type?
|
|
42
|
+
|
|
43
|
+
kwargs.pairs.any? { |pair| pair.key.value == :optional && pair.value.true_type? }
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rubocop'
|
|
4
|
+
|
|
5
|
+
module RuboCop
|
|
6
|
+
module Cop
|
|
7
|
+
module Rails
|
|
8
|
+
# Requires class-level macro declarations of the same kind to be sorted
|
|
9
|
+
# alphabetically by their first symbol argument. Checked per macro name:
|
|
10
|
+
# all `belongs_to` sorted among themselves, all `validates` sorted, etc.
|
|
11
|
+
#
|
|
12
|
+
# Exceptions (lifecycle callbacks, dependency-ordered items, logical
|
|
13
|
+
# grouping) are NOT modelled — this is an experimental cop; if it flags
|
|
14
|
+
# more legitimate cases than it helps, drop it.
|
|
15
|
+
#
|
|
16
|
+
# @example
|
|
17
|
+
# # bad
|
|
18
|
+
# validates :name
|
|
19
|
+
# validates :email
|
|
20
|
+
#
|
|
21
|
+
# # good
|
|
22
|
+
# validates :email
|
|
23
|
+
# validates :name
|
|
24
|
+
#
|
|
25
|
+
class OrderedMacros < ::RuboCop::Cop::Base
|
|
26
|
+
MSG = 'Sort `%<macro>s` declarations alphabetically (`%<name>s` should come before `%<previous>s`).'
|
|
27
|
+
|
|
28
|
+
MACROS = %i[belongs_to has_one has_many has_and_belongs_to_many validates scope].freeze
|
|
29
|
+
|
|
30
|
+
def self.default_configuration
|
|
31
|
+
super.merge('Include' => ['app/models/**/*.rb'])
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def on_class(node)
|
|
35
|
+
body = node.body
|
|
36
|
+
return unless body
|
|
37
|
+
|
|
38
|
+
statements = body.begin_type? ? body.children : [body]
|
|
39
|
+
|
|
40
|
+
MACROS.each do |macro|
|
|
41
|
+
flag_unsorted(statements.select { |statement| macro_call?(statement, macro) })
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def flag_unsorted(calls)
|
|
48
|
+
calls.each_cons(2) do |previous, current|
|
|
49
|
+
previous_name = macro_name(previous)
|
|
50
|
+
current_name = macro_name(current)
|
|
51
|
+
next if current_name >= previous_name
|
|
52
|
+
|
|
53
|
+
add_offense(
|
|
54
|
+
current.loc.selector,
|
|
55
|
+
message: format(MSG, macro: current.method_name, name: current_name, previous: previous_name)
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def macro_call?(node, macro)
|
|
61
|
+
return false unless node.is_a?(::RuboCop::AST::Node) && node.send_type?
|
|
62
|
+
return false unless node.method?(macro)
|
|
63
|
+
|
|
64
|
+
first = node.first_argument
|
|
65
|
+
first && first.sym_type?
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def macro_name(node)
|
|
69
|
+
node.first_argument.value.to_s
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rubocop'
|
|
4
|
+
|
|
5
|
+
module RuboCop
|
|
6
|
+
module Cop
|
|
7
|
+
module RSpec
|
|
8
|
+
# Forbids conditional logic (`if`/`case`) inside a `let`/`let!` block.
|
|
9
|
+
# Branching object setup belongs in separate `context` blocks, not in a
|
|
10
|
+
# single `let`. Ternaries are allowed.
|
|
11
|
+
#
|
|
12
|
+
# @example
|
|
13
|
+
# # bad
|
|
14
|
+
# let(:user) do
|
|
15
|
+
# if admin?
|
|
16
|
+
# create(:user, :admin)
|
|
17
|
+
# else
|
|
18
|
+
# create(:user)
|
|
19
|
+
# end
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
# # good — one context each
|
|
23
|
+
# context 'when admin' do
|
|
24
|
+
# let(:user) { create(:user, :admin) }
|
|
25
|
+
# end
|
|
26
|
+
#
|
|
27
|
+
class ConditionalInLet < ::RuboCop::Cop::Base
|
|
28
|
+
MSG = 'Do not put conditional logic in a `let` — use separate contexts.'
|
|
29
|
+
|
|
30
|
+
LET_METHODS = %i[let let!].freeze
|
|
31
|
+
|
|
32
|
+
def on_block(node)
|
|
33
|
+
return unless let_block?(node)
|
|
34
|
+
|
|
35
|
+
body = node.body
|
|
36
|
+
return unless body
|
|
37
|
+
|
|
38
|
+
body.each_node(:if, :case) do |conditional|
|
|
39
|
+
next if conditional.if_type? && conditional.ternary?
|
|
40
|
+
|
|
41
|
+
add_offense(conditional.loc.keyword)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
alias on_numblock on_block
|
|
46
|
+
alias on_itblock on_block
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def let_block?(node)
|
|
51
|
+
send = node.send_node
|
|
52
|
+
LET_METHODS.include?(send.method_name) && send.receiver.nil?
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rubocop'
|
|
4
|
+
|
|
5
|
+
module RuboCop
|
|
6
|
+
module Cop
|
|
7
|
+
module RSpec
|
|
8
|
+
# Forbids object creation (`create`/`build`/`create_list`/`build_list`)
|
|
9
|
+
# inside a `before` block. Object creation belongs in `let`; `before` is
|
|
10
|
+
# for actions only.
|
|
11
|
+
#
|
|
12
|
+
# @example
|
|
13
|
+
# # bad
|
|
14
|
+
# before { @user = create(:user) }
|
|
15
|
+
#
|
|
16
|
+
# # good
|
|
17
|
+
# let(:user) { create(:user) }
|
|
18
|
+
#
|
|
19
|
+
class FactoryBotInBefore < ::RuboCop::Cop::Base
|
|
20
|
+
MSG = 'Do not create objects in `before` — use `let` for object creation.'
|
|
21
|
+
|
|
22
|
+
CREATION_METHODS = %i[create build create_list build_list].freeze
|
|
23
|
+
|
|
24
|
+
def on_block(node)
|
|
25
|
+
return unless before_block?(node)
|
|
26
|
+
|
|
27
|
+
node.each_node(:send) do |send|
|
|
28
|
+
next unless CREATION_METHODS.include?(send.method_name) && send.receiver.nil?
|
|
29
|
+
|
|
30
|
+
add_offense(send.loc.selector)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
alias on_numblock on_block
|
|
35
|
+
alias on_itblock on_block
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def before_block?(node)
|
|
40
|
+
send = node.send_node
|
|
41
|
+
send.method?(:before) && send.receiver.nil?
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rubocop'
|
|
4
|
+
|
|
5
|
+
module RuboCop
|
|
6
|
+
module Cop
|
|
7
|
+
module RSpec
|
|
8
|
+
# Ensures that RSpec association specs handle `.inverse_of` correctly
|
|
9
|
+
# depending on whether the model is a root (directly `< ApplicationRecord`)
|
|
10
|
+
# or an STI subclass.
|
|
11
|
+
#
|
|
12
|
+
# Rules:
|
|
13
|
+
# - Root models (direct superclass = `ApplicationRecord`) must include `.inverse_of`.
|
|
14
|
+
# - STI subclasses (superclass is another model) must NOT include `.inverse_of` (it belongs to the parent).
|
|
15
|
+
# - Associations marked as polymorphic or through are ignored.
|
|
16
|
+
# - Classes whose model file is missing or whose superclass is not a model are ignored.
|
|
17
|
+
#
|
|
18
|
+
# The root/subclass decision is made **statically** — by reading the model
|
|
19
|
+
# file and parsing its `class X < Y` declaration. The cop never loads the
|
|
20
|
+
# model class (a linter runs without the Rails app booted).
|
|
21
|
+
#
|
|
22
|
+
class InverseOfMatcher < ::RuboCop::Cop::Base
|
|
23
|
+
MSG_MISSING_INVERSE = 'Root models must include `.inverse_of` in association specs.'
|
|
24
|
+
MSG_FORBIDDEN_INVERSE = 'Subclasses must NOT include `.inverse_of` in specs (it belongs to the parent).'
|
|
25
|
+
|
|
26
|
+
def self.default_configuration
|
|
27
|
+
super.merge(
|
|
28
|
+
'Include' => ['spec/models/**/*_spec.rb']
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def on_send(node)
|
|
33
|
+
return unless node.method?(:belong_to)
|
|
34
|
+
return if chain_has_option?(node, :polymorphic)
|
|
35
|
+
return if chain_has_option?(node, :through)
|
|
36
|
+
|
|
37
|
+
model_name = model_class_from_spec
|
|
38
|
+
return unless model_name
|
|
39
|
+
|
|
40
|
+
classification = classify_model(model_name)
|
|
41
|
+
return if classification == :non_ar
|
|
42
|
+
|
|
43
|
+
if classification == :root
|
|
44
|
+
add_offense(node.loc.selector, message: MSG_MISSING_INVERSE) unless chain_has_inverse_of?(node)
|
|
45
|
+
elsif classification == :subclass
|
|
46
|
+
add_offense(node.loc.selector, message: MSG_FORBIDDEN_INVERSE) if chain_has_inverse_of?(node)
|
|
47
|
+
end
|
|
48
|
+
rescue StandardError => e
|
|
49
|
+
source_name =
|
|
50
|
+
if processed_source && processed_source.buffer
|
|
51
|
+
processed_source.file_path
|
|
52
|
+
else
|
|
53
|
+
'unknown'
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
warn "RSpec/InverseOfMatcher failed on #{source_name}: #{e.message}"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
# Check if .inverse_of exists in the send chain
|
|
62
|
+
def chain_has_inverse_of?(node)
|
|
63
|
+
node.each_ancestor(:send).any? { |ancestor| ancestor.method?(:inverse_of) }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Check if association has a given option (e.g., :polymorphic, :through)
|
|
67
|
+
def chain_has_option?(node, option_name)
|
|
68
|
+
node.each_ancestor(:send).any? do |ancestor|
|
|
69
|
+
ancestor.arguments.any? do |arg|
|
|
70
|
+
arg.hash_type? && arg.keys.any? { |k| k.value == option_name }
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Convert spec file path into model class name
|
|
76
|
+
def model_class_from_spec
|
|
77
|
+
path = processed_source.file_path if processed_source && processed_source.buffer
|
|
78
|
+
return nil unless path
|
|
79
|
+
|
|
80
|
+
match = path.match(%r{spec/models/(.+)_spec\.rb})
|
|
81
|
+
return nil unless match
|
|
82
|
+
|
|
83
|
+
relative = match[1]
|
|
84
|
+
return nil if relative.empty?
|
|
85
|
+
|
|
86
|
+
relative.split('/').map { |part| part.split('_').map(&:capitalize).join }.join('::')
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Classify the model as :root, :subclass, or :non_ar by reading the
|
|
90
|
+
# model file statically (no class loading).
|
|
91
|
+
def classify_model(model_name)
|
|
92
|
+
content = model_source(model_name)
|
|
93
|
+
return :non_ar unless content
|
|
94
|
+
|
|
95
|
+
superclass = superclass_of(content)
|
|
96
|
+
return :non_ar unless superclass
|
|
97
|
+
return :root if superclass == 'ApplicationRecord'
|
|
98
|
+
return :subclass if model_source(superclass)
|
|
99
|
+
|
|
100
|
+
:non_ar
|
|
101
|
+
rescue StandardError
|
|
102
|
+
:non_ar
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def model_source(model_name)
|
|
106
|
+
path = File.join(Dir.pwd, 'app/models', "#{camel_to_snake(model_name)}.rb")
|
|
107
|
+
File.exist?(path) ? File.read(path) : nil
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def superclass_of(content)
|
|
111
|
+
match = content.match(/^\s*class\s+[\w:]+\s*<\s*([\w:]+)/)
|
|
112
|
+
match && match[1]
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# "UserAccount" → "user_account"; "Plan::Statement" → "plan/statement"
|
|
116
|
+
def camel_to_snake(name)
|
|
117
|
+
name.gsub('::', '/')
|
|
118
|
+
.gsub(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2')
|
|
119
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
120
|
+
.downcase
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rubocop'
|
|
4
|
+
|
|
5
|
+
module RuboCop
|
|
6
|
+
module Cop
|
|
7
|
+
module RSpec
|
|
8
|
+
# Forbids overriding a `let`/`let!` that is already defined in an outer
|
|
9
|
+
# example group. Shadowing an ancestor's `let` makes it ambiguous which
|
|
10
|
+
# value applies in a given example.
|
|
11
|
+
#
|
|
12
|
+
# A `let` that is specific to a single scenario is allowed, even when a
|
|
13
|
+
# sibling context defines a `let` of the same name — siblings are separate
|
|
14
|
+
# scopes and do not override each other. Only an inner `let` that shadows
|
|
15
|
+
# an `let` from an ancestor example group is flagged.
|
|
16
|
+
#
|
|
17
|
+
# @example
|
|
18
|
+
# # bad — inner let shadows the outer one
|
|
19
|
+
# let(:user) { create(:user) }
|
|
20
|
+
#
|
|
21
|
+
# context 'when admin' do
|
|
22
|
+
# let(:user) { create(:user, :admin) }
|
|
23
|
+
# end
|
|
24
|
+
#
|
|
25
|
+
# # good — scenario-specific let, no ancestor defines it
|
|
26
|
+
# context 'when admin' do
|
|
27
|
+
# let(:user) { create(:user, :admin) }
|
|
28
|
+
# end
|
|
29
|
+
#
|
|
30
|
+
# context 'when regular' do
|
|
31
|
+
# let(:user) { create(:user) }
|
|
32
|
+
# end
|
|
33
|
+
#
|
|
34
|
+
class OverwrittenLet < ::RuboCop::Cop::Base
|
|
35
|
+
MSG = 'Do not override the outer `let` `%<name>s`; use a distinct name or set the value in `before`.'
|
|
36
|
+
|
|
37
|
+
LET_METHODS = %i[let let!].freeze
|
|
38
|
+
EXAMPLE_GROUP_METHODS = %i[describe context].freeze
|
|
39
|
+
|
|
40
|
+
def on_send(node)
|
|
41
|
+
return unless LET_METHODS.include?(node.method_name) && node.receiver.nil?
|
|
42
|
+
|
|
43
|
+
name = let_name(node)
|
|
44
|
+
return unless name
|
|
45
|
+
|
|
46
|
+
immediate_group = node.each_ancestor(:block).find { |block| example_group?(block) }
|
|
47
|
+
return unless immediate_group
|
|
48
|
+
return unless outer_groups(immediate_group).any? { |group| defines_let?(group, name) }
|
|
49
|
+
|
|
50
|
+
add_offense(node.loc.selector, message: format(MSG, name: name))
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def outer_groups(group)
|
|
56
|
+
group.each_ancestor(:block).select { |block| example_group?(block) }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def example_group?(block)
|
|
60
|
+
send = block.send_node
|
|
61
|
+
EXAMPLE_GROUP_METHODS.include?(send.method_name)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def defines_let?(group, name)
|
|
65
|
+
body = group.body
|
|
66
|
+
return false unless body
|
|
67
|
+
|
|
68
|
+
statements = body.begin_type? ? body.children : [body]
|
|
69
|
+
|
|
70
|
+
statements.any? { |statement| let_named?(statement, name) }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def let_named?(statement, name)
|
|
74
|
+
return false unless statement.is_a?(::RuboCop::AST::Node)
|
|
75
|
+
|
|
76
|
+
send = statement.respond_to?(:send_node) ? statement.send_node : statement
|
|
77
|
+
return false unless send.send_type?
|
|
78
|
+
return false unless LET_METHODS.include?(send.method_name) && send.receiver.nil?
|
|
79
|
+
|
|
80
|
+
first = send.first_argument
|
|
81
|
+
first && first.sym_type? && first.value == name
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def let_name(node)
|
|
85
|
+
first = node.first_argument
|
|
86
|
+
first && first.sym_type? ? first.value : nil
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rubocop'
|
|
4
|
+
|
|
5
|
+
module RuboCop
|
|
6
|
+
module Cop
|
|
7
|
+
module Style
|
|
8
|
+
class DisallowSafeNavigation < ::RuboCop::Cop::Base
|
|
9
|
+
MSG = 'Do not use safe navigation (`&.`). Use explicit conditionals instead.'
|
|
10
|
+
|
|
11
|
+
def on_csend(node)
|
|
12
|
+
add_offense(node)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rubocop'
|
|
4
|
+
|
|
5
|
+
module RuboCop
|
|
6
|
+
module Cop
|
|
7
|
+
module Style
|
|
8
|
+
class DisallowTry < ::RuboCop::Cop::Base
|
|
9
|
+
MSG = 'Do not use `try` or `try!`. Use explicit conditionals instead.'
|
|
10
|
+
|
|
11
|
+
RESTRICT_ON_SEND = %i[try try!].freeze
|
|
12
|
+
|
|
13
|
+
def on_send(node)
|
|
14
|
+
add_offense(node)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'lint_roller'
|
|
4
|
+
|
|
5
|
+
module RuboCop
|
|
6
|
+
module Fourshark
|
|
7
|
+
# A plugin that integrates rubocop-fourshark with RuboCop's plugin system.
|
|
8
|
+
class Plugin < LintRoller::Plugin
|
|
9
|
+
def about
|
|
10
|
+
LintRoller::About.new(
|
|
11
|
+
name: 'rubocop-fourshark',
|
|
12
|
+
version: VERSION,
|
|
13
|
+
homepage: 'https://github.com/4shark/rubocop-fourshark',
|
|
14
|
+
description: "4Shark's Ruby, Rails, and RSpec conventions as RuboCop cops."
|
|
15
|
+
)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def supported?(context)
|
|
19
|
+
context.engine == :rubocop
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def rules(_context)
|
|
23
|
+
LintRoller::Rules.new(
|
|
24
|
+
type: :path,
|
|
25
|
+
config_format: :rubocop,
|
|
26
|
+
value: Pathname.new(__dir__).join('../../../config/default.yml')
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|