Skip to content

Commit 6899764

Browse files
committed
Let RSpec/SpecFilePathFormat leverage ActiveSupport inflections, if defined.
Fix #740
1 parent b0fda47 commit 6899764

File tree

3 files changed

+115
-4
lines changed

3 files changed

+115
-4
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Changelog
22

33
## Master (Unreleased)
4+
- Let `RSpec/SpecFilePathFormat` leverage ActiveSupport inflections, if defined. ([@corsonknowles][@jeromedalbert])
45

56
## 3.6.0 (2025-04-18)
67

lib/rubocop/cop/rspec/spec_file_path_format.rb

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ class SpecFilePathFormat < Base
3939

4040
MSG = 'Spec path should end with `%<suffix>s`.'
4141

42+
# Class variable to cache ActiveSupport availability for performance
43+
@@activesupport_available = nil
44+
4245
# @!method example_group_arguments(node)
4346
def_node_matcher :example_group_arguments, <<~PATTERN
4447
(block $(send #rspec? #ExampleGroups.all $_ $...) ...)
@@ -106,10 +109,36 @@ def expected_path(constant)
106109
end
107110

108111
def camel_to_snake_case(string)
109-
string
110-
.gsub(/([^A-Z])([A-Z]+)/, '\1_\2')
111-
.gsub(/([A-Z])([A-Z][^A-Z\d]+)/, '\1_\2')
112-
.downcase
112+
if activesupport_inflections_available?
113+
ActiveSupport::Inflector.underscore(string)
114+
else
115+
string
116+
.gsub(/([^A-Z])([A-Z]+)/, '\1_\2')
117+
.gsub(/([A-Z])([A-Z][^A-Z\d]+)/, '\1_\2')
118+
.downcase
119+
end
120+
end
121+
122+
def activesupport_inflections_available?
123+
self.class.activesupport_inflections_available?
124+
end
125+
126+
def self.activesupport_inflections_available?
127+
return @@activesupport_available unless @@activesupport_available.nil?
128+
129+
@@activesupport_available = begin
130+
require 'active_support/inflector'
131+
# Since the require is the heavy lifting here, we can use a more robust check
132+
# that actually calls the method to ensure it works
133+
ActiveSupport::Inflector.underscore('TestClass') == 'test_class'
134+
rescue LoadError, NameError, StandardError
135+
false
136+
end
137+
end
138+
139+
# For testing and debugging
140+
def self.reset_activesupport_cache!
141+
@@activesupport_available = nil
113142
end
114143

115144
def custom_transform

spec/rubocop/cop/rspec/spec_file_path_format_spec.rb

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,4 +281,85 @@ class Foo
281281
RUBY
282282
end
283283
end
284+
285+
context 'when ActiveSupport inflections are available' do
286+
before do
287+
allow(described_class).to receive(:require).with('active_support/inflector').and_return(true)
288+
stub_const('ActiveSupport::Inflector', double('ActiveSupport::Inflector'))
289+
allow(ActiveSupport::Inflector).to receive(:underscore).with('TestClass').and_return('test_class')
290+
end
291+
292+
around do |example|
293+
described_class.reset_activesupport_cache!
294+
example.run
295+
described_class.reset_activesupport_cache!
296+
end
297+
298+
it 'uses ActiveSupport inflections for custom acronyms' do
299+
allow(ActiveSupport::Inflector).to receive(:underscore).with('PvPClass').and_return('pvp_class')
300+
301+
expect_no_offenses(<<~RUBY, 'pvp_class_spec.rb')
302+
describe PvPClass do; end
303+
RUBY
304+
end
305+
306+
it 'registers an offense when ActiveSupport inflections suggest different path' do
307+
allow(ActiveSupport::Inflector).to receive(:underscore).with('PvPClass').and_return('pvp_class')
308+
309+
expect_offense(<<~RUBY, 'pv_p_class_spec.rb')
310+
describe PvPClass do; end
311+
^^^^^^^^^^^^^^^^^ Spec path should end with `pvp_class*_spec.rb`.
312+
RUBY
313+
end
314+
315+
it 'does not register complex acronyms with method names' do
316+
allow(ActiveSupport::Inflector).to receive(:underscore).with('PvPClass').and_return('pvp_class')
317+
318+
expect_no_offenses(<<~RUBY, 'pvp_class_foo_spec.rb')
319+
describe PvPClass, 'foo' do; end
320+
RUBY
321+
end
322+
323+
it 'does not register nested namespaces with custom acronyms' do
324+
allow(ActiveSupport::Inflector).to receive(:underscore).with('API').and_return('api')
325+
allow(ActiveSupport::Inflector).to receive(:underscore).with('HTTPClient').and_return('http_client')
326+
327+
expect_no_offenses(<<~RUBY, 'api/http_client_spec.rb')
328+
describe API::HTTPClient do; end
329+
RUBY
330+
end
331+
end
332+
333+
context 'when ActiveSupport inflections are not available' do
334+
before do
335+
allow(described_class).to receive(:require).with('active_support/inflector').and_raise(LoadError)
336+
end
337+
338+
it 'falls back to default inflection behavior' do
339+
expect_no_offenses(<<~RUBY, 'pv_p_class_spec.rb')
340+
describe PvPClass do; end
341+
RUBY
342+
end
343+
344+
it 'registers offense when default inflection does not match' do
345+
expect_offense(<<~RUBY, 'pvp_class_spec.rb')
346+
describe PvPClass do; end
347+
^^^^^^^^^^^^^^^^^ Spec path should end with `pv_p_class*_spec.rb`.
348+
RUBY
349+
end
350+
end
351+
352+
context 'when ActiveSupport loading raises an error' do
353+
before do
354+
allow(described_class).to receive(:require).with('active_support/inflector').and_raise(
355+
StandardError, 'Something went wrong'
356+
)
357+
end
358+
359+
it 'gracefully falls back to default behavior' do
360+
expect_no_offenses(<<~RUBY, 'pv_p_class_spec.rb')
361+
describe PvPClass do; end
362+
RUBY
363+
end
364+
end
284365
end

0 commit comments

Comments
 (0)