Skip to content

Commit 82fc6ae

Browse files
committed
Fix handling of expression-block spacing.
1 parent d1ad132 commit 82fc6ae

File tree

2 files changed

+139
-9
lines changed

2 files changed

+139
-9
lines changed

lib/rubocop/socketry/layout/block_delimiter_spacing.rb

Lines changed: 61 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,21 @@ module Layout
1111
# A RuboCop cop that enforces consistent spacing before block delimiters.
1212
#
1313
# 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-
# - `->(foo){foo}` - no space for lambdas (stabby lambda syntax)
18-
# - `lambda{foo}` - no space for lambda keyword
19-
# - `proc{foo}` - no space for proc keyword
20-
# - `Proc.new{foo}` - no space for Proc.new
14+
# - `foo {bar}` - space for top-level statements without parentheses.
15+
# - `x = foo{bar}` - no space when part of an expression (assignment, argument, etc).
16+
# - `foo(1, 2) {bar}` - space after closing paren for top-level statements.
17+
# - `array.each{|x| x*2}.reverse` - no space for method chains.
18+
# - `->(foo){foo}` - no space for lambdas (stabby lambda syntax).
19+
# - `lambda{foo}` - no space for lambda keyword.
20+
# - `proc{foo}` - no space for proc keyword.
21+
# - `Proc.new{foo}` - no space for `Proc.new`.
2122
class BlockDelimiterSpacing < RuboCop::Cop::Base
2223
extend Cop::AutoCorrector
2324

2425
MSG_ADD_SPACE = "Add a space before the opening brace."
2526
MSG_REMOVE_SPACE = "Remove space before the opening brace for method chains."
2627
MSG_REMOVE_SPACE_LAMBDA = "Remove space before the opening brace for lambdas/procs."
28+
MSG_REMOVE_SPACE_EXPRESSION = "Remove space before the opening brace for expressions."
2729

2830
def on_block(node)
2931
return unless node.braces?
@@ -44,6 +46,12 @@ def on_block(node)
4446
# array.each{|x| x*2}.reverse - no space
4547
# obj.method(1, 2){|x| x}.other - also no space
4648
check_no_space_before_brace(node, send_node)
49+
# Priority 3: Check if it's part of an expression (not top-level)
50+
# Blocks within expressions should have no space
51+
elsif part_of_expression?(node)
52+
# x = Async{server.run} - no space (part of assignment)
53+
# foo(bar{baz}) - no space (part of argument)
54+
check_no_space_for_expression(node, send_node)
4755
elsif has_parentheses?(send_node)
4856
# foo(1, 2) {bar} - space after ) for standalone methods
4957
check_space_after_parentheses(node, send_node)
@@ -71,6 +79,18 @@ def lambda_or_proc?(send_node)
7179
false
7280
end
7381

82+
# Check if the block is part of an expression (not a top-level statement)
83+
# Top-level statements are directly inside a :begin node (file/method body)
84+
# and should have space. Everything else (expressions, nested blocks) should not.
85+
def part_of_expression?(node)
86+
parent = node.parent
87+
return false unless parent
88+
89+
# If parent is a :begin node (sequence of statements), this is top-level
90+
# Otherwise, it's part of an expression or nested context
91+
parent.type != :begin
92+
end
93+
7494
# Check that there's no space before the opening brace for lambdas
7595
def check_no_space_for_lambda(block_node, send_node)
7696
brace_begin = block_node.loc.begin
@@ -105,6 +125,40 @@ def check_no_space_for_lambda(block_node, send_node)
105125
end
106126
end
107127

128+
# Check that there's no space before the opening brace for expressions
129+
def check_no_space_for_expression(block_node, send_node)
130+
brace_begin = block_node.loc.begin
131+
132+
# Find the position just before the brace
133+
char_before_pos = brace_begin.begin_pos - 1
134+
135+
return if char_before_pos < 0
136+
137+
char_before = processed_source.buffer.source[char_before_pos]
138+
139+
# If there's no space before the brace, we're good
140+
return unless char_before == " "
141+
142+
# Find the extent of whitespace before the brace
143+
start_pos = char_before_pos
144+
while start_pos > 0 && processed_source.buffer.source[start_pos - 1] =~ /\s/
145+
start_pos -= 1
146+
end
147+
148+
space_range = Parser::Source::Range.new(
149+
processed_source.buffer,
150+
start_pos,
151+
brace_begin.begin_pos
152+
)
153+
154+
add_offense(
155+
space_range,
156+
message: MSG_REMOVE_SPACE_EXPRESSION
157+
) do |corrector|
158+
corrector.remove(space_range)
159+
end
160+
end
161+
108162
# Check if the block is part of a method chain (e.g., foo{}.bar or foo.bar{}.baz)
109163
def part_of_method_chain?(block_node)
110164
send_node = block_node.send_node

test/rubocop/socketry/layout/block_delimiter_spacing.rb

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -187,9 +187,9 @@
187187
end
188188

189189
with "nested blocks" do
190-
let(:source) {"foo {bar {baz}}"}
190+
let(:source) {"foo {bar{baz}}"}
191191

192-
it "does not register an offense for nested blocks" do
192+
it "does not register an offense for nested blocks without inner space" do
193193
processed_source = RuboCop::ProcessedSource.new(source, RUBY_VERSION.to_f)
194194
investigator = RuboCop::Cop::Commissioner.new([cop], [], raise_error: true)
195195
report = investigator.investigate(processed_source)
@@ -349,4 +349,80 @@
349349
expect(offenses.first.message).to be(:include?, "Remove space")
350350
end
351351
end
352+
353+
# Expression context cases (assignments, arguments, etc.)
354+
with "a block in an assignment without space" do
355+
let(:source) {"x = Async{server.run}"}
356+
357+
it "does not register an offense for expression context" do
358+
processed_source = RuboCop::ProcessedSource.new(source, RUBY_VERSION.to_f)
359+
investigator = RuboCop::Cop::Commissioner.new([cop], [], raise_error: true)
360+
report = investigator.investigate(processed_source)
361+
offenses = report.offenses
362+
expect(offenses).to be(:empty?)
363+
end
364+
end
365+
366+
with "a block in an assignment with space" do
367+
let(:source) {"x = Async {server.run}"}
368+
369+
it "registers an offense for space in expression context" do
370+
processed_source = RuboCop::ProcessedSource.new(source, RUBY_VERSION.to_f)
371+
investigator = RuboCop::Cop::Commissioner.new([cop], [], raise_error: true)
372+
report = investigator.investigate(processed_source)
373+
offenses = report.offenses
374+
expect(offenses).not.to be(:empty?)
375+
expect(offenses.first.message).to be(:include?, "Remove space")
376+
end
377+
end
378+
379+
with "a block as a method argument without space" do
380+
let(:source) {"foo(bar{baz})"}
381+
382+
it "does not register an offense for expression context" do
383+
processed_source = RuboCop::ProcessedSource.new(source, RUBY_VERSION.to_f)
384+
investigator = RuboCop::Cop::Commissioner.new([cop], [], raise_error: true)
385+
report = investigator.investigate(processed_source)
386+
offenses = report.offenses
387+
expect(offenses).to be(:empty?)
388+
end
389+
end
390+
391+
with "a block as a method argument with space" do
392+
let(:source) {"foo(bar {baz})"}
393+
394+
it "registers an offense for space in expression context" do
395+
processed_source = RuboCop::ProcessedSource.new(source, RUBY_VERSION.to_f)
396+
investigator = RuboCop::Cop::Commissioner.new([cop], [], raise_error: true)
397+
report = investigator.investigate(processed_source)
398+
offenses = report.offenses
399+
expect(offenses).not.to be(:empty?)
400+
expect(offenses.first.message).to be(:include?, "Remove space")
401+
end
402+
end
403+
404+
with "a block in a return statement without space" do
405+
let(:source) {"return foo{bar}"}
406+
407+
it "does not register an offense for expression context" do
408+
processed_source = RuboCop::ProcessedSource.new(source, RUBY_VERSION.to_f)
409+
investigator = RuboCop::Cop::Commissioner.new([cop], [], raise_error: true)
410+
report = investigator.investigate(processed_source)
411+
offenses = report.offenses
412+
expect(offenses).to be(:empty?)
413+
end
414+
end
415+
416+
with "a block in a return statement with space" do
417+
let(:source) {"return foo {bar}"}
418+
419+
it "registers an offense for space in expression context" do
420+
processed_source = RuboCop::ProcessedSource.new(source, RUBY_VERSION.to_f)
421+
investigator = RuboCop::Cop::Commissioner.new([cop], [], raise_error: true)
422+
report = investigator.investigate(processed_source)
423+
offenses = report.offenses
424+
expect(offenses).not.to be(:empty?)
425+
expect(offenses.first.message).to be(:include?, "Remove space")
426+
end
427+
end
352428
end

0 commit comments

Comments
 (0)