Skip to content

Commit

Permalink
Add new Rails/RootPathnameMethods cop
Browse files Browse the repository at this point in the history
`Rails.root` is an instance of `Pathname`. So instead of

```ruby
File.open(Rails.root.join('db', 'schema.rb'))
File.open(Rails.root.join('db', 'schema.rb'), 'w')
File.read(Rails.root.join('db', 'schema.rb'))
File.binread(Rails.root.join('db', 'schema.rb'))
File.write(Rails.root.join('db', 'schema.rb'), content)
File.binwrite(Rails.root.join('db', 'schema.rb'), content)
```

we can simply write

```ruby
Rails.root.join('db', 'schema.rb').open
Rails.root.join('db', 'schema.rb').open('w')
Rails.root.join('db', 'schema.rb').read
Rails.root.join('db', 'schema.rb').binread
Rails.root.join('db', 'schema.rb').write(content)
Rails.root.join('db', 'schema.rb').binwrite(content)
```

This cop works best when used together with
[`Style/FileRead`](rubocop/rubocop#10261),
[`Style/FileWrite`](rubocop/rubocop#10260),
and [`Rails/RootJoinChain`](#586).
  • Loading branch information
leoarnold committed Nov 21, 2021
1 parent 19369de commit 88fffc7
Show file tree
Hide file tree
Showing 6 changed files with 221 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -484,3 +484,4 @@
[@theunraveler]: https://github.com/theunraveler
[@pirj]: https://github.com/pirj
[@vitormd]: https://github.com/vitormd
[@leoarnold]: https://github.com/leoarnold
1 change: 1 addition & 0 deletions changelog/new_add_new_railsrootpathnamemethods_cop.md
@@ -0,0 +1 @@
* [#587](https://github.com/rubocop/rubocop-rails/pull/587): Add new `Rails/RootPathnameMethods` cop. ([@leoarnold][])
5 changes: 5 additions & 0 deletions config/default.yml
Expand Up @@ -701,6 +701,11 @@ Rails/ReversibleMigrationMethodDefinition:
Include:
- db/migrate/*.rb

Rails/RootPathnameMethods:
Description: 'Use `Rails.root` IO methods instead of passing it to `File`.'
Enabled: pending
VersionAdded: '2.13'

Rails/SafeNavigation:
Description: "Use Ruby's safe navigation operator (`&.`) instead of `try!`."
Enabled: true
Expand Down
109 changes: 109 additions & 0 deletions lib/rubocop/cop/rails/root_pathname_methods.rb
@@ -0,0 +1,109 @@
# frozen_string_literal: true

module RuboCop
module Cop
module Rails
# Use `Rails.root` IO methods instead of passing it to `File`
#
# `Rails.root` is an instance of `Pathname`
# so we can apply many IO methods directly.
#
# This cop works best when used together with
# `Style/FileRead`, `Style/FileWrite` and `Rails/RootJoinChain`.
#
# @example
# # bad
# File.open(Rails.root.join('db', 'schema.rb'))
# File.open(Rails.root.join('db', 'schema.rb'), 'w')
# File.read(Rails.root.join('db', 'schema.rb'))
# File.binread(Rails.root.join('db', 'schema.rb'))
# File.write(Rails.root.join('db', 'schema.rb'), content)
# File.binwrite(Rails.root.join('db', 'schema.rb'), content)
#
# # good
# Rails.root.join('db', 'schema.rb').open
# Rails.root.join('db', 'schema.rb').open('w')
# Rails.root.join('db', 'schema.rb').read
# Rails.root.join('db', 'schema.rb').binread
# Rails.root.join('db', 'schema.rb').write(content)
# Rails.root.join('db', 'schema.rb').binwrite(content)
#
class RootPathnameMethods < Base
extend AutoCorrector

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

RESTRICT_ON_SEND = %i[binread binwrite open read write].to_set.freeze

def_node_matcher :file_open, <<~PATTERN
(send
(const nil? :File) :open
$(send
(send
(const nil? :Rails) :root
) :join ...
)
$...
)
PATTERN

def_node_matcher :file_read, <<~PATTERN
(send
(const nil? :File) ${:binread :read}
$(send
(send
(const nil? :Rails) :root
) :join ...
)
)
PATTERN

def_node_matcher :file_write, <<~PATTERN
(send
(const nil? :File) ${:binwrite :write}
$(send
(send
(const nil? :Rails) :root
) :join ...
)
$_
)
PATTERN

def on_send(node)
investigate_file_open(node) || investigate_file_read(node) || investigate_file_write(node)
end

private

def investigate_file_open(node)
return if node.parent?
return unless (rails_root, args = file_open(node))

add_offense(node, message: format(MSG, method: :open)) do |corrector|
replacement = "#{rails_root.source}.open"
replacement += "(#{args.map(&:source).join(', ')})" unless args.empty?

corrector.replace(node, replacement)
end
end

def investigate_file_read(node)
return unless (method, rails_root = file_read(node))

add_offense(node, message: format(MSG, method: method)) do |corrector|
corrector.replace(node, "#{rails_root.source}.#{method}")
end
end

def investigate_file_write(node)
return unless (method, rails_root, content = file_write(node))

add_offense(node, message: format(MSG, method: method)) do |corrector|
corrector.replace(node, "#{rails_root.source}.#{method}(#{content.source})")
end
end
end
end
end
end
1 change: 1 addition & 0 deletions lib/rubocop/cop/rails_cops.rb
Expand Up @@ -11,6 +11,7 @@
require_relative 'rails/active_record_callbacks_order'
require_relative 'rails/active_record_override'
require_relative 'rails/active_support_aliases'
require_relative 'rails/root_pathname_methods'
require_relative 'rails/schema_comment'
require_relative 'rails/add_column_index'
require_relative 'rails/after_commit_override'
Expand Down
104 changes: 104 additions & 0 deletions spec/rubocop/cop/rails/root_pathname_methods_spec.rb
@@ -0,0 +1,104 @@
# frozen_string_literal: true

RSpec.describe RuboCop::Cop::Rails::RootPathnameMethods, :config do
it 'registers an offense when using `File.open(Rails.root.join(...))`' do
expect_offense(<<~RUBY)
File.open(Rails.root.join('db', 'schema.rb'))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `Rails.root` is a `Pathname` so you can just append `#open`.
RUBY

expect_correction(<<~RUBY)
Rails.root.join('db', 'schema.rb').open
RUBY
end

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

expect_correction(<<~RUBY)
Rails.root.join('db', 'schema.rb').open('w')
RUBY
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)
File.read(Rails.root.join('db').join('schema.rb'))
RUBY
end

# This is handled by `Style/FileRead`
it 'does not register an offense when using `File.open(Rails.root.join(...)).read`' do
expect_no_offenses(<<~RUBY)
File.open(Rails.root.join('db', 'schema.rb')).read
RUBY
end

# This is handled by `Style/FileRead`
it 'does not register an offense when using `File.open(Rails.root.join(...)).binread`' do
expect_no_offenses(<<~RUBY)
File.open(Rails.root.join('db', 'schema.rb')).binread
RUBY
end

# This is handled by `Style/FileWrite`
it 'does not register an offense when using `File.open(Rails.root.join(...)).write(content)`' do
expect_no_offenses(<<~RUBY)
File.open(Rails.root.join('db', 'schema.rb')).write(content)
RUBY
end

# This is handled by `Style/FileWrite`
it 'does not register an offense when using `File.open(Rails.root.join(...)).binwrite(content)`' do
expect_no_offenses(<<~RUBY)
File.open(Rails.root.join('db', 'schema.rb')).binwrite(content)
RUBY
end

it 'registers an offense when using `File.read(Rails.root.join(...))`' do
expect_offense(<<~RUBY)
File.read(Rails.root.join('db', 'schema.rb'))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `Rails.root` is a `Pathname` so you can just append `#read`.
RUBY

expect_correction(<<~RUBY)
Rails.root.join('db', 'schema.rb').read
RUBY
end

it 'registers an offense when using `File.binread(Rails.root.join(...))`' do
expect_offense(<<~RUBY)
File.binread(Rails.root.join('db', 'schema.rb'))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `Rails.root` is a `Pathname` so you can just append `#binread`.
RUBY

expect_correction(<<~RUBY)
Rails.root.join('db', 'schema.rb').binread
RUBY
end

it 'registers an offense when using `File.write(Rails.root.join(...), content)`' do
expect_offense(<<~RUBY)
File.write(Rails.root.join('db', 'schema.rb'), content)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `Rails.root` is a `Pathname` so you can just append `#write`.
RUBY

expect_correction(<<~RUBY)
Rails.root.join('db', 'schema.rb').write(content)
RUBY
end

it 'registers an offense when using `File.binwrite(Rails.root.join(...), content)`' do
expect_offense(<<~RUBY)
File.binwrite(Rails.root.join('db', 'schema.rb'), content)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `Rails.root` is a `Pathname` so you can just append `#binwrite`.
RUBY

expect_correction(<<~RUBY)
Rails.root.join('db', 'schema.rb').binwrite(content)
RUBY
end
end

0 comments on commit 88fffc7

Please sign in to comment.