/
non_atomic_file_operation.rb
122 lines (106 loc) · 4.1 KB
/
non_atomic_file_operation.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# 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 node.parent.else_branch
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