rubocop-fourshark 0.8.6 → 0.9.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 +4 -4
- data/.ruby-version +1 -1
- data/CHANGELOG.md +28 -0
- data/README.md +3 -0
- data/Rakefile +0 -4
- data/config/default.yml +19 -0
- data/lib/rubocop/cop/factory_bot/association_in_factory.rb +2 -2
- data/lib/rubocop/cop/fourshark_cops.rb +3 -0
- data/lib/rubocop/cop/layout/empty_line_before_guard_clause.rb +100 -0
- data/lib/rubocop/cop/layout/single_line_statement_spacing.rb +107 -0
- data/lib/rubocop/cop/rails/bidirectional_association.rb +3 -1
- data/lib/rubocop/cop/rails/optional_belongs_to.rb +1 -0
- data/lib/rubocop/cop/rails/ordered_macros.rb +3 -2
- data/lib/rubocop/cop/rspec/conditional_in_let.rb +2 -2
- data/lib/rubocop/cop/rspec/factory_bot_in_before.rb +1 -2
- data/lib/rubocop/cop/rspec/inverse_of_matcher.rb +12 -1
- data/lib/rubocop/cop/rspec/overwritten_let.rb +6 -3
- data/lib/rubocop/cop/style/disallow_delegate.rb +5 -5
- data/lib/rubocop/cop/style/disallow_try.rb +0 -1
- data/lib/rubocop/cop/style/ordered_constants.rb +45 -0
- data/lib/rubocop/fourshark/version.rb +1 -1
- data/lib/rubocop-fourshark.rb +0 -2
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f809e807d128c7ab3f1bd4f3f32217bfde29b24c45c3c3e610531fa08fff38c2
|
|
4
|
+
data.tar.gz: cf503705817938741d2b144fbb38d1887f72a47bbd0ac0b1e761c46cc3299f70
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a37d20936f3bc14e9a5d7c92777a222fb2b364840501775d5ea301d27de15ba69863a5959aca707187d10053495d3753c5012e790fbba5d914500eacea0ea481
|
|
7
|
+
data.tar.gz: 85896dc4843fdb5cb2f86ac8366f3fd3284e63928c0bd95779d280fb9807bb61866d7aa68cdc8645f6bdb3543c3b0886e1b2518246b68331ccc02fb8f108f6c4
|
data/.ruby-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
4.0.
|
|
1
|
+
4.0.6
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,31 @@
|
|
|
1
|
+
## [0.9.0] - 2026-08-31
|
|
2
|
+
|
|
3
|
+
### Added
|
|
4
|
+
|
|
5
|
+
- `Layout/EmptyLineBeforeGuardClause` — a guard clause that follows ordinary code must be preceded by a blank line
|
|
6
|
+
- Automated dependency-update runs
|
|
7
|
+
- Minimum-age verification on pull requests, re-evaluated daily
|
|
8
|
+
- `Layout/SingleLineStatementSpacing` — a blank line between two consecutive single-line statements is removed
|
|
9
|
+
- `Style/OrderedConstants` — a run of consecutive constant assignments must be sorted alphabetically
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Development Ruby version to 4.0.6
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- Weekly transitive dependency updates
|
|
18
|
+
- Transitive dependency pull request now covers every lockfile the update touched
|
|
19
|
+
- Lockfile discovery no longer publishes a short set when the search itself fails
|
|
20
|
+
- Transitive resolution no longer runs on a short lockfile set when the search itself fails
|
|
21
|
+
- Lockfile discovery skips the bundles vendored under node_modules
|
|
22
|
+
- Held-back gem list pinned to the collation that sorted it
|
|
23
|
+
- Daily dependency age re-check leaving pull requests without the status they need to merge
|
|
24
|
+
- Dependency age checks running with no configured minimum
|
|
25
|
+
- Minimum release age applied to the weekly transitive dependency update
|
|
26
|
+
- Ruby setup action pinned like every other action in the workflows
|
|
27
|
+
- Minimum-age check no longer errors on the gem's own unreleased version
|
|
28
|
+
|
|
1
29
|
## [0.8.6] - 2026-08-08
|
|
2
30
|
|
|
3
31
|
### Fixed
|
data/README.md
CHANGED
|
@@ -80,12 +80,15 @@ Where a 4Shark cop supersedes or contradicts a stock cop, `config/default.yml` t
|
|
|
80
80
|
| `Style/DisallowSafeNavigation` | Flags safe navigation (`&.`). 4Shark rejects it — a `&.` chain silently swallows a `nil` that usually signals a real bug. Use an explicit conditional so the `nil` case is handled on purpose. |
|
|
81
81
|
| `Style/DisallowTernary` | Flags the ternary conditional (`cond ? a : b`). `?` and `:` are punctuation that already mean other things in Ruby (`valid?`, `:symbol`), and the ternary hides a branch inside an expression where skimming misses it. Use an explicit `if`/`else`. |
|
|
82
82
|
| `Style/DisallowTry` | Flags `try` / `try!`. Same rationale — it hides the `nil`/missing-method case instead of handling it explicitly. |
|
|
83
|
+
| `Style/OrderedConstants` | A run of consecutive constant assignments must be sorted alphabetically by name. A lone constant is never flagged, and any non-constant statement breaks the run. Flags without autocorrecting — a constant can reference one declared above it, so a human decides the order. Experimental. |
|
|
83
84
|
|
|
84
85
|
### Layout
|
|
85
86
|
|
|
86
87
|
| Cop | Intent |
|
|
87
88
|
|---|---|
|
|
89
|
+
| `Layout/EmptyLineBeforeGuardClause` | Requires a blank line before a guard clause that follows ordinary code, so a clause that breaks the flow is findable at a glance instead of read for. Consecutive guard clauses are one block and stay glued together — only the first of a run needs the blank line, and a clause that opens a body needs none. Mirrors the stock `Layout/EmptyLineAfterGuardClause`, which sets the run off below; enable both. |
|
|
88
90
|
| `Layout/MultilineStatementSpacing` | Requires a blank line between two consecutive statements when either spans multiple lines, so multi-line statements read as distinct units. |
|
|
91
|
+
| `Layout/SingleLineStatementSpacing` | Forbids a blank line between two consecutive single-line statements — they are one run, so the blank reveals nothing. The complement of `Layout/MultilineStatementSpacing`; the two never fire on the same pair. Autocorrects by removing the blank. |
|
|
89
92
|
|
|
90
93
|
### Naming
|
|
91
94
|
|
data/Rakefile
CHANGED
|
@@ -9,9 +9,7 @@ RSpec::Core::RakeTask.new(:spec) do |spec|
|
|
|
9
9
|
end
|
|
10
10
|
|
|
11
11
|
RuboCop::RakeTask.new
|
|
12
|
-
|
|
13
12
|
task default: %i[spec rubocop]
|
|
14
|
-
|
|
15
13
|
desc 'Generate a new cop with a template'
|
|
16
14
|
|
|
17
15
|
task :new_cop, [:cop] do |_task, args|
|
|
@@ -23,11 +21,9 @@ task :new_cop, [:cop] do |_task, args|
|
|
|
23
21
|
end
|
|
24
22
|
|
|
25
23
|
generator = RuboCop::Cop::Generator.new(cop_name)
|
|
26
|
-
|
|
27
24
|
generator.write_source
|
|
28
25
|
generator.write_spec
|
|
29
26
|
generator.inject_require(root_file_path: 'lib/rubocop/cop/fourshark_cops.rb')
|
|
30
27
|
generator.inject_config(config_file_path: 'config/default.yml')
|
|
31
|
-
|
|
32
28
|
puts generator.todo
|
|
33
29
|
end
|
data/config/default.yml
CHANGED
|
@@ -3,11 +3,25 @@
|
|
|
3
3
|
# so the names read like any other cop. Where a 4Shark cop supersedes or
|
|
4
4
|
# contradicts a stock cop, the stock cop is disabled here (see the bottom block).
|
|
5
5
|
|
|
6
|
+
# Mirrors the stock `Layout/EmptyLineAfterGuardClause` rather than replacing it:
|
|
7
|
+
# that one sets the run off below, this one above, and both are wanted.
|
|
8
|
+
Layout/EmptyLineBeforeGuardClause:
|
|
9
|
+
Description: 'Add a blank line before a guard clause that follows ordinary code.'
|
|
10
|
+
Enabled: true
|
|
11
|
+
VersionAdded: '0.9.0'
|
|
12
|
+
|
|
6
13
|
Layout/MultilineStatementSpacing:
|
|
7
14
|
Description: 'Add a blank line between consecutive statements when either spans multiple lines.'
|
|
8
15
|
Enabled: true
|
|
9
16
|
VersionAdded: '0.2.0'
|
|
10
17
|
|
|
18
|
+
# The complement of Layout/MultilineStatementSpacing: no blank line between two
|
|
19
|
+
# single-line statements. Autocorrects (removing a blank has no placement ambiguity).
|
|
20
|
+
Layout/SingleLineStatementSpacing:
|
|
21
|
+
Description: 'Do not put a blank line between two consecutive single-line statements.'
|
|
22
|
+
Enabled: true
|
|
23
|
+
VersionAdded: '0.9.0'
|
|
24
|
+
|
|
11
25
|
# Stock cop configured (not a 4Shark cop): enforce the `indented` style for
|
|
12
26
|
# multiline method-call chains — continuation calls are indented one step from
|
|
13
27
|
# the receiver instead of aligning with the first call.
|
|
@@ -42,6 +56,11 @@ Style/DisallowTry:
|
|
|
42
56
|
Enabled: true
|
|
43
57
|
VersionAdded: '0.1.0'
|
|
44
58
|
|
|
59
|
+
Style/OrderedConstants:
|
|
60
|
+
Description: 'Sort a run of consecutive constant assignments alphabetically by name.'
|
|
61
|
+
Enabled: true
|
|
62
|
+
VersionAdded: '0.9.0'
|
|
63
|
+
|
|
45
64
|
Rails/MandatoryInverseOf:
|
|
46
65
|
Description: 'Every association must declare `inverse_of` (stricter than stock `Rails/InverseOf`).'
|
|
47
66
|
Enabled: true
|
|
@@ -23,16 +23,16 @@ module RuboCop
|
|
|
23
23
|
# end
|
|
24
24
|
#
|
|
25
25
|
class AssociationInFactory < ::RuboCop::Cop::Base
|
|
26
|
-
MSG = 'Do not declare associations in factories — set them manually in the spec.'
|
|
27
|
-
|
|
28
26
|
CONTAINER_METHODS = %i[factory trait].freeze
|
|
29
27
|
# FactoryBot DSL methods that are bare calls but are NOT associations.
|
|
30
28
|
DSL_KEYWORDS = %i[skip_create initialize_with].freeze
|
|
29
|
+
MSG = 'Do not declare associations in factories — set them manually in the spec.'
|
|
31
30
|
|
|
32
31
|
def on_block(node)
|
|
33
32
|
return unless container_block?(node)
|
|
34
33
|
|
|
35
34
|
body = node.body
|
|
35
|
+
|
|
36
36
|
return unless body
|
|
37
37
|
|
|
38
38
|
statements = if body.begin_type?
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative 'factory_bot/association_in_factory'
|
|
4
|
+
require_relative 'layout/empty_line_before_guard_clause'
|
|
4
5
|
require_relative 'layout/multiline_statement_spacing'
|
|
6
|
+
require_relative 'layout/single_line_statement_spacing'
|
|
5
7
|
require_relative 'rails/bidirectional_association'
|
|
6
8
|
require_relative 'rails/mandatory_inverse_of'
|
|
7
9
|
require_relative 'rails/optional_belongs_to'
|
|
@@ -14,3 +16,4 @@ require_relative 'style/disallow_delegate'
|
|
|
14
16
|
require_relative 'style/disallow_safe_navigation'
|
|
15
17
|
require_relative 'style/disallow_ternary'
|
|
16
18
|
require_relative 'style/disallow_try'
|
|
19
|
+
require_relative 'style/ordered_constants'
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rubocop'
|
|
4
|
+
|
|
5
|
+
module RuboCop
|
|
6
|
+
module Cop
|
|
7
|
+
module Layout
|
|
8
|
+
# Requires a blank line before a guard clause that follows ordinary code.
|
|
9
|
+
# A guard clause breaks the flow, so it has to be findable at a glance
|
|
10
|
+
# rather than read for — the same reason a multi-line statement is set off
|
|
11
|
+
# from its neighbours.
|
|
12
|
+
#
|
|
13
|
+
# Consecutive guard clauses are one block and stay glued together: the run
|
|
14
|
+
# is what the reader scans, and blank lines inside it would break the shape
|
|
15
|
+
# instead of revealing it. Only the first clause of a run needs the blank
|
|
16
|
+
# line above, and a guard that opens a body needs none because the `def`
|
|
17
|
+
# or `do` already delimits it.
|
|
18
|
+
#
|
|
19
|
+
# What counts as a guard clause is `guard_clause?` from `rubocop-ast`,
|
|
20
|
+
# which covers `return`, `break`, `next`, `raise` and `fail`.
|
|
21
|
+
#
|
|
22
|
+
# The blank line *after* the run is the stock
|
|
23
|
+
# `Layout/EmptyLineAfterGuardClause`, which this cop mirrors rather than
|
|
24
|
+
# replaces — enable both to get a run that is set off on both sides.
|
|
25
|
+
#
|
|
26
|
+
# Detection-only (no autocorrect), matching the sibling
|
|
27
|
+
# `Layout/MultilineStatementSpacing`: where a comment sits above the
|
|
28
|
+
# clause the blank line belongs above the comment, and that placement
|
|
29
|
+
# needs validation against the real repos before it is applied for anyone.
|
|
30
|
+
#
|
|
31
|
+
# @example
|
|
32
|
+
# # bad
|
|
33
|
+
# def foo
|
|
34
|
+
# bar
|
|
35
|
+
# return if baz?
|
|
36
|
+
#
|
|
37
|
+
# qux
|
|
38
|
+
# end
|
|
39
|
+
#
|
|
40
|
+
# # good
|
|
41
|
+
# def foo
|
|
42
|
+
# bar
|
|
43
|
+
#
|
|
44
|
+
# return if baz?
|
|
45
|
+
#
|
|
46
|
+
# qux
|
|
47
|
+
# end
|
|
48
|
+
#
|
|
49
|
+
# # good — a run of guard clauses stays together
|
|
50
|
+
# def foo
|
|
51
|
+
# return if baz?
|
|
52
|
+
# return if qux?
|
|
53
|
+
#
|
|
54
|
+
# bar
|
|
55
|
+
# end
|
|
56
|
+
#
|
|
57
|
+
# # good — nothing above it to separate from
|
|
58
|
+
# def foo
|
|
59
|
+
# return if baz?
|
|
60
|
+
#
|
|
61
|
+
# bar
|
|
62
|
+
# end
|
|
63
|
+
#
|
|
64
|
+
class EmptyLineBeforeGuardClause < ::RuboCop::Cop::Base
|
|
65
|
+
MSG = 'Add a blank line before the guard clause.'
|
|
66
|
+
|
|
67
|
+
def on_begin(node)
|
|
68
|
+
node.children.each_cons(2) do |previous, current|
|
|
69
|
+
next unless guard_clause?(current)
|
|
70
|
+
next if guard_clause?(previous)
|
|
71
|
+
next if blank_line_between?(previous, current)
|
|
72
|
+
|
|
73
|
+
add_offense(current)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def guard_clause?(node)
|
|
80
|
+
return false unless node.is_a?(::RuboCop::AST::Node)
|
|
81
|
+
return false unless node.if_type?
|
|
82
|
+
|
|
83
|
+
branch = node.if_branch
|
|
84
|
+
|
|
85
|
+
return false if branch.nil?
|
|
86
|
+
|
|
87
|
+
branch.guard_clause?
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def blank_line_between?(previous, current)
|
|
91
|
+
return false if current.first_line - previous.last_line < 2
|
|
92
|
+
|
|
93
|
+
((previous.last_line + 1)...current.first_line).any? do |line|
|
|
94
|
+
processed_source.lines[line - 1].strip.empty?
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rubocop'
|
|
4
|
+
|
|
5
|
+
module RuboCop
|
|
6
|
+
module Cop
|
|
7
|
+
module Layout
|
|
8
|
+
# Forbids a blank line between two consecutive single-line statements —
|
|
9
|
+
# they are one run, so the blank reveals nothing. A blank that separates
|
|
10
|
+
# code from an adjacent comment is kept, as is one owned by another cop
|
|
11
|
+
# (a guard clause, a flow-control statement, an access modifier) or one
|
|
12
|
+
# beside a multi-line or heredoc neighbour. Autocorrects by removing it.
|
|
13
|
+
class SingleLineStatementSpacing < ::RuboCop::Cop::Base
|
|
14
|
+
extend ::RuboCop::Cop::AutoCorrector
|
|
15
|
+
ACCESS_MODIFIERS = %i[private protected public module_function].freeze
|
|
16
|
+
FLOW_CONTROL_METHODS = %i[raise fail throw].freeze
|
|
17
|
+
FLOW_CONTROL_TYPES = %i[return next break redo retry].freeze
|
|
18
|
+
MSG = 'Remove the blank line between consecutive single-line statements.'
|
|
19
|
+
|
|
20
|
+
def on_begin(node)
|
|
21
|
+
node.children.each_cons(2) do |first, second|
|
|
22
|
+
next unless first.is_a?(::RuboCop::AST::Node) && second.is_a?(::RuboCop::AST::Node)
|
|
23
|
+
next if structural?(first) || structural?(second)
|
|
24
|
+
|
|
25
|
+
blanks = removable_blank_lines(first, second)
|
|
26
|
+
|
|
27
|
+
next if blanks.empty?
|
|
28
|
+
|
|
29
|
+
add_offense(second) do |corrector|
|
|
30
|
+
blanks.each { |line| corrector.remove(line_range_with_newline(line)) }
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
# A statement whose surrounding blank another cop owns, or which reads as
|
|
38
|
+
# a block. Its blank is preserved: this cop only glues ordinary runs.
|
|
39
|
+
def structural?(node)
|
|
40
|
+
multiline_or_heredoc?(node) || flow_control?(node) || guard_clause?(node) || access_modifier?(node)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def multiline_or_heredoc?(node)
|
|
44
|
+
node.multiline? || node.each_node(:any_str).any?(&:heredoc?)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def flow_control?(node)
|
|
48
|
+
return true if FLOW_CONTROL_TYPES.include?(node.type)
|
|
49
|
+
|
|
50
|
+
node.send_type? && node.receiver.nil? && FLOW_CONTROL_METHODS.include?(node.method_name)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def guard_clause?(node)
|
|
54
|
+
return false unless node.if_type?
|
|
55
|
+
|
|
56
|
+
branch = node.if_branch
|
|
57
|
+
|
|
58
|
+
return false if branch.nil?
|
|
59
|
+
|
|
60
|
+
branch.guard_clause?
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def access_modifier?(node)
|
|
64
|
+
node.send_type? && node.receiver.nil? && ACCESS_MODIFIERS.include?(node.method_name)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# A blank whose nearest non-blank line on each side is the same kind
|
|
68
|
+
# (code-to-code or comment-to-comment). A blank on a code/comment border
|
|
69
|
+
# separates the two deliberately and is left alone.
|
|
70
|
+
def removable_blank_lines(first, second)
|
|
71
|
+
gap = (first.last_line + 1)...second.first_line
|
|
72
|
+
|
|
73
|
+
gap.select do |line|
|
|
74
|
+
blank_line?(line) && same_kind_neighbours?(line, first.last_line, second.first_line)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def same_kind_neighbours?(line, floor, ceiling)
|
|
79
|
+
above = nearest_non_blank(line - 1, floor, -1)
|
|
80
|
+
below = nearest_non_blank(line + 1, ceiling, 1)
|
|
81
|
+
line_kind(above) == line_kind(below)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def nearest_non_blank(start_line, boundary, step)
|
|
85
|
+
line = start_line
|
|
86
|
+
line += step while line != boundary && blank_line?(line)
|
|
87
|
+
line
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def line_kind(line)
|
|
91
|
+
return :comment if processed_source.lines[line - 1].strip.start_with?('#')
|
|
92
|
+
|
|
93
|
+
:code
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def blank_line?(line)
|
|
97
|
+
processed_source.lines[line - 1].strip.empty?
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def line_range_with_newline(line)
|
|
101
|
+
line_range = processed_source.buffer.line_range(line)
|
|
102
|
+
line_range.resize(line_range.length + 1)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -43,6 +43,7 @@ module RuboCop
|
|
|
43
43
|
next if model_name.nil? || target_class.nil?
|
|
44
44
|
|
|
45
45
|
opposite_model_path = File.join(Dir.pwd, "app/models/#{camel_to_snake(target_class)}.rb")
|
|
46
|
+
|
|
46
47
|
next unless File.exist?(opposite_model_path)
|
|
47
48
|
|
|
48
49
|
opposite_content = File.read(opposite_model_path)
|
|
@@ -81,7 +82,6 @@ module RuboCop
|
|
|
81
82
|
assoc_name = literal_value(node.first_argument)
|
|
82
83
|
inverse_name = extract_inverse_of(node)
|
|
83
84
|
target_class = extract_class_name(node, assoc_name)
|
|
84
|
-
|
|
85
85
|
[model_name, assoc_name, inverse_name, target_class]
|
|
86
86
|
end
|
|
87
87
|
end
|
|
@@ -107,6 +107,7 @@ module RuboCop
|
|
|
107
107
|
# plain symbol (string keys, double-splats) are skipped rather than read.
|
|
108
108
|
def option_pair(node, key)
|
|
109
109
|
kwargs = node.last_argument
|
|
110
|
+
|
|
110
111
|
return nil unless kwargs.respond_to?(:hash_type?) && kwargs.hash_type?
|
|
111
112
|
|
|
112
113
|
kwargs.pairs.find { |pair| pair.key.sym_type? && pair.key.value == key }
|
|
@@ -125,6 +126,7 @@ module RuboCop
|
|
|
125
126
|
|
|
126
127
|
def class_name_from_ast
|
|
127
128
|
class_node = processed_source.ast.each_descendant(:class).first
|
|
129
|
+
|
|
128
130
|
return nil unless class_node
|
|
129
131
|
|
|
130
132
|
const_node = class_node.children.first
|
|
@@ -27,9 +27,8 @@ module RuboCop
|
|
|
27
27
|
# validates :name
|
|
28
28
|
#
|
|
29
29
|
class OrderedMacros < ::RuboCop::Cop::Base
|
|
30
|
-
MSG = 'Sort `%<macro>s` declarations alphabetically (`%<name>s` should come before `%<previous>s`).'
|
|
31
|
-
|
|
32
30
|
MACROS = %i[belongs_to has_one has_many has_and_belongs_to_many validates scope].freeze
|
|
31
|
+
MSG = 'Sort `%<macro>s` declarations alphabetically (`%<name>s` should come before `%<previous>s`).'
|
|
33
32
|
|
|
34
33
|
def self.default_configuration
|
|
35
34
|
super.merge('Include' => ['app/models/**/*.rb'])
|
|
@@ -37,6 +36,7 @@ module RuboCop
|
|
|
37
36
|
|
|
38
37
|
def on_class(node)
|
|
39
38
|
body = node.body
|
|
39
|
+
|
|
40
40
|
return unless body
|
|
41
41
|
|
|
42
42
|
statements = if body.begin_type?
|
|
@@ -68,6 +68,7 @@ module RuboCop
|
|
|
68
68
|
calls.each_cons(2) do |previous, current|
|
|
69
69
|
previous_name = macro_name(previous)
|
|
70
70
|
current_name = macro_name(current)
|
|
71
|
+
|
|
71
72
|
next if current_name >= previous_name
|
|
72
73
|
|
|
73
74
|
add_offense(
|
|
@@ -25,14 +25,14 @@ module RuboCop
|
|
|
25
25
|
# end
|
|
26
26
|
#
|
|
27
27
|
class ConditionalInLet < ::RuboCop::Cop::Base
|
|
28
|
-
MSG = 'Do not put conditional logic in a `let` — use separate contexts.'
|
|
29
|
-
|
|
30
28
|
LET_METHODS = %i[let let!].freeze
|
|
29
|
+
MSG = 'Do not put conditional logic in a `let` — use separate contexts.'
|
|
31
30
|
|
|
32
31
|
def on_block(node)
|
|
33
32
|
return unless let_block?(node)
|
|
34
33
|
|
|
35
34
|
body = node.body
|
|
35
|
+
|
|
36
36
|
return unless body
|
|
37
37
|
|
|
38
38
|
body.each_node(:if, :case) do |conditional|
|
|
@@ -17,9 +17,8 @@ module RuboCop
|
|
|
17
17
|
# let(:user) { create(:user) }
|
|
18
18
|
#
|
|
19
19
|
class FactoryBotInBefore < ::RuboCop::Cop::Base
|
|
20
|
-
MSG = 'Do not create objects in `before` — use `let` for object creation.'
|
|
21
|
-
|
|
22
20
|
CREATION_METHODS = %i[create build create_list build_list].freeze
|
|
21
|
+
MSG = 'Do not create objects in `before` — use `let` for object creation.'
|
|
23
22
|
|
|
24
23
|
def on_block(node)
|
|
25
24
|
return unless before_block?(node)
|
|
@@ -20,8 +20,8 @@ module RuboCop
|
|
|
20
20
|
# model class (a linter runs without the Rails app booted).
|
|
21
21
|
#
|
|
22
22
|
class InverseOfMatcher < ::RuboCop::Cop::Base
|
|
23
|
-
MSG_MISSING_INVERSE = 'Root models must include `.inverse_of` in association specs.'
|
|
24
23
|
MSG_FORBIDDEN_INVERSE = 'Subclasses must NOT include `.inverse_of` in specs (it belongs to the parent).'
|
|
24
|
+
MSG_MISSING_INVERSE = 'Root models must include `.inverse_of` in association specs.'
|
|
25
25
|
|
|
26
26
|
def self.default_configuration
|
|
27
27
|
super.merge(
|
|
@@ -33,12 +33,15 @@ module RuboCop
|
|
|
33
33
|
return unless node.method?(:belong_to)
|
|
34
34
|
|
|
35
35
|
model_name = model_class_from_spec
|
|
36
|
+
|
|
36
37
|
return unless model_name
|
|
37
38
|
|
|
38
39
|
association_name = association_name_from(node)
|
|
40
|
+
|
|
39
41
|
return if association_name && polymorphic_in_model?(model_name, association_name)
|
|
40
42
|
|
|
41
43
|
classification = classify_model(model_name)
|
|
44
|
+
|
|
42
45
|
return if classification == :non_ar
|
|
43
46
|
|
|
44
47
|
if classification == :root
|
|
@@ -66,6 +69,7 @@ module RuboCop
|
|
|
66
69
|
# The association name passed to the matcher, e.g. belong_to(:user) -> :user
|
|
67
70
|
def association_name_from(node)
|
|
68
71
|
argument = node.first_argument
|
|
72
|
+
|
|
69
73
|
return nil unless argument && argument.sym_type?
|
|
70
74
|
|
|
71
75
|
argument.value
|
|
@@ -76,6 +80,7 @@ module RuboCop
|
|
|
76
80
|
# and the declaration lives in the model, not in the spec matcher chain.
|
|
77
81
|
def polymorphic_in_model?(model_name, association_name)
|
|
78
82
|
content = model_source(model_name)
|
|
83
|
+
|
|
79
84
|
return false unless content
|
|
80
85
|
|
|
81
86
|
content.match?(/belongs_to\s+:#{Regexp.escape(association_name.to_s)}\b[^\n]*polymorphic:\s*true/)
|
|
@@ -84,12 +89,15 @@ module RuboCop
|
|
|
84
89
|
# Convert spec file path into model class name
|
|
85
90
|
def model_class_from_spec
|
|
86
91
|
path = processed_source.file_path if processed_source && processed_source.buffer
|
|
92
|
+
|
|
87
93
|
return nil unless path
|
|
88
94
|
|
|
89
95
|
match = path.match(%r{spec/models/(.+)_spec\.rb})
|
|
96
|
+
|
|
90
97
|
return nil unless match
|
|
91
98
|
|
|
92
99
|
relative = match[1]
|
|
100
|
+
|
|
93
101
|
return nil if relative.empty?
|
|
94
102
|
|
|
95
103
|
relative.split('/').map { |part| part.split('_').map(&:capitalize).join }.join('::')
|
|
@@ -99,9 +107,11 @@ module RuboCop
|
|
|
99
107
|
# model file statically (no class loading).
|
|
100
108
|
def classify_model(model_name)
|
|
101
109
|
content = model_source(model_name)
|
|
110
|
+
|
|
102
111
|
return :non_ar unless content
|
|
103
112
|
|
|
104
113
|
superclass = superclass_of(content, model_name)
|
|
114
|
+
|
|
105
115
|
return :non_ar unless superclass
|
|
106
116
|
return :root if superclass == 'ApplicationRecord'
|
|
107
117
|
return :subclass if model_source(superclass)
|
|
@@ -113,6 +123,7 @@ module RuboCop
|
|
|
113
123
|
|
|
114
124
|
def model_source(model_name)
|
|
115
125
|
path = File.join(Dir.pwd, 'app/models', "#{camel_to_snake(model_name)}.rb")
|
|
126
|
+
|
|
116
127
|
return File.read(path) if File.exist?(path)
|
|
117
128
|
|
|
118
129
|
nil
|
|
@@ -32,18 +32,19 @@ module RuboCop
|
|
|
32
32
|
# end
|
|
33
33
|
#
|
|
34
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
35
|
EXAMPLE_GROUP_METHODS = %i[describe context].freeze
|
|
36
|
+
LET_METHODS = %i[let let!].freeze
|
|
37
|
+
MSG = 'Do not override the outer `let` `%<name>s`; use a distinct name or set the value in `before`.'
|
|
39
38
|
|
|
40
39
|
def on_send(node)
|
|
41
40
|
return unless LET_METHODS.include?(node.method_name) && node.receiver.nil?
|
|
42
41
|
|
|
43
42
|
name = let_name(node)
|
|
43
|
+
|
|
44
44
|
return unless name
|
|
45
45
|
|
|
46
46
|
immediate_group = node.each_ancestor(:block).find { |block| example_group?(block) }
|
|
47
|
+
|
|
47
48
|
return unless immediate_group
|
|
48
49
|
return unless outer_groups(immediate_group).any? { |group| defines_let?(group, name) }
|
|
49
50
|
|
|
@@ -63,6 +64,7 @@ module RuboCop
|
|
|
63
64
|
|
|
64
65
|
def defines_let?(group, name)
|
|
65
66
|
body = group.body
|
|
67
|
+
|
|
66
68
|
return false unless body
|
|
67
69
|
|
|
68
70
|
statements = if body.begin_type?
|
|
@@ -92,6 +94,7 @@ module RuboCop
|
|
|
92
94
|
|
|
93
95
|
def let_name(node)
|
|
94
96
|
first = node.first_argument
|
|
97
|
+
|
|
95
98
|
return first.value if first && first.sym_type?
|
|
96
99
|
|
|
97
100
|
nil
|
|
@@ -120,15 +120,13 @@ module RuboCop
|
|
|
120
120
|
# end
|
|
121
121
|
#
|
|
122
122
|
class DisallowDelegate < ::RuboCop::Cop::Base
|
|
123
|
-
MACRO_MSG = 'Do not use automatic delegation. Delete the forwarder and let the caller navigate.'
|
|
124
|
-
FORWARDER_MSG = 'Do not forward a collaborator\'s message. Delete the forwarder and let the caller navigate.'
|
|
125
|
-
|
|
126
|
-
RESTRICT_ON_SEND = %i[delegate delegate_missing_to def_delegator def_delegators DelegateClass].freeze
|
|
127
|
-
|
|
128
123
|
# An argument reaches the body wrapped in one of these when the call site
|
|
129
124
|
# spreads or blocks it, and the wrapper says nothing about whose state it
|
|
130
125
|
# carries.
|
|
131
126
|
ARGUMENT_WRAPPERS = %i[hash pair array splat kwsplat block_pass].freeze
|
|
127
|
+
FORWARDER_MSG = 'Do not forward a collaborator\'s message. Delete the forwarder and let the caller navigate.'
|
|
128
|
+
MACRO_MSG = 'Do not use automatic delegation. Delete the forwarder and let the caller navigate.'
|
|
129
|
+
RESTRICT_ON_SEND = %i[delegate delegate_missing_to def_delegator def_delegators DelegateClass].freeze
|
|
132
130
|
|
|
133
131
|
# @!method mixes_in_enumerable?(node)
|
|
134
132
|
def_node_matcher :mixes_in_enumerable?, <<~PATTERN
|
|
@@ -170,6 +168,7 @@ module RuboCop
|
|
|
170
168
|
# same thing and both are read as one.
|
|
171
169
|
def own_class_target?(node)
|
|
172
170
|
target = to_option(node) || leading_target(node)
|
|
171
|
+
|
|
173
172
|
return false if target.nil?
|
|
174
173
|
return false unless target.type?(:sym, :str)
|
|
175
174
|
|
|
@@ -199,6 +198,7 @@ module RuboCop
|
|
|
199
198
|
return false unless node.method?(:each)
|
|
200
199
|
|
|
201
200
|
enclosing = node.each_ancestor(:class, :module, :sclass).first
|
|
201
|
+
|
|
202
202
|
return false if enclosing.nil?
|
|
203
203
|
return false if enclosing.sclass_type?
|
|
204
204
|
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rubocop'
|
|
4
|
+
|
|
5
|
+
module RuboCop
|
|
6
|
+
module Cop
|
|
7
|
+
module Style
|
|
8
|
+
# Requires a run of consecutive constant assignments to be sorted
|
|
9
|
+
# alphabetically by name. A lone constant is never flagged; a run is
|
|
10
|
+
# broken by any non-constant statement. Flags without autocorrecting — a
|
|
11
|
+
# constant may reference one declared above it, so a human decides the
|
|
12
|
+
# order. Experimental: if it flags more than it helps, drop it.
|
|
13
|
+
class OrderedConstants < ::RuboCop::Cop::Base
|
|
14
|
+
MSG = 'Sort constant assignments alphabetically (`%<name>s` should come before `%<previous>s`).'
|
|
15
|
+
|
|
16
|
+
def on_begin(node)
|
|
17
|
+
node.children.chunk { |child| constant_assignment?(child) }.each do |constants, run|
|
|
18
|
+
flag_unsorted(run) if constants
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def flag_unsorted(run)
|
|
25
|
+
run.each_cons(2) do |previous, current|
|
|
26
|
+
previous_name = constant_name(previous)
|
|
27
|
+
current_name = constant_name(current)
|
|
28
|
+
|
|
29
|
+
next if current_name >= previous_name
|
|
30
|
+
|
|
31
|
+
add_offense(current.loc.name, message: format(MSG, name: current_name, previous: previous_name))
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def constant_assignment?(node)
|
|
36
|
+
node.is_a?(::RuboCop::AST::Node) && node.casgn_type?
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def constant_name(node)
|
|
40
|
+
node.name.to_s
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
data/lib/rubocop-fourshark.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rubocop-fourshark
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.9.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Paulo Ribeiro
|
|
@@ -138,7 +138,9 @@ files:
|
|
|
138
138
|
- lib/rubocop-fourshark.rb
|
|
139
139
|
- lib/rubocop/cop/factory_bot/association_in_factory.rb
|
|
140
140
|
- lib/rubocop/cop/fourshark_cops.rb
|
|
141
|
+
- lib/rubocop/cop/layout/empty_line_before_guard_clause.rb
|
|
141
142
|
- lib/rubocop/cop/layout/multiline_statement_spacing.rb
|
|
143
|
+
- lib/rubocop/cop/layout/single_line_statement_spacing.rb
|
|
142
144
|
- lib/rubocop/cop/rails/bidirectional_association.rb
|
|
143
145
|
- lib/rubocop/cop/rails/mandatory_inverse_of.rb
|
|
144
146
|
- lib/rubocop/cop/rails/optional_belongs_to.rb
|
|
@@ -151,6 +153,7 @@ files:
|
|
|
151
153
|
- lib/rubocop/cop/style/disallow_safe_navigation.rb
|
|
152
154
|
- lib/rubocop/cop/style/disallow_ternary.rb
|
|
153
155
|
- lib/rubocop/cop/style/disallow_try.rb
|
|
156
|
+
- lib/rubocop/cop/style/ordered_constants.rb
|
|
154
157
|
- lib/rubocop/fourshark.rb
|
|
155
158
|
- lib/rubocop/fourshark/plugin.rb
|
|
156
159
|
- lib/rubocop/fourshark/version.rb
|
|
@@ -177,7 +180,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
177
180
|
- !ruby/object:Gem::Version
|
|
178
181
|
version: '0'
|
|
179
182
|
requirements: []
|
|
180
|
-
rubygems_version: 4.0.
|
|
183
|
+
rubygems_version: 4.0.16
|
|
181
184
|
specification_version: 4
|
|
182
185
|
summary: RuboCop extension enforcing 4Shark's Ruby, Rails, and RSpec conventions.
|
|
183
186
|
test_files: []
|