rubocop-vicenzo 0.2.0 → 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: b430ab160562cd140cd498e015dbf4e2c03a6bd99189ae24adc00881a3bbab48
4
- data.tar.gz: 3757afc640413d2ed6548319003ecf62be4cc84658673d430e5ff8236936f2d6
3
+ metadata.gz: 685f19680a9d5721ed006772467155092d9d441c95c035191e3fa9dc449eac65
4
+ data.tar.gz: 22fa73c161855ced3561903c9a901ca19beec2904fe326402f426ec93040dd78
5
5
  SHA512:
6
- metadata.gz: 4b2cbdc348b8faa2dd97e722e78e5c8956b67da26df8541314990331a645b156c758540d6602960e8f96340a6294bf0a35a833b4f2d5a1a237349bbb91f58ba6
7
- data.tar.gz: 88b77a6119a722404ad6d263f61ed53423e4991ae216bd8ea2295712fa72c5536b902948bea8c61e2f32e832444e356c2a0b16ae2f4a8727993acd063db40130
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
data/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
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
+
3
9
  ## [0.2.0] - 2025-11-27
4
10
 
5
11
  - Remove RuboCop::Cop::Vicenzo::RSpec::MixedExampleGroups in favor of InconsistentSiblingStructure #10;
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,3 +1,10 @@
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
@@ -38,3 +45,9 @@ Vicenzo/RSpec/LeakyDefinition:
38
45
  Exclude:
39
46
  - '**/spec/support/**/*'
40
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,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
@@ -6,3 +6,5 @@ require_relative 'vicenzo/rspec/nested_let_redefinition'
6
6
  require_relative 'vicenzo/rspec/nested_subject_redefinition'
7
7
  require_relative 'vicenzo/rspec/leaky_definition'
8
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.2.0'
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,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubocop-vicenzo
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bruno Vicenzo
@@ -68,16 +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
73
  - lib/rubocop/cop/vicenzo/rspec/inconsistent_sibling_structure.rb
73
74
  - lib/rubocop/cop/vicenzo/rspec/leaky_definition.rb
74
75
  - lib/rubocop/cop/vicenzo/rspec/nested_context_improper_start.rb
75
76
  - lib/rubocop/cop/vicenzo/rspec/nested_let_redefinition.rb
76
77
  - lib/rubocop/cop/vicenzo/rspec/nested_subject_redefinition.rb
78
+ - lib/rubocop/cop/vicenzo/style/multiline_method_call_parentheses.rb
77
79
  - lib/rubocop/cop/vicenzo_cops.rb
78
80
  - lib/rubocop/vicenzo.rb
79
81
  - lib/rubocop/vicenzo/plugin.rb
80
82
  - lib/rubocop/vicenzo/version.rb
83
+ - rakelib/release.rake
81
84
  - sig/rubocop/vicenzo.rbs
82
85
  homepage: https://github.com/bvicenzo/rubocop-vicenzo
83
86
  licenses: