Skip to content

Commit 11f3e2a

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

File tree

4 files changed

+161
-4
lines changed

4 files changed

+161
-4
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## Master (Unreleased)
44

5+
- Let `RSpec/SpecFilePathFormat` leverage ActiveSupport inflections, if defined. ([@corsonknowles])
6+
57
## 3.6.0 (2025-04-18)
68

79
- Fix false positive in `RSpec/Pending`, where it would mark the default block `it` as an offense. ([@bquorning])

config/default.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -924,6 +924,7 @@ RSpec/SpecFilePathFormat:
924924
IgnoreMethods: false
925925
IgnoreMetadata:
926926
type: routing
927+
UseActiveSupportInflections: false
927928
VersionAdded: '2.24'
928929
Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/SpecFilePathFormat
929930

lib/rubocop/cop/rspec/spec_file_path_format.rb

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,20 @@ module RSpec
3232
# # good
3333
# whatever_spec.rb # describe MyClass, type: :routing do; end
3434
#
35+
# @example `UseActiveSupportInflections: true`
36+
# # When enabled, uses ActiveSupport's inflector for handling custom acronyms
37+
# # like PvP, HTTP, etc. Set to false by default.
38+
#
3539
class SpecFilePathFormat < Base
3640
include TopLevelGroup
3741
include Namespace
3842
include FileHelp
3943

4044
MSG = 'Spec path should end with `%<suffix>s`.'
4145

46+
# Class variable to cache ActiveSupport availability for performance
47+
@@activesupport_available = nil
48+
4249
# @!method example_group_arguments(node)
4350
def_node_matcher :example_group_arguments, <<~PATTERN
4451
(block $(send #rspec? #ExampleGroups.all $_ $...) ...)
@@ -106,10 +113,40 @@ def expected_path(constant)
106113
end
107114

108115
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
116+
if use_activesupport_inflections? && activesupport_inflections_available?
117+
ActiveSupport::Inflector.underscore(string)
118+
else
119+
string
120+
.gsub(/([^A-Z])([A-Z]+)/, '\1_\2')
121+
.gsub(/([A-Z])([A-Z][^A-Z\d]+)/, '\1_\2')
122+
.downcase
123+
end
124+
end
125+
126+
def use_activesupport_inflections?
127+
cop_config.fetch('UseActiveSupportInflections', false)
128+
end
129+
130+
def activesupport_inflections_available?
131+
self.class.activesupport_inflections_available?
132+
end
133+
134+
def self.activesupport_inflections_available?
135+
return @@activesupport_available unless @@activesupport_available.nil?
136+
137+
@@activesupport_available = begin
138+
require 'active_support/inflector'
139+
# Since the require is the heavy lifting here, we can use a more robust check
140+
# that actually calls the method to ensure it works
141+
ActiveSupport::Inflector.underscore('TestClass') == 'test_class'
142+
rescue LoadError, NameError, StandardError
143+
false
144+
end
145+
end
146+
147+
# For testing and debugging
148+
def self.reset_activesupport_cache!
149+
@@activesupport_available = nil
113150
end
114151

115152
def custom_transform

spec/rubocop/cop/rspec/spec_file_path_format_spec.rb

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,4 +281,121 @@ class Foo
281281
RUBY
282282
end
283283
end
284+
285+
context 'when ActiveSupport inflections are available' do
286+
let(:cop_config) { { 'UseActiveSupportInflections' => true } }
287+
288+
before do
289+
allow(described_class).to receive(:require).with('active_support/inflector').and_return(true)
290+
stub_const('ActiveSupport::Inflector', double('ActiveSupport::Inflector'))
291+
allow(ActiveSupport::Inflector).to receive(:underscore).with('TestClass').and_return('test_class')
292+
end
293+
294+
around do |example|
295+
described_class.reset_activesupport_cache!
296+
example.run
297+
described_class.reset_activesupport_cache!
298+
end
299+
300+
it 'uses ActiveSupport inflections for custom acronyms' do
301+
allow(ActiveSupport::Inflector).to receive(:underscore).with('PvPClass').and_return('pvp_class')
302+
303+
expect_no_offenses(<<~RUBY, 'pvp_class_spec.rb')
304+
describe PvPClass do; end
305+
RUBY
306+
end
307+
308+
it 'registers an offense when ActiveSupport inflections suggest different path' do
309+
allow(ActiveSupport::Inflector).to receive(:underscore).with('PvPClass').and_return('pvp_class')
310+
311+
expect_offense(<<~RUBY, 'pv_p_class_spec.rb')
312+
describe PvPClass do; end
313+
^^^^^^^^^^^^^^^^^ Spec path should end with `pvp_class*_spec.rb`.
314+
RUBY
315+
end
316+
317+
it 'does not register complex acronyms with method names' do
318+
allow(ActiveSupport::Inflector).to receive(:underscore).with('PvPClass').and_return('pvp_class')
319+
320+
expect_no_offenses(<<~RUBY, 'pvp_class_foo_spec.rb')
321+
describe PvPClass, 'foo' do; end
322+
RUBY
323+
end
324+
325+
it 'does not register nested namespaces with custom acronyms' do
326+
allow(ActiveSupport::Inflector).to receive(:underscore).with('API').and_return('api')
327+
allow(ActiveSupport::Inflector).to receive(:underscore).with('HTTPClient').and_return('http_client')
328+
329+
expect_no_offenses(<<~RUBY, 'api/http_client_spec.rb')
330+
describe API::HTTPClient do; end
331+
RUBY
332+
end
333+
end
334+
335+
context 'when UseActiveSupportInflections is disabled' do
336+
let(:cop_config) { { 'UseActiveSupportInflections' => false } }
337+
338+
before do
339+
# Even if ActiveSupport is available, it should not be used when disabled
340+
allow(described_class).to receive(:require).with('active_support/inflector').and_return(true)
341+
stub_const('ActiveSupport::Inflector', double('ActiveSupport::Inflector'))
342+
allow(ActiveSupport::Inflector).to receive(:underscore).with('TestClass').and_return('test_class')
343+
end
344+
345+
around do |example|
346+
described_class.reset_activesupport_cache!
347+
example.run
348+
described_class.reset_activesupport_cache!
349+
end
350+
351+
it 'uses default inflection instead of ActiveSupport' do
352+
expect_no_offenses(<<~RUBY, 'pv_p_class_spec.rb')
353+
describe PvPClass do; end
354+
RUBY
355+
end
356+
357+
it 'registers offense when default inflection does not match ActiveSupport result' do
358+
expect_offense(<<~RUBY, 'pvp_class_spec.rb')
359+
describe PvPClass do; end
360+
^^^^^^^^^^^^^^^^^ Spec path should end with `pv_p_class*_spec.rb`.
361+
RUBY
362+
end
363+
end
364+
365+
context 'when ActiveSupport inflections are not available' do
366+
let(:cop_config) { { 'UseActiveSupportInflections' => true } }
367+
368+
before do
369+
allow(described_class).to receive(:require).with('active_support/inflector').and_raise(LoadError)
370+
end
371+
372+
it 'falls back to default inflection behavior' do
373+
expect_no_offenses(<<~RUBY, 'pv_p_class_spec.rb')
374+
describe PvPClass do; end
375+
RUBY
376+
end
377+
378+
it 'registers offense when default inflection does not match' do
379+
expect_offense(<<~RUBY, 'pvp_class_spec.rb')
380+
describe PvPClass do; end
381+
^^^^^^^^^^^^^^^^^ Spec path should end with `pv_p_class*_spec.rb`.
382+
RUBY
383+
end
384+
end
385+
386+
context 'when ActiveSupport loading raises an error' do
387+
let(:cop_config) { { 'UseActiveSupportInflections' => true } }
388+
389+
before do
390+
allow(described_class).to receive(:require).with('active_support/inflector').and_raise(
391+
StandardError, 'Something went wrong'
392+
)
393+
end
394+
395+
it 'gracefully falls back to default behavior' do
396+
expect_no_offenses(<<~RUBY, 'pv_p_class_spec.rb')
397+
describe PvPClass do; end
398+
RUBY
399+
end
400+
end
284401
end

0 commit comments

Comments
 (0)