Skip to content

Commit a903b09

Browse files
committed
Make Rails/RootPathnameMethods autocorrection safe
1 parent 2235d8c commit a903b09

File tree

4 files changed

+227
-32
lines changed

4 files changed

+227
-32
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* [#993](https://github.com/rubocop/rubocop-rails/pull/993): Make `Rails/RootPathnameMethods` autocorrection safe. ([@r7kamura][])

config/default.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -878,8 +878,8 @@ Rails/RootJoinChain:
878878
Rails/RootPathnameMethods:
879879
Description: 'Use `Rails.root` IO methods instead of passing it to `File`.'
880880
Enabled: pending
881-
SafeAutoCorrect: false
882881
VersionAdded: '2.16'
882+
VersionChanged: '<<next>>'
883883

884884
Rails/RootPublicPath:
885885
Description: "Favor `Rails.public_path` over `Rails.root` with `'public'`."

lib/rubocop/cop/rails/root_pathname_methods.rb

Lines changed: 46 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,6 @@ module Rails
1111
# This cop works best when used together with
1212
# `Style/FileRead`, `Style/FileWrite` and `Rails/RootJoinChain`.
1313
#
14-
# @safety
15-
# This cop is unsafe for autocorrection because `Dir`'s `children`, `each_child`, `entries`, and `glob`
16-
# methods return string element, but these methods of `Pathname` return `Pathname` element.
17-
#
1814
# @example
1915
# # bad
2016
# File.open(Rails.root.join('db', 'schema.rb'))
@@ -32,17 +28,20 @@ module Rails
3228
# Rails.root.join('db', 'schema.rb').write(content)
3329
# Rails.root.join('db', 'schema.rb').binwrite(content)
3430
#
35-
class RootPathnameMethods < Base
31+
class RootPathnameMethods < Base # rubocop:disable Metrics/ClassLength
3632
extend AutoCorrector
3733
include RangeHelp
3834

3935
MSG = '`%<rails_root>s` is a `Pathname` so you can just append `#%<method>s`.'
4036

41-
DIR_METHODS = %i[children delete each_child empty? entries exist? glob mkdir open rmdir unlink].to_set.freeze
37+
DIR_NON_PATHNAMES_RETURNED_METHODS = %i[delete empty? exist? mkdir open rmdir unlink].to_set.freeze
38+
39+
DIR_PATHNAMES_RETURNED_METHODS = %i[children each_child entries glob].to_set.freeze
4240

43-
FILE_METHODS = %i[
41+
DIR_METHODS = (DIR_PATHNAMES_RETURNED_METHODS + DIR_NON_PATHNAMES_RETURNED_METHODS).freeze
42+
43+
FILE_NON_PATHNAME_RETURNED_METHODS = %i[
4444
atime
45-
basename
4645
binread
4746
binwrite
4847
birthtime
@@ -53,19 +52,16 @@ class RootPathnameMethods < Base
5352
ctime
5453
delete
5554
directory?
56-
dirname
5755
empty?
5856
executable?
5957
executable_real?
6058
exist?
61-
expand_path
6259
extname
6360
file?
6461
fnmatch
6562
fnmatch?
6663
ftype
6764
grpowned?
68-
join
6965
lchmod
7066
lchown
7167
lstat
@@ -77,9 +73,6 @@ class RootPathnameMethods < Base
7773
readable?
7874
readable_real?
7975
readlines
80-
readlink
81-
realdirpath
82-
realpath
8376
rename
8477
setgid?
8578
setuid?
@@ -102,6 +95,18 @@ class RootPathnameMethods < Base
10295
zero?
10396
].to_set.freeze
10497

98+
FILE_PATHNAME_RETURNED_METHODS = %i[
99+
basename
100+
dirname
101+
expand_path
102+
join
103+
readlink
104+
realdirpath
105+
realpath
106+
].to_set.freeze
107+
108+
FILE_METHODS = (FILE_PATHNAME_RETURNED_METHODS + FILE_NON_PATHNAME_RETURNED_METHODS).freeze
109+
105110
FILE_TEST_METHODS = %i[
106111
blockdev?
107112
chardev?
@@ -160,13 +165,24 @@ class RootPathnameMethods < Base
160165
(send (const {nil? cbase} :Rails) {:root :public_path})
161166
PATTERN
162167

168+
# @!method dir_pathnames_returned_method?(node)
169+
def_node_matcher :dir_pathnames_returned_method?, <<~PATTERN
170+
(send (const {nil? cbase} :Dir) DIR_PATHNAMES_RETURNED_METHODS ...)
171+
PATTERN
172+
173+
# @!method file_pathname_returned_method?(node)
174+
def_node_matcher :file_pathname_returned_method?, <<~PATTERN
175+
(send (const {nil? cbase} {:IO :File}) FILE_PATHNAME_RETURNED_METHODS ...)
176+
PATTERN
177+
163178
def on_send(node)
164179
evidence(node) do |method, path, args, rails_root|
165180
add_offense(node, message: format(MSG, method: method, rails_root: rails_root.source)) do |corrector|
181+
suffix = build_replacement_suffix(node)
166182
replacement = if dir_glob?(node)
167-
build_path_glob_replacement(path, method)
183+
build_path_glob_replacement(path, method, suffix)
168184
else
169-
build_path_replacement(path, method, args)
185+
build_path_replacement(path, method, args, suffix)
170186
end
171187

172188
corrector.replace(node, replacement)
@@ -183,15 +199,15 @@ def evidence(node)
183199
yield(method, path, args, rails_root)
184200
end
185201

186-
def build_path_glob_replacement(path, method)
202+
def build_path_glob_replacement(path, method, suffix)
187203
receiver = range_between(path.source_range.begin_pos, path.children.first.loc.selector.end_pos).source
188204

189205
argument = path.arguments.one? ? path.first_argument.source : join_arguments(path.arguments)
190206

191-
"#{receiver}.#{method}(#{argument})"
207+
"#{receiver}.#{method}(#{argument})#{suffix}"
192208
end
193209

194-
def build_path_replacement(path, method, args)
210+
def build_path_replacement(path, method, args, suffix)
195211
path_replacement = path.source
196212
if path.arguments? && !path.parenthesized_call?
197213
path_replacement[' '] = '('
@@ -200,9 +216,20 @@ def build_path_replacement(path, method, args)
200216

201217
replacement = "#{path_replacement}.#{method}"
202218
replacement += "(#{args.map(&:source).join(', ')})" unless args.empty?
219+
replacement += suffix
203220
replacement
204221
end
205222

223+
def build_replacement_suffix(node)
224+
if dir_pathnames_returned_method?(node)
225+
'.map(&:to_s)'
226+
elsif file_pathname_returned_method?(node)
227+
'.to_s'
228+
else
229+
''
230+
end
231+
end
232+
206233
def include_interpolation?(arguments)
207234
arguments.any? do |argument|
208235
argument.children.any? { |child| child.respond_to?(:begin_type?) && child.begin_type? }

0 commit comments

Comments
 (0)