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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ed82c28ffcefb60cfdace3b56e7076dccfdfc8bdd936250554ee3adb99c74903
4
- data.tar.gz: fc149cec1118966ed088ddf9b0d6b9f2375c25d364396fb5d2069991337dc2ac
3
+ metadata.gz: 685f19680a9d5721ed006772467155092d9d441c95c035191e3fa9dc449eac65
4
+ data.tar.gz: 22fa73c161855ced3561903c9a901ca19beec2904fe326402f426ec93040dd78
5
5
  SHA512:
6
- metadata.gz: 79466db290631d6846ac960fcc8c30c7feb6b89e824443ada09c42f7b4d57a474268fe7c9fa37a686297bfcf538d715190da643dbeb53633c866e9dba3d3bf36
7
- data.tar.gz: a0f924f733ba96918a8b5199f9a876c168184cc52cd476c3a3976929edc9cac2239ccf2fbc53e617c6cb340d60de4bf72a46a9d7072de675b461a83c1581b47e
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/MixedExampleGroups:
8
- Description: 'Check if there are example and groups at same level'
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.1.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) && context_block?(node.parent)
40
+ return unless context_block?(node)
31
41
 
32
- context_description = description_for(node)
33
- return unless context_description
42
+ parent = find_closest_example_group(node)
34
43
 
35
- first_word = context_description.split.first&.downcase
36
- return unless FORBIDDEN_PREFIXES.include?(first_word)
44
+ return unless parent && context_block?(parent)
37
45
 
38
- add_offense(node.send_node)
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 context_block?(node)
46
- !node.nil? && node.block_type? && node.send_node.command?(:context)
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 description_for(context_node)
50
- description = context_node.send_node.first_argument
57
+ def check_description(node)
58
+ description_node = node.send_node.first_argument
51
59
 
52
- return if description.nil?
60
+ return unless description_node&.str_type?
53
61
 
54
- description.source.delete_prefix("'").delete_suffix("'")
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)).to_s.to_sym
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
- let_definitions[name] << line_location(let_node)
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] << line_location(subject_node)
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/mixed_example_groups'
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'
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RuboCop
4
4
  module Vicenzo
5
- VERSION = '0.1.1'
5
+ VERSION = '0.3.0'
6
6
  end
7
7
  end
@@ -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.1.1
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: 2025-08-12 00:00:00.000000000 Z
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/mixed_example_groups.rb
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.6.2
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