greenroom 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +16 -0
- data/LICENSE.txt +21 -0
- data/README.md +165 -0
- data/Rakefile +10 -0
- data/exe/greenroom +6 -0
- data/lib/greenroom/census/cursor.rb +48 -0
- data/lib/greenroom/census/job.rb +118 -0
- data/lib/greenroom/census/middleware.rb +30 -0
- data/lib/greenroom/census/run.rb +59 -0
- data/lib/greenroom/cli/check.rb +454 -0
- data/lib/greenroom/cli/judge.rb +273 -0
- data/lib/greenroom/cli.rb +30 -0
- data/lib/greenroom/digest.rb +28 -0
- data/lib/greenroom/experiment_behavior.rb +82 -0
- data/lib/greenroom/recorder.rb +219 -0
- data/lib/greenroom/reporter/new_relic.rb +15 -0
- data/lib/greenroom/reporter.rb +61 -0
- data/lib/greenroom/version.rb +5 -0
- data/lib/greenroom.rb +57 -0
- data/sig/greenroom.rbs +4 -0
- metadata +105 -0
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "open3"
|
|
5
|
+
require "optparse"
|
|
6
|
+
|
|
7
|
+
module Greenroom
|
|
8
|
+
module CLI
|
|
9
|
+
module Check
|
|
10
|
+
Result = Data.define(:accepted, :message, :experiment, :candidate)
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
def run(arguments, stdout: $stdout, stderr: $stderr)
|
|
14
|
+
json = false
|
|
15
|
+
parser = OptionParser.new do |options|
|
|
16
|
+
options.banner = "Usage: greenroom check [--json] [commit]"
|
|
17
|
+
options.on("--json") { json = true }
|
|
18
|
+
end
|
|
19
|
+
parser.parse!(arguments)
|
|
20
|
+
return usage(stderr, parser) if arguments.length > 1
|
|
21
|
+
|
|
22
|
+
require "rubocop-ast"
|
|
23
|
+
result = Repository.new(Dir.pwd).check(arguments.first || "HEAD")
|
|
24
|
+
print_result(stdout, result, json)
|
|
25
|
+
result.accepted ? 0 : 1
|
|
26
|
+
rescue OptionParser::ParseError => error
|
|
27
|
+
stderr.puts(error.message)
|
|
28
|
+
stderr.puts(parser)
|
|
29
|
+
2
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def usage(stderr, parser)
|
|
35
|
+
stderr.puts("One commit is allowed.")
|
|
36
|
+
stderr.puts(parser)
|
|
37
|
+
2
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def print_result(stdout, result, json)
|
|
41
|
+
if json
|
|
42
|
+
stdout.puts(JSON.generate({
|
|
43
|
+
accepted: result.accepted,
|
|
44
|
+
message: result.message,
|
|
45
|
+
experiment: result.experiment,
|
|
46
|
+
candidate: result.candidate
|
|
47
|
+
}))
|
|
48
|
+
else
|
|
49
|
+
stdout.puts(result.message)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# This check gates an automatic merge. The RuboCop check sees one file
|
|
55
|
+
# version, so it cannot compare the new method body with the old body.
|
|
56
|
+
class Repository
|
|
57
|
+
def initialize(directory)
|
|
58
|
+
@directory = directory
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def check(commit)
|
|
62
|
+
resolved = git("rev-parse", "--verify", "#{commit}^{commit}").strip
|
|
63
|
+
parents = git("rev-list", "--parents", "-n", "1", resolved).split.drop(1)
|
|
64
|
+
# A merge has more than one source state. A comparison with its first
|
|
65
|
+
# parent would give a different meaning to the acceptance result.
|
|
66
|
+
return reject("The commit is a merge commit.") if parents.length > 1
|
|
67
|
+
return reject("The commit has no parent.") if parents.empty?
|
|
68
|
+
|
|
69
|
+
changes = git("diff-tree", "--no-commit-id", "--name-status", "-r", "-M20%", parents.first, resolved).lines
|
|
70
|
+
# A rejected shape goes to human review. Thus, an uncertain result
|
|
71
|
+
# must reject instead of accepting a change for an automatic merge.
|
|
72
|
+
return reject("The commit must modify one Ruby file.") unless changes.one?
|
|
73
|
+
|
|
74
|
+
fields = changes.first.chomp.split("\t")
|
|
75
|
+
# Git can report a rename as R or as an addition and a deletion.
|
|
76
|
+
# Requiring one M entry rejects both forms and their identity change.
|
|
77
|
+
return reject("The changed file must have status M.") unless fields.length == 2 && fields.first == "M"
|
|
78
|
+
|
|
79
|
+
path = fields.last
|
|
80
|
+
return reject("The changed file must have the .rb extension.") unless path.end_with?(".rb")
|
|
81
|
+
|
|
82
|
+
# Complete blobs support a syntax tree comparison across both file
|
|
83
|
+
# versions. Parsing unified diff lines would mix syntax with context.
|
|
84
|
+
before = git("show", "#{parents.first}:#{path}")
|
|
85
|
+
after = git("show", "#{resolved}:#{path}")
|
|
86
|
+
Analyzer.new(before, after).check
|
|
87
|
+
rescue GitError, Analyzer::SourceError => error
|
|
88
|
+
reject(error.message)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
GitError = Class.new(StandardError)
|
|
94
|
+
|
|
95
|
+
def git(*arguments)
|
|
96
|
+
stdout, _stderr, status = Open3.capture3("git", *arguments, chdir: @directory)
|
|
97
|
+
raise GitError, "Git could not read the commit." unless status.success?
|
|
98
|
+
|
|
99
|
+
stdout
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def reject(message)
|
|
103
|
+
Result.new(false, message, nil, nil)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
class Analyzer
|
|
108
|
+
SourceError = Class.new(StandardError)
|
|
109
|
+
Scope = Data.define(:name, :node, :methods, :statements)
|
|
110
|
+
Experiment = Data.define(:name, :method, :use_block, :candidate)
|
|
111
|
+
|
|
112
|
+
def initialize(before, after)
|
|
113
|
+
# Syntax trees omit source locations. Therefore, comment and white
|
|
114
|
+
# space positions do not affect the acceptance result.
|
|
115
|
+
@before = parse(before)
|
|
116
|
+
@after = parse(after)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def check
|
|
120
|
+
return reject("Unsupported Ruby method or class syntax is present.") if unsupported?(@before) || unsupported?(@after)
|
|
121
|
+
|
|
122
|
+
before_scopes = scopes(@before)
|
|
123
|
+
after_scopes = scopes(@after)
|
|
124
|
+
return reject("The class and module structure changed.") unless before_scopes.keys == after_scopes.keys
|
|
125
|
+
return reject("The class or module header changed.") unless before_scopes.all? { |name, scope| same_header?(scope, after_scopes.fetch(name)) }
|
|
126
|
+
return reject("A top-level statement changed.") unless top_level(@before) == top_level(@after)
|
|
127
|
+
|
|
128
|
+
changed = []
|
|
129
|
+
added = []
|
|
130
|
+
before_scopes.each do |name, before_scope|
|
|
131
|
+
after_scope = after_scopes.fetch(name)
|
|
132
|
+
removed_names = before_scope.methods.keys - after_scope.methods.keys
|
|
133
|
+
return reject("An existing method was removed.") unless removed_names.empty?
|
|
134
|
+
|
|
135
|
+
common_names = before_scope.methods.keys & after_scope.methods.keys
|
|
136
|
+
common_names.each do |method_name|
|
|
137
|
+
changed << [name, before_scope.methods.fetch(method_name), after_scope.methods.fetch(method_name)] unless same_ast?(before_scope.methods.fetch(method_name), after_scope.methods.fetch(method_name))
|
|
138
|
+
end
|
|
139
|
+
(after_scope.methods.keys - before_scope.methods.keys).each do |method_name|
|
|
140
|
+
added << [name, after_scope.methods.fetch(method_name)]
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
return reject("Exactly one existing method must change.") unless changed.one?
|
|
145
|
+
return reject("Exactly one candidate method must be added.") unless added.one?
|
|
146
|
+
|
|
147
|
+
scope_name, before_method, after_method = changed.first
|
|
148
|
+
added_scope_name, candidate = added.first
|
|
149
|
+
return reject("The candidate method must be in the changed class.") unless scope_name == added_scope_name
|
|
150
|
+
return reject("The candidate method must be private.") unless private_method?(after_scopes.fetch(scope_name), candidate)
|
|
151
|
+
return reject("An existing method visibility changed.") unless same_visibilities?(before_scopes.fetch(scope_name), after_scopes.fetch(scope_name))
|
|
152
|
+
return reject("Another class body statement changed.") unless unchanged_statements?(before_scopes.fetch(scope_name), after_scopes.fetch(scope_name), candidate)
|
|
153
|
+
return reject("The changed method signature must stay the same.") unless same_ast?(before_method.children[1], after_method.children[1])
|
|
154
|
+
other_scope_changed = before_scopes.any? do |name, before_scope|
|
|
155
|
+
next false if name == scope_name
|
|
156
|
+
|
|
157
|
+
after_scope = after_scopes.fetch(name)
|
|
158
|
+
!unchanged_statements?(before_scope, after_scope)
|
|
159
|
+
end
|
|
160
|
+
return reject("Another class or module changed.") if other_scope_changed
|
|
161
|
+
|
|
162
|
+
science = science_blocks(after_method)
|
|
163
|
+
unless science
|
|
164
|
+
shape_error = science_shape_error(after_method)
|
|
165
|
+
return reject(shape_error || "The changed method body must contain one Scientist.run call.")
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
use_block, try_block, _compare_block, experiment, science_statement = science
|
|
169
|
+
return reject("The use block must match the old method body.") unless matching_span?(before_method, after_method, science_statement, use_block)
|
|
170
|
+
# Tests found behavior changes when statements move into a block.
|
|
171
|
+
# A return in the moved part bypasses compare and publish.
|
|
172
|
+
# A binding sees the block's local variables. Tests found the same
|
|
173
|
+
# behavior for yield, rescue, ensure, __method__, local variables,
|
|
174
|
+
# and constant references, so those forms remain valid.
|
|
175
|
+
return reject("A return in a use or try block is unsafe.") if unsafe_return?(use_block) || unsafe_return?(try_block)
|
|
176
|
+
return reject("A binding in a use or try block is unsafe.") if binding_call?(use_block) || binding_call?(try_block)
|
|
177
|
+
|
|
178
|
+
candidate_name = candidate.children[0]
|
|
179
|
+
Result.new(true, "Accepted experiment #{experiment} with candidate #{candidate_name}.", experiment, candidate_name.to_s)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# The judge uses the same structural relation that the merge gate
|
|
183
|
+
# verifies. A second parser could accept a shape that the gate rejects.
|
|
184
|
+
def experiments
|
|
185
|
+
scopes(@after).values.flat_map do |scope|
|
|
186
|
+
scope.methods.values.filter_map do |method|
|
|
187
|
+
science = science_blocks(method)
|
|
188
|
+
next unless science
|
|
189
|
+
|
|
190
|
+
use_block, try_block, _compare_block, name = science
|
|
191
|
+
try_body = block_body(try_block)
|
|
192
|
+
next unless try_body&.send_type? && try_body.receiver.nil?
|
|
193
|
+
|
|
194
|
+
candidate = scope.methods[try_body.method_name]
|
|
195
|
+
Experiment.new(name, method, use_block, candidate) if candidate
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
private
|
|
201
|
+
|
|
202
|
+
def parse(source)
|
|
203
|
+
processed = RuboCop::AST::ProcessedSource.new(source, 3.2)
|
|
204
|
+
raise SourceError, "The Ruby source has a syntax error." unless processed.valid_syntax?
|
|
205
|
+
|
|
206
|
+
processed.ast
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def scopes(root)
|
|
210
|
+
found = {}
|
|
211
|
+
walk_scopes(root, [], found)
|
|
212
|
+
found
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def walk_scopes(node, namespace, found)
|
|
216
|
+
return unless node.is_a?(RuboCop::AST::Node)
|
|
217
|
+
|
|
218
|
+
if node.class_type? || node.module_type?
|
|
219
|
+
name_node = node.children[0]
|
|
220
|
+
raise SourceError, "Anonymous classes are not supported." unless name_node&.const_type?
|
|
221
|
+
|
|
222
|
+
name = constant_name(name_node, namespace)
|
|
223
|
+
raise SourceError, "A class or module is reopened in this file." if found.key?(name)
|
|
224
|
+
|
|
225
|
+
body = node.class_type? ? node.children[2] : node.children[1]
|
|
226
|
+
statements = body&.begin_type? ? body.children : [body].compact
|
|
227
|
+
definitions = statements.select(&:def_type?)
|
|
228
|
+
methods = definitions.to_h { |method| [method.children[0], method] }
|
|
229
|
+
raise SourceError, "Duplicate method definitions are not supported." unless definitions.length == methods.length
|
|
230
|
+
|
|
231
|
+
found[name] = Scope.new(name, node, methods, statements)
|
|
232
|
+
statements.each { |statement| walk_scopes(statement, name.split("::"), found) unless statement.def_type? }
|
|
233
|
+
else
|
|
234
|
+
node.children.each { |child| walk_scopes(child, namespace, found) }
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def constant_name(node, namespace)
|
|
239
|
+
parent, name = node.children
|
|
240
|
+
return (namespace + [name.to_s]).join("::") unless parent
|
|
241
|
+
|
|
242
|
+
[constant_name(parent, []), name].join("::")
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def same_ast?(left, right)
|
|
246
|
+
ast_value(left) == ast_value(right)
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def ast_value(value)
|
|
250
|
+
return value unless value.is_a?(RuboCop::AST::Node)
|
|
251
|
+
|
|
252
|
+
[value.type, *value.children.map { |child| ast_value(child) }]
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def method_body(method)
|
|
256
|
+
method.children[2]
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def block_body(block)
|
|
260
|
+
block.children[2]
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def science_blocks(method)
|
|
264
|
+
science_statements = method_statements(method).filter_map do |statement|
|
|
265
|
+
science_block = if statement.block_type?
|
|
266
|
+
statement
|
|
267
|
+
elsif statement.lvasgn_type? && statement.children[1]&.block_type?
|
|
268
|
+
statement.children[1]
|
|
269
|
+
end
|
|
270
|
+
[statement, science_block] if scientist_run_block?(science_block)
|
|
271
|
+
end
|
|
272
|
+
return unless science_statements.one?
|
|
273
|
+
|
|
274
|
+
science_statement, science_block = science_statements.first
|
|
275
|
+
|
|
276
|
+
send_node, arguments, science_body = science_block.children
|
|
277
|
+
return unless send_node.send_type? && send_node.method?(:run)
|
|
278
|
+
return unless send_node.receiver&.const_type? && send_node.receiver.const_name == "Scientist"
|
|
279
|
+
return unless send_node.arguments.one? && send_node.arguments.first.str_type?
|
|
280
|
+
return unless arguments.one?
|
|
281
|
+
|
|
282
|
+
experiment_variable = arguments.children.first.children.first
|
|
283
|
+
expressions = science_body&.begin_type? ? science_body.children : [science_body].compact
|
|
284
|
+
return unless expressions.length == 3
|
|
285
|
+
|
|
286
|
+
methods = %i[use try compare]
|
|
287
|
+
return unless expressions.each_with_index.all? do |expression, index|
|
|
288
|
+
expression.block_type? && expression.send_node.method?(methods[index]) &&
|
|
289
|
+
expression.send_node.receiver&.lvar_type? && expression.send_node.receiver.children.first == experiment_variable
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
[*expressions, send_node.arguments.first.value, science_statement]
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def science_shape_error(method)
|
|
296
|
+
statement = method_statements(method).find do |item|
|
|
297
|
+
each_node(item).any? { |node| scientist_run_block?(node) }
|
|
298
|
+
end
|
|
299
|
+
return unless statement
|
|
300
|
+
|
|
301
|
+
direct_block = statement.lvasgn_type? ? statement.children[1] : statement
|
|
302
|
+
return if scientist_run_block?(direct_block)
|
|
303
|
+
|
|
304
|
+
if statement.masgn_type?
|
|
305
|
+
"The Scientist.run statement must be a bare call or a local variable assignment."
|
|
306
|
+
else
|
|
307
|
+
"The Scientist.run call must be a direct method body statement."
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def scientist_run_block?(node)
|
|
312
|
+
return false unless node&.block_type?
|
|
313
|
+
|
|
314
|
+
send_node = node.send_node
|
|
315
|
+
send_node.method?(:run) && send_node.receiver&.const_type? && send_node.receiver.const_name == "Scientist"
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def matching_span?(before_method, after_method, science_statement, use_block)
|
|
319
|
+
before = method_statements(before_method)
|
|
320
|
+
after = method_statements(after_method)
|
|
321
|
+
science_index = after.index(science_statement)
|
|
322
|
+
suffix_length = after.length - science_index - 1
|
|
323
|
+
middle_end = before.length - suffix_length
|
|
324
|
+
return false unless middle_end > science_index
|
|
325
|
+
|
|
326
|
+
# A statement outside the block keeps its position and behavior.
|
|
327
|
+
# Thus, the science block can wrap one body span.
|
|
328
|
+
prefix_matches = statement_values(before.take(science_index)) == statement_values(after.take(science_index))
|
|
329
|
+
suffix_matches = statement_values(before.drop(middle_end)) == statement_values(after.drop(science_index + 1))
|
|
330
|
+
middle = before[science_index...middle_end]
|
|
331
|
+
use = node_statements(block_body(use_block))
|
|
332
|
+
use_matches = if science_statement.lvasgn_type?
|
|
333
|
+
assignment = middle.last
|
|
334
|
+
assignment&.lvasgn_type? && assignment.children.first == science_statement.children.first &&
|
|
335
|
+
statement_values([*middle[0...-1], assignment.children[1]]) == statement_values(use)
|
|
336
|
+
else
|
|
337
|
+
statement_values(middle) == statement_values(use)
|
|
338
|
+
end
|
|
339
|
+
prefix_matches && suffix_matches && use_matches
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
def method_statements(method)
|
|
343
|
+
node_statements(method_body(method))
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def node_statements(node)
|
|
347
|
+
node&.begin_type? ? node.children : [node].compact
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def private_method?(scope, method)
|
|
351
|
+
visibility_at(scope, method) == :private
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
def visibility_at(scope, method)
|
|
355
|
+
visibility = :public
|
|
356
|
+
scope.statements.each do |statement|
|
|
357
|
+
visibility = statement.method_name if visibility_call?(statement)
|
|
358
|
+
return visibility if statement.equal?(method)
|
|
359
|
+
end
|
|
360
|
+
nil
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
def visibility_call?(node)
|
|
364
|
+
node.send_type? && node.receiver.nil? && %i[public protected private].include?(node.method_name) && node.arguments.empty?
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
def unchanged_statements?(before_scope, after_scope, candidate = nil)
|
|
368
|
+
before = before_scope.statements.reject(&:def_type?)
|
|
369
|
+
after = after_scope.statements.reject(&:def_type?).dup
|
|
370
|
+
if candidate
|
|
371
|
+
candidate_index = after_scope.statements.index(candidate)
|
|
372
|
+
marker = after_scope.statements[0...candidate_index].reverse.find { |statement| visibility_call?(statement) }
|
|
373
|
+
before_private_count = before.count { |statement| statement.send_type? && statement.method?(:private) }
|
|
374
|
+
after_private_count = after.count { |statement| statement.send_type? && statement.method?(:private) }
|
|
375
|
+
after.delete_at(after.index(marker)) if marker&.method?(:private) && after_private_count == before_private_count + 1
|
|
376
|
+
end
|
|
377
|
+
statement_values(before) == statement_values(after)
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
def same_visibilities?(before_scope, after_scope)
|
|
381
|
+
before_scope.methods.all? do |name, method|
|
|
382
|
+
visibility_at(before_scope, method) == visibility_at(after_scope, after_scope.methods.fetch(name))
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
def same_header?(before_scope, after_scope)
|
|
387
|
+
before_node = before_scope.node
|
|
388
|
+
after_node = after_scope.node
|
|
389
|
+
count = before_node.class_type? ? 2 : 1
|
|
390
|
+
same_ast?(sequence(before_node.children.take(count)), sequence(after_node.children.take(count)))
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
def top_level(root)
|
|
394
|
+
statements = root&.begin_type? ? root.children : [root].compact
|
|
395
|
+
statement_values(statements)
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
def statement_values(statements)
|
|
399
|
+
statements.map do |statement|
|
|
400
|
+
if statement.class_type? || statement.module_type?
|
|
401
|
+
count = statement.class_type? ? 2 : 1
|
|
402
|
+
[:scope, statement.type, *statement.children.take(count).map { |child| ast_value(child) }]
|
|
403
|
+
else
|
|
404
|
+
ast_value(statement)
|
|
405
|
+
end
|
|
406
|
+
end
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
def unsupported?(root)
|
|
410
|
+
each_node(root).any? do |node|
|
|
411
|
+
node.defs_type? || node.sclass_type? ||
|
|
412
|
+
(node.send_type? && node.method?(:define_method)) ||
|
|
413
|
+
(node.send_type? && node.method?(:new) && node.receiver&.const_type? && node.receiver.const_name == "Class")
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
def sequence(nodes)
|
|
418
|
+
RuboCop::AST::Node.new(:begin, nodes)
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
def unsafe_return?(block)
|
|
422
|
+
unsafe_return_in?(block_body(block), [])
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
def unsafe_return_in?(node, scopes)
|
|
426
|
+
return false unless node.is_a?(RuboCop::AST::Node)
|
|
427
|
+
return true if node.return_type? && scopes.none? { |scope| scope == :safe }
|
|
428
|
+
|
|
429
|
+
# A nested method or lambda owns its return. A proc or a plain block
|
|
430
|
+
# keeps the science block as the nearest return scope.
|
|
431
|
+
child_scopes = scopes
|
|
432
|
+
child_scopes = scopes + [:safe] if node.def_type? || node.defs_type? || (node.block_type? && node.lambda?)
|
|
433
|
+
node.children.any? { |child| unsafe_return_in?(child, child_scopes) }
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
def binding_call?(block)
|
|
437
|
+
each_node(block_body(block)).any? { |node| node.send_type? && node.receiver.nil? && node.method?(:binding) }
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
def each_node(node, values = [])
|
|
441
|
+
return values unless node.is_a?(RuboCop::AST::Node)
|
|
442
|
+
|
|
443
|
+
values << node
|
|
444
|
+
node.children.each { |child| each_node(child, values) }
|
|
445
|
+
values
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
def reject(message)
|
|
449
|
+
Result.new(false, message, nil, nil)
|
|
450
|
+
end
|
|
451
|
+
end
|
|
452
|
+
end
|
|
453
|
+
end
|
|
454
|
+
end
|