Skip to content

Commit 6eae52a

Browse files
committed
Add new Rails/RootJoinChain cop
Instead of chaining `#join` on `Rails.root` or `Rails.public_path` ```ruby Rails.root.join('db').join('schema.rb') Rails.root.join('db').join(migrate).join('migration.rb') Rails.public_path.join('path').join('file.pdf') Rails.public_path.join('path').join(to).join('file.pdf') ``` we can combine all arguments into a single join ```ruby Rails.root.join('db', 'schema.rb') Rails.root.join('db', migrate, 'migration.rb') Rails.public_path.join('path', 'file.pdf') Rails.public_path.join('path', to, 'file.pdf') ```
1 parent 19369de commit 6eae52a

File tree

6 files changed

+179
-5
lines changed

6 files changed

+179
-5
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* [#586](https://github.com/rubocop/rubocop-rails/pull/586): Add new `Rails/RootJoinChain` cop. ([@leoarnold][])

config/default.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -701,6 +701,11 @@ Rails/ReversibleMigrationMethodDefinition:
701701
Include:
702702
- db/migrate/*.rb
703703

704+
Rails/RootJoinChain:
705+
Description: 'Use a single `#join` instead of chaining on `Rails.root` or `Rails.public_path`.'
706+
Enabled: pending
707+
VersionAdded: '<<next>>'
708+
704709
Rails/SafeNavigation:
705710
Description: "Use Ruby's safe navigation operator (`&.`) instead of `try!`."
706711
Enabled: true
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# frozen_string_literal: true
2+
3+
module RuboCop
4+
module Cop
5+
module Rails
6+
# Use a single `#join` instead of chaining on `Rails.root` or `Rails.public_path`.
7+
#
8+
# @example
9+
# # bad
10+
# Rails.root.join('db').join('schema.rb')
11+
# Rails.root.join('db').join(migrate).join('migration.rb')
12+
# Rails.public_path.join('path').join('file.pdf')
13+
# Rails.public_path.join('path').join(to).join('file.pdf')
14+
#
15+
# # good
16+
# Rails.root.join('db', 'schema.rb')
17+
# Rails.root.join('db', migrate, 'migration.rb')
18+
# Rails.public_path.join('path', 'file.pdf')
19+
# Rails.public_path.join('path', to, 'file.pdf')
20+
#
21+
class RootJoinChain < Base
22+
extend AutoCorrector
23+
include RangeHelp
24+
25+
MSG = 'Use `%<root>s.join(...)` instead of chaining `#join` calls.'
26+
27+
RESTRICT_ON_SEND = %i[join].to_set.freeze
28+
29+
# @!method rails_root?(node)
30+
def_node_matcher :rails_root?, <<~PATTERN
31+
(send (const {nil? cbase} :Rails) {:root :public_path})
32+
PATTERN
33+
34+
# @!method join?(node)
35+
def_node_matcher :join?, <<~PATTERN
36+
(send _ :join $...)
37+
PATTERN
38+
39+
def on_send(node)
40+
evidence(node) do |rails_node, args|
41+
add_offense(node, message: format(MSG, root: rails_node.source)) do |corrector|
42+
range = range_between(rails_node.loc.selector.end_pos, node.loc.expression.end_pos)
43+
replacement = ".join(#{args.map(&:source).join(', ')})"
44+
45+
corrector.replace(range, replacement)
46+
end
47+
end
48+
end
49+
50+
private
51+
52+
def evidence(node)
53+
# Are we at the *end* of the join chain?
54+
return if join?(node.parent)
55+
# Is there only one join?
56+
return if rails_root?(node.receiver)
57+
58+
all_args = []
59+
60+
while (args = join?(node))
61+
all_args = args + all_args
62+
node = node.receiver
63+
end
64+
65+
rails_root?(node) do
66+
yield(node, all_args)
67+
end
68+
end
69+
end
70+
end
71+
end
72+
end

lib/rubocop/cop/rails_cops.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
require_relative 'rails/active_record_callbacks_order'
1212
require_relative 'rails/active_record_override'
1313
require_relative 'rails/active_support_aliases'
14-
require_relative 'rails/schema_comment'
1514
require_relative 'rails/add_column_index'
1615
require_relative 'rails/after_commit_override'
1716
require_relative 'rails/application_controller'
@@ -85,9 +84,11 @@
8584
require_relative 'rails/require_dependency'
8685
require_relative 'rails/reversible_migration'
8786
require_relative 'rails/reversible_migration_method_definition'
87+
require_relative 'rails/root_join_chain'
8888
require_relative 'rails/safe_navigation'
8989
require_relative 'rails/safe_navigation_with_blank'
9090
require_relative 'rails/save_bang'
91+
require_relative 'rails/schema_comment'
9192
require_relative 'rails/scope_args'
9293
require_relative 'rails/short_i18n'
9394
require_relative 'rails/skips_model_validations'

spec/project_spec.rb

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
config.tap { |c| c.delete('inherit_mode') }.keys
1414
end
1515

16+
let(:version_regexp) { /\A\d+\.\d+\z|\A<<next>>\z/ }
17+
1618
it 'has a nicely formatted description for all cops' do
1719
cop_names.each do |name|
1820
description = config[name]['Description']
@@ -23,11 +25,23 @@
2325

2426
it 'requires a nicely formatted `VersionAdded` metadata for all cops' do
2527
cop_names.each do |name|
26-
version = config[name]['VersionAdded']
28+
version = config.dig(name, 'VersionAdded')
2729
expect(version.nil?).to(be(false),
28-
"VersionAdded is required for #{name}.")
29-
expect(version).to(match(/\A\d+\.\d+\z/),
30-
"#{version} should be format ('X.Y') for #{name}.")
30+
"`VersionAdded` configuration is required for `#{name}`.")
31+
expect(version).to(match(version_regexp),
32+
"#{version} should be format ('X.Y' or '<<next>>') for #{name}.")
33+
end
34+
end
35+
36+
%w[VersionChanged VersionRemoved].each do |version_type|
37+
it "requires a nicely formatted `#{version_type}` metadata for all cops" do
38+
cop_names.each do |name|
39+
version = config.dig(name, version_type)
40+
next unless version
41+
42+
expect(version).to(match(version_regexp),
43+
"#{version} should be format ('X.Y' or '<<next>>') for #{name}.")
44+
end
3145
end
3246
end
3347

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# frozen_string_literal: true
2+
3+
RSpec.describe RuboCop::Cop::Rails::RootJoinChain, :config do
4+
it 'does not register an offense for `Rails.root.join(...)`' do
5+
expect_no_offenses(<<~RUBY)
6+
Rails.root.join('db', 'schema.rb')
7+
RUBY
8+
end
9+
10+
it 'registers and offense and corrects for `::Rails.root.join(...).join(...)`' do
11+
expect_offense(<<~RUBY)
12+
::Rails.root.join('db').join('sch' + 'ema.rb')
13+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `::Rails.root.join(...)` instead of chaining `#join` calls.
14+
RUBY
15+
16+
expect_correction(<<~RUBY)
17+
::Rails.root.join('db', 'sch' + 'ema.rb')
18+
RUBY
19+
end
20+
21+
it 'registers and offense and corrects for `::Rails.root.join(...).join(...).read`' do
22+
expect_offense(<<~RUBY)
23+
::Rails.root.join('db').join('sch' + 'ema.rb').read
24+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `::Rails.root.join(...)` instead of chaining `#join` calls.
25+
RUBY
26+
27+
expect_correction(<<~RUBY)
28+
::Rails.root.join('db', 'sch' + 'ema.rb').read
29+
RUBY
30+
end
31+
32+
it 'registers and offense and corrects for `Rails.root.join(...).join(...)`' do
33+
expect_offense(<<~RUBY)
34+
Rails.root.join('db').join('sch' + 'ema.rb')
35+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `Rails.root.join(...)` instead of chaining `#join` calls.
36+
RUBY
37+
38+
expect_correction(<<~RUBY)
39+
Rails.root.join('db', 'sch' + 'ema.rb')
40+
RUBY
41+
end
42+
43+
it 'registers and offense and corrects for `Rails.root` with any number of joins greater one' do
44+
expect_offense(<<~RUBY)
45+
Rails.root.join.join.join('db').join(migrate).join.join("migration.\#{rb}")
46+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `Rails.root.join(...)` instead of chaining `#join` calls.
47+
RUBY
48+
49+
expect_correction(<<~RUBY)
50+
Rails.root.join('db', migrate, "migration.\#{rb}")
51+
RUBY
52+
end
53+
54+
it 'does not register an offense for `Rails.public_path.join(...)`' do
55+
expect_no_offenses(<<~RUBY)
56+
Rails.public_path.join('path', 'file.pdf')
57+
RUBY
58+
end
59+
60+
it 'registers and offense and corrects for `Rails.public_path.join(...).join(...)`' do
61+
expect_offense(<<~RUBY)
62+
Rails.public_path.join('path').join('fi' + 'le.pdf')
63+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `Rails.public_path.join(...)` instead of chaining `#join` calls.
64+
RUBY
65+
66+
expect_correction(<<~RUBY)
67+
Rails.public_path.join('path', 'fi' + 'le.pdf')
68+
RUBY
69+
end
70+
71+
it 'registers and offense and corrects for `Rails.public_path` with any number of joins greater one' do
72+
expect_offense(<<~RUBY)
73+
Rails.public_path.join.join.join('path').join(to).join.join("file.\#{pdf}")
74+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `Rails.public_path.join(...)` instead of chaining `#join` calls.
75+
RUBY
76+
77+
expect_correction(<<~RUBY)
78+
Rails.public_path.join('path', to, "file.\#{pdf}")
79+
RUBY
80+
end
81+
end

0 commit comments

Comments
 (0)