rubocop-canon 0.1.0 → 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 +4 -4
- data/README.md +30 -1
- data/config/default.yml +23 -3
- data/lib/rubocop/canon/version.rb +1 -1
- data/lib/rubocop/cop/canon/blank_line_help.rb +87 -0
- data/lib/rubocop/cop/canon/block_phases.rb +155 -0
- data/lib/rubocop/cop/canon/declaration_groups.rb +157 -0
- data/lib/rubocop/cop/canon/method_body_blank_lines.rb +142 -0
- data/lib/rubocop-canon.rb +4 -0
- metadata +21 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 24d224c2ecf5e361f702c300a79778baf8ad63247fb726cb8c39b89534d7dd3f
|
|
4
|
+
data.tar.gz: 8e4e0ca31f844a61bdba984c2efd2ccadb2d0ddea11693f38dc452c9b356e40c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b1eea01a2c5652d7513ccfe08b048cd61817f4f20c3dcb757c5e7b0e96ac99c92d0b52f1840d40b69a9c3c5b2eced42444c22ae29ed3461eedc83c1dbd4b5081
|
|
7
|
+
data.tar.gz: 98f0a6237ae0b71995af7bf03e8d8b46bcf83ebd92f3fa0ddf81f607a503ab71061b365f2e19a05279702791998be00d7cd1d621224dcdd965ae79d2d3fc2d84
|
data/README.md
CHANGED
|
@@ -6,7 +6,10 @@ Deterministic RuboCop cops that reduce Ruby code to canonical form. Given any in
|
|
|
6
6
|
|
|
7
7
|
| Cop | What it does |
|
|
8
8
|
|-----|-------------|
|
|
9
|
+
| `Canon/BlockPhases` | A block body ends in one unbroken run of calls, opened by one blank line |
|
|
10
|
+
| `Canon/DeclarationGroups` | One blank line between declaration groups, none inside a group |
|
|
9
11
|
| `Canon/KeywordShorthand` | `foo(bar: bar)` becomes `foo(bar:)` |
|
|
12
|
+
| `Canon/MethodBodyBlankLines` | One blank line around a method body's standalone calls, none anywhere else |
|
|
10
13
|
| `Canon/SortHash` | `{b: 1, a: 2}` becomes `{a: 2, b: 1}` |
|
|
11
14
|
| `Canon/SortKeywords` | `method(z: 1, a: 2)` becomes `method(a: 2, z: 1)` |
|
|
12
15
|
| `Canon/SortMethodArguments` | `attr_reader :z, :a` becomes `attr_reader :a, :z` |
|
|
@@ -29,6 +32,32 @@ plugins:
|
|
|
29
32
|
|
|
30
33
|
## Configuration
|
|
31
34
|
|
|
35
|
+
The blank-line cops are driven by name lists:
|
|
36
|
+
|
|
37
|
+
```yaml
|
|
38
|
+
Canon/BlockPhases:
|
|
39
|
+
Blocks: # only check blocks of these methods (required)
|
|
40
|
+
- it
|
|
41
|
+
TrailingMethods: # calls that make up the trailing phase (required)
|
|
42
|
+
- expect
|
|
43
|
+
MaxPhases: 3 # phases allowed before the trailing one
|
|
44
|
+
|
|
45
|
+
Canon/DeclarationGroups:
|
|
46
|
+
GroupedMethods: # method names that form one group
|
|
47
|
+
- [belongs_to, has_many, has_one]
|
|
48
|
+
- [validate, validates]
|
|
49
|
+
|
|
50
|
+
Canon/MethodBodyBlankLines:
|
|
51
|
+
SeparatedMethods: # calls that take a blank line beside them
|
|
52
|
+
- expose
|
|
53
|
+
- mail
|
|
54
|
+
- errors.add # dotted: matches a receiver and method together
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`Canon/BlockPhases` does nothing without both lists. `Canon/DeclarationGroups` treats every
|
|
58
|
+
method name as its own group until `GroupedMethods` merges them. `Canon/MethodBodyBlankLines`
|
|
59
|
+
only removes blank lines until `SeparatedMethods` says where they belong.
|
|
60
|
+
|
|
32
61
|
`Canon/SortHash` and the three sort cops accept:
|
|
33
62
|
|
|
34
63
|
```yaml
|
|
@@ -49,7 +78,7 @@ Canon/SortMethodArguments:
|
|
|
49
78
|
- delegate
|
|
50
79
|
```
|
|
51
80
|
|
|
52
|
-
|
|
81
|
+
Every cop is disabled by default. Enable the ones you want in your `.rubocop.yml`.
|
|
53
82
|
|
|
54
83
|
## License
|
|
55
84
|
|
data/config/default.yml
CHANGED
|
@@ -1,11 +1,31 @@
|
|
|
1
|
+
Canon/BlockPhases:
|
|
2
|
+
Description: 'Separate a block body into phases, ending in a trailing run of calls.'
|
|
3
|
+
Enabled: false
|
|
4
|
+
VersionAdded: '0.2'
|
|
5
|
+
Blocks: []
|
|
6
|
+
MaxPhases: 3
|
|
7
|
+
TrailingMethods: []
|
|
8
|
+
|
|
9
|
+
Canon/DeclarationGroups:
|
|
10
|
+
Description: 'Separate declaration groups with one blank line and nothing inside a group.'
|
|
11
|
+
Enabled: false
|
|
12
|
+
VersionAdded: '0.2'
|
|
13
|
+
GroupedMethods: []
|
|
14
|
+
|
|
1
15
|
Canon/KeywordShorthand:
|
|
2
16
|
Description: 'Use Ruby 3.1+ keyword shorthand where a local variable matches the key name.'
|
|
3
|
-
Enabled:
|
|
17
|
+
Enabled: false
|
|
4
18
|
VersionAdded: '0.1'
|
|
5
19
|
|
|
20
|
+
Canon/MethodBodyBlankLines:
|
|
21
|
+
Description: "One blank line around a method body's standalone calls, none anywhere else."
|
|
22
|
+
Enabled: false
|
|
23
|
+
VersionAdded: '0.2'
|
|
24
|
+
SeparatedMethods: []
|
|
25
|
+
|
|
6
26
|
Canon/SortHash:
|
|
7
27
|
Description: 'Sort hash keys alphabetically.'
|
|
8
|
-
Enabled:
|
|
28
|
+
Enabled: false
|
|
9
29
|
VersionAdded: '0.1'
|
|
10
30
|
ShorthandsFirst: false
|
|
11
31
|
ExcludeMethods: []
|
|
@@ -25,5 +45,5 @@ Canon/SortMethodArguments:
|
|
|
25
45
|
|
|
26
46
|
Canon/SortMethodDefinition:
|
|
27
47
|
Description: 'Sort keyword arguments in method definitions alphabetically.'
|
|
28
|
-
Enabled:
|
|
48
|
+
Enabled: false
|
|
29
49
|
VersionAdded: '0.1'
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Canon
|
|
6
|
+
# Shared mechanics for the cops that decide where a blank line belongs.
|
|
7
|
+
#
|
|
8
|
+
# A cop mixing this in supplies the decision — how many blank lines a
|
|
9
|
+
# pair of statements takes — and leaves the counting, the comment guard,
|
|
10
|
+
# the source ranges and the corrections here.
|
|
11
|
+
module BlankLineHelp
|
|
12
|
+
include RangeHelp
|
|
13
|
+
|
|
14
|
+
BLOCK_TYPES = %i[block numblock].freeze
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def body_statements(node)
|
|
19
|
+
body = node.body
|
|
20
|
+
return [] if body.nil?
|
|
21
|
+
return body.children if body.begin_type?
|
|
22
|
+
|
|
23
|
+
[body]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def blank_lines_between(previous, following)
|
|
27
|
+
following.first_line - previous.last_line - 1
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def comments_between?(previous, following)
|
|
31
|
+
processed_source.comments.any? do |comment|
|
|
32
|
+
comment.loc.line >= previous.last_line && comment.loc.line < following.first_line
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def register_extra_blank_line(previous, message)
|
|
37
|
+
add_offense(blank_line_range(previous), message:) do |corrector|
|
|
38
|
+
corrector.remove(newline_range(previous))
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def register_missing_blank_line(previous, following, message)
|
|
43
|
+
add_offense(first_line_range(following), message:) do |corrector|
|
|
44
|
+
corrector.insert_after(line_range(previous.last_line), "\n")
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def call_chain(node)
|
|
49
|
+
chain = []
|
|
50
|
+
current = unwrap_block(node)
|
|
51
|
+
|
|
52
|
+
while current.respond_to?(:send_type?) && current.send_type?
|
|
53
|
+
chain.unshift(current.method_name.to_s)
|
|
54
|
+
current = unwrap_block(current.receiver)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
chain
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def unwrap_block(node)
|
|
61
|
+
return if node.nil?
|
|
62
|
+
return node.send_node if BLOCK_TYPES.include?(node.type)
|
|
63
|
+
|
|
64
|
+
node
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def line_range(line)
|
|
68
|
+
processed_source.buffer.line_range(line)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def blank_line_range(previous)
|
|
72
|
+
line_range(previous.last_line + 1)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def newline_range(previous)
|
|
76
|
+
range_between(line_range(previous.last_line).end_pos, line_range(previous.last_line + 1).end_pos)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def first_line_range(node)
|
|
80
|
+
line_end = line_range(node.first_line).end_pos
|
|
81
|
+
|
|
82
|
+
range_between(node.source_range.begin_pos, [node.source_range.end_pos, line_end].min)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Canon
|
|
6
|
+
# Enforces the phase structure of a block body.
|
|
7
|
+
#
|
|
8
|
+
# A body checked by this cop ends in a *trailing phase*: the run of
|
|
9
|
+
# statements that call one of `TrailingMethods`. Exactly one blank line
|
|
10
|
+
# opens that run, no blank line splits it, and the statements before it
|
|
11
|
+
# form at most `MaxPhases` - 1 further phases.
|
|
12
|
+
#
|
|
13
|
+
# `Blocks` names the methods whose blocks are checked and
|
|
14
|
+
# `TrailingMethods` the calls that make up the trailing phase, matched
|
|
15
|
+
# against the leftmost call in a chain. Both are empty by default, so
|
|
16
|
+
# the cop does nothing until it is configured.
|
|
17
|
+
#
|
|
18
|
+
# A body carrying more phases than `MaxPhases` allows keeps the last
|
|
19
|
+
# separator before the trailing phase and loses the earlier ones —
|
|
20
|
+
# everything ahead of the last leading statement is one phase.
|
|
21
|
+
#
|
|
22
|
+
# @example Blocks: ['step'], TrailingMethods: ['report']
|
|
23
|
+
# # bad — no blank line opens the trailing phase
|
|
24
|
+
# step 'totals the order' do
|
|
25
|
+
# order = build_order
|
|
26
|
+
# report order.total
|
|
27
|
+
# end
|
|
28
|
+
#
|
|
29
|
+
# # bad — a blank line splits the trailing phase
|
|
30
|
+
# step 'totals the order' do
|
|
31
|
+
# order = build_order
|
|
32
|
+
#
|
|
33
|
+
# report order.total
|
|
34
|
+
#
|
|
35
|
+
# report order.currency
|
|
36
|
+
# end
|
|
37
|
+
#
|
|
38
|
+
# # good
|
|
39
|
+
# step 'totals the order' do
|
|
40
|
+
# order = build_order
|
|
41
|
+
#
|
|
42
|
+
# report order.total
|
|
43
|
+
# report order.currency
|
|
44
|
+
# end
|
|
45
|
+
class BlockPhases < Base
|
|
46
|
+
extend AutoCorrector
|
|
47
|
+
include BlankLineHelp
|
|
48
|
+
|
|
49
|
+
MSG_EXTRA = 'Remove the blank line inside the trailing phase.'
|
|
50
|
+
MSG_MISSING = 'Add a blank line before the trailing phase.'
|
|
51
|
+
MSG_TOO_MANY_PHASES = 'Use at most %<max_phases>d phases in a block body.'
|
|
52
|
+
|
|
53
|
+
def on_block(node)
|
|
54
|
+
return unless configured_block?(node)
|
|
55
|
+
|
|
56
|
+
check_phases(node)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
alias on_numblock on_block
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def check_phases(node)
|
|
64
|
+
statements = body_statements(node)
|
|
65
|
+
return if statements.size < 2
|
|
66
|
+
|
|
67
|
+
trailing_start = detect_trailing_start(statements)
|
|
68
|
+
return if trailing_start.nil?
|
|
69
|
+
|
|
70
|
+
check_trailing_phase(statements, trailing_start)
|
|
71
|
+
check_leading_phases(statements, trailing_start)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def check_trailing_phase(statements, trailing_start)
|
|
75
|
+
statements[trailing_start..].each_cons(2) do |previous, following|
|
|
76
|
+
next if comments_between?(previous, following)
|
|
77
|
+
next if blank_lines_between(previous, following) != 1
|
|
78
|
+
|
|
79
|
+
register_extra_blank_line(previous, MSG_EXTRA)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def check_leading_phases(statements, trailing_start)
|
|
84
|
+
return if trailing_start.zero?
|
|
85
|
+
|
|
86
|
+
check_separator(statements[trailing_start - 1], statements[trailing_start])
|
|
87
|
+
check_phase_count(statements[..(trailing_start - 1)])
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def check_separator(previous, following)
|
|
91
|
+
return if comments_between?(previous, following)
|
|
92
|
+
return if blank_lines_between(previous, following) == 1
|
|
93
|
+
|
|
94
|
+
register_missing_blank_line(previous, following, MSG_MISSING)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def check_phase_count(statements)
|
|
98
|
+
separators = leading_separators(statements)
|
|
99
|
+
return if separators.size < max_phases - 1
|
|
100
|
+
|
|
101
|
+
surplus = separators[0..-(max_phases - 1)]
|
|
102
|
+
|
|
103
|
+
surplus.each do |previous|
|
|
104
|
+
register_extra_blank_line(previous, format(MSG_TOO_MANY_PHASES, max_phases:))
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def leading_separators(statements)
|
|
109
|
+
statements.each_cons(2).filter_map do |previous, following|
|
|
110
|
+
next if comments_between?(previous, following)
|
|
111
|
+
|
|
112
|
+
previous if blank_lines_between(previous, following) == 1
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def detect_trailing_start(statements)
|
|
117
|
+
trailing_start = nil
|
|
118
|
+
|
|
119
|
+
statements.each_with_index.reverse_each do |statement, index|
|
|
120
|
+
break unless trailing?(statement)
|
|
121
|
+
|
|
122
|
+
trailing_start = index
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
trailing_start
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def configured_block?(node)
|
|
129
|
+
return false unless node.send_node.receiver.nil?
|
|
130
|
+
|
|
131
|
+
block_methods.include?(node.method_name.to_s)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def trailing?(node)
|
|
135
|
+
chain = call_chain(node)
|
|
136
|
+
return false if chain.empty?
|
|
137
|
+
|
|
138
|
+
trailing_methods.include?(chain.first)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def block_methods
|
|
142
|
+
@block_methods ||= Array(cop_config['Blocks']).map(&:to_s)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def trailing_methods
|
|
146
|
+
@trailing_methods ||= Array(cop_config['TrailingMethods']).map(&:to_s)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def max_phases
|
|
150
|
+
cop_config['MaxPhases']
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Canon
|
|
6
|
+
# Enforces blank lines between declaration groups in a class, module or
|
|
7
|
+
# DSL block body.
|
|
8
|
+
#
|
|
9
|
+
# `GroupedMethods` is the whole specification: two declarations calling
|
|
10
|
+
# methods listed in the same group take no blank line between them, and
|
|
11
|
+
# two calling methods listed in different groups take exactly one. The
|
|
12
|
+
# cop says nothing about a method name it has not been given, so a DSL
|
|
13
|
+
# it does not know stays as its author wrote it.
|
|
14
|
+
#
|
|
15
|
+
# A block is a group of one whatever it calls, so a DSL block always
|
|
16
|
+
# stands apart from the declarations around it. Everything else the cop
|
|
17
|
+
# has not been told about — an unlisted method name, a constant — it
|
|
18
|
+
# leaves alone.
|
|
19
|
+
#
|
|
20
|
+
# @example GroupedMethods: [[belongs_to, has_many], [validate, validates]]
|
|
21
|
+
# # bad
|
|
22
|
+
# class Shift < ApplicationRecord
|
|
23
|
+
# belongs_to :site
|
|
24
|
+
#
|
|
25
|
+
# has_many :shift_assignments, dependent: :destroy
|
|
26
|
+
# validates :starts_at, presence: true
|
|
27
|
+
# end
|
|
28
|
+
#
|
|
29
|
+
# # good
|
|
30
|
+
# class Shift < ApplicationRecord
|
|
31
|
+
# belongs_to :site
|
|
32
|
+
# has_many :shift_assignments, dependent: :destroy
|
|
33
|
+
#
|
|
34
|
+
# validates :starts_at, presence: true
|
|
35
|
+
# end
|
|
36
|
+
#
|
|
37
|
+
# # good — neither name is listed, so the cop leaves the body alone
|
|
38
|
+
# class Filtering < Capability::Base
|
|
39
|
+
# request_transformer RequestTransformer
|
|
40
|
+
# api_builder APIBuilder
|
|
41
|
+
# end
|
|
42
|
+
class DeclarationGroups < Base
|
|
43
|
+
extend AutoCorrector
|
|
44
|
+
include BlankLineHelp
|
|
45
|
+
|
|
46
|
+
MSG_EXTRA = 'Remove the blank line between declarations of the same group.'
|
|
47
|
+
MSG_MISSING = 'Add a blank line between declaration groups.'
|
|
48
|
+
ACCESS_MODIFIERS = %i[private protected public module_function private_class_method].freeze
|
|
49
|
+
DEFINITION_TYPES = %i[def defs].freeze
|
|
50
|
+
BLOCK_TYPES = %i[block numblock].freeze
|
|
51
|
+
STANDALONE_TYPES = %i[block numblock def defs].freeze
|
|
52
|
+
|
|
53
|
+
def on_class(node)
|
|
54
|
+
check_declarations(node)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def on_module(node)
|
|
58
|
+
check_declarations(node)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def on_block(node)
|
|
62
|
+
return unless declaration_context?(node)
|
|
63
|
+
|
|
64
|
+
check_declarations(node)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
alias on_numblock on_block
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def check_declarations(node)
|
|
72
|
+
statements = body_statements(node)
|
|
73
|
+
return if statements.size < 2
|
|
74
|
+
return unless statements.all? { |statement| declaration?(statement) }
|
|
75
|
+
|
|
76
|
+
statements.each_cons(2) do |previous, following|
|
|
77
|
+
next if definitions?(previous, following)
|
|
78
|
+
next if access_modifier?(previous)
|
|
79
|
+
next if access_modifier?(following)
|
|
80
|
+
next unless grouped?(previous, following)
|
|
81
|
+
|
|
82
|
+
check_gap(previous, following, expected_blank_lines(previous, following))
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def check_gap(previous, following, expected)
|
|
87
|
+
return if comments_between?(previous, following)
|
|
88
|
+
|
|
89
|
+
blank_lines = blank_lines_between(previous, following)
|
|
90
|
+
return if blank_lines == expected
|
|
91
|
+
return if blank_lines > 1
|
|
92
|
+
|
|
93
|
+
return register_extra_blank_line(previous, MSG_EXTRA) if expected.zero?
|
|
94
|
+
|
|
95
|
+
register_missing_blank_line(previous, following, MSG_MISSING)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def grouped?(previous, following)
|
|
99
|
+
return true if standalone?(previous)
|
|
100
|
+
return true if standalone?(following)
|
|
101
|
+
return false if group_key(previous).nil?
|
|
102
|
+
|
|
103
|
+
!group_key(following).nil?
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def expected_blank_lines(previous, following)
|
|
107
|
+
return 1 if standalone?(previous)
|
|
108
|
+
return 1 if standalone?(following)
|
|
109
|
+
return 0 if group_key(previous) == group_key(following)
|
|
110
|
+
|
|
111
|
+
1
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def standalone?(node)
|
|
115
|
+
STANDALONE_TYPES.include?(node.type)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def group_key(node)
|
|
119
|
+
return unless node.send_type?
|
|
120
|
+
|
|
121
|
+
grouped_methods.index { |group| group.include?(node.method_name.to_s) }
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def declaration_context?(node)
|
|
125
|
+
enclosing = node.each_ancestor(:def, :defs, :class, :module).first
|
|
126
|
+
return false if enclosing.nil?
|
|
127
|
+
return true if enclosing.class_type?
|
|
128
|
+
|
|
129
|
+
enclosing.module_type?
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def declaration?(node)
|
|
133
|
+
return true if node.casgn_type?
|
|
134
|
+
return true if DEFINITION_TYPES.include?(node.type)
|
|
135
|
+
return node.send_node.receiver.nil? if BLOCK_TYPES.include?(node.type)
|
|
136
|
+
|
|
137
|
+
node.send_type? && node.receiver.nil?
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def definitions?(previous, following)
|
|
141
|
+
DEFINITION_TYPES.include?(previous.type) && DEFINITION_TYPES.include?(following.type)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def access_modifier?(node)
|
|
145
|
+
return false unless node.send_type?
|
|
146
|
+
return false unless node.arguments.empty?
|
|
147
|
+
|
|
148
|
+
ACCESS_MODIFIERS.include?(node.method_name)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def grouped_methods
|
|
152
|
+
@grouped_methods ||= Array(cop_config['GroupedMethods']).map { |group| Array(group).map(&:to_s) }
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Canon
|
|
6
|
+
# Enforces blank lines around a method body's standalone calls and
|
|
7
|
+
# nowhere else.
|
|
8
|
+
#
|
|
9
|
+
# `SeparatedMethods` names the calls that stand on their own —
|
|
10
|
+
# `authorize!`, `expose`, `mail`. Each takes exactly one blank line
|
|
11
|
+
# between it and any statement next to it. Every other pair of
|
|
12
|
+
# statements takes none, because a blank line there marks a step that
|
|
13
|
+
# wants a name: extract a method instead.
|
|
14
|
+
#
|
|
15
|
+
# The list is empty by default, so the cop only removes until it is
|
|
16
|
+
# configured.
|
|
17
|
+
#
|
|
18
|
+
# The position after a guard clause belongs to
|
|
19
|
+
# `Layout/EmptyLineAfterGuardClause`, so this cop leaves it alone.
|
|
20
|
+
#
|
|
21
|
+
# An entry matches a method name anywhere in the call chain. Write it
|
|
22
|
+
# dotted — `errors.add` — to match a receiver and method together, so
|
|
23
|
+
# `coverage_request.errors.add(...)` matches and a bare `add` does not.
|
|
24
|
+
#
|
|
25
|
+
# @example SeparatedMethods: ['authorize!', 'expose']
|
|
26
|
+
# # bad
|
|
27
|
+
# def index
|
|
28
|
+
# authorize!
|
|
29
|
+
# shift = find_shift
|
|
30
|
+
# employees = Employee.for_shift(shift)
|
|
31
|
+
# expose employees
|
|
32
|
+
# end
|
|
33
|
+
#
|
|
34
|
+
# # good
|
|
35
|
+
# def index
|
|
36
|
+
# authorize!
|
|
37
|
+
#
|
|
38
|
+
# shift = find_shift
|
|
39
|
+
# employees = Employee.for_shift(shift)
|
|
40
|
+
#
|
|
41
|
+
# expose employees
|
|
42
|
+
# end
|
|
43
|
+
#
|
|
44
|
+
# # good — no standalone call, so no blank line
|
|
45
|
+
# def reset
|
|
46
|
+
# Current.session = nil
|
|
47
|
+
# Current.account = nil
|
|
48
|
+
# end
|
|
49
|
+
#
|
|
50
|
+
# # good — the guard clause owns its own blank line
|
|
51
|
+
# def total
|
|
52
|
+
# items = order.items
|
|
53
|
+
# return 0 if items.empty?
|
|
54
|
+
#
|
|
55
|
+
# items.sum(&:amount)
|
|
56
|
+
# end
|
|
57
|
+
class MethodBodyBlankLines < Base
|
|
58
|
+
extend AutoCorrector
|
|
59
|
+
include BlankLineHelp
|
|
60
|
+
|
|
61
|
+
MSG_EXTRA = 'Remove the blank line inside the method body.'
|
|
62
|
+
MSG_MISSING = 'Add a blank line around the standalone call.'
|
|
63
|
+
JUMP_METHODS = %i[raise fail throw].freeze
|
|
64
|
+
|
|
65
|
+
def on_def(node)
|
|
66
|
+
check_method_body(node)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def on_defs(node)
|
|
70
|
+
check_method_body(node)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def check_method_body(node)
|
|
76
|
+
statements = body_statements(node)
|
|
77
|
+
return if statements.size < 2
|
|
78
|
+
|
|
79
|
+
statements.each_cons(2) do |previous, following|
|
|
80
|
+
next if guard?(previous)
|
|
81
|
+
|
|
82
|
+
check_gap(previous, following, expected_blank_lines(previous, following))
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def check_gap(previous, following, expected)
|
|
87
|
+
return if comments_between?(previous, following)
|
|
88
|
+
|
|
89
|
+
blank_lines = blank_lines_between(previous, following)
|
|
90
|
+
return if blank_lines == expected
|
|
91
|
+
return if blank_lines > 1
|
|
92
|
+
|
|
93
|
+
return register_extra_blank_line(previous, MSG_EXTRA) if expected.zero?
|
|
94
|
+
|
|
95
|
+
register_missing_blank_line(previous, following, MSG_MISSING)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def expected_blank_lines(previous, following)
|
|
99
|
+
return 1 if separated?(previous)
|
|
100
|
+
return 1 if separated?(following)
|
|
101
|
+
|
|
102
|
+
0
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def separated?(node)
|
|
106
|
+
chain = call_chain(node)
|
|
107
|
+
return false if chain.empty?
|
|
108
|
+
|
|
109
|
+
separated_methods.any? { |name| matches_chain?(chain, name) }
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def matches_chain?(chain, name)
|
|
113
|
+
return chain.include?(name) unless name.include?('.')
|
|
114
|
+
|
|
115
|
+
chain.each_cons(2).any? { |pair| pair.join('.') == name }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def guard?(node)
|
|
119
|
+
return true if jump?(node)
|
|
120
|
+
return false unless node.if_type?
|
|
121
|
+
return false unless node.modifier_form?
|
|
122
|
+
return true if jump?(node.if_branch)
|
|
123
|
+
|
|
124
|
+
jump?(node.else_branch)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def jump?(node)
|
|
128
|
+
return false if node.nil?
|
|
129
|
+
return true if node.return_type?
|
|
130
|
+
return true if node.break_type?
|
|
131
|
+
return true if node.next_type?
|
|
132
|
+
|
|
133
|
+
node.send_type? && JUMP_METHODS.include?(node.method_name)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def separated_methods
|
|
137
|
+
@separated_methods ||= Array(cop_config['SeparatedMethods']).map(&:to_s)
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
data/lib/rubocop-canon.rb
CHANGED
|
@@ -3,7 +3,11 @@
|
|
|
3
3
|
require 'rubocop'
|
|
4
4
|
require_relative 'rubocop/canon/version'
|
|
5
5
|
require_relative 'rubocop/canon/plugin'
|
|
6
|
+
require_relative 'rubocop/cop/canon/blank_line_help'
|
|
7
|
+
require_relative 'rubocop/cop/canon/block_phases'
|
|
8
|
+
require_relative 'rubocop/cop/canon/declaration_groups'
|
|
6
9
|
require_relative 'rubocop/cop/canon/keyword_shorthand'
|
|
10
|
+
require_relative 'rubocop/cop/canon/method_body_blank_lines'
|
|
7
11
|
require_relative 'rubocop/cop/canon/sort_hash'
|
|
8
12
|
require_relative 'rubocop/cop/canon/sort_keywords'
|
|
9
13
|
require_relative 'rubocop/cop/canon/sort_method_arguments'
|
metadata
CHANGED
|
@@ -1,15 +1,29 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rubocop-canon
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1
|
|
4
|
+
version: 0.2.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- skiftle
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-09-06 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: lefthook
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '2.1'
|
|
20
|
+
type: :development
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '2.1'
|
|
13
27
|
- !ruby/object:Gem::Dependency
|
|
14
28
|
name: lint_roller
|
|
15
29
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -56,7 +70,11 @@ files:
|
|
|
56
70
|
- lib/rubocop-canon.rb
|
|
57
71
|
- lib/rubocop/canon/plugin.rb
|
|
58
72
|
- lib/rubocop/canon/version.rb
|
|
73
|
+
- lib/rubocop/cop/canon/blank_line_help.rb
|
|
74
|
+
- lib/rubocop/cop/canon/block_phases.rb
|
|
75
|
+
- lib/rubocop/cop/canon/declaration_groups.rb
|
|
59
76
|
- lib/rubocop/cop/canon/keyword_shorthand.rb
|
|
77
|
+
- lib/rubocop/cop/canon/method_body_blank_lines.rb
|
|
60
78
|
- lib/rubocop/cop/canon/sort_hash.rb
|
|
61
79
|
- lib/rubocop/cop/canon/sort_keywords.rb
|
|
62
80
|
- lib/rubocop/cop/canon/sort_method_arguments.rb
|
|
@@ -84,7 +102,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
84
102
|
- !ruby/object:Gem::Version
|
|
85
103
|
version: '0'
|
|
86
104
|
requirements: []
|
|
87
|
-
rubygems_version: 3.4.
|
|
105
|
+
rubygems_version: 3.4.19
|
|
88
106
|
signing_key:
|
|
89
107
|
specification_version: 4
|
|
90
108
|
summary: Deterministic RuboCop cops for canonical Ruby form
|