Skip to content

Commit 3ad75cc

Browse files
authored
Merge pull request #587 from leoarnold/leoarnold/rails-pathname
Add new `Rails/RootPathnameMethods` cop
2 parents 0610d73 + 467da4e commit 3ad75cc

File tree

6 files changed

+294
-0
lines changed

6 files changed

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

codespell.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
developpment
2+
filetest

config/default.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -795,6 +795,11 @@ Rails/RootJoinChain:
795795
Enabled: pending
796796
VersionAdded: '2.13'
797797

798+
Rails/RootPathnameMethods:
799+
Description: 'Use `Rails.root` IO methods instead of passing it to `File`.'
800+
Enabled: pending
801+
VersionAdded: '<<next>>'
802+
798803
Rails/RootPublicPath:
799804
Description: "Favor `Rails.public_path` over `Rails.root` with `'public'`."
800805
Enabled: pending
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
# frozen_string_literal: true
2+
3+
module RuboCop
4+
module Cop
5+
module Rails
6+
# Use `Rails.root` IO methods instead of passing it to `File`.
7+
#
8+
# `Rails.root` is an instance of `Pathname`
9+
# so we can apply many IO methods directly.
10+
#
11+
# This cop works best when used together with
12+
# `Style/FileRead`, `Style/FileWrite` and `Rails/RootJoinChain`.
13+
#
14+
# @example
15+
# # bad
16+
# File.open(Rails.root.join('db', 'schema.rb'))
17+
# File.open(Rails.root.join('db', 'schema.rb'), 'w')
18+
# File.read(Rails.root.join('db', 'schema.rb'))
19+
# File.binread(Rails.root.join('db', 'schema.rb'))
20+
# File.write(Rails.root.join('db', 'schema.rb'), content)
21+
# File.binwrite(Rails.root.join('db', 'schema.rb'), content)
22+
#
23+
# # good
24+
# Rails.root.join('db', 'schema.rb').open
25+
# Rails.root.join('db', 'schema.rb').open('w')
26+
# Rails.root.join('db', 'schema.rb').read
27+
# Rails.root.join('db', 'schema.rb').binread
28+
# Rails.root.join('db', 'schema.rb').write(content)
29+
# Rails.root.join('db', 'schema.rb').binwrite(content)
30+
#
31+
class RootPathnameMethods < Base
32+
extend AutoCorrector
33+
34+
MSG = '`%<rails_root>s` is a `Pathname` so you can just append `#%<method>s`.'
35+
36+
DIR_METHODS = %i[
37+
children
38+
delete
39+
each_child
40+
empty?
41+
entries
42+
exist?
43+
glob
44+
mkdir
45+
open
46+
rmdir
47+
unlink
48+
].to_set.freeze
49+
50+
FILE_METHODS = %i[
51+
atime
52+
basename
53+
binread
54+
binwrite
55+
birthtime
56+
blockdev?
57+
chardev?
58+
chmod
59+
chown
60+
ctime
61+
delete
62+
directory?
63+
dirname
64+
empty?
65+
executable?
66+
executable_real?
67+
exist?
68+
expand_path
69+
extname
70+
file?
71+
fnmatch
72+
fnmatch?
73+
ftype
74+
grpowned?
75+
join
76+
lchmod
77+
lchown
78+
lstat
79+
mtime
80+
open
81+
owned?
82+
pipe?
83+
read
84+
readable?
85+
readable_real?
86+
readlines
87+
readlink
88+
realdirpath
89+
realpath
90+
rename
91+
setgid?
92+
setuid?
93+
size
94+
size?
95+
socket?
96+
split
97+
stat
98+
sticky?
99+
symlink?
100+
sysopen
101+
truncate
102+
unlink
103+
utime
104+
world_readable?
105+
world_writable?
106+
writable?
107+
writable_real?
108+
write
109+
zero?
110+
].to_set.freeze
111+
112+
FILE_TEST_METHODS = %i[
113+
blockdev?
114+
chardev?
115+
directory?
116+
empty?
117+
executable?
118+
executable_real?
119+
exist?
120+
file?
121+
grpowned?
122+
owned?
123+
pipe?
124+
readable?
125+
readable_real?
126+
setgid?
127+
setuid?
128+
size
129+
size?
130+
socket?
131+
sticky?
132+
symlink?
133+
world_readable?
134+
world_writable?
135+
writable?
136+
writable_real?
137+
zero?
138+
].to_set.freeze
139+
140+
FILE_UTILS_METHODS = %i[
141+
chmod
142+
chown
143+
mkdir
144+
mkpath
145+
rmdir
146+
rmtree
147+
].to_set.freeze
148+
149+
RESTRICT_ON_SEND = (DIR_METHODS + FILE_METHODS + FILE_TEST_METHODS + FILE_UTILS_METHODS).to_set.freeze
150+
151+
def_node_matcher :pathname_method, <<~PATTERN
152+
{
153+
(send (const {nil? cbase} :Dir) $DIR_METHODS $_ $...)
154+
(send (const {nil? cbase} {:IO :File}) $FILE_METHODS $_ $...)
155+
(send (const {nil? cbase} :FileTest) $FILE_TEST_METHODS $_ $...)
156+
(send (const {nil? cbase} :FileUtils) $FILE_UTILS_METHODS $_ $...)
157+
}
158+
PATTERN
159+
160+
def_node_matcher :rails_root_pathname?, <<~PATTERN
161+
{
162+
$#rails_root?
163+
(send $#rails_root? :join ...)
164+
}
165+
PATTERN
166+
167+
# @!method rails_root?(node)
168+
def_node_matcher :rails_root?, <<~PATTERN
169+
(send (const {nil? cbase} :Rails) {:root :public_path})
170+
PATTERN
171+
172+
def on_send(node)
173+
evidence(node) do |method, path, args, rails_root|
174+
add_offense(node, message: format(MSG, method: method, rails_root: rails_root.source)) do |corrector|
175+
replacement = "#{path.source}.#{method}"
176+
replacement += "(#{args.map(&:source).join(', ')})" unless args.empty?
177+
178+
corrector.replace(node, replacement)
179+
end
180+
end
181+
end
182+
183+
private
184+
185+
def evidence(node)
186+
return if node.method?(:open) && node.parent&.send_type?
187+
return unless (method, path, args = pathname_method(node)) && (rails_root = rails_root_pathname?(path))
188+
189+
yield(method, path, args, rails_root)
190+
end
191+
end
192+
end
193+
end
194+
end

lib/rubocop/cop/rails_cops.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@
9898
require_relative 'rails/reversible_migration'
9999
require_relative 'rails/reversible_migration_method_definition'
100100
require_relative 'rails/root_join_chain'
101+
require_relative 'rails/root_pathname_methods'
101102
require_relative 'rails/root_public_path'
102103
require_relative 'rails/safe_navigation'
103104
require_relative 'rails/safe_navigation_with_blank'
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# frozen_string_literal: true
2+
3+
RSpec.describe RuboCop::Cop::Rails::RootPathnameMethods, :config do
4+
{
5+
Dir: described_class::DIR_METHODS,
6+
File: described_class::FILE_METHODS,
7+
FileTest: described_class::FILE_TEST_METHODS,
8+
FileUtils: described_class::FILE_UTILS_METHODS,
9+
IO: described_class::FILE_METHODS
10+
}.each do |receiver, methods|
11+
methods.each do |method|
12+
it "registers an offense when using `#{receiver}.#{method}(Rails.public_path)` (if arity exists)" do
13+
expect_offense(<<~RUBY, receiver: receiver, method: method)
14+
%{receiver}.%{method}(Rails.public_path)
15+
^{receiver}^^{method}^^^^^^^^^^^^^^^^^^^ `Rails.public_path` is a `Pathname` so you can just append `#%{method}`.
16+
RUBY
17+
18+
expect_correction(<<~RUBY)
19+
Rails.public_path.#{method}
20+
RUBY
21+
end
22+
23+
it "registers an offense when using `::#{receiver}.#{method}(::Rails.root.join(...))` (if arity exists)" do
24+
expect_offense(<<~RUBY, receiver: receiver, method: method)
25+
::%{receiver}.%{method}(::Rails.root.join('db', 'schema.rb'))
26+
^^^{receiver}^^{method}^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `::Rails.root` is a `Pathname` so you can just append `#%{method}`.
27+
RUBY
28+
29+
expect_correction(<<~RUBY)
30+
::Rails.root.join('db', 'schema.rb').#{method}
31+
RUBY
32+
end
33+
34+
it "registers an offense when using `::#{receiver}.#{method}(::Rails.root.join(...), ...)` (if arity exists)" do
35+
expect_offense(<<~RUBY, receiver: receiver, method: method)
36+
::%{receiver}.%{method}(::Rails.root.join('db', 'schema.rb'), 20, 5)
37+
^^^{receiver}^^{method}^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `::Rails.root` is a `Pathname` so you can just append `#%{method}`.
38+
RUBY
39+
40+
expect_correction(<<~RUBY)
41+
::Rails.root.join('db', 'schema.rb').#{method}(20, 5)
42+
RUBY
43+
end
44+
end
45+
end
46+
47+
# This is handled by `Rails/RootJoinChain`
48+
it 'does not register an offense when using `File.read(Rails.root.join(...).join(...))`' do
49+
expect_no_offenses(<<~RUBY)
50+
File.read(Rails.root.join('db').join('schema.rb'))
51+
RUBY
52+
end
53+
54+
# This is handled by `Style/FileRead`
55+
it 'does not register an offense when using `File.open(Rails.root.join(...)).read`' do
56+
expect_no_offenses(<<~RUBY)
57+
File.open(Rails.root.join('db', 'schema.rb')).read
58+
RUBY
59+
end
60+
61+
# This is handled by `Style/FileRead`
62+
it 'does not register an offense when using `File.open(Rails.root.join(...)).binread`' do
63+
expect_no_offenses(<<~RUBY)
64+
File.open(Rails.root.join('db', 'schema.rb')).binread
65+
RUBY
66+
end
67+
68+
# This is handled by `Style/FileWrite`
69+
it 'does not register an offense when using `File.open(Rails.root.join(...)).write(content)`' do
70+
expect_no_offenses(<<~RUBY)
71+
File.open(Rails.root.join('db', 'schema.rb')).write(content)
72+
RUBY
73+
end
74+
75+
# This is handled by `Style/FileWrite`
76+
it 'does not register an offense when using `File.open(Rails.root.join(...)).binwrite(content)`' do
77+
expect_no_offenses(<<~RUBY)
78+
File.open(Rails.root.join('db', 'schema.rb')).binwrite(content)
79+
RUBY
80+
end
81+
82+
it 'registers an offense when using `File.open(Rails.root.join(...), ...)` inside an iterator' do
83+
expect_offense(<<~RUBY)
84+
files.map { |file| File.open(Rails.root.join('db', file), 'wb') }
85+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `Rails.root` is a `Pathname` so you can just append `#open`.
86+
RUBY
87+
88+
expect_correction(<<~RUBY)
89+
files.map { |file| Rails.root.join('db', file).open('wb') }
90+
RUBY
91+
end
92+
end

0 commit comments

Comments
 (0)