Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Fix #763] Fix a false positive for Rails/RootPathnameMethods #768

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -0,0 +1 @@
* [#763](https://github.com/rubocop/rubocop-rails/issues/763): Mark `Rails/RootPathnameMethods` as unsafe and fix an incorrect autocorrect when using `Dir.glob`. ([@koic][])
1 change: 1 addition & 0 deletions config/default.yml
Expand Up @@ -845,6 +845,7 @@ Rails/RootJoinChain:
Rails/RootPathnameMethods:
Description: 'Use `Rails.root` IO methods instead of passing it to `File`.'
Enabled: pending
SafeAutocorrect: false
VersionAdded: '2.16'

Rails/RootPublicPath:
Expand Down
43 changes: 41 additions & 2 deletions lib/rubocop/cop/rails/root_pathname_methods.rb
Expand Up @@ -11,6 +11,10 @@ module Rails
# This cop works best when used together with
# `Style/FileRead`, `Style/FileWrite` and `Rails/RootJoinChain`.
#
# @safety
# This cop is unsafe for autocorrection because `Dir`'s `children`, `each_child`, `entries`, and `glob`
# methods return string element, but these methods of `Pathname` return `Pathname` element.
#
# @example
# # bad
# File.open(Rails.root.join('db', 'schema.rb'))
Expand All @@ -30,6 +34,7 @@ module Rails
#
class RootPathnameMethods < Base
extend AutoCorrector
include RangeHelp

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

Expand Down Expand Up @@ -138,6 +143,11 @@ class RootPathnameMethods < Base
}
PATTERN

def_node_matcher :dir_glob?, <<~PATTERN
(send
(const {cbase nil?} :Dir) :glob ...)
PATTERN

def_node_matcher :rails_root_pathname?, <<~PATTERN
{
$#rails_root?
Expand All @@ -153,8 +163,12 @@ class RootPathnameMethods < Base
def on_send(node)
evidence(node) do |method, path, args, rails_root|
add_offense(node, message: format(MSG, method: method, rails_root: rails_root.source)) do |corrector|
replacement = "#{path.source}.#{method}"
replacement += "(#{args.map(&:source).join(', ')})" unless args.empty?
if dir_glob?(node)
replacement = build_path_glob(path, method)
else
replacement = "#{path.source}.#{method}"
replacement += "(#{args.map(&:source).join(', ')})" unless args.empty?
end

corrector.replace(node, replacement)
end
Expand All @@ -169,6 +183,31 @@ def evidence(node)

yield(method, path, args, rails_root)
end

def build_path_glob(path, method)
receiver = range_between(path.loc.expression.begin_pos, path.children.first.loc.selector.end_pos).source

argument = if path.arguments.one?
path.first_argument.source
else
join_arguments(path.arguments)
end

"#{receiver}.#{method}(#{argument})"
end

def include_interpolation?(arguments)
arguments.any? do |argument|
argument.children.any? { |child| child.respond_to?(:begin_type?) && child.begin_type? }
end
end

def join_arguments(arguments)
quote = include_interpolation?(arguments) ? '"' : "'"
joined_arguments = arguments.map(&:value).join('/')

"#{quote}#{joined_arguments}#{quote}"
end
end
end
end
Expand Down
59 changes: 59 additions & 0 deletions spec/rubocop/cop/rails/root_pathname_methods_spec.rb
Expand Up @@ -9,6 +9,8 @@
IO: described_class::FILE_METHODS
}.each do |receiver, methods|
methods.each do |method|
next if method == :glob

it "registers an offense when using `#{receiver}.#{method}(Rails.public_path)` (if arity exists)" do
expect_offense(<<~RUBY, receiver: receiver, method: method)
%{receiver}.%{method}(Rails.public_path)
Expand Down Expand Up @@ -44,6 +46,63 @@
end
end

context 'when using `Dir.glob`' do
it "registers an offense when using `Dir.glob(Rails.root.join('**/*.rb'))`" do
expect_offense(<<~RUBY)
Dir.glob(Rails.root.join('**/*.rb'))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `Rails.root` is a `Pathname` so you can just append `#glob`.
RUBY

expect_correction(<<~RUBY)
Rails.root.glob('**/*.rb')
RUBY
end

it "registers an offense when using `::Dir.glob(Rails.root.join('**/*.rb'))`" do
expect_offense(<<~RUBY)
::Dir.glob(Rails.root.join('**/*.rb'))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `Rails.root` is a `Pathname` so you can just append `#glob`.
RUBY

expect_correction(<<~RUBY)
Rails.root.glob('**/*.rb')
RUBY
end

it "registers an offense when using `Dir.glob(Rails.root.join('**/\#{path}/*.rb'))`" do
expect_offense(<<~'RUBY')
Dir.glob(Rails.root.join("**/#{path}/*.rb"))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `Rails.root` is a `Pathname` so you can just append `#glob`.
RUBY

expect_correction(<<~'RUBY')
Rails.root.glob("**/#{path}/*.rb")
RUBY
end

it "registers an offense when using `Dir.glob(Rails.root.join('**', '*.rb'))`" do
expect_offense(<<~RUBY)
Dir.glob(Rails.root.join('**', '*.rb'))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `Rails.root` is a `Pathname` so you can just append `#glob`.
RUBY

expect_correction(<<~RUBY)
Rails.root.glob('**/*.rb')
RUBY
end

it "registers an offense when using `Dir.glob(Rails.root.join('**', \"\#{path}\", '*.rb'))`" do
expect_offense(<<~'RUBY')
Dir.glob(Rails.root.join('**', "#{path}", '*.rb'))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `Rails.root` is a `Pathname` so you can just append `#glob`.
RUBY

expect_correction(<<~'RUBY')
Rails.root.glob("**/#{path}/*.rb")
RUBY
end
end

# This is handled by `Rails/RootJoinChain`
it 'does not register an offense when using `File.read(Rails.root.join(...).join(...))`' do
expect_no_offenses(<<~RUBY)
Expand Down