forked from rubocop/rubocop
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add new
Lint/NonAtomicFileOperation
cop
Checks for non-atomic file operation. And then replace it with a nearly equivalent and atomic method. These can cause problems that are difficult to reproduce, especially in cases of frequent file operations in parallel, such as test runs with parallel_rspec. For examples: creating a directory if there is none, has the following problems An exception occurs when the directory didn't exist at the time of `exist?`, but someone else created it before `mkdir` was executed. Subsequent processes are executed without the directory that should be there when the directory existed at the time of `exist?`, but someone else deleted it shortly afterwards. ## @safety This cop is unsafe, because autocorrection change to atomic processing. The atomic processing of the replacement destination is not guaranteed to be strictly equivalent to that before the replacement. ## @example ```ruby # bad unless FileTest.exist?(path) FileUtils.makedirs(path) end if FileTest.exist?(path) FileUtils.remove(path) end # good FileUtils.mkdir_p(path) FileUtils.rm_rf(path) ```
- Loading branch information
Showing
11 changed files
with
314 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
* [#10696](https://github.com/rubocop/rubocop/pull/10696): Add new `Lint/NonAtomicFileOperation` cop. ([@ydah][]) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
# frozen_string_literal: true | ||
|
||
module RuboCop | ||
module Cop | ||
module Lint | ||
# Checks for non-atomic file operation. | ||
# And then replace it with a nearly equivalent and atomic method. | ||
# | ||
# These can cause problems that are difficult to reproduce, | ||
# especially in cases of frequent file operations in parallel, | ||
# such as test runs with parallel_rspec. | ||
# | ||
# For examples: creating a directory if there is none, has the following problems | ||
# | ||
# An exception occurs when the directory didn't exist at the time of `exist?`, | ||
# but someone else created it before `mkdir` was executed. | ||
# | ||
# Subsequent processes are executed without the directory that should be there | ||
# when the directory existed at the time of `exist?`, | ||
# but someone else deleted it shortly afterwards. | ||
# | ||
# @safety | ||
# This cop is unsafe, because autocorrection change to atomic processing. | ||
# The atomic processing of the replacement destination is not guaranteed | ||
# to be strictly equivalent to that before the replacement. | ||
# | ||
# @example | ||
# # bad | ||
# unless FileTest.exist?(path) | ||
# FileUtils.makedirs(path) | ||
# end | ||
# | ||
# if FileTest.exist?(path) | ||
# FileUtils.remove(path) | ||
# end | ||
# | ||
# # good | ||
# FileUtils.mkdir_p(path) | ||
# | ||
# FileUtils.rm_rf(path) | ||
# | ||
class NonAtomicFileOperation < Base | ||
extend AutoCorrector | ||
include Alignment | ||
include RangeHelp | ||
|
||
MSG = 'Remove unnecessary existence checks `%<receiver>s.%<method_name>s`.' | ||
MAKE_METHODS = %i[makedirs mkdir mkdir_p mkpath].freeze | ||
REMOVE_METHODS = %i[remove remove_dir remove_entry remove_entry_secure delete unlink | ||
remove_file rm rm_f rm_r rm_rf rmdir rmtree safe_unlink].freeze | ||
RESTRICT_ON_SEND = (MAKE_METHODS + REMOVE_METHODS).freeze | ||
|
||
# @!method send_exist_node(node) | ||
def_node_search :send_exist_node, <<-PATTERN | ||
$(send (const nil? {:FileTest :File :Dir :Shell}) {:exist? :exists?} ...) | ||
PATTERN | ||
|
||
# @!method receiver_and_method_name(node) | ||
def_node_matcher :receiver_and_method_name, <<-PATTERN | ||
(send (const nil? $_) $_ ...) | ||
PATTERN | ||
|
||
# @!method force?(node) | ||
def_node_search :force?, <<~PATTERN | ||
(pair (sym :force) (:true)) | ||
PATTERN | ||
|
||
# @!method explicit_not_force?(node) | ||
def_node_search :explicit_not_force?, <<~PATTERN | ||
(pair (sym :force) (:false)) | ||
PATTERN | ||
|
||
def on_send(node) | ||
return unless node.parent&.if_type? | ||
return if explicit_not_force?(node) | ||
return unless (exist_node = send_exist_node(node.parent).first) | ||
return unless exist_node.first_argument == node.first_argument | ||
|
||
offense(node, exist_node) | ||
end | ||
|
||
private | ||
|
||
def offense(node, exist_node) | ||
range = range_between(node.parent.loc.keyword.begin_pos, | ||
exist_node.loc.expression.end_pos) | ||
|
||
add_offense(range, message: message(exist_node)) do |corrector| | ||
autocorrect(corrector, node, range) | ||
end | ||
end | ||
|
||
def message(node) | ||
receiver, method_name = receiver_and_method_name(node) | ||
format(MSG, receiver: receiver, method_name: method_name) | ||
end | ||
|
||
def autocorrect(corrector, node, range) | ||
corrector.remove(range) | ||
corrector.replace(node.child_nodes.first.loc.name, 'FileUtils') | ||
corrector.replace(node.loc.selector, replacement_method(node)) | ||
corrector.remove(node.parent.loc.end) if node.parent.multiline? | ||
end | ||
|
||
def replacement_method(node) | ||
return node.method_name if force_option?(node) | ||
|
||
if MAKE_METHODS.include?(node.method_name) | ||
'mkdir_p' | ||
elsif REMOVE_METHODS.include?(node.method_name) | ||
'rm_rf' | ||
end | ||
end | ||
|
||
def force_option?(node) | ||
node.arguments.any? { |arg| force?(arg) } | ||
end | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
176 changes: 176 additions & 0 deletions
176
spec/rubocop/cop/lint/non_atomic_file_operation_spec.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,176 @@ | ||
# frozen_string_literal: true | ||
|
||
RSpec.describe RuboCop::Cop::Lint::NonAtomicFileOperation, :config do | ||
%i[makedirs mkdir mkdir_p mkpath].each do |make_method| | ||
it 'registers an offense when use `FileTest.exist?` before creating file' do | ||
expect_offense(<<~RUBY) | ||
unless FileTest.exist?(path) | ||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Remove unnecessary existence checks `FileTest.exist?`. | ||
FileUtils.#{make_method}(path) | ||
end | ||
RUBY | ||
|
||
expect_correction(<<~RUBY) | ||
#{trailing_whitespace}#{trailing_whitespace}FileUtils.mkdir_p(path) | ||
RUBY | ||
end | ||
end | ||
|
||
%i[remove remove_dir remove_entry remove_entry_secure delete unlink | ||
remove_file rm rm_f rm_r rm_rf rmdir rmtree safe_unlink].each do |remove_method| | ||
it 'registers an offense when use `FileTest.exist?` before remove file' do | ||
expect_offense(<<~RUBY) | ||
if FileTest.exist?(path) | ||
^^^^^^^^^^^^^^^^^^^^^^^^ Remove unnecessary existence checks `FileTest.exist?`. | ||
FileUtils.#{remove_method}(path) | ||
end | ||
RUBY | ||
|
||
expect_correction(<<~RUBY) | ||
#{trailing_whitespace}#{trailing_whitespace}FileUtils.rm_rf(path) | ||
RUBY | ||
end | ||
end | ||
|
||
it 'registers an offense when use `FileTest.exist?` before creating file with an option `force: true`' do | ||
expect_offense(<<~RUBY) | ||
unless FileTest.exists?(path) | ||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Remove unnecessary existence checks `FileTest.exists?`. | ||
FileUtils.makedirs(path, force: true) | ||
end | ||
RUBY | ||
|
||
expect_correction(<<~RUBY) | ||
#{trailing_whitespace}#{trailing_whitespace}FileUtils.makedirs(path, force: true) | ||
RUBY | ||
end | ||
|
||
it 'does not register an offense when use `FileTest.exist?` before creating file with an option `force: false`' do | ||
expect_no_offenses(<<~RUBY) | ||
unless FileTest.exists?(path) | ||
FileUtils.makedirs(path, force: false) | ||
end | ||
RUBY | ||
end | ||
|
||
it 'registers an offense when use `FileTest.exist?` before creating file with an option not `force`' do | ||
expect_offense(<<~RUBY) | ||
unless FileTest.exists?(path) | ||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Remove unnecessary existence checks `FileTest.exists?`. | ||
FileUtils.makedirs(path, verbose: true) | ||
end | ||
RUBY | ||
|
||
expect_correction(<<~RUBY) | ||
#{trailing_whitespace}#{trailing_whitespace}FileUtils.mkdir_p(path, verbose: true) | ||
RUBY | ||
end | ||
|
||
it 'registers an offense when use `FileTest.exists?` before creating file' do | ||
expect_offense(<<~RUBY) | ||
unless FileTest.exists?(path) | ||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Remove unnecessary existence checks `FileTest.exists?`. | ||
FileUtils.makedirs(path) | ||
end | ||
RUBY | ||
|
||
expect_correction(<<~RUBY) | ||
#{trailing_whitespace}#{trailing_whitespace}FileUtils.mkdir_p(path) | ||
RUBY | ||
end | ||
|
||
it 'registers an offense when use `FileTest.exist?` with negated `if` before creating file' do | ||
expect_offense(<<~RUBY) | ||
if !FileTest.exist?(path) | ||
^^^^^^^^^^^^^^^^^^^^^^^^^ Remove unnecessary existence checks `FileTest.exist?`. | ||
FileUtils.makedirs(path) | ||
end | ||
RUBY | ||
|
||
expect_correction(<<~RUBY) | ||
#{trailing_whitespace}#{trailing_whitespace}FileUtils.mkdir_p(path) | ||
RUBY | ||
end | ||
|
||
it 'registers an offense when use file existence checks `unless` by postfix before creating file' do | ||
expect_offense(<<~RUBY) | ||
FileUtils.makedirs(path) unless FileTest.exist?(path) | ||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Remove unnecessary existence checks `FileTest.exist?`. | ||
RUBY | ||
|
||
expect_correction(<<~RUBY) | ||
FileUtils.mkdir_p(path)#{trailing_whitespace} | ||
RUBY | ||
end | ||
|
||
it 'registers an offense when use file existence checks `if` by postfix before removing file' do | ||
expect_offense(<<~RUBY) | ||
FileUtils.remove(path) if FileTest.exist?(path) | ||
^^^^^^^^^^^^^^^^^^^^^^^^ Remove unnecessary existence checks `FileTest.exist?`. | ||
RUBY | ||
|
||
expect_correction(<<~RUBY) | ||
FileUtils.rm_rf(path)#{trailing_whitespace} | ||
RUBY | ||
end | ||
|
||
it 'does not register an offense when not checking for the existence' do | ||
expect_no_offenses(<<~RUBY) | ||
FileUtils.mkdir_p(path) | ||
RUBY | ||
end | ||
|
||
it 'does not register an offense when checking for the existence of different files' do | ||
expect_no_offenses(<<~RUBY) | ||
FileUtils.mkdir_p(y) unless FileTest.exist?(path) | ||
RUBY | ||
end | ||
|
||
it 'does not register an offense when not a method of file operation' do | ||
expect_no_offenses(<<~RUBY) | ||
unless FileUtils.exist?(path) | ||
FileUtils.options_of(:rm) | ||
end | ||
unless FileUtils.exist?(path) | ||
NotFile.remove(path) | ||
end | ||
RUBY | ||
end | ||
|
||
it 'does not register an offense when not an exist check' do | ||
expect_no_offenses(<<~RUBY) | ||
unless FileUtils.options_of(:rm) | ||
FileUtils.mkdir_p(path) | ||
end | ||
if FileTest.executable?(path) | ||
FileUtils.remove(path) | ||
end | ||
RUBY | ||
end | ||
|
||
it 'does not register an offense when processing other than file operations' do | ||
expect_no_offenses(<<~RUBY) | ||
unless FileTest.exist?(path) | ||
FileUtils.makedirs(path) | ||
do_something | ||
end | ||
unless FileTest.exist?(path) | ||
do_something | ||
FileUtils.makedirs(path) | ||
end | ||
RUBY | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.