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
RECURSIVE_REMOVE_METHODS
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 121
def autocorrect(corrector, node, range)
  corrector.remove(range)
  autocorrect_replace_method(corrector, node)

  if node.parent.modifier_form?
    corrector.remove(node.source_range.end.join(node.parent.loc.keyword.begin))
  else
    corrector.remove(node.parent.loc.end)
  end
end
autocorrect_replace_method(corrector, node) click to toggle source
# File lib/rubocop/cop/lint/non_atomic_file_operation.rb, line 132
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))
  corrector.insert_before(node.last_argument, 'mode: ') if require_mode_keyword?(node)
end
force_method?(node) click to toggle source
# File lib/rubocop/cop/lint/non_atomic_file_operation.rb, line 152
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 166
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 162
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 112
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 116
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)

  parent = node.parent
  range = parent.loc.keyword.begin.join(parent.condition.source_range.end)

  add_offense(range, message: message_remove_file_exist_check(exist_node)) do |corrector|
    autocorrect(corrector, node, range) unless parent.elsif?
  end
end
replacement_method(node) click to toggle source
# File lib/rubocop/cop/lint/non_atomic_file_operation.rb, line 140
def replacement_method(node)
  if MAKE_METHODS.include?(node.method_name)
    'mkdir_p'
  elsif REMOVE_METHODS.include?(node.method_name)
    'rm_f'
  elsif RECURSIVE_REMOVE_METHODS.include?(node.method_name)
    'rm_rf'
  else
    node.method_name
  end
end
require_mode_keyword?(node) click to toggle source
# File lib/rubocop/cop/lint/non_atomic_file_operation.rb, line 156
def require_mode_keyword?(node)
  return false unless node.receiver.const_name == 'Dir'

  replacement_method(node) == 'mkdir_p' && node.arguments.length == 2
end