class RuboCop::Cop::Lint::NonAtomicFileOperation

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 - race condition with another process may result in an error in `mkdir`
unless Dir.exist?(path)
  FileUtils.mkdir(path)
end

# good - atomic and idempotent creation
FileUtils.mkdir_p(path)

# bad - race condition with another process may result in an error in `remove`
if File.exist?(path)
  FileUtils.remove(path)
end

# good - atomic and idempotent removal
FileUtils.rm_f(path)

Constants

MAKE_FORCE_METHODS
MAKE_METHODS
MSG_CHANGE_FORCE_METHOD
MSG_REMOVE_FILE_EXIST_CHECK
REMOVE_FORCE_METHODS
REMOVE_METHODS
RESTRICT_ON_SEND

Public Instance Methods

on_send(node) click to toggle source
# File lib/rubocop/cop/lint/non_atomic_file_operation.rb, line 80
def on_send(node)
  return unless if_node_child?(node)
  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

  register_offense(node, exist_node)
end

Private Instance Methods

allowable_use_with_if?(if_node) click to toggle source
# File lib/rubocop/cop/lint/non_atomic_file_operation.rb, line 97
def allowable_use_with_if?(if_node)
  if_node.condition.and_type? || if_node.condition.or_type? || if_node.else_branch
end
autocorrect(corrector, node, range) click to toggle source
# File lib/rubocop/cop/lint/non_atomic_file_operation.rb, line 120
def autocorrect(corrector, node, range)
  corrector.remove(range)
  autocorrect_replace_method(corrector, node)
  corrector.remove(node.parent.loc.end) if node.parent.multiline?
end
autocorrect_replace_method(corrector, node) click to toggle source
# File lib/rubocop/cop/lint/non_atomic_file_operation.rb, line 126
def autocorrect_replace_method(corrector, node)
  return if force_method?(node)

  corrector.replace(node.child_nodes.first.loc.name, 'FileUtils')
  corrector.replace(node.loc.selector, replacement_method(node))
end
force_method?(node) click to toggle source
# File lib/rubocop/cop/lint/non_atomic_file_operation.rb, line 143
def force_method?(node)
  force_method_name?(node) || force_option?(node)
end
force_method_name?(node) click to toggle source
# File lib/rubocop/cop/lint/non_atomic_file_operation.rb, line 151
def force_method_name?(node)
  (MAKE_FORCE_METHODS + REMOVE_FORCE_METHODS).include?(node.method_name)
end
force_option?(node) click to toggle source
# File lib/rubocop/cop/lint/non_atomic_file_operation.rb, line 147
def force_option?(node)
  node.arguments.any? { |arg| force?(arg) }
end
if_node_child?(node) click to toggle source
# File lib/rubocop/cop/lint/non_atomic_file_operation.rb, line 91
def if_node_child?(node)
  return false unless (parent = node.parent)

  parent.if_type? && !allowable_use_with_if?(parent)
end
message_change_force_method(node) click to toggle source
# File lib/rubocop/cop/lint/non_atomic_file_operation.rb, line 111
def message_change_force_method(node)
  format(MSG_CHANGE_FORCE_METHOD, method_name: replacement_method(node))
end
message_remove_file_exist_check(node) click to toggle source
# File lib/rubocop/cop/lint/non_atomic_file_operation.rb, line 115
def message_remove_file_exist_check(node)
  receiver, method_name = receiver_and_method_name(node)
  format(MSG_REMOVE_FILE_EXIST_CHECK, receiver: receiver, method_name: method_name)
end
register_offense(node, exist_node) click to toggle source
# File lib/rubocop/cop/lint/non_atomic_file_operation.rb, line 101
def register_offense(node, exist_node)
  add_offense(node, message: message_change_force_method(node)) unless force_method?(node)

  range = range_between(node.parent.loc.keyword.begin_pos,
                        exist_node.loc.expression.end_pos)
  add_offense(range, message: message_remove_file_exist_check(exist_node)) do |corrector|
    autocorrect(corrector, node, range) unless node.parent.elsif?
  end
end
replacement_method(node) click to toggle source
# File lib/rubocop/cop/lint/non_atomic_file_operation.rb, line 133
def replacement_method(node)
  if MAKE_METHODS.include?(node.method_name)
    'mkdir_p'
  elsif REMOVE_METHODS.include?(node.method_name)
    'rm_f'
  else
    node.method_name
  end
end