Skip to content

Commit bca2a89

Browse files
committed
Let RSpec/SpecFilePathFormat leverage ActiveSupport inflections when defined and configured
Fix #740
1 parent 3bd4d24 commit bca2a89

File tree

4 files changed

+74
-201
lines changed

4 files changed

+74
-201
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@
88
- Fix a false positive for `RSpec/ReceiveNever` cop when `allow(...).to receive(...).never`. ([@ydah])
99
- Fix detection of nameless doubles with methods in `RSpec/VerifiedDoubles`. ([@ushi-as])
1010
- Improve an offense message for `RSpec/RepeatedExample` cop. ([@ydah])
11+
- Let `RSpec/SpecFilePathFormat` leverage ActiveSupport inflections when configured. ([@corsonknowles])
1112

1213
## 3.7.0 (2025-09-01)
1314

1415
- Mark `RSpec/IncludeExamples` as `SafeAutoCorrect: false`. ([@yujideveloper])
1516
- Fix a false positive for `RSpec/LeakyConstantDeclaration` when defining constants in explicit namespaces. ([@naveg])
16-
- Let `RSpec/SpecFilePathFormat` leverage ActiveSupport inflections when configured. ([@corsonknowles])
1717
- Add support for error matchers (`raise_exception` and `raise_error`) to `RSpec/Dialect`. ([@lovro-bikic])
1818
- Don't register offenses for `RSpec/DescribedClass` within `Data.define` blocks. ([@lovro-bikic])
1919
- Add autocorrection support for `RSpec/IteratedExpectation` for single expectations. ([@lovro-bikic])

config/default.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -940,8 +940,12 @@ RSpec/SpecFilePathFormat:
940940
IgnoreMetadata:
941941
type: routing
942942
InflectorPath: "./config/initializers/inflections.rb"
943-
UseActiveSupportInflections: false
943+
SupportedInflectors:
944+
- default
945+
- active_support
946+
EnforcedInflector: default
944947
VersionAdded: '2.24'
948+
VersionChanged: "<<next>>"
945949
Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/SpecFilePathFormat
946950

947951
RSpec/SpecFilePathSuffix:

lib/rubocop/cop/rspec/spec_file_path_format.rb

Lines changed: 22 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,10 @@ module RSpec
3232
# # good
3333
# whatever_spec.rb # describe MyClass, type: :routing do; end
3434
#
35-
# @example `UseActiveSupportInflections: true`
35+
# @example `EnforcedInflector: active_support`
3636
# # Enable to use ActiveSupport's inflector for custom acronyms
37-
# # like HTTP, etc. Set to false by default.
38-
# # The InflectorPath provides the path to the inflector file.
37+
# # like HTTP, etc. Set to "default" by default.
38+
# # Configure `InflectorPath` with the path to the inflector file.
3939
# # The default is ./config/initializers/inflections.rb.
4040
#
4141
class SpecFilePathFormat < Base
@@ -63,11 +63,6 @@ def on_top_level_example_group(node)
6363
end
6464
end
6565

66-
# For testing and debugging
67-
def self.reset_activesupport_cache!
68-
ActiveSupportInflector.reset_cache!
69-
end
70-
7166
private
7267

7368
# Inflector module that uses ActiveSupport for advanced inflection rules
@@ -76,33 +71,20 @@ def self.call(string)
7671
ActiveSupport::Inflector.underscore(string)
7772
end
7873

79-
def self.available?(cop_config)
80-
return @available unless @available.nil?
74+
def self.prepare_availability(config)
75+
return if @prepared
8176

82-
unless cop_config.fetch('UseActiveSupportInflections', false)
83-
return @available = false
84-
end
77+
@prepared = true
8578

86-
unless File.exist?(inflector_path(cop_config))
87-
return @available = false
88-
end
79+
inflector_path = config.fetch('InflectorPath')
8980

90-
@available = begin
91-
require 'active_support/inflector'
92-
require inflector_path(cop_config)
93-
true
94-
rescue LoadError, StandardError
95-
false
81+
unless File.exist?(inflector_path)
82+
raise "The configured `InflectorPath` #{inflector_path} does " \
83+
'not exist.'
9684
end
97-
end
98-
99-
def self.inflector_path(cop_config)
100-
cop_config.fetch('InflectorPath',
101-
'./config/initializers/inflections.rb')
102-
end
10385

104-
def self.reset_cache!
105-
@available = nil
86+
require 'active_support/inflector'
87+
require inflector_path
10688
end
10789
end
10890

@@ -117,11 +99,16 @@ def self.call(string)
11799
end
118100

119101
def inflector
120-
@inflector ||= if ActiveSupportInflector.available?(cop_config)
121-
ActiveSupportInflector
122-
else
123-
DefaultInflector
124-
end
102+
case cop_config.fetch('EnforcedInflector')
103+
when 'active_support'
104+
ActiveSupportInflector.prepare_availability(cop_config)
105+
ActiveSupportInflector
106+
when 'default'
107+
DefaultInflector
108+
else
109+
raise 'Unknown value for `EnforcedInflector`: ' \
110+
"#{cop_config.fetch('EnforcedInflector')}"
111+
end
125112
end
126113

127114
def ensure_correct_file_path(send_node, class_name, arguments)

spec/rubocop/cop/rspec/spec_file_path_format_spec.rb

Lines changed: 46 additions & 164 deletions
Original file line numberDiff line numberDiff line change
@@ -286,38 +286,35 @@ class Foo
286286
# rubocop:disable RSpec/NestedGroups
287287
context 'when using ActiveSupport integration' do
288288
around do |example|
289-
described_class.reset_activesupport_cache!
289+
reset_activesupport_cache!
290290
example.run
291-
described_class.reset_activesupport_cache!
291+
reset_activesupport_cache!
292292
end
293293

294-
before do
295-
# We cannot verify this double because it's not a Rubocop dependency
296-
# rubocop:disable RSpec/RSpec/VerifiedDoubles
297-
inflector = double('ActiveSupport::Inflector')
298-
# rubocop:enable RSpec/RSpec/VerifiedDoubles
299-
stub_const('ActiveSupport::Inflector', inflector)
300-
301-
allow(File).to receive(:exist?)
302-
.with(cop_config['InflectorPath']).and_return(file_exists)
294+
def reset_activesupport_cache!
295+
described_class::ActiveSupportInflector.instance_variable_set(
296+
:@prepared, nil
297+
)
303298
end
304299

305-
let(:file_exists) { true }
306300
let(:cop_config) do
307301
{
308-
'UseActiveSupportInflections' => true,
302+
'EnforcedInflector' => 'active_support',
309303
'InflectorPath' => './config/initializers/inflections.rb'
310304
}
311305
end
312306

313307
context 'when ActiveSupport inflections are available' do
314308
before do
315-
allow(described_class::ActiveSupportInflector)
316-
.to receive(:available?)
317-
.and_return(true)
318-
allow(described_class).to receive(:require)
319-
.with('./config/initializers/inflections.rb').and_return(true)
309+
allow(File).to receive(:exist?)
310+
.with(cop_config['InflectorPath']).and_return(true)
320311

312+
allow(described_class::ActiveSupportInflector).to receive(:require)
313+
.with('active_support/inflector')
314+
stub_const('ActiveSupport::Inflector', Module.new)
315+
316+
allow(described_class::ActiveSupportInflector).to receive(:require)
317+
.with('./config/initializers/inflections.rb')
321318
allow(ActiveSupport::Inflector).to receive(:underscore)
322319
.with('PvPClass').and_return('pvp_class')
323320
allow(ActiveSupport::Inflector).to receive(:underscore)
@@ -355,184 +352,69 @@ class Foo
355352
end
356353
end
357354

358-
context 'when ActiveSupport loading raises an error' do
359-
let(:cop_config) do
360-
{
361-
'UseActiveSupportInflections' => true,
362-
'InflectorPath' => './config/initializers/inflections.rb'
363-
}
364-
end
365-
366-
it 'returns false from available? when ActiveSupport cannot be loaded' do
367-
result = described_class::ActiveSupportInflector.available?(cop_config)
368-
expect(result).to be false
369-
end
370-
371-
it 'gracefully falls back to default behavior' do
372-
expect_no_offenses(<<~RUBY, 'pv_p_class_spec.rb')
373-
describe PvPClass do; end
374-
RUBY
375-
376-
expect_offense(<<~RUBY, 'pvp_class_spec.rb')
377-
describe PvPClass do; end
378-
^^^^^^^^^^^^^^^^^ Spec path should end with `pv_p_class*_spec.rb`.
379-
RUBY
380-
end
381-
end
382-
383-
context 'when configured with custom InflectorPath' do
384-
let(:cop_config) do
385-
{
386-
'UseActiveSupportInflections' => true,
387-
'InflectorPath' => './config/custom_inflections.rb'
388-
}
389-
end
390-
391-
context 'when inflector file exists' do
392-
before do
393-
allow(TOPLEVEL_BINDING.receiver).to receive(:require)
394-
.and_call_original
395-
allow(TOPLEVEL_BINDING.receiver).to receive(:require)
396-
.with('active_support/inflector').and_return(true)
397-
allow(TOPLEVEL_BINDING.receiver).to receive(:require)
398-
.with(cop_config['InflectorPath']).and_return(true)
399-
end
400-
401-
it 'loads the custom inflector file when it exists' do
402-
expect_no_offenses(<<~RUBY, 'https_client_spec.rb')
403-
describe HTTPSClient do; end
404-
RUBY
405-
end
406-
407-
it 'does not register with nested namespaces using ' \
408-
'custom inflections' do
409-
expect_no_offenses(<<~RUBY, 'api/https_client_spec.rb')
410-
describe API::HTTPSClient do; end
411-
RUBY
412-
end
413-
414-
it 'registers offense when path does not match custom inflections' do
415-
expect_offense(<<~RUBY, 'http_s_client_spec.rb')
416-
describe HTTPSClient do; end
417-
^^^^^^^^^^^^^^^^^^^^ Spec path should end with `https_client*_spec.rb`.
418-
RUBY
419-
end
420-
end
421-
422-
context 'when inflector file loading fails' do
423-
before do
424-
# Stub the global require method
425-
allow(TOPLEVEL_BINDING.receiver).to receive(:require)
426-
.and_call_original
427-
allow(TOPLEVEL_BINDING.receiver).to receive(:require)
428-
.with('./config/custom_inflections.rb')
429-
.and_raise(LoadError, 'Cannot load file')
430-
end
431-
432-
it 'gracefully falls back and handles inflector file loading errors' do
433-
expect_no_offenses(<<~RUBY, 'https_client_spec.rb')
434-
describe HTTPSClient do; end
435-
RUBY
436-
end
437-
end
438-
439-
context 'when inflector file does not exist' do
440-
let(:file_exists) { false }
441-
442-
it 'falls back to camel_to_snake_case conversion ' \
443-
'without ActiveSupport' do
444-
expect_no_offenses(<<~RUBY, 'https_client_spec.rb')
445-
describe HTTPSClient do; end
446-
RUBY
447-
end
448-
end
449-
end
450-
451-
context 'when using default inflector path' do
452-
before do
453-
allow(described_class::ActiveSupportInflector)
454-
.to receive(:available?)
455-
.and_return(true)
456-
end
457-
458-
it 'uses default inflector path when not configured' do
459-
allow(described_class::ActiveSupportInflector).to receive(:available?)
460-
.and_call_original
461-
462-
expect_no_offenses(<<~RUBY, 'http_client_spec.rb')
463-
describe HTTPClient do; end
464-
RUBY
355+
describe 'errors during preparation' do
356+
it 'shows an error when the configured inflector file does not exist' do
357+
allow(File).to receive(:exist?)
358+
.with(cop_config['InflectorPath']).and_return(false)
465359

466-
expect(File).to have_received(:exist?)
467-
.with('./config/initializers/inflections.rb')
360+
expect do
361+
inspect_source('describe PvPClass do; end', 'pv_p_class_spec.rb')
362+
end.to raise_error('The configured `InflectorPath` ./config' \
363+
'/initializers/inflections.rb does not exist.')
468364
end
469365

470-
context 'when inflector file does not exist' do
471-
let(:file_exists) { false }
366+
it 'lets LoadError pass all the way up when ActiveSupport loading ' \
367+
'raises an error' do
368+
allow(File).to receive(:exist?)
369+
.with(cop_config['InflectorPath']).and_return(true)
472370

473-
it 'does not require default inflector file when it does not exist' do
474-
allow(described_class::ActiveSupportInflector).to receive(:available?)
475-
.and_call_original
371+
allow(described_class::ActiveSupportInflector).to receive(:require)
372+
.with('active_support/inflector').and_raise(LoadError)
476373

477-
expect_no_offenses(<<~RUBY, 'http_client_spec.rb')
478-
describe HTTPClient do; end
479-
RUBY
480-
end
374+
expect do
375+
inspect_source('describe PvPClass do; end', 'pv_p_class_spec.rb')
376+
end.to raise_error(LoadError)
481377
end
482378
end
483379

484380
context 'when testing custom InflectorPath configuration precedence' do
485381
let(:cop_config) do
486382
{
487-
'UseActiveSupportInflections' => true,
383+
'EnforcedInflector' => 'active_support',
488384
'InflectorPath' => '/custom/path/to/inflections.rb'
489385
}
490386
end
491-
let(:file_exists) { false }
492387

493388
before do
389+
allow(File).to receive(:exist?).and_call_original
494390
# Ensure default path is not checked when custom path is configured
495391
allow(File).to receive(:exist?)
496392
.with('./config/initializers/inflections.rb').and_return(false)
497-
end
393+
allow(File).to receive(:exist?)
394+
.with(cop_config['InflectorPath']).and_return(true)
498395

499-
it 'reads the InflectorPath configuration correctly and checks ' \
500-
'for file existence' do
501-
expect_no_offenses(<<~RUBY, 'http_client_spec.rb')
502-
describe HTTPClient do; end
503-
RUBY
396+
allow(described_class::ActiveSupportInflector).to receive(:require)
397+
.with('active_support/inflector')
398+
stub_const('ActiveSupport::Inflector', Module.new)
504399

505-
expect(File).to have_received(:exist?)
506-
.with('/custom/path/to/inflections.rb')
400+
allow(described_class::ActiveSupportInflector).to receive(:require)
401+
.with(cop_config['InflectorPath'])
402+
allow(ActiveSupport::Inflector).to receive(:underscore)
403+
.and_return('')
507404
end
508405

509406
it 'reads the InflectorPath configuration correctly and does not ' \
510-
'fall back to the default inflector path' do
407+
'fall back to the default inflector path', :aggregate_failures do
511408
expect_no_offenses(<<~RUBY, 'http_client_spec.rb')
512409
describe HTTPClient do; end
513410
RUBY
514411

412+
expect(File).to have_received(:exist?)
413+
.with('/custom/path/to/inflections.rb')
515414
expect(File).not_to have_received(:exist?)
516415
.with('./config/initializers/inflections.rb')
517416
end
518417
end
519-
520-
context 'when testing ActiveSupportInflector success path' do
521-
before do
522-
# Stub the require calls to succeed
523-
allow(described_class::ActiveSupportInflector).to receive(:require)
524-
.and_call_original
525-
allow(described_class::ActiveSupportInflector).to receive(:require)
526-
.with('active_support/inflector').and_return(true)
527-
allow(described_class::ActiveSupportInflector).to receive(:require)
528-
.with('./config/initializers/inflections.rb').and_return(true)
529-
end
530-
531-
it 'returns true when ActiveSupport is successfully loaded' do
532-
result = described_class::ActiveSupportInflector.available?(cop_config)
533-
expect(result).to be true
534-
end
535-
end
536418
end
537419
# rubocop:enable RSpec/NestedGroups
538420
end

0 commit comments

Comments
 (0)