Skip to content

Commit 183c59f

Browse files
committed
Introduce Layout/BlockDelimiterSpacing.
1 parent d5716e0 commit 183c59f

File tree

6 files changed

+429
-1
lines changed

6 files changed

+429
-1
lines changed

.rubocop.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ AllCops:
77
Layout/ConsistentBlankLineIndentation:
88
Enabled: true
99

10+
Layout/BlockDelimiterSpacing:
11+
Enabled: true
12+
1013
Layout/IndentationStyle:
1114
Enabled: true
1215
EnforcedStyle: tabs

lib/rubocop/socketry.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
require_relative "socketry/version"
77
require_relative "socketry/plugin"
88
require_relative "socketry/layout/consistent_blank_line_indentation"
9+
require_relative "socketry/layout/block_delimiter_spacing"
910

1011
# @namespace
1112
module RuboCop

lib/rubocop/socketry/config.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,8 @@ Layout/ConsistentBlankLineIndentation:
77
Description: "Ensures that blank lines have the same indentation as the previous non-blank line."
88
Enabled: true
99
VersionAdded: "0.1.0"
10+
11+
Layout/BlockDelimiterSpacing:
12+
Description: "Enforces consistent spacing before block delimiters."
13+
Enabled: true
14+
VersionAdded: "0.4.0"
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
# frozen_string_literal: true
2+
3+
# Released under the MIT License.
4+
# Copyright, 2025, by Samuel Williams.
5+
6+
require "rubocop"
7+
8+
module RuboCop
9+
module Socketry
10+
module Layout
11+
# A RuboCop cop that enforces consistent spacing before block delimiters.
12+
#
13+
# This cop enforces the following style:
14+
# - `foo {bar}` - space when method has no parentheses and is not chained
15+
# - `foo(1, 2) {bar}` - space after closing paren for standalone methods
16+
# - `array.each{|x| x*2}.reverse` - no space for method chains (even with parens)
17+
class BlockDelimiterSpacing < RuboCop::Cop::Base
18+
extend Cop::AutoCorrector
19+
20+
MSG_ADD_SPACE = "Add a space before the opening brace."
21+
MSG_REMOVE_SPACE = "Remove space before the opening brace for method chains."
22+
23+
def on_block(node)
24+
return unless node.braces?
25+
26+
send_node = node.send_node
27+
28+
# Priority: Check if it's part of a method chain first
29+
# Method chains should never have space, even with parentheses
30+
if part_of_method_chain?(node)
31+
# array.each{|x| x*2}.reverse - no space
32+
# obj.method(1, 2){|x| x}.other - also no space
33+
check_no_space_before_brace(node, send_node)
34+
elsif has_parentheses?(send_node)
35+
# foo(1, 2) {bar} - space after ) for standalone methods
36+
check_space_after_paren(node, send_node)
37+
else
38+
# foo {bar} - space for standalone methods without parens
39+
check_space_before_brace(node, send_node)
40+
end
41+
end
42+
43+
private
44+
45+
# Check if the block is part of a method chain (e.g., foo{}.bar or foo.bar{}.baz)
46+
def part_of_method_chain?(block_node)
47+
send_node = block_node.send_node
48+
parent = block_node.parent
49+
50+
# Check if there's a method call after the block (foo{}.bar)
51+
has_chained_method_after = parent&.send_type? && parent.receiver == block_node
52+
53+
# Check if the block's receiver exists (foo.bar{} or array.map{})
54+
# Any method call with a receiver is part of a chain
55+
has_receiver_before = send_node.receiver
56+
57+
has_chained_method_after || has_receiver_before
58+
end
59+
60+
# Check if the method call has parentheses
61+
def has_parentheses?(send_node)
62+
send_node.parenthesized?
63+
end
64+
65+
# Check that there's a space between closing paren and opening brace
66+
def check_space_after_paren(block_node, send_node)
67+
paren_end = send_node.loc.end
68+
brace_begin = block_node.loc.begin
69+
70+
return unless paren_end && brace_begin
71+
72+
# Get the source between ) and {
73+
space_range = Parser::Source::Range.new(
74+
processed_source.buffer,
75+
paren_end.end_pos,
76+
brace_begin.begin_pos
77+
)
78+
79+
space_between = space_range.source
80+
81+
# Should have exactly one space
82+
return if space_between == " "
83+
84+
if space_between.empty?
85+
add_offense(
86+
brace_begin,
87+
message: MSG_ADD_SPACE
88+
) do |corrector|
89+
corrector.insert_before(brace_begin, " ")
90+
end
91+
elsif space_between.match?(/\A\s+\z/)
92+
# Multiple spaces or tabs - replace with single space
93+
add_offense(
94+
space_range,
95+
message: MSG_ADD_SPACE
96+
) do |corrector|
97+
corrector.replace(space_range, " ")
98+
end
99+
end
100+
end
101+
102+
# Check that there's no space before the opening brace
103+
def check_no_space_before_brace(block_node, send_node)
104+
brace_begin = block_node.loc.begin
105+
106+
# Find the position just before the brace
107+
char_before_pos = brace_begin.begin_pos - 1
108+
109+
return if char_before_pos < 0
110+
111+
char_before = processed_source.buffer.source[char_before_pos]
112+
113+
# If there's a space before the brace, we need to remove it
114+
return unless char_before == " "
115+
116+
# Don't remove space if it's after a closing paren (that case is handled separately)
117+
if send_node.loc.end && send_node.loc.end.end_pos == char_before_pos + 1
118+
return
119+
end
120+
121+
# Find the extent of whitespace before the brace
122+
start_pos = char_before_pos
123+
while start_pos > 0 && processed_source.buffer.source[start_pos - 1] =~ /\s/
124+
start_pos -= 1
125+
end
126+
127+
space_range = Parser::Source::Range.new(
128+
processed_source.buffer,
129+
start_pos,
130+
brace_begin.begin_pos
131+
)
132+
133+
add_offense(
134+
space_range,
135+
message: MSG_REMOVE_SPACE
136+
) do |corrector|
137+
corrector.remove(space_range)
138+
end
139+
end
140+
141+
# Check that there's a space before the opening brace (for standalone methods)
142+
def check_space_before_brace(block_node, send_node)
143+
brace_begin = block_node.loc.begin
144+
145+
# Find the position just before the brace
146+
char_before_pos = brace_begin.begin_pos - 1
147+
148+
return if char_before_pos < 0
149+
150+
char_before = processed_source.buffer.source[char_before_pos]
151+
152+
# If there's already a space, we're good
153+
return if char_before == " "
154+
155+
# Otherwise, we need to add a space
156+
add_offense(
157+
brace_begin,
158+
message: MSG_ADD_SPACE
159+
) do |corrector|
160+
corrector.insert_before(brace_begin, " ")
161+
end
162+
end
163+
end
164+
end
165+
end
166+
end

releases.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Releases
22

3+
## Unreleased
4+
5+
- Added `Layout/BlockDelimiterSpacing` cop to enforce consistent spacing before block delimiters.
6+
37
## v0.1.0
48

5-
- Initial implementaiton of `Layout/ConsistentBlankLineIndentation`.
9+
- Initial implementation of `Layout/ConsistentBlankLineIndentation`.

0 commit comments

Comments
 (0)