Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 2777fa2

Browse filesBrowse files
Let RSpec/SpecFilePathFormat leverage ActiveSupport inflections when defined and configured
Fix #740 Co-Authored-By: Dave Corson-Knowles <david.corsonknowles@gusto.com> Co-Authored-By: Benjamin Quorning <benjamin@quorning.net>
1 parent 961389a commit 2777fa2
Copy full SHA for 2777fa2

File tree

Expand file treeCollapse file tree

5 files changed

+219
-5
lines changed
Open diff view settings
Filter options
Expand file treeCollapse file tree

5 files changed

+219
-5
lines changed
Open diff view settings
Collapse file

‎CHANGELOG.md‎

Copy file name to clipboardExpand all lines: CHANGELOG.md
+1Lines changed: 1 addition & 0 deletions
  • Display the source diff
  • Display the rich diff
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

Collapse file

‎config/default.yml‎

Copy file name to clipboardExpand all lines: config/default.yml
+6Lines 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:
Collapse file

‎docs/modules/ROOT/pages/cops_rspec.adoc‎

Copy file name to clipboardExpand all lines: docs/modules/ROOT/pages/cops_rspec.adoc
+20-1Lines changed: 20 additions & 1 deletion
  • Display the source diff
  • Display the rich diff
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]
Collapse file

‎lib/rubocop/cop/rspec/spec_file_path_format.rb‎

Copy file name to clipboardExpand all lines: lib/rubocop/cop/rspec/spec_file_path_format.rb
+54-4Lines 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
Collapse file

‎spec/rubocop/cop/rspec/spec_file_path_format_spec.rb‎

Copy file name to clipboardExpand all lines: spec/rubocop/cop/rspec/spec_file_path_format_spec.rb
+138Lines 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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.