forked from rubocop/rubocop
/
non_atomic_file_operation.rb
157 lines (135 loc) · 5.54 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
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
# 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 - 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)
#
class NonAtomicFileOperation < Base
extend AutoCorrector
include Alignment
include RangeHelp
MSG_REMOVE_FILE_EXIST_CHECK = 'Remove unnecessary existence check ' \
'`%<receiver>s.%<method_name>s`.'
MSG_CHANGE_FORCE_METHOD = 'Use atomic file operation method `FileUtils.%<method_name>s`.'
MAKE_FORCE_METHODS = %i[makedirs mkdir_p mkpath].freeze
MAKE_METHODS = %i[mkdir].freeze
REMOVE_FORCE_METHODS = %i[rm_f rm_rf].freeze
REMOVE_METHODS = %i[remove remove_dir remove_entry remove_entry_secure
delete unlink remove_file rm rmdir safe_unlink].freeze
RESTRICT_ON_SEND = (MAKE_METHODS + MAKE_FORCE_METHODS + REMOVE_METHODS +
REMOVE_FORCE_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 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
def if_node_child?(node)
return false unless (parent = node.parent)
parent.if_type? && !allowable_use_with_if?(parent)
end
def allowable_use_with_if?(if_node)
if_node.condition.and_type? || if_node.condition.or_type? || if_node.else_branch
end
def register_offense(node, exist_node)
unless force_method?(node)
add_offense(node,
message: format(MSG_CHANGE_FORCE_METHOD,
method_name: replacement_method(node)))
end
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)
end
end
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
def autocorrect(corrector, node, range)
corrector.remove(range)
autocorrect_replace_method(corrector, node)
corrector.remove(node.parent.loc.end) if node.parent.multiline?
end
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
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
def force_method?(node)
force_method_name?(node) || force_option?(node)
end
def force_option?(node)
node.arguments.any? { |arg| force?(arg) }
end
def force_method_name?(node)
(MAKE_FORCE_METHODS + REMOVE_FORCE_METHODS).include?(node.method_name)
end
end
end
end
end