Skip to content

Commit 3f31059

Browse files
authored
Merge pull request #2090 from corsonknowles/fix_spec_file_path_format_for_inflections
Let `RSpec/SpecFilePathFormat` leverage ActiveSupport inflections when configured
2 parents 961389a + 2777fa2 commit 3f31059

File tree

5 files changed

+219
-5
lines changed

5 files changed

+219
-5
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
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], [@bquorning])
1112

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

config/default.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -939,7 +939,13 @@ RSpec/SpecFilePathFormat:
939939
IgnoreMethods: false
940940
IgnoreMetadata:
941941
type: routing
942+
InflectorPath: "./config/initializers/inflections.rb"
943+
SupportedInflectors:
944+
- default
945+
- active_support
946+
EnforcedInflector: default
942947
VersionAdded: '2.24'
948+
VersionChanged: "<<next>>"
943949
Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/SpecFilePathFormat
944950

945951
RSpec/SpecFilePathSuffix:

docs/modules/ROOT/pages/cops_rspec.adoc

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6015,7 +6015,7 @@ context 'Something', :z, variable, :a, :b
60156015
| Yes
60166016
| No
60176017
| 2.24
6018-
| -
6018+
| <<next>>
60196019
|===
60206020
60216021
Checks that spec file paths are consistent and well-formed.
@@ -6072,6 +6072,17 @@ my_class_spec.rb # describe MyClass, '#method'
60726072
whatever_spec.rb # describe MyClass, type: :routing do; end
60736073
----
60746074
6075+
[#_enforcedinflector_-active_support_-rspecspecfilepathformat]
6076+
==== `EnforcedInflector: active_support`
6077+
6078+
[source,ruby]
6079+
----
6080+
# Enable to use ActiveSupport's inflector for custom acronyms
6081+
# like HTTP, etc. Set to "default" by default.
6082+
# Configure `InflectorPath` with the path to the inflector file.
6083+
# The default is ./config/initializers/inflections.rb.
6084+
----
6085+
60756086
[#configurable-attributes-rspecspecfilepathformat]
60766087
=== Configurable attributes
60776088
@@ -6097,6 +6108,14 @@ whatever_spec.rb # describe MyClass, type: :routing do; end
60976108
| IgnoreMetadata
60986109
| `{"type" => "routing"}`
60996110
|
6111+
6112+
| InflectorPath
6113+
| `./config/initializers/inflections.rb`
6114+
| String
6115+
6116+
| EnforcedInflector
6117+
| `default`
6118+
| `<none>`
61006119
|===
61016120
61026121
[#references-rspecspecfilepathformat]

lib/rubocop/cop/rspec/spec_file_path_format.rb

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@ module RSpec
3232
# # good
3333
# whatever_spec.rb # describe MyClass, type: :routing do; end
3434
#
35+
# @example `EnforcedInflector: active_support`
36+
# # Enable to use ActiveSupport's inflector for custom acronyms
37+
# # like HTTP, etc. Set to "default" by default.
38+
# # Configure `InflectorPath` with the path to the inflector file.
39+
# # The default is ./config/initializers/inflections.rb.
40+
#
3541
class SpecFilePathFormat < Base
3642
include TopLevelGroup
3743
include Namespace
@@ -59,6 +65,53 @@ def on_top_level_example_group(node)
5965

6066
private
6167

68+
# Inflector module that uses ActiveSupport for advanced inflection rules
69+
module ActiveSupportInflector
70+
def self.call(string)
71+
ActiveSupport::Inflector.underscore(string)
72+
end
73+
74+
def self.prepare_availability(config)
75+
return if @prepared
76+
77+
@prepared = true
78+
79+
inflector_path = config.fetch('InflectorPath')
80+
81+
unless File.exist?(inflector_path)
82+
raise "The configured `InflectorPath` #{inflector_path} does " \
83+
'not exist.'
84+
end
85+
86+
require 'active_support/inflector'
87+
require inflector_path
88+
end
89+
end
90+
91+
# Inflector module that uses basic regex-based conversion
92+
module DefaultInflector
93+
def self.call(string)
94+
string
95+
.gsub(/([^A-Z])([A-Z]+)/, '\1_\2')
96+
.gsub(/([A-Z])([A-Z][^A-Z\d]+)/, '\1_\2')
97+
.downcase
98+
end
99+
end
100+
101+
def inflector
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+
# :nocov:
110+
:noop
111+
# :nocov:
112+
end
113+
end
114+
62115
def ensure_correct_file_path(send_node, class_name, arguments)
63116
pattern = correct_path_pattern(class_name, arguments)
64117
return if filename_ends_with?(pattern)
@@ -106,10 +159,7 @@ def expected_path(constant)
106159
end
107160

108161
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
162+
inflector.call(string)
113163
end
114164

115165
def custom_transform

spec/rubocop/cop/rspec/spec_file_path_format_spec.rb

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,4 +281,142 @@ class Foo
281281
RUBY
282282
end
283283
end
284+
285+
# We intentionally isolate all of the plugin specs in this context
286+
# rubocop:disable RSpec/NestedGroups
287+
context 'when using ActiveSupport integration' do
288+
around do |example|
289+
reset_activesupport_cache!
290+
example.run
291+
reset_activesupport_cache!
292+
end
293+
294+
def reset_activesupport_cache!
295+
described_class::ActiveSupportInflector.instance_variable_set(
296+
:@prepared, nil
297+
)
298+
end
299+
300+
let(:cop_config) do
301+
{
302+
'EnforcedInflector' => 'active_support',
303+
'InflectorPath' => './config/initializers/inflections.rb'
304+
}
305+
end
306+
307+
context 'when ActiveSupport inflections are available' do
308+
before do
309+
allow(File).to receive(:exist?)
310+
.with(cop_config['InflectorPath']).and_return(true)
311+
312+
allow(described_class::ActiveSupportInflector).to receive(:require)
313+
.with('active_support/inflector')
314+
stub_const('ActiveSupport::Inflector',
315+
Module.new { def self.underscore(_); end })
316+
317+
allow(described_class::ActiveSupportInflector).to receive(:require)
318+
.with('./config/initializers/inflections.rb')
319+
allow(ActiveSupport::Inflector).to receive(:underscore)
320+
.with('PvPClass').and_return('pvp_class')
321+
allow(ActiveSupport::Inflector).to receive(:underscore)
322+
.with('HTTPClient').and_return('http_client')
323+
allow(ActiveSupport::Inflector).to receive(:underscore)
324+
.with('HTTPSClient').and_return('https_client')
325+
allow(ActiveSupport::Inflector).to receive(:underscore)
326+
.with('API').and_return('api')
327+
end
328+
329+
it 'uses ActiveSupport inflections for custom acronyms' do
330+
expect_no_offenses(<<~RUBY, 'pvp_class_spec.rb')
331+
describe PvPClass do; end
332+
RUBY
333+
end
334+
335+
it 'registers an offense when ActiveSupport inflections ' \
336+
'suggest different path' do
337+
expect_offense(<<~RUBY, 'pv_p_class_spec.rb')
338+
describe PvPClass do; end
339+
^^^^^^^^^^^^^^^^^ Spec path should end with `pvp_class*_spec.rb`.
340+
RUBY
341+
end
342+
343+
it 'does not register complex acronyms with method names' do
344+
expect_no_offenses(<<~RUBY, 'pvp_class_foo_spec.rb')
345+
describe PvPClass, 'foo' do; end
346+
RUBY
347+
end
348+
349+
it 'does not register nested namespaces with custom acronyms' do
350+
expect_no_offenses(<<~RUBY, 'api/http_client_spec.rb')
351+
describe API::HTTPClient do; end
352+
RUBY
353+
end
354+
end
355+
356+
describe 'errors during preparation' do
357+
it 'shows an error when the configured inflector file does not exist' do
358+
allow(File).to receive(:exist?)
359+
.with(cop_config['InflectorPath']).and_return(false)
360+
361+
expect do
362+
inspect_source('describe PvPClass do; end', 'pv_p_class_spec.rb')
363+
end.to raise_error('The configured `InflectorPath` ./config' \
364+
'/initializers/inflections.rb does not exist.')
365+
end
366+
367+
it 'lets LoadError pass all the way up when ActiveSupport loading ' \
368+
'raises an error' do
369+
allow(File).to receive(:exist?)
370+
.with(cop_config['InflectorPath']).and_return(true)
371+
372+
allow(described_class::ActiveSupportInflector).to receive(:require)
373+
.with('active_support/inflector').and_raise(LoadError)
374+
375+
expect do
376+
inspect_source('describe PvPClass do; end', 'pv_p_class_spec.rb')
377+
end.to raise_error(LoadError)
378+
end
379+
end
380+
381+
context 'when testing custom InflectorPath configuration precedence' do
382+
let(:cop_config) do
383+
{
384+
'EnforcedInflector' => 'active_support',
385+
'InflectorPath' => '/custom/path/to/inflections.rb'
386+
}
387+
end
388+
389+
before do
390+
allow(File).to receive(:exist?).and_call_original
391+
# Ensure default path is not checked when custom path is configured
392+
allow(File).to receive(:exist?)
393+
.with('./config/initializers/inflections.rb').and_return(false)
394+
allow(File).to receive(:exist?)
395+
.with(cop_config['InflectorPath']).and_return(true)
396+
397+
allow(described_class::ActiveSupportInflector).to receive(:require)
398+
.with('active_support/inflector')
399+
stub_const('ActiveSupport::Inflector',
400+
Module.new { def self.underscore(_); end })
401+
402+
allow(described_class::ActiveSupportInflector).to receive(:require)
403+
.with(cop_config['InflectorPath'])
404+
allow(ActiveSupport::Inflector).to receive(:underscore)
405+
.and_return('')
406+
end
407+
408+
it 'reads the InflectorPath configuration correctly and does not ' \
409+
'fall back to the default inflector path', :aggregate_failures do
410+
expect_no_offenses(<<~RUBY, 'http_client_spec.rb')
411+
describe HTTPClient do; end
412+
RUBY
413+
414+
expect(File).to have_received(:exist?)
415+
.with('/custom/path/to/inflections.rb')
416+
expect(File).not_to have_received(:exist?)
417+
.with('./config/initializers/inflections.rb')
418+
end
419+
end
420+
end
421+
# rubocop:enable RSpec/NestedGroups
284422
end

0 commit comments

Comments
 (0)