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 +4 -4
- data/.rubocop.yml +12 -0
- data/CHANGELOG.md +6 -0
- data/README.md +8 -0
- data/config/default.yml +13 -0
- data/lib/rubocop/cop/vicenzo/layout/multiline_method_call_line_breaks.rb +164 -0
- data/lib/rubocop/cop/vicenzo/style/multiline_method_call_parentheses.rb +78 -0
- data/lib/rubocop/cop/vicenzo_cops.rb +2 -0
- data/lib/rubocop/vicenzo/version.rb +1 -1
- data/rakelib/release.rake +73 -0
- metadata +4 -1
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
|
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'
|
|
@@ -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.
|
|
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:
|