rubocop-vicenzo 0.1.1 → 0.3.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/.rubocop.yml +17 -0
- data/CHANGELOG.md +18 -0
- data/README.md +8 -0
- data/config/default.yml +27 -3
- data/lib/rubocop/cop/vicenzo/layout/multiline_method_call_line_breaks.rb +164 -0
- data/lib/rubocop/cop/vicenzo/rspec/inconsistent_sibling_structure.rb +117 -0
- data/lib/rubocop/cop/vicenzo/rspec/leaky_definition.rb +104 -0
- data/lib/rubocop/cop/vicenzo/rspec/nested_context_improper_start.rb +25 -12
- data/lib/rubocop/cop/vicenzo/rspec/nested_let_redefinition.rb +3 -2
- data/lib/rubocop/cop/vicenzo/rspec/nested_subject_redefinition.rb +1 -1
- data/lib/rubocop/cop/vicenzo/style/multiline_method_call_parentheses.rb +78 -0
- data/lib/rubocop/cop/vicenzo_cops.rb +4 -1
- data/lib/rubocop/vicenzo/version.rb +1 -1
- data/rakelib/release.rake +73 -0
- metadata +8 -4
- data/lib/rubocop/cop/vicenzo/rspec/mixed_example_groups.rb +0 -65
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 685f19680a9d5721ed006772467155092d9d441c95c035191e3fa9dc449eac65
|
|
4
|
+
data.tar.gz: 22fa73c161855ced3561903c9a901ca19beec2904fe326402f426ec93040dd78
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8ea51ee079b7a796cb1f76c8630f9e78a8c8e4fe191f06e7096d4813744a8623a8b35b43288c7b24a671383d65b1ec4a76db72601bc32484ad5945009bb6a1d0
|
|
7
|
+
data.tar.gz: 795995a84e448e543fbc5d68a6c0434fd0f7ad62dc19e57727d6da251bcfc0bc9624c7eaa78e40a802d5e08d924dc85a2a3512b3d7a868388bb62e22df7447ce
|
data/.rubocop.yml
CHANGED
|
@@ -6,8 +6,20 @@ plugins:
|
|
|
6
6
|
- rubocop-internal_affairs
|
|
7
7
|
|
|
8
8
|
AllCops:
|
|
9
|
+
Exclude:
|
|
10
|
+
- 'bin/**/*'
|
|
11
|
+
- 'vendor/**/*'
|
|
9
12
|
NewCops: enable
|
|
10
13
|
|
|
14
|
+
InternalAffairs/UndefinedConfig: # False positive because conflicts with Rubocop Layout namespace
|
|
15
|
+
Exclude:
|
|
16
|
+
- 'lib/rubocop/cop/vicenzo/layout/**/*'
|
|
17
|
+
|
|
18
|
+
Metrics/BlockLength:
|
|
19
|
+
Exclude:
|
|
20
|
+
- 'rakelib/**/*'
|
|
21
|
+
- 'Rakefile'
|
|
22
|
+
|
|
11
23
|
Naming/FileName:
|
|
12
24
|
Exclude:
|
|
13
25
|
- lib/rubocop-vicenzo.rb
|
|
@@ -25,3 +37,8 @@ RSpec/ExampleLength:
|
|
|
25
37
|
|
|
26
38
|
RSpec/NestedGroups:
|
|
27
39
|
Enabled: false
|
|
40
|
+
|
|
41
|
+
Vicenzo/RSpec/LeakyDefinition:
|
|
42
|
+
Exclude:
|
|
43
|
+
- 'spec/support/**/*'
|
|
44
|
+
- 'spec/rubocop/cop/vicenzo/rspec/leaky_definition_spec.rb'
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.3.0] - 2025-12-17
|
|
4
|
+
|
|
5
|
+
- Add RuboCop::Cop::Vicenzo::Layout::MultilineMethodCallLineBreaks #12;
|
|
6
|
+
- Add RuboCop::Cop::Vicenzo::Style::MultilineMethodCallParentheses #13;
|
|
7
|
+
- Add `AllowedMethods` configuration to `Vicenzo/Style/MultilineMethodCallParentheses` to allow excluding specific methods (e.g., RSpec DSLs like `to` and `change`) from the rule #15;
|
|
8
|
+
|
|
9
|
+
## [0.2.0] - 2025-11-27
|
|
10
|
+
|
|
11
|
+
- Remove RuboCop::Cop::Vicenzo::RSpec::MixedExampleGroups in favor of InconsistentSiblingStructure #10;
|
|
12
|
+
|
|
13
|
+
- Add RoboCop::Cop::Vicenzo::RSpec::LeakyDefinition #9;
|
|
14
|
+
- Add RoboCop::Cop::Vicenzo::RSpec::InconsistentSiblingStructure #10;
|
|
15
|
+
|
|
16
|
+
- Fix NestedContextImproperStart to deal with all nested contexts #10;
|
|
17
|
+
- Fix NestedLetRedefinition to not point sibling lets as nested #10;
|
|
18
|
+
- Fix NestedSubjectRedefinition to not point sibling lets as nested #10;
|
|
19
|
+
|
|
20
|
+
|
|
3
21
|
## [0.1.1] - 2025-08-12
|
|
4
22
|
|
|
5
23
|
- Add Rightly enable all cops #7;
|
data/README.md
CHANGED
|
@@ -55,6 +55,14 @@ After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
|
|
55
55
|
|
|
56
56
|
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
|
57
57
|
|
|
58
|
+
### Generate binstubs
|
|
59
|
+
|
|
60
|
+
If you want is possible change the command `bundle exec something` by `bin/something` generating binstubs
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
bundle binstubs rake rspec-core rubocop
|
|
64
|
+
```
|
|
65
|
+
|
|
58
66
|
### Creating a new cop
|
|
59
67
|
|
|
60
68
|
```bash
|
data/config/default.yml
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
|
+
Vicenzo/Layout/MultilineMethodCallLineBreaks:
|
|
2
|
+
Description: 'Enforces that method calls in a multiline chain must each be on their own line.'
|
|
3
|
+
Enabled: true
|
|
4
|
+
Severity: convention
|
|
5
|
+
IndentationWidth: 2
|
|
6
|
+
VersionAdded: '0.3.0'
|
|
7
|
+
|
|
1
8
|
Vicenzo/Rails/EnumInclusionOfValidation:
|
|
2
9
|
Description: 'Check if the enum has the inclusion of validation defined.'
|
|
3
10
|
Enabled: true
|
|
4
11
|
Severity: convention
|
|
5
12
|
VersionAdded: '0.1.0'
|
|
6
13
|
|
|
7
|
-
Vicenzo/RSpec/
|
|
8
|
-
Description: '
|
|
14
|
+
Vicenzo/RSpec/InconsistentSiblingStructure:
|
|
15
|
+
Description: 'Enforces strict structural consistency (e.g. prevents mixing describe with context or examples with groups).'
|
|
9
16
|
Enabled: true
|
|
10
17
|
Severity: warning
|
|
11
|
-
VersionAdded: '0.
|
|
18
|
+
VersionAdded: '0.2.0'
|
|
12
19
|
|
|
13
20
|
Vicenzo/RSpec/NestedContextImproperStart:
|
|
14
21
|
Description: 'Check if the nested context does not start as a root one.'
|
|
@@ -27,3 +34,20 @@ Vicenzo/RSpec/NestedSubjectRedefinition:
|
|
|
27
34
|
Enabled: true
|
|
28
35
|
Severity: warning
|
|
29
36
|
VersionAdded: '0.1.0'
|
|
37
|
+
|
|
38
|
+
Vicenzo/RSpec/LeakyDefinition:
|
|
39
|
+
Description: 'Do not define methods, classes, or modules directly in spec files (unless inside spec/support).'
|
|
40
|
+
Enabled: true
|
|
41
|
+
Severity: warning
|
|
42
|
+
VersionAdded: '0.2.0'
|
|
43
|
+
Include:
|
|
44
|
+
- '**/spec/**/*_spec.rb'
|
|
45
|
+
Exclude:
|
|
46
|
+
- '**/spec/support/**/*'
|
|
47
|
+
- '**/spec/factories/**/*'
|
|
48
|
+
|
|
49
|
+
Vicenzo/Style/MultilineMethodCallParentheses:
|
|
50
|
+
Description: 'Enforces parentheses for method calls with arguments that span multiple lines.'
|
|
51
|
+
Enabled: true
|
|
52
|
+
AllowedMethods: []
|
|
53
|
+
VersionAdded: '0.3.0'
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Vicenzo
|
|
6
|
+
module Layout
|
|
7
|
+
# Enforces that method calls in a multiline chain are each on their own line.
|
|
8
|
+
#
|
|
9
|
+
# If a method chain spans more than one line, this cop ensures that every
|
|
10
|
+
# call in the chain is placed on a new line. It prevents "mixed" styles
|
|
11
|
+
# where some methods are on the same line as the receiver while others are broken.
|
|
12
|
+
#
|
|
13
|
+
# @example
|
|
14
|
+
# # bad
|
|
15
|
+
# object.method_one
|
|
16
|
+
# .method_two
|
|
17
|
+
#
|
|
18
|
+
# # bad
|
|
19
|
+
# object
|
|
20
|
+
# .method_one.method_two
|
|
21
|
+
# .method_three
|
|
22
|
+
#
|
|
23
|
+
# # good - single line chain
|
|
24
|
+
# object.method_one.method_two
|
|
25
|
+
#
|
|
26
|
+
# # good - multiline chain
|
|
27
|
+
# object
|
|
28
|
+
# .method_one
|
|
29
|
+
# .method_two
|
|
30
|
+
#
|
|
31
|
+
# # good - arguments causing the break (if configured implicitly)
|
|
32
|
+
# object.method_one(
|
|
33
|
+
# arg1,
|
|
34
|
+
# arg2
|
|
35
|
+
# ).method_two
|
|
36
|
+
#
|
|
37
|
+
# ## Configuration
|
|
38
|
+
#
|
|
39
|
+
# This cop allows you to customize the indentation width used during auto-correction.
|
|
40
|
+
# The default width is 2 spaces relative to the previous line.
|
|
41
|
+
#
|
|
42
|
+
# ```yaml
|
|
43
|
+
# CustomCops/MultilineMethodCallLineBreaks:
|
|
44
|
+
# IndentationWidth: 4 # (default is 2)
|
|
45
|
+
# ```
|
|
46
|
+
#
|
|
47
|
+
class MultilineMethodCallLineBreaks < Base
|
|
48
|
+
extend RuboCop::Cop::AutoCorrector
|
|
49
|
+
|
|
50
|
+
MSG = 'Method calls in a multiline chain must each be on their own line.'
|
|
51
|
+
DEFAULT_INDENTATION_WIDTH = 2
|
|
52
|
+
LEADING_SPACES_PATTERN = /\A */
|
|
53
|
+
CHAIN_START_PATTERN = /\A\s*&?\./
|
|
54
|
+
|
|
55
|
+
OPERATOR_METHODS = %i[[] []= + - * / % ** << >>].freeze
|
|
56
|
+
|
|
57
|
+
def on_send(node)
|
|
58
|
+
check_node(node)
|
|
59
|
+
end
|
|
60
|
+
alias on_csend on_send
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def check_node(node)
|
|
65
|
+
return if part_of_larger_chain?(node)
|
|
66
|
+
return if single_line_chain?(node)
|
|
67
|
+
|
|
68
|
+
check_chain_structure(node)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def check_chain_structure(node)
|
|
72
|
+
current = node
|
|
73
|
+
|
|
74
|
+
while current.call_type?
|
|
75
|
+
receiver = current.receiver
|
|
76
|
+
break unless receiver
|
|
77
|
+
|
|
78
|
+
check_violation(current, receiver)
|
|
79
|
+
current = receiver
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def check_violation(node, receiver)
|
|
84
|
+
return unless same_line?(receiver, node)
|
|
85
|
+
return if valid_same_line_exception?(node)
|
|
86
|
+
|
|
87
|
+
add_offense(offense_range(node)) do |corrector|
|
|
88
|
+
break_line_before_dot(corrector, node, receiver)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def valid_same_line_exception?(node)
|
|
93
|
+
arguments_cause_multiline?(node) || operator_method?(node)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def operator_method?(node)
|
|
97
|
+
OPERATOR_METHODS.include?(node.method_name)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def part_of_larger_chain?(node)
|
|
101
|
+
parent = node.parent
|
|
102
|
+
parent&.call_type? && parent.receiver == node
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def single_line_chain?(node)
|
|
106
|
+
root = root_node(node)
|
|
107
|
+
root.loc.last_line == node.loc.last_line
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def root_node(node)
|
|
111
|
+
current = node
|
|
112
|
+
current = current.receiver while current.respond_to?(:receiver) && current.receiver
|
|
113
|
+
current
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def same_line?(receiver, node)
|
|
117
|
+
receiver.loc.last_line == call_start_line(node)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def call_start_line(node)
|
|
121
|
+
node.loc.dot ? node.loc.dot.line : node.loc.selector.line
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def arguments_cause_multiline?(node)
|
|
125
|
+
return false if node.arguments.empty?
|
|
126
|
+
return false if node.receiver.loc.last_line != call_start_line(node)
|
|
127
|
+
|
|
128
|
+
return false if node.receiver.call_type? && node.receiver.loc.dot
|
|
129
|
+
|
|
130
|
+
node.multiline?
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def offense_range(node)
|
|
134
|
+
return node.loc.selector unless node.loc.dot
|
|
135
|
+
return node.loc.dot unless node.loc.selector
|
|
136
|
+
|
|
137
|
+
node.loc.dot.join(node.loc.selector)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def break_line_before_dot(corrector, node, receiver)
|
|
141
|
+
dot = node.loc.dot
|
|
142
|
+
return unless dot
|
|
143
|
+
|
|
144
|
+
last_line_index = receiver.loc.last_line - 1
|
|
145
|
+
last_line_source = processed_source.lines[last_line_index]
|
|
146
|
+
|
|
147
|
+
current_indentation = last_line_source[LEADING_SPACES_PATTERN].length
|
|
148
|
+
|
|
149
|
+
previous_line_is_chain = last_line_source.match?(CHAIN_START_PATTERN)
|
|
150
|
+
extra_indent = previous_line_is_chain ? 0 : indentation_width
|
|
151
|
+
|
|
152
|
+
indentation = ' ' * (current_indentation + extra_indent)
|
|
153
|
+
|
|
154
|
+
corrector.insert_before(dot, "\n#{indentation}")
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def indentation_width
|
|
158
|
+
cop_config.fetch('IndentationWidth', DEFAULT_INDENTATION_WIDTH)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Vicenzo
|
|
6
|
+
module RSpec
|
|
7
|
+
# Enforces strict structural consistency in RSpec files.
|
|
8
|
+
#
|
|
9
|
+
# It forbids mixing:
|
|
10
|
+
# 1. Examples (`it`) with Groups (`describe` or `context`)
|
|
11
|
+
# 2. Different types of Groups (`describe` with `context`)
|
|
12
|
+
#
|
|
13
|
+
# @example
|
|
14
|
+
# # bad (Mixing Describe and Context)
|
|
15
|
+
# RSpec.describe User do
|
|
16
|
+
# describe '#admin?' do ... end
|
|
17
|
+
# context 'when user is logged' do ... end
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# # bad (Mixing Example and Context)
|
|
21
|
+
# RSpec.describe User do
|
|
22
|
+
# it { is_expected.to be_valid }
|
|
23
|
+
# context 'when invalid' do ... end
|
|
24
|
+
# end
|
|
25
|
+
#
|
|
26
|
+
# # good
|
|
27
|
+
# RSpec.describe User do
|
|
28
|
+
# describe '#admin?' do ... end
|
|
29
|
+
# describe '#client?' do ... end
|
|
30
|
+
# end
|
|
31
|
+
#
|
|
32
|
+
class InconsistentSiblingStructure < RuboCop::Cop::RSpec::Base
|
|
33
|
+
MSG = 'Do not mix %<type_a>s with %<type_b>s at the same level.'
|
|
34
|
+
|
|
35
|
+
EXAMPLES = %i[it specify example scenario focus].freeze
|
|
36
|
+
DESCRIBES = %i[describe feature experiment].freeze
|
|
37
|
+
CONTEXTS = %i[context].freeze
|
|
38
|
+
|
|
39
|
+
# @!method example_definition?(node)
|
|
40
|
+
def_node_matcher :example_definition?, <<~PATTERN
|
|
41
|
+
(block (send nil? {#{EXAMPLES.map(&:inspect).join(' ')}} ...) ...)
|
|
42
|
+
PATTERN
|
|
43
|
+
|
|
44
|
+
# @!method describe_definition?(node)
|
|
45
|
+
def_node_matcher :describe_definition?, <<~PATTERN
|
|
46
|
+
(block (send nil? {#{DESCRIBES.map(&:inspect).join(' ')}} ...) ...)
|
|
47
|
+
PATTERN
|
|
48
|
+
|
|
49
|
+
# @!method context_definition?(node)
|
|
50
|
+
def_node_matcher :context_definition?, <<~PATTERN
|
|
51
|
+
(block (send nil? {#{CONTEXTS.map(&:inspect).join(' ')}} ...) ...)
|
|
52
|
+
PATTERN
|
|
53
|
+
|
|
54
|
+
def on_block(node)
|
|
55
|
+
return unless example_group?(node)
|
|
56
|
+
return unless node.body
|
|
57
|
+
|
|
58
|
+
# Normaliza e classifica em passos separados
|
|
59
|
+
children = child_nodes_for(node)
|
|
60
|
+
found_nodes = classify_children(children)
|
|
61
|
+
|
|
62
|
+
validate_consistency(found_nodes)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
alias on_numblock on_block
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def child_nodes_for(node)
|
|
70
|
+
node.body.begin_type? ? node.body.each_child_node : [node.body]
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def classify_children(nodes)
|
|
74
|
+
classified = { example: [], describe: [], context: [] }
|
|
75
|
+
|
|
76
|
+
nodes.each do |child|
|
|
77
|
+
next unless child.block_type?
|
|
78
|
+
|
|
79
|
+
type = node_type(child)
|
|
80
|
+
classified[type] << child if type
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
classified
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def node_type(node)
|
|
87
|
+
if example_definition?(node)
|
|
88
|
+
:example
|
|
89
|
+
elsif describe_definition?(node)
|
|
90
|
+
:describe
|
|
91
|
+
elsif context_definition?(node)
|
|
92
|
+
:context
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def validate_consistency(nodes)
|
|
97
|
+
present_types = nodes.keys.select { |type| nodes[type].any? }
|
|
98
|
+
|
|
99
|
+
return if present_types.size <= 1
|
|
100
|
+
|
|
101
|
+
check_pair(nodes, :example, :describe)
|
|
102
|
+
check_pair(nodes, :example, :context)
|
|
103
|
+
check_pair(nodes, :describe, :context)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def check_pair(nodes, type_a, type_b)
|
|
107
|
+
return unless nodes[type_a].any? && nodes[type_b].any?
|
|
108
|
+
|
|
109
|
+
nodes[type_b].each do |node|
|
|
110
|
+
add_offense(node, message: format(MSG, type_a: type_a, type_b: type_b))
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rubocop'
|
|
4
|
+
|
|
5
|
+
module RuboCop
|
|
6
|
+
module Cop
|
|
7
|
+
module Vicenzo
|
|
8
|
+
module RSpec
|
|
9
|
+
# Checks for methods, classes, or modules defined directly within
|
|
10
|
+
# RSpec blocks or at the top level of a spec file.
|
|
11
|
+
#
|
|
12
|
+
# Such definitions pollute the global namespace or the test class scope,
|
|
13
|
+
# leading to state leaking and intermittent test failures.
|
|
14
|
+
#
|
|
15
|
+
# @example
|
|
16
|
+
# # bad
|
|
17
|
+
# describe User do
|
|
18
|
+
# def setup_user
|
|
19
|
+
# # ...
|
|
20
|
+
# end
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# # bad
|
|
24
|
+
# def global_helper
|
|
25
|
+
# end
|
|
26
|
+
#
|
|
27
|
+
# # bad
|
|
28
|
+
# def stub_service
|
|
29
|
+
# allow(Service).to receive(:call)
|
|
30
|
+
# end
|
|
31
|
+
#
|
|
32
|
+
# # good
|
|
33
|
+
# describe User do
|
|
34
|
+
# let(:user) { create(:user) }
|
|
35
|
+
# end
|
|
36
|
+
#
|
|
37
|
+
# # good (inside anonymous class)
|
|
38
|
+
# let(:model) do
|
|
39
|
+
# Class.new do
|
|
40
|
+
# def safe_method; end
|
|
41
|
+
# end
|
|
42
|
+
# end
|
|
43
|
+
#
|
|
44
|
+
# # good
|
|
45
|
+
# before do
|
|
46
|
+
# allow(Service).to receive(:call)
|
|
47
|
+
# end
|
|
48
|
+
#
|
|
49
|
+
class LeakyDefinition < RuboCop::Cop::RSpec::Base
|
|
50
|
+
MSG = 'Do not define methods, classes, or modules directly in the global scope or within spec blocks. ' \
|
|
51
|
+
'This pollutes the namespace. ' \
|
|
52
|
+
'Move this logic to `spec/support`, use `let`, before, ' \
|
|
53
|
+
'or encapsulate it within a safe structure (e.g., `Class.new`).'
|
|
54
|
+
|
|
55
|
+
# Matcher to identify dynamic class/module definitions commonly used in specs
|
|
56
|
+
# Looks for blocks applied to Class.new, Module.new, or Struct.new
|
|
57
|
+
# considers controller {} a valid mock
|
|
58
|
+
# @!method dynamic_definition?(node)
|
|
59
|
+
def_node_matcher :dynamic_definition?, <<~PATTERN
|
|
60
|
+
(block
|
|
61
|
+
{
|
|
62
|
+
(send (const {nil? cbase} {:Class :Module :Struct}) :new ...)
|
|
63
|
+
(send nil? :controller ...)
|
|
64
|
+
}
|
|
65
|
+
...
|
|
66
|
+
)
|
|
67
|
+
PATTERN
|
|
68
|
+
|
|
69
|
+
def on_def(node)
|
|
70
|
+
check_node(node)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def on_class(node)
|
|
74
|
+
check_node(node)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def on_module(node)
|
|
78
|
+
check_node(node)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
def check_node(node)
|
|
84
|
+
# If inside a safe scope (static or dynamic class/module), it's allowed.
|
|
85
|
+
return if inside_safe_scope?(node)
|
|
86
|
+
|
|
87
|
+
add_offense(node)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def inside_safe_scope?(node)
|
|
91
|
+
# Traverse ancestors to find a "protective shield"
|
|
92
|
+
node.each_ancestor.any? do |ancestor|
|
|
93
|
+
# 1. Is it a traditional definition? (class Foo; end)
|
|
94
|
+
next true if ancestor.type?(:class, :module)
|
|
95
|
+
|
|
96
|
+
# 2. Is it a dynamic definition? (Class.new do; end)
|
|
97
|
+
next true if dynamic_definition?(ancestor)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -26,32 +26,45 @@ module RuboCop
|
|
|
26
26
|
|
|
27
27
|
FORBIDDEN_PREFIXES = %w[when with without].freeze
|
|
28
28
|
|
|
29
|
+
# @!method context_block?(node)
|
|
30
|
+
def_node_matcher :context_block?, <<~PATTERN
|
|
31
|
+
(block (send nil? :context ...) ...)
|
|
32
|
+
PATTERN
|
|
33
|
+
|
|
34
|
+
# @!method example_group_block?(node)
|
|
35
|
+
def_node_matcher :example_group_block?, <<~PATTERN
|
|
36
|
+
(block (send nil? {:describe :context :feature :example_group} ...) ...)
|
|
37
|
+
PATTERN
|
|
38
|
+
|
|
29
39
|
def on_block(node)
|
|
30
|
-
return unless context_block?(node)
|
|
40
|
+
return unless context_block?(node)
|
|
31
41
|
|
|
32
|
-
|
|
33
|
-
return unless context_description
|
|
42
|
+
parent = find_closest_example_group(node)
|
|
34
43
|
|
|
35
|
-
|
|
36
|
-
return unless FORBIDDEN_PREFIXES.include?(first_word)
|
|
44
|
+
return unless parent && context_block?(parent)
|
|
37
45
|
|
|
38
|
-
|
|
46
|
+
check_description(node)
|
|
39
47
|
end
|
|
40
48
|
|
|
41
49
|
alias on_numblock on_block
|
|
42
50
|
|
|
43
51
|
private
|
|
44
52
|
|
|
45
|
-
def
|
|
46
|
-
|
|
53
|
+
def find_closest_example_group(node)
|
|
54
|
+
node.each_ancestor(:block).find { |ancestor| example_group_block?(ancestor) }
|
|
47
55
|
end
|
|
48
56
|
|
|
49
|
-
def
|
|
50
|
-
|
|
57
|
+
def check_description(node)
|
|
58
|
+
description_node = node.send_node.first_argument
|
|
51
59
|
|
|
52
|
-
return
|
|
60
|
+
return unless description_node&.str_type?
|
|
53
61
|
|
|
54
|
-
|
|
62
|
+
text = description_node.value.to_s.strip
|
|
63
|
+
first_word = text.split.first&.downcase
|
|
64
|
+
|
|
65
|
+
return unless FORBIDDEN_PREFIXES.include?(first_word)
|
|
66
|
+
|
|
67
|
+
add_offense(node.send_node)
|
|
55
68
|
end
|
|
56
69
|
end
|
|
57
70
|
end
|
|
@@ -90,11 +90,12 @@ module RuboCop
|
|
|
90
90
|
end
|
|
91
91
|
|
|
92
92
|
def check_let(let_node, let_definitions)
|
|
93
|
-
name = (let_name(let_node) || let_it_be_name(let_node))
|
|
93
|
+
name = (let_name(let_node) || let_it_be_name(let_node))&.to_s&.to_sym
|
|
94
94
|
|
|
95
95
|
if let_definitions.key?(name)
|
|
96
96
|
add_offense(let_node, message: redefined_let_message(name, let_definitions))
|
|
97
|
-
|
|
97
|
+
|
|
98
|
+
let_definitions[name] += [line_location(let_node)]
|
|
98
99
|
else
|
|
99
100
|
let_definitions[name] = [line_location(let_node)]
|
|
100
101
|
end
|
|
@@ -96,7 +96,7 @@ module RuboCop
|
|
|
96
96
|
|
|
97
97
|
if subject_definitions.key?(name)
|
|
98
98
|
add_offense(subject_node, message: redefined_subject_message(name, subject_definitions))
|
|
99
|
-
subject_definitions[name]
|
|
99
|
+
subject_definitions[name] += [line_location(subject_node)]
|
|
100
100
|
else
|
|
101
101
|
subject_definitions[name] = [line_location(subject_node)]
|
|
102
102
|
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Vicenzo
|
|
6
|
+
module Style
|
|
7
|
+
# Enforces parentheses for method calls with arguments that span multiple lines.
|
|
8
|
+
# Single-line calls are ignored (parentheses are optional).
|
|
9
|
+
#
|
|
10
|
+
# This cop accepts an `AllowedMethods` configuration to exempt specific methods
|
|
11
|
+
# from this rule. This is particularly useful for Fluent DSLs (like RSpec's
|
|
12
|
+
# `to`, `change`, etc.) where parentheses might hurt readability or conflict
|
|
13
|
+
# with layout rules.
|
|
14
|
+
#
|
|
15
|
+
# @example
|
|
16
|
+
# # bad
|
|
17
|
+
# method_name arg1,
|
|
18
|
+
# arg2
|
|
19
|
+
#
|
|
20
|
+
# # good
|
|
21
|
+
# method_name(arg1,
|
|
22
|
+
# arg2)
|
|
23
|
+
#
|
|
24
|
+
# # good (single line is always allowed)
|
|
25
|
+
# method_name arg1, arg2
|
|
26
|
+
#
|
|
27
|
+
# @example AllowedMethods: ['to']
|
|
28
|
+
# # good (allowed by configuration)
|
|
29
|
+
# expect { action }.to change {
|
|
30
|
+
# model.attribute
|
|
31
|
+
# }
|
|
32
|
+
#
|
|
33
|
+
class MultilineMethodCallParentheses < Base
|
|
34
|
+
extend RuboCop::Cop::AutoCorrector
|
|
35
|
+
include RuboCop::Cop::RangeHelp
|
|
36
|
+
|
|
37
|
+
MSG = 'Use parentheses for method calls with arguments that span multiple lines.'
|
|
38
|
+
|
|
39
|
+
def on_send(node)
|
|
40
|
+
check_node(node)
|
|
41
|
+
end
|
|
42
|
+
alias on_csend on_send
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def check_node(node)
|
|
47
|
+
return unless node.arguments?
|
|
48
|
+
return unless node.multiline?
|
|
49
|
+
return if node.parenthesized? || node.operator_method? || node.setter_method? || allowed_method?(node)
|
|
50
|
+
|
|
51
|
+
add_offense(node) do |corrector|
|
|
52
|
+
autocorrect(corrector, node)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def allowed_method?(node)
|
|
57
|
+
allowed_methods.include?(node.method_name.to_s)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def allowed_methods
|
|
61
|
+
cop_config.fetch('AllowedMethods', [])
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def autocorrect(corrector, node)
|
|
65
|
+
if node.loc.selector
|
|
66
|
+
gap_range = range_between(node.loc.selector.end_pos, node.first_argument.source_range.begin_pos)
|
|
67
|
+
corrector.replace(gap_range, '(')
|
|
68
|
+
else
|
|
69
|
+
corrector.insert_before(node.first_argument, '(')
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
corrector.insert_after(node.last_argument, ')')
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative 'vicenzo/rspec/
|
|
3
|
+
require_relative 'vicenzo/rspec/inconsistent_sibling_structure'
|
|
4
4
|
require_relative 'vicenzo/rspec/nested_context_improper_start'
|
|
5
5
|
require_relative 'vicenzo/rspec/nested_let_redefinition'
|
|
6
6
|
require_relative 'vicenzo/rspec/nested_subject_redefinition'
|
|
7
|
+
require_relative 'vicenzo/rspec/leaky_definition'
|
|
7
8
|
require_relative 'vicenzo/rails/enum_inclusion_of_validation'
|
|
9
|
+
require_relative 'vicenzo/layout/multiline_method_call_line_breaks'
|
|
10
|
+
require_relative 'vicenzo/style/multiline_method_call_parentheses'
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'date'
|
|
4
|
+
|
|
5
|
+
desc 'Prepare release: update config, version file, and changelog'
|
|
6
|
+
task :cut_release, [:version] do |_t, args|
|
|
7
|
+
version = args[:version]
|
|
8
|
+
|
|
9
|
+
# 1. Validation
|
|
10
|
+
abort 'Error: You must provide a version. Example: rake cut_release[0.3.0]' unless version
|
|
11
|
+
|
|
12
|
+
config_file = 'config/default.yml'
|
|
13
|
+
version_file = 'lib/rubocop/vicenzo/version.rb'
|
|
14
|
+
changelog_file = 'CHANGELOG.md'
|
|
15
|
+
|
|
16
|
+
# Check if files exist
|
|
17
|
+
[config_file, version_file, changelog_file].each do |file|
|
|
18
|
+
abort "Error: File not found at #{file}" unless File.exist?(file)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
puts "✂️ Cutting release for version #{version}..."
|
|
22
|
+
|
|
23
|
+
# -------------------------------------------------------
|
|
24
|
+
# 2. Update config/default.yml (<<next>> -> version)
|
|
25
|
+
# -------------------------------------------------------
|
|
26
|
+
config_content = File.read(config_file)
|
|
27
|
+
if config_content.include?("'<<next>>'")
|
|
28
|
+
updated_config = config_content.gsub("'<<next>>'", "'#{version}'")
|
|
29
|
+
File.write(config_file, updated_config)
|
|
30
|
+
puts " ✅ Updated 'VersionAdded' in #{config_file}"
|
|
31
|
+
else
|
|
32
|
+
puts " ⚠️ No '<<next>>' found in #{config_file} (skipping)"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# -------------------------------------------------------
|
|
36
|
+
# 3. Update lib/rubocop/vicenzo/version.rb
|
|
37
|
+
# -------------------------------------------------------
|
|
38
|
+
version_content = File.read(version_file)
|
|
39
|
+
# Regex looks for: VERSION = '...' or VERSION = "..."
|
|
40
|
+
if version_content.match?(/VERSION\s*=\s*['"](.+)['"]/)
|
|
41
|
+
updated_version = version_content.gsub(/VERSION\s*=\s*['"](.+)['"]/, "VERSION = '#{version}'")
|
|
42
|
+
File.write(version_file, updated_version)
|
|
43
|
+
puts " ✅ Updated VERSION constant in #{version_file}"
|
|
44
|
+
else
|
|
45
|
+
puts " ❌ Could not find VERSION constant in #{version_file}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# -------------------------------------------------------
|
|
49
|
+
# 4. Update CHANGELOG.md
|
|
50
|
+
# -------------------------------------------------------
|
|
51
|
+
changelog_content = File.read(changelog_file)
|
|
52
|
+
unreleased_header = '## [Unreleased]'
|
|
53
|
+
date = Date.today.to_s # YYYY-MM-DD
|
|
54
|
+
|
|
55
|
+
# We replace "## [Unreleased]" with:
|
|
56
|
+
# ## [Unreleased]
|
|
57
|
+
#
|
|
58
|
+
# ## [version] - date
|
|
59
|
+
#
|
|
60
|
+
# This pushes the existing unreleased items down under the new version header.
|
|
61
|
+
new_entry_header = "#{unreleased_header}\n\n## [#{version}] - #{date}"
|
|
62
|
+
|
|
63
|
+
if changelog_content.include?(unreleased_header)
|
|
64
|
+
# We use 'sub' to replace only the first occurrence (the top one)
|
|
65
|
+
updated_changelog = changelog_content.sub(unreleased_header, new_entry_header)
|
|
66
|
+
File.write(changelog_file, updated_changelog)
|
|
67
|
+
puts " ✅ Updated #{changelog_file} (moved items to [#{version}])"
|
|
68
|
+
else
|
|
69
|
+
puts " ❌ Could not find '#{unreleased_header}' in #{changelog_file}"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
puts "\n🎉 Release preparation complete! Don't forget to commit."
|
|
73
|
+
end
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rubocop-vicenzo
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Bruno Vicenzo
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: lint_roller
|
|
@@ -68,15 +68,19 @@ files:
|
|
|
68
68
|
- Rakefile
|
|
69
69
|
- config/default.yml
|
|
70
70
|
- lib/rubocop-vicenzo.rb
|
|
71
|
+
- lib/rubocop/cop/vicenzo/layout/multiline_method_call_line_breaks.rb
|
|
71
72
|
- lib/rubocop/cop/vicenzo/rails/enum_inclusion_of_validation.rb
|
|
72
|
-
- lib/rubocop/cop/vicenzo/rspec/
|
|
73
|
+
- lib/rubocop/cop/vicenzo/rspec/inconsistent_sibling_structure.rb
|
|
74
|
+
- lib/rubocop/cop/vicenzo/rspec/leaky_definition.rb
|
|
73
75
|
- lib/rubocop/cop/vicenzo/rspec/nested_context_improper_start.rb
|
|
74
76
|
- lib/rubocop/cop/vicenzo/rspec/nested_let_redefinition.rb
|
|
75
77
|
- lib/rubocop/cop/vicenzo/rspec/nested_subject_redefinition.rb
|
|
78
|
+
- lib/rubocop/cop/vicenzo/style/multiline_method_call_parentheses.rb
|
|
76
79
|
- lib/rubocop/cop/vicenzo_cops.rb
|
|
77
80
|
- lib/rubocop/vicenzo.rb
|
|
78
81
|
- lib/rubocop/vicenzo/plugin.rb
|
|
79
82
|
- lib/rubocop/vicenzo/version.rb
|
|
83
|
+
- rakelib/release.rake
|
|
80
84
|
- sig/rubocop/vicenzo.rbs
|
|
81
85
|
homepage: https://github.com/bvicenzo/rubocop-vicenzo
|
|
82
86
|
licenses:
|
|
@@ -102,7 +106,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
102
106
|
- !ruby/object:Gem::Version
|
|
103
107
|
version: '0'
|
|
104
108
|
requirements: []
|
|
105
|
-
rubygems_version: 3.
|
|
109
|
+
rubygems_version: 3.7.2
|
|
106
110
|
specification_version: 4
|
|
107
111
|
summary: Cops of Bruno Vicenzo
|
|
108
112
|
test_files: []
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module RuboCop
|
|
4
|
-
module Cop
|
|
5
|
-
module Vicenzo
|
|
6
|
-
module RSpec
|
|
7
|
-
# Ensures that examples (`it`, `specify`, `example`)
|
|
8
|
-
# are not mixed with groups (`describe`, `context`) at the same level.
|
|
9
|
-
#
|
|
10
|
-
# @example
|
|
11
|
-
# # bad
|
|
12
|
-
# RSpec.describe User do
|
|
13
|
-
# it { is_expected.to validate_presence_of(:name) }
|
|
14
|
-
# describe '#admin?' do
|
|
15
|
-
# it { expect(true).to eq(true) }
|
|
16
|
-
# end
|
|
17
|
-
# end
|
|
18
|
-
#
|
|
19
|
-
# # bad
|
|
20
|
-
# RSpec.describe User do
|
|
21
|
-
# describe '#admin?' do
|
|
22
|
-
# it { expect(true).to eq(true) }
|
|
23
|
-
# context 'when email starts with' do
|
|
24
|
-
# end
|
|
25
|
-
# end
|
|
26
|
-
# end
|
|
27
|
-
#
|
|
28
|
-
# # good
|
|
29
|
-
# RSpec.describe User do
|
|
30
|
-
# describe '#admin?' do
|
|
31
|
-
# context 'when email starts with' do
|
|
32
|
-
# it { expect(true).to eq(true) }
|
|
33
|
-
# end
|
|
34
|
-
# end
|
|
35
|
-
# end
|
|
36
|
-
class MixedExampleGroups < RuboCop::Cop::RSpec::Base
|
|
37
|
-
MSG = 'Do not mix examples (`it`, `specify`, `example`) with groups (`describe`, `context`) ' \
|
|
38
|
-
'at the same level.'
|
|
39
|
-
|
|
40
|
-
def on_block(node)
|
|
41
|
-
return unless example_or_group?(node)
|
|
42
|
-
|
|
43
|
-
parent = node.parent
|
|
44
|
-
return unless parent
|
|
45
|
-
|
|
46
|
-
children = parent.children.select { |child| example_or_group?(child) }
|
|
47
|
-
example_nodes, group_nodes = children.partition { |n| example?(n) }
|
|
48
|
-
|
|
49
|
-
return if example_nodes.empty? || group_nodes.empty?
|
|
50
|
-
|
|
51
|
-
add_offense(node)
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
alias on_numblock on_block
|
|
55
|
-
|
|
56
|
-
private
|
|
57
|
-
|
|
58
|
-
def example_or_group?(node)
|
|
59
|
-
example?(node) || example_group?(node)
|
|
60
|
-
end
|
|
61
|
-
end
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
end
|