@@ -15,12 +15,12 @@ def capture(file)
1515 previous_names = @captured [ normalized_file ] . dup
1616 previous_assignment_definitions = @assignment_definitions . fetch ( normalized_file , { } )
1717 current_assignment_definitions = assigned_constant_definitions ( normalized_file )
18- executed_lines = [ ]
18+ executed_line_contexts = Hash . new { | hash , line | hash [ line ] = [ ] }
1919 assignment_events = Hash . new { |hash , line | hash [ line ] = [ ] }
2020 observed_events = [ ]
2121 @captured [ normalized_file ] = [ ]
2222 before_snapshot = constant_snapshot ( normalized_file )
23- trace = TracePoint . new ( :class , :line , :c_call , :c_return ) do |tp |
23+ trace = TracePoint . new ( :class , :line , :call , :return , :b_call , :b_return , : c_call, :c_return ) do |tp |
2424 location = tp . path
2525 next unless location && same_file? ( normalized_file , location )
2626
@@ -32,7 +32,12 @@ def capture(file)
3232 ensure
3333 trace &.disable
3434 if normalized_file && before_snapshot
35- apply_trace_events ( observed_events , executed_lines , assignment_events , normalized_file )
35+ apply_trace_events (
36+ observed_events ,
37+ executed_line_contexts ,
38+ assignment_events ,
39+ normalized_file
40+ )
3641 after_snapshot = constant_snapshot ( normalized_file )
3742 changed_names = after_snapshot . keys . select do |name |
3843 before_snapshot [ name ] != after_snapshot [ name ]
@@ -43,7 +48,7 @@ def capture(file)
4348 definition_active = active_definition_retained? (
4449 previous_definitions ,
4550 current_definitions ,
46- executed_lines ,
51+ executed_line_contexts ,
4752 assignment_events
4853 )
4954 after_snapshot . key? ( name ) && definition_active
@@ -128,11 +133,18 @@ def qualified_constant_name(owner_name, constant_name)
128133 "#{ owner_name } ::#{ constant_name } "
129134 end
130135
131- def apply_trace_events ( events , executed_lines , assignment_events , file )
136+ def apply_trace_events ( events , executed_line_contexts , assignment_events , file )
137+ active_context_events = [ ]
132138 events . each do |event , target , line , method_id |
133139 case event
134140 when :line
135- executed_lines << line
141+ executed_line_contexts [ line ] << active_context_events . dup
142+ when :call , :b_call
143+ active_context_events << event
144+ when :return
145+ remove_active_context_event ( active_context_events , :call )
146+ when :b_return
147+ remove_active_context_event ( active_context_events , :b_call )
136148 when :c_call
137149 if method_id == :const_added || ( method_id == :warn && target . equal? ( Warning ) )
138150 assignment_events [ line ] << method_id
@@ -146,6 +158,11 @@ def apply_trace_events(events, executed_lines, assignment_events, file)
146158 end
147159 end
148160
161+ def remove_active_context_event ( active_context_events , event )
162+ index = active_context_events . rindex ( event )
163+ active_context_events . delete_at ( index ) if index
164+ end
165+
149166 def constants_set_at ( owner , file , line )
150167 owner_name = owner . equal? ( Object ) ? '' : owner . name
151168 return [ ] if owner_name . nil? || owner_name . start_with? ( '#<' )
@@ -161,20 +178,37 @@ def constants_set_at(owner, file, line)
161178 end
162179 end
163180
164- def active_definition_retained? ( previous_definitions , current_definitions , executed_lines , assignment_events )
181+ def active_definition_retained? (
182+ previous_definitions ,
183+ current_definitions ,
184+ executed_line_contexts ,
185+ assignment_events
186+ )
165187 previous_definitions . any? do |previous |
166188 current_definitions . any? do |current |
167189 next false unless previous [ :signature ] == current [ :signature ]
168190
169191 events = assignment_events [ current [ :line ] ]
170192 assignment_observed = events . include? ( :const_added ) || events . count ( :warn ) >= 2
171- current [ :persistent ] ||
172- assignment_observed ||
173- ( !current [ :ambiguous_line ] && executed_lines . include? ( current [ :line ] ) )
193+ persistence_observed = current [ :persistence_line ] &&
194+ !current [ :persistence_ambiguous ] &&
195+ executed_line_contexts [ current [ :persistence_line ] ] . any? do |active_context_events |
196+ context_requirements_met? (
197+ active_context_events ,
198+ current [ :required_context_events ]
199+ )
200+ end
201+ assignment_observed || persistence_observed
174202 end
175203 end
176204 end
177205
206+ def context_requirements_met? ( active_context_events , required_context_events )
207+ required_context_events . uniq . all? do |event |
208+ active_context_events . count ( event ) >= required_context_events . count ( event )
209+ end
210+ end
211+
178212 def assigned_constant_definitions ( file )
179213 syntax_tree = Ripper . sexp ( File . read ( file ) )
180214 return { } unless syntax_tree
@@ -248,11 +282,24 @@ def collect_assigned_constant_definitions(node, namespace, contexts, definitions
248282 collect_assigned_constant_definitions ( node [ 2 ] , namespace , contexts , definitions )
249283 body_context = contexts + [ [ [ node . first , canonical_syntax ( node [ 2 ] ) ] , source_line ( node [ 1 ] ) ] ]
250284 collect_assigned_constant_definitions ( node [ 3 ] , namespace , body_context , definitions )
251- when :def , :defs , :lambda , :do_block , :brace_block
252- lazy_context = contexts + [ [ [ node . first ] , source_line ( node ) ] ]
285+ when :method_add_block
286+ collect_assigned_constant_definitions ( node [ 1 ] , namespace , contexts , definitions )
287+ lazy_context = contexts + [ [ [ :block ] , source_line ( node [ 1 ] ) , :b_call ] ]
288+ collect_assigned_constant_definitions ( node [ 2 ] , namespace , lazy_context , definitions )
289+ when :def , :defs
290+ lazy_context = contexts + [ [ [ node . first ] , source_line ( node ) , :call ] ]
253291 node . drop ( 1 ) . each do |child |
254292 collect_assigned_constant_definitions ( child , namespace , lazy_context , definitions )
255293 end
294+ when :lambda
295+ lazy_context = contexts + [ [ [ node . first ] , source_line ( node ) , :b_call ] ]
296+ node . drop ( 1 ) . each do |child |
297+ collect_assigned_constant_definitions ( child , namespace , lazy_context , definitions )
298+ end
299+ when :do_block , :brace_block
300+ node . drop ( 1 ) . each do |child |
301+ collect_assigned_constant_definitions ( child , namespace , contexts , definitions )
302+ end
256303 else
257304 node . each do |child |
258305 collect_assigned_constant_definitions ( child , namespace , contexts , definitions )
@@ -266,14 +313,19 @@ def collect_assignment_targets(node, namespace, definitions, signature, contexts
266313 assigned_name = constant_name_from_node ( node , namespace )
267314 if assigned_name
268315 line = source_line ( node )
269- # A line event can precede a skipped postfix/short-circuit assignment.
270- ambiguous_line = contexts . any? { |context | context [ 1 ] == line }
271- persistent = constant_existence_guard? ( contexts , assigned_name ) && !lazy_context? ( contexts )
316+ persistence = persistence_metadata (
317+ contexts ,
318+ assigned_name ,
319+ namespace ,
320+ line ,
321+ signature [ 1 ] == '||='
322+ )
272323 definition = {
273324 signature : signature ,
274325 line : line ,
275- ambiguous_line : ambiguous_line ,
276- persistent : persistent
326+ persistence_line : persistence [ :line ] ,
327+ persistence_ambiguous : persistence [ :ambiguous ] ,
328+ required_context_events : persistence [ :required_context_events ]
277329 }
278330 definitions [ assigned_name ] = Array ( definitions [ assigned_name ] ) | [ definition ]
279331 elsif node . is_a? ( Array )
@@ -292,7 +344,7 @@ def collect_const_set_definition(node, namespace, contexts, definitions)
292344 constant_name = literal_constant_name ( arguments . first )
293345 return unless constant_name
294346
295- owner_name = if receiver . nil?
347+ owner_name = if receiver . nil? || self_receiver? ( receiver )
296348 namespace . join ( '::' )
297349 elsif object_receiver? ( receiver )
298350 ''
@@ -304,13 +356,13 @@ def collect_const_set_definition(node, namespace, contexts, definitions)
304356 assigned_name = [ owner_name , constant_name ] . reject ( &:empty? ) . join ( '::' )
305357 signature = [ :const_set , nil , contexts . map ( &:first ) ]
306358 line = source_line ( node )
307- ambiguous_line = contexts . any? { | context | context [ 1 ] == line }
359+ persistence = persistence_metadata ( contexts , assigned_name , namespace , line , false )
308360 definition = {
309361 signature : signature ,
310362 line : line ,
311- ambiguous_line : ambiguous_line ,
312- persistent : contexts . empty? ||
313- ( constant_existence_guard? ( contexts , assigned_name ) && ! lazy_context? ( contexts ) )
363+ persistence_line : persistence [ :line ] ,
364+ persistence_ambiguous : persistence [ :ambiguous ] ,
365+ required_context_events : persistence [ :required_context_events ]
314366 }
315367 definitions [ assigned_name ] = Array ( definitions [ assigned_name ] ) | [ definition ]
316368 end
@@ -349,28 +401,103 @@ def object_receiver?(node)
349401 node &.first == :var_ref && node . dig ( 1 , 0 ) == :@const && node . dig ( 1 , 1 ) == 'Object'
350402 end
351403
352- def constant_existence_guard? ( contexts , assigned_name )
353- constant_name = assigned_name . split ( '::' ) . last
354- contexts . any? do |context , _line |
355- has_method = find_syntax_token ( context ) { |_type , value | value == 'const_defined?' }
356- has_constant = find_syntax_token ( context ) { |_type , value | value == constant_name }
357- defined_guard = syntax_node_with_token? ( context , :defined , constant_name )
358- ( has_method && has_constant ) || defined_guard
404+ def self_receiver? ( node )
405+ node &.first == :var_ref && node . dig ( 1 , 0 ) == :@kw && node . dig ( 1 , 1 ) == 'self'
406+ end
407+
408+ def persistence_metadata ( contexts , assigned_name , namespace , assignment_line , short_circuit_assignment )
409+ guard = contexts . reverse . find do |context |
410+ missing_constant_guard? ( context . first , assigned_name , namespace )
411+ end
412+ persistence_line = guard &.[]( 1 )
413+ persistence_line ||= assignment_line if short_circuit_assignment
414+ return { line : nil , ambiguous : false , required_context_events : [ ] } unless persistence_line
415+
416+ same_line_contexts = contexts . select { |context | context [ 1 ] == persistence_line }
417+ ambiguous = same_line_contexts . any? do |context |
418+ !lazy_context? ( context ) &&
419+ !missing_constant_guard? ( context . first , assigned_name , namespace )
359420 end
421+ required_context_events = same_line_contexts . filter_map do |context |
422+ context [ 2 ]
423+ end
424+ {
425+ line : persistence_line ,
426+ ambiguous : ambiguous ,
427+ required_context_events : required_context_events
428+ }
429+ end
430+
431+ def missing_constant_guard? ( context , assigned_name , namespace )
432+ condition , expected = guarded_condition ( context )
433+ return false unless condition
434+
435+ condition , expected = unwrap_guard_condition ( condition , expected )
436+ expected == false && guarded_constant_name ( condition , namespace ) == assigned_name
360437 end
361438
362- def syntax_node_with_token? ( node , node_type , token_value )
363- return false unless node . is_a? ( Array )
364- if node . first == node_type
365- return true if find_syntax_token ( node ) { |_type , value | value == token_value }
439+ def guarded_condition ( context )
440+ case context . first
441+ when :if
442+ [ context [ 2 ] , context [ 1 ] == :then ]
443+ when :unless
444+ [ context [ 2 ] , context [ 1 ] != :then ]
445+ when :if_mod
446+ [ context [ 1 ] , true ]
447+ when :unless_mod
448+ [ context [ 1 ] , false ]
449+ when :ifop
450+ [ context [ 2 ] , context [ 1 ] == :then ]
451+ when :binary
452+ [ context [ 2 ] , context [ 1 ] == :'&&' ]
453+ end
454+ end
455+
456+ def unwrap_guard_condition ( condition , expected )
457+ loop do
458+ case condition &.first
459+ when :paren
460+ expressions = condition [ 1 ]
461+ break unless expressions . is_a? ( Array ) && expressions . length == 1
462+
463+ condition = expressions . first
464+ when :unary
465+ break unless condition [ 1 ] == :!
466+
467+ condition = condition [ 2 ]
468+ expected = !expected
469+ else
470+ break
471+ end
366472 end
473+ [ condition , expected ]
474+ end
475+
476+ def guarded_constant_name ( condition , namespace )
477+ if condition &.first == :defined
478+ return constant_name_from_node ( condition [ 1 ] , namespace )
479+ end
480+
481+ receiver , method_name , arguments = call_parts ( condition )
482+ return nil unless method_name == 'const_defined?'
483+
484+ constant_name = literal_constant_name ( arguments . first )
485+ return nil unless constant_name
486+
487+ owner_name = if receiver . nil? || self_receiver? ( receiver )
488+ namespace . join ( '::' )
489+ elsif object_receiver? ( receiver )
490+ ''
491+ else
492+ constant_name_from_node ( receiver , namespace )
493+ end
494+ return nil if owner_name . nil?
367495
368- node . any? { | child | syntax_node_with_token? ( child , node_type , token_value ) }
496+ [ owner_name , constant_name ] . reject ( & :empty? ) . join ( '::' )
369497 end
370498
371- def lazy_context? ( contexts )
372- lazy_types = %i[ def defs lambda do_block brace_block ]
373- contexts . any? { |context , _line | lazy_types . include? ( context . first ) }
499+ def lazy_context? ( context )
500+ !context [ 2 ] . nil?
374501 end
375502
376503 def find_syntax_token ( node , &predicate )
0 commit comments