rubycli 0.1.7 → 0.2.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 +4 -4
- data/CHANGELOG.md +77 -2
- data/LICENSE +1 -1
- data/README.ja.md +357 -252
- data/README.md +386 -250
- data/examples/documentation_style_showcase.rb +110 -0
- data/examples/fallback_example.rb +14 -0
- data/examples/fallback_example_with_extra_docs.rb +13 -0
- data/examples/hello_app.rb +10 -0
- data/examples/hello_app_with_docs.rb +17 -0
- data/examples/hello_app_with_require.rb +19 -0
- data/examples/mismatched_constant_runner.rb +16 -0
- data/examples/multi_constant_runner.rb +20 -0
- data/examples/new_mode_runner.rb +46 -0
- data/examples/strict_choices_demo.rb +22 -0
- data/examples/typed_arguments_demo.rb +42 -0
- data/lib/rubycli/argument_mode_controller.rb +7 -11
- data/lib/rubycli/argument_parser.rb +295 -96
- data/lib/rubycli/cli.rb +26 -30
- data/lib/rubycli/command_line.rb +151 -86
- data/lib/rubycli/constant_capture.rb +563 -6
- data/lib/rubycli/documentation/metadata_parser.rb +108 -74
- data/lib/rubycli/environment.rb +17 -9
- data/lib/rubycli/eval_coercer.rb +27 -10
- data/lib/rubycli/help_renderer.rb +67 -62
- data/lib/rubycli/json_coercer.rb +2 -0
- data/lib/rubycli/result_emitter.rb +18 -8
- data/lib/rubycli/runner.rb +513 -0
- data/lib/rubycli/type_utils.rb +8 -6
- data/lib/rubycli/types.rb +2 -0
- data/lib/rubycli/version.rb +1 -1
- data/lib/rubycli.rb +7 -456
- metadata +59 -4
|
@@ -1,27 +1,62 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'ripper'
|
|
4
|
+
|
|
3
5
|
module Rubycli
|
|
4
6
|
# Observes constants defined while loading a file.
|
|
5
7
|
class ConstantCapture
|
|
6
8
|
def initialize
|
|
7
9
|
@captured = Hash.new { |hash, key| hash[key] = [] }
|
|
10
|
+
@assignment_definitions = {}
|
|
8
11
|
end
|
|
9
12
|
|
|
10
13
|
def capture(file)
|
|
11
|
-
|
|
14
|
+
normalized_file = normalize(file)
|
|
15
|
+
source = File.read(normalized_file)
|
|
16
|
+
previous_names = @captured[normalized_file].dup
|
|
17
|
+
previous_assignment_definitions = @assignment_definitions.fetch(normalized_file, {})
|
|
18
|
+
executed_line_contexts = Hash.new { |hash, line| hash[line] = [] }
|
|
19
|
+
assignment_events = Hash.new { |hash, line| hash[line] = [] }
|
|
20
|
+
observed_events = []
|
|
21
|
+
@captured[normalized_file] = []
|
|
22
|
+
before_snapshot = constant_snapshot(normalized_file)
|
|
23
|
+
trace = TracePoint.new(:class, :line, :call, :return, :b_call, :b_return, :c_call, :c_return) do |tp|
|
|
12
24
|
location = tp.path
|
|
13
|
-
next unless location && same_file?(
|
|
14
|
-
|
|
15
|
-
constant_name = qualified_name_for(tp.self)
|
|
16
|
-
next unless constant_name
|
|
25
|
+
next unless location && same_file?(normalized_file, location)
|
|
17
26
|
|
|
18
|
-
|
|
27
|
+
observed_events << [tp.event, tp.self, tp.lineno, tp.method_id]
|
|
19
28
|
end
|
|
20
29
|
|
|
21
30
|
trace.enable
|
|
22
31
|
yield
|
|
23
32
|
ensure
|
|
24
33
|
trace&.disable
|
|
34
|
+
if normalized_file && before_snapshot
|
|
35
|
+
current_assignment_definitions = assigned_constant_definitions(source)
|
|
36
|
+
apply_trace_events(
|
|
37
|
+
observed_events,
|
|
38
|
+
executed_line_contexts,
|
|
39
|
+
assignment_events,
|
|
40
|
+
normalized_file
|
|
41
|
+
)
|
|
42
|
+
after_snapshot = constant_snapshot(normalized_file)
|
|
43
|
+
changed_names = after_snapshot.keys.select do |name|
|
|
44
|
+
before_snapshot[name] != after_snapshot[name]
|
|
45
|
+
end
|
|
46
|
+
retained_names = previous_names.select do |name|
|
|
47
|
+
previous_definitions = previous_assignment_definitions.fetch(name, [])
|
|
48
|
+
current_definitions = current_assignment_definitions.fetch(name, [])
|
|
49
|
+
definition_active = active_definition_retained?(
|
|
50
|
+
previous_definitions,
|
|
51
|
+
current_definitions,
|
|
52
|
+
executed_line_contexts,
|
|
53
|
+
assignment_events
|
|
54
|
+
)
|
|
55
|
+
after_snapshot.key?(name) && definition_active
|
|
56
|
+
end
|
|
57
|
+
@captured[normalized_file].concat(changed_names).concat(retained_names)
|
|
58
|
+
@assignment_definitions[normalized_file] = current_assignment_definitions
|
|
59
|
+
end
|
|
25
60
|
end
|
|
26
61
|
|
|
27
62
|
def constants_for(file)
|
|
@@ -46,5 +81,527 @@ module Rubycli
|
|
|
46
81
|
|
|
47
82
|
name
|
|
48
83
|
end
|
|
84
|
+
|
|
85
|
+
def constant_snapshot(file)
|
|
86
|
+
ObjectSpace.each_object(Module).each_with_object({}) do |owner, snapshot|
|
|
87
|
+
owner_name = owner.equal?(Object) ? '' : owner.name
|
|
88
|
+
next if owner_name.nil? || owner_name.start_with?('#<')
|
|
89
|
+
|
|
90
|
+
safe_module_constants(owner).each do |constant_name|
|
|
91
|
+
next if safe_autoload?(owner, constant_name)
|
|
92
|
+
|
|
93
|
+
location = safe_const_source_location(owner, constant_name)
|
|
94
|
+
next unless location && same_file?(file, location[0])
|
|
95
|
+
|
|
96
|
+
value = safe_const_get(owner, constant_name)
|
|
97
|
+
next unless value.is_a?(Module)
|
|
98
|
+
|
|
99
|
+
name = qualified_constant_name(owner_name, constant_name)
|
|
100
|
+
snapshot[name] = [location, value.object_id]
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
rescue StandardError
|
|
104
|
+
{}
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def safe_module_constants(owner)
|
|
108
|
+
owner.constants(false)
|
|
109
|
+
rescue StandardError
|
|
110
|
+
[]
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def safe_autoload?(owner, constant_name)
|
|
114
|
+
owner.autoload?(constant_name, false)
|
|
115
|
+
rescue StandardError
|
|
116
|
+
false
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def safe_const_source_location(owner, constant_name)
|
|
120
|
+
owner.const_source_location(constant_name, false)
|
|
121
|
+
rescue StandardError
|
|
122
|
+
nil
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def safe_const_get(owner, constant_name)
|
|
126
|
+
owner.const_get(constant_name, false)
|
|
127
|
+
rescue StandardError
|
|
128
|
+
nil
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def qualified_constant_name(owner_name, constant_name)
|
|
132
|
+
return constant_name.to_s if owner_name.empty?
|
|
133
|
+
|
|
134
|
+
"#{owner_name}::#{constant_name}"
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def apply_trace_events(events, executed_line_contexts, assignment_events, file)
|
|
138
|
+
active_context_events = []
|
|
139
|
+
events.each do |event, target, line, method_id|
|
|
140
|
+
case event
|
|
141
|
+
when :line
|
|
142
|
+
executed_line_contexts[line] << active_context_events.dup
|
|
143
|
+
when :call, :b_call
|
|
144
|
+
active_context_events << event
|
|
145
|
+
when :return
|
|
146
|
+
remove_active_context_event(active_context_events, :call)
|
|
147
|
+
when :b_return
|
|
148
|
+
remove_active_context_event(active_context_events, :b_call)
|
|
149
|
+
when :c_call
|
|
150
|
+
if method_id == :const_added || (method_id == :warn && target.equal?(Warning))
|
|
151
|
+
assignment_events[line] << method_id
|
|
152
|
+
end
|
|
153
|
+
when :c_return
|
|
154
|
+
@captured[file].concat(constants_set_at(target, file, line)) if method_id == :const_set
|
|
155
|
+
when :class
|
|
156
|
+
constant_name = qualified_name_for(target)
|
|
157
|
+
@captured[file] << constant_name if constant_name
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def remove_active_context_event(active_context_events, event)
|
|
163
|
+
index = active_context_events.rindex(event)
|
|
164
|
+
active_context_events.delete_at(index) if index
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def constants_set_at(owner, file, line)
|
|
168
|
+
owner_name = owner.equal?(Object) ? '' : owner.name
|
|
169
|
+
return [] if owner_name.nil? || owner_name.start_with?('#<')
|
|
170
|
+
|
|
171
|
+
safe_module_constants(owner).filter_map do |constant_name|
|
|
172
|
+
location = safe_const_source_location(owner, constant_name)
|
|
173
|
+
next unless location && same_file?(file, location[0]) && location[1] == line
|
|
174
|
+
|
|
175
|
+
value = safe_const_get(owner, constant_name)
|
|
176
|
+
next unless value.is_a?(Module)
|
|
177
|
+
|
|
178
|
+
qualified_constant_name(owner_name, constant_name)
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def active_definition_retained?(
|
|
183
|
+
previous_definitions,
|
|
184
|
+
current_definitions,
|
|
185
|
+
executed_line_contexts,
|
|
186
|
+
assignment_events
|
|
187
|
+
)
|
|
188
|
+
previous_definitions.any? do |previous|
|
|
189
|
+
current_definitions.any? do |current|
|
|
190
|
+
next false unless previous[:signature] == current[:signature]
|
|
191
|
+
|
|
192
|
+
events = assignment_events[current[:line]]
|
|
193
|
+
assignment_observed = events.include?(:const_added) || events.count(:warn) >= 2
|
|
194
|
+
persistence_observed = current[:persistence_line] &&
|
|
195
|
+
!current[:persistence_ambiguous] &&
|
|
196
|
+
executed_line_contexts[current[:persistence_line]].any? do |active_context_events|
|
|
197
|
+
context_requirements_met?(
|
|
198
|
+
active_context_events,
|
|
199
|
+
current[:required_context_events]
|
|
200
|
+
)
|
|
201
|
+
end
|
|
202
|
+
assignment_observed || persistence_observed
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def context_requirements_met?(active_context_events, required_context_events)
|
|
208
|
+
required_context_events.uniq.all? do |event|
|
|
209
|
+
active_context_events.count(event) >= required_context_events.count(event)
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def assigned_constant_definitions(source)
|
|
214
|
+
syntax_tree = Ripper.sexp(source)
|
|
215
|
+
return {} unless syntax_tree
|
|
216
|
+
|
|
217
|
+
collect_assigned_constant_definitions(syntax_tree, [], [], {})
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def collect_assigned_constant_definitions(node, namespace, contexts, definitions)
|
|
221
|
+
return definitions unless node.is_a?(Array)
|
|
222
|
+
|
|
223
|
+
case node.first
|
|
224
|
+
when :module
|
|
225
|
+
nested_name = constant_name_from_node(node[1], namespace)
|
|
226
|
+
collect_assigned_constant_definitions(node[2], Array(nested_name&.split('::')), contexts, definitions)
|
|
227
|
+
when :class
|
|
228
|
+
nested_name = constant_name_from_node(node[1], namespace)
|
|
229
|
+
collect_assigned_constant_definitions(node[3], Array(nested_name&.split('::')), contexts, definitions)
|
|
230
|
+
when :assign, :opassign
|
|
231
|
+
signature = [node.first, assignment_operator(node), contexts.map(&:first)]
|
|
232
|
+
collect_assignment_targets(node[1], namespace, definitions, signature, contexts)
|
|
233
|
+
node.drop(2).each do |child|
|
|
234
|
+
collect_assigned_constant_definitions(child, namespace, contexts, definitions)
|
|
235
|
+
end
|
|
236
|
+
when :massign
|
|
237
|
+
signature = [node.first, nil, contexts.map(&:first)]
|
|
238
|
+
collect_assignment_targets(node[1], namespace, definitions, signature, contexts)
|
|
239
|
+
node.drop(2).each do |child|
|
|
240
|
+
collect_assigned_constant_definitions(child, namespace, contexts, definitions)
|
|
241
|
+
end
|
|
242
|
+
when :method_add_arg, :command_call, :command
|
|
243
|
+
collect_const_set_definition(node, namespace, contexts, definitions)
|
|
244
|
+
node.each do |child|
|
|
245
|
+
collect_assigned_constant_definitions(child, namespace, contexts, definitions)
|
|
246
|
+
end
|
|
247
|
+
when :if, :unless
|
|
248
|
+
collect_assigned_constant_definitions(node[1], namespace, contexts, definitions)
|
|
249
|
+
condition = canonical_syntax(node[1])
|
|
250
|
+
condition_line = source_line(node[1])
|
|
251
|
+
then_context = contexts + [[[node.first, :then, condition], condition_line]]
|
|
252
|
+
else_context = contexts + [[[node.first, :else, condition], condition_line]]
|
|
253
|
+
collect_assigned_constant_definitions(node[2], namespace, then_context, definitions)
|
|
254
|
+
collect_assigned_constant_definitions(node[3], namespace, else_context, definitions)
|
|
255
|
+
when :while, :until
|
|
256
|
+
collect_assigned_constant_definitions(node[1], namespace, contexts, definitions)
|
|
257
|
+
body_context = contexts + [[[node.first, canonical_syntax(node[1])], source_line(node[1])]]
|
|
258
|
+
collect_assigned_constant_definitions(node[2], namespace, body_context, definitions)
|
|
259
|
+
when :if_mod, :unless_mod, :while_mod, :until_mod
|
|
260
|
+
collect_assigned_constant_definitions(node[1], namespace, contexts, definitions)
|
|
261
|
+
branch_context = contexts + [[[node.first, canonical_syntax(node[1])], source_line(node[1])]]
|
|
262
|
+
collect_assigned_constant_definitions(node[2], namespace, branch_context, definitions)
|
|
263
|
+
when :ifop
|
|
264
|
+
collect_assigned_constant_definitions(node[1], namespace, contexts, definitions)
|
|
265
|
+
condition = canonical_syntax(node[1])
|
|
266
|
+
condition_line = source_line(node[1])
|
|
267
|
+
then_context = contexts + [[[node.first, :then, condition], condition_line]]
|
|
268
|
+
else_context = contexts + [[[node.first, :else, condition], condition_line]]
|
|
269
|
+
collect_assigned_constant_definitions(node[2], namespace, then_context, definitions)
|
|
270
|
+
collect_assigned_constant_definitions(node[3], namespace, else_context, definitions)
|
|
271
|
+
when :binary
|
|
272
|
+
collect_assigned_constant_definitions(node[1], namespace, contexts, definitions)
|
|
273
|
+
right_contexts = contexts
|
|
274
|
+
if %i[&& ||].include?(node[2])
|
|
275
|
+
right_contexts += [[[node.first, node[2], canonical_syntax(node[1])], source_line(node[1])]]
|
|
276
|
+
end
|
|
277
|
+
collect_assigned_constant_definitions(node[3], namespace, right_contexts, definitions)
|
|
278
|
+
when :case
|
|
279
|
+
collect_assigned_constant_definitions(node[1], namespace, contexts, definitions)
|
|
280
|
+
case_context = contexts + [[[node.first, canonical_syntax(node[1])], source_line(node[1])]]
|
|
281
|
+
collect_assigned_constant_definitions(node[2], namespace, case_context, definitions)
|
|
282
|
+
when :for
|
|
283
|
+
collect_assigned_constant_definitions(node[2], namespace, contexts, definitions)
|
|
284
|
+
body_context = contexts + [[[node.first, canonical_syntax(node[2])], source_line(node[1])]]
|
|
285
|
+
collect_assigned_constant_definitions(node[3], namespace, body_context, definitions)
|
|
286
|
+
when :method_add_block
|
|
287
|
+
collect_assigned_constant_definitions(node[1], namespace, contexts, definitions)
|
|
288
|
+
lazy_context = contexts + [[[:block], source_line(node[1]), :b_call]]
|
|
289
|
+
collect_assigned_constant_definitions(node[2], namespace, lazy_context, definitions)
|
|
290
|
+
when :def, :defs
|
|
291
|
+
lazy_context = contexts + [[[node.first], source_line(node), :call]]
|
|
292
|
+
node.drop(1).each do |child|
|
|
293
|
+
collect_assigned_constant_definitions(child, namespace, lazy_context, definitions)
|
|
294
|
+
end
|
|
295
|
+
when :lambda
|
|
296
|
+
lazy_context = contexts + [[[node.first], source_line(node), :b_call]]
|
|
297
|
+
node.drop(1).each do |child|
|
|
298
|
+
collect_assigned_constant_definitions(child, namespace, lazy_context, definitions)
|
|
299
|
+
end
|
|
300
|
+
when :do_block, :brace_block
|
|
301
|
+
node.drop(1).each do |child|
|
|
302
|
+
collect_assigned_constant_definitions(child, namespace, contexts, definitions)
|
|
303
|
+
end
|
|
304
|
+
else
|
|
305
|
+
node.each do |child|
|
|
306
|
+
collect_assigned_constant_definitions(child, namespace, contexts, definitions)
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
definitions
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def collect_assignment_targets(node, namespace, definitions, signature, contexts)
|
|
314
|
+
assigned_name = constant_name_from_node(node, namespace)
|
|
315
|
+
if assigned_name
|
|
316
|
+
line = source_line(node)
|
|
317
|
+
persistence = persistence_metadata(
|
|
318
|
+
contexts,
|
|
319
|
+
assigned_name,
|
|
320
|
+
namespace,
|
|
321
|
+
line,
|
|
322
|
+
signature[1] == '||='
|
|
323
|
+
)
|
|
324
|
+
definition = {
|
|
325
|
+
signature: signature,
|
|
326
|
+
line: line,
|
|
327
|
+
persistence_line: persistence[:line],
|
|
328
|
+
persistence_ambiguous: persistence[:ambiguous],
|
|
329
|
+
required_context_events: persistence[:required_context_events]
|
|
330
|
+
}
|
|
331
|
+
definitions[assigned_name] = Array(definitions[assigned_name]) | [definition]
|
|
332
|
+
elsif node.is_a?(Array)
|
|
333
|
+
node.each { |child| collect_assignment_targets(child, namespace, definitions, signature, contexts) }
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def assignment_operator(node)
|
|
338
|
+
node.first == :opassign ? node.dig(2, 1) : nil
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def collect_const_set_definition(node, namespace, contexts, definitions)
|
|
342
|
+
receiver, method_name, arguments = call_parts(node)
|
|
343
|
+
return unless method_name == 'const_set'
|
|
344
|
+
|
|
345
|
+
constant_name = literal_constant_name(arguments.first)
|
|
346
|
+
return unless constant_name
|
|
347
|
+
|
|
348
|
+
owner_name = if receiver.nil? || self_receiver?(receiver)
|
|
349
|
+
namespace.join('::')
|
|
350
|
+
elsif object_receiver?(receiver)
|
|
351
|
+
''
|
|
352
|
+
else
|
|
353
|
+
receiver_constant_name_from_node(receiver, namespace)
|
|
354
|
+
end
|
|
355
|
+
return if owner_name.nil?
|
|
356
|
+
|
|
357
|
+
assigned_name = [owner_name, constant_name].reject(&:empty?).join('::')
|
|
358
|
+
signature = [:const_set, nil, contexts.map(&:first)]
|
|
359
|
+
line = source_line(node)
|
|
360
|
+
persistence = persistence_metadata(contexts, assigned_name, namespace, line, false)
|
|
361
|
+
definition = {
|
|
362
|
+
signature: signature,
|
|
363
|
+
line: line,
|
|
364
|
+
persistence_line: persistence[:line],
|
|
365
|
+
persistence_ambiguous: persistence[:ambiguous],
|
|
366
|
+
required_context_events: persistence[:required_context_events]
|
|
367
|
+
}
|
|
368
|
+
definitions[assigned_name] = Array(definitions[assigned_name]) | [definition]
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
def call_parts(node)
|
|
372
|
+
case node.first
|
|
373
|
+
when :method_add_arg
|
|
374
|
+
call = node[1]
|
|
375
|
+
receiver = call[1] if call&.first == :call
|
|
376
|
+
method_token = call&.first == :call ? call[3] : call&.[](1)
|
|
377
|
+
[receiver, method_token&.[](1), call_arguments(node[2])]
|
|
378
|
+
when :command_call
|
|
379
|
+
[node[1], node.dig(3, 1), call_arguments(node[4])]
|
|
380
|
+
when :command
|
|
381
|
+
[nil, node.dig(1, 1), call_arguments(node[2])]
|
|
382
|
+
end
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
def call_arguments(node)
|
|
386
|
+
return [] unless node.is_a?(Array)
|
|
387
|
+
return call_arguments(node[1]) if node.first == :arg_paren
|
|
388
|
+
return Array(node[1]) if node.first == :args_add_block
|
|
389
|
+
|
|
390
|
+
[]
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
def literal_constant_name(node)
|
|
394
|
+
return nil unless node.is_a?(Array)
|
|
395
|
+
return nil unless %i[symbol_literal string_literal].include?(node.first)
|
|
396
|
+
|
|
397
|
+
token = find_syntax_token(node) { |type, _value| %i[@const @ident @tstring_content].include?(type) }
|
|
398
|
+
token&.[](1)
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
def object_receiver?(node)
|
|
402
|
+
node&.first == :var_ref && node.dig(1, 0) == :@const && node.dig(1, 1) == 'Object'
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
def self_receiver?(node)
|
|
406
|
+
node&.first == :var_ref && node.dig(1, 0) == :@kw && node.dig(1, 1) == 'self'
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
def persistence_metadata(contexts, assigned_name, namespace, assignment_line, short_circuit_assignment)
|
|
410
|
+
guard = contexts.reverse.find do |context|
|
|
411
|
+
missing_constant_guard?(context.first, assigned_name, namespace)
|
|
412
|
+
end
|
|
413
|
+
persistence_line = guard&.[](1)
|
|
414
|
+
persistence_line ||= assignment_line if short_circuit_assignment
|
|
415
|
+
return { line: nil, ambiguous: false, required_context_events: [] } unless persistence_line
|
|
416
|
+
|
|
417
|
+
same_line_contexts = contexts.select { |context| context[1] == persistence_line }
|
|
418
|
+
ambiguous = same_line_contexts.any? do |context|
|
|
419
|
+
!lazy_context?(context) &&
|
|
420
|
+
!missing_constant_guard?(context.first, assigned_name, namespace)
|
|
421
|
+
end
|
|
422
|
+
required_context_events = same_line_contexts.filter_map do |context|
|
|
423
|
+
context[2]
|
|
424
|
+
end
|
|
425
|
+
{
|
|
426
|
+
line: persistence_line,
|
|
427
|
+
ambiguous: ambiguous,
|
|
428
|
+
required_context_events: required_context_events
|
|
429
|
+
}
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
def missing_constant_guard?(context, assigned_name, namespace)
|
|
433
|
+
condition, expected = guarded_condition(context)
|
|
434
|
+
return false unless condition
|
|
435
|
+
|
|
436
|
+
condition, expected = unwrap_guard_condition(condition, expected)
|
|
437
|
+
expected == false && guarded_constant_name(condition, namespace) == assigned_name
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
def guarded_condition(context)
|
|
441
|
+
case context.first
|
|
442
|
+
when :if
|
|
443
|
+
[context[2], context[1] == :then]
|
|
444
|
+
when :unless
|
|
445
|
+
[context[2], context[1] != :then]
|
|
446
|
+
when :if_mod
|
|
447
|
+
[context[1], true]
|
|
448
|
+
when :unless_mod
|
|
449
|
+
[context[1], false]
|
|
450
|
+
when :while, :while_mod
|
|
451
|
+
[context[1], true]
|
|
452
|
+
when :until, :until_mod
|
|
453
|
+
[context[1], false]
|
|
454
|
+
when :ifop
|
|
455
|
+
[context[2], context[1] == :then]
|
|
456
|
+
when :binary
|
|
457
|
+
[context[2], context[1] == :'&&']
|
|
458
|
+
end
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
def unwrap_guard_condition(condition, expected)
|
|
462
|
+
loop do
|
|
463
|
+
case condition&.first
|
|
464
|
+
when :paren
|
|
465
|
+
expressions = condition[1]
|
|
466
|
+
break unless expressions.is_a?(Array) && expressions.length == 1
|
|
467
|
+
|
|
468
|
+
condition = expressions.first
|
|
469
|
+
when :unary
|
|
470
|
+
break unless condition[1] == :!
|
|
471
|
+
|
|
472
|
+
condition = condition[2]
|
|
473
|
+
expected = !expected
|
|
474
|
+
when :call
|
|
475
|
+
break unless condition.dig(3, 1) == 'nil?'
|
|
476
|
+
|
|
477
|
+
condition = condition[1]
|
|
478
|
+
expected = !expected
|
|
479
|
+
when :binary
|
|
480
|
+
left, operator, right = condition.drop(1)
|
|
481
|
+
break unless operator == :== && nil_literal?(right)
|
|
482
|
+
|
|
483
|
+
condition = left
|
|
484
|
+
expected = !expected
|
|
485
|
+
else
|
|
486
|
+
break
|
|
487
|
+
end
|
|
488
|
+
end
|
|
489
|
+
[condition, expected]
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
def nil_literal?(node)
|
|
493
|
+
node&.first == :var_ref && node.dig(1, 0) == :@kw && node.dig(1, 1) == 'nil'
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
def guarded_constant_name(condition, namespace)
|
|
497
|
+
return constant_name_from_node(condition[1], namespace) if condition&.first == :defined
|
|
498
|
+
|
|
499
|
+
receiver, method_name, arguments = call_parts(condition)
|
|
500
|
+
return nil unless method_name == 'const_defined?'
|
|
501
|
+
|
|
502
|
+
constant_name = literal_constant_name(arguments.first)
|
|
503
|
+
return nil unless constant_name
|
|
504
|
+
|
|
505
|
+
owner_name = if receiver.nil? || self_receiver?(receiver)
|
|
506
|
+
namespace.join('::')
|
|
507
|
+
elsif object_receiver?(receiver)
|
|
508
|
+
''
|
|
509
|
+
else
|
|
510
|
+
receiver_constant_name_from_node(receiver, namespace)
|
|
511
|
+
end
|
|
512
|
+
return nil if owner_name.nil?
|
|
513
|
+
|
|
514
|
+
[owner_name, constant_name].reject(&:empty?).join('::')
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
def receiver_constant_name_from_node(node, namespace)
|
|
518
|
+
return nil unless node.is_a?(Array)
|
|
519
|
+
|
|
520
|
+
case node.first
|
|
521
|
+
when :var_ref, :const_ref
|
|
522
|
+
receiver_constant_name_from_node(node[1], namespace)
|
|
523
|
+
when :@const
|
|
524
|
+
resolve_lexical_constant_name(node[1], namespace)
|
|
525
|
+
when :const_path_ref
|
|
526
|
+
parent_name = receiver_constant_name_from_node(node[1], namespace)
|
|
527
|
+
child_name = node.dig(2, 1)
|
|
528
|
+
[parent_name, child_name].compact.join('::')
|
|
529
|
+
when :top_const_ref
|
|
530
|
+
node.dig(1, 1)
|
|
531
|
+
end
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
def resolve_lexical_constant_name(constant_name, namespace)
|
|
535
|
+
namespace.length.downto(0) do |depth|
|
|
536
|
+
candidate = (namespace.first(depth) + [constant_name]).join('::')
|
|
537
|
+
return candidate if constant_path_defined?(candidate)
|
|
538
|
+
end
|
|
539
|
+
|
|
540
|
+
(namespace + [constant_name]).join('::')
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
def constant_path_defined?(name)
|
|
544
|
+
parts = name.split('::')
|
|
545
|
+
parts.each_with_index.reduce(Object) do |owner, (part, index)|
|
|
546
|
+
return false unless owner.is_a?(Module) && owner.const_defined?(part, false)
|
|
547
|
+
return true if index == parts.length - 1
|
|
548
|
+
return false if owner.autoload?(part, false)
|
|
549
|
+
|
|
550
|
+
owner.const_get(part, false)
|
|
551
|
+
end
|
|
552
|
+
true
|
|
553
|
+
rescue StandardError
|
|
554
|
+
false
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
def lazy_context?(context)
|
|
558
|
+
!context[2].nil?
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
def find_syntax_token(node, &predicate)
|
|
562
|
+
return nil unless node.is_a?(Array)
|
|
563
|
+
return node if node.first.to_s.start_with?('@') && predicate.call(node[0], node[1])
|
|
564
|
+
|
|
565
|
+
node.each do |child|
|
|
566
|
+
token = find_syntax_token(child, &predicate)
|
|
567
|
+
return token if token
|
|
568
|
+
end
|
|
569
|
+
nil
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
def canonical_syntax(node)
|
|
573
|
+
return node unless node.is_a?(Array)
|
|
574
|
+
return [node[0], node[1]] if node.first.to_s.start_with?('@')
|
|
575
|
+
|
|
576
|
+
node.map { |child| canonical_syntax(child) }
|
|
577
|
+
end
|
|
578
|
+
|
|
579
|
+
def source_line(node)
|
|
580
|
+
return nil unless node.is_a?(Array)
|
|
581
|
+
return node.dig(2, 0) if node.first.to_s.start_with?('@')
|
|
582
|
+
|
|
583
|
+
node.each do |child|
|
|
584
|
+
line = source_line(child)
|
|
585
|
+
return line if line
|
|
586
|
+
end
|
|
587
|
+
nil
|
|
588
|
+
end
|
|
589
|
+
|
|
590
|
+
def constant_name_from_node(node, namespace)
|
|
591
|
+
return nil unless node.is_a?(Array)
|
|
592
|
+
|
|
593
|
+
case node.first
|
|
594
|
+
when :var_field, :const_ref, :var_ref
|
|
595
|
+
constant_name_from_node(node[1], namespace)
|
|
596
|
+
when :@const
|
|
597
|
+
(namespace + [node[1]]).join('::')
|
|
598
|
+
when :const_path_field, :const_path_ref
|
|
599
|
+
parent_name = receiver_constant_name_from_node(node[1], namespace)
|
|
600
|
+
child_name = node.dig(2, 1)
|
|
601
|
+
[parent_name, child_name].compact.join('::')
|
|
602
|
+
when :top_const_field, :top_const_ref
|
|
603
|
+
node.dig(1, 1)
|
|
604
|
+
end
|
|
605
|
+
end
|
|
49
606
|
end
|
|
50
607
|
end
|