@@ -2,132 +2,144 @@ module RuboCop
22 module Cop
33 module Migration
44 class TooManyMigrationRuns < Base
5- MSG = 'Too many migration runs (%d). Combine tests to reduce migrations. See spec/migrations/README.md for further guidance.' . freeze
6- MAX_CALLS = 4
7-
85 def on_new_investigation
9- calls = 0
10- migrator_subject_names = [ ]
11- migrator_method_names = [ ]
12- migrator_let_names = [ ]
13- migrator_before_after_blocks = Set . new
6+ return unless processed_source . ast
7+
8+ definitions = extract_migrator_definitions
9+ call_count = count_migrator_calls ( definitions )
1410
15- extract_migrator_definitions ( migrator_subject_names , migrator_method_names ,
16- migrator_let_names , migrator_before_after_blocks )
11+ return unless call_count > 4
1712
18- count_migrator_calls ( calls , migrator_subject_names , migrator_method_names ,
19- migrator_let_names , migrator_before_after_blocks )
13+ add_offense ( processed_source . ast ,
14+ message : "Too many migration runs ( #{ call_count } ). Combine tests to reduce migrations. See spec/migrations/README.md for further guidance." )
2015 end
2116
22- def extract_migrator_definitions ( subject_names , method_names , let_names , before_after_blocks )
23- processed_source . ast . each_descendant ( :def ) do |node |
24- method_name = extract_migrator_method_name ( node )
25- method_names << method_name if method_name
17+ private
18+
19+ def extract_migrator_definitions
20+ definitions = {
21+ subject_names : [ ] ,
22+ method_names : [ ] ,
23+ let_names : [ ] ,
24+ before_after_blocks : Set . new
25+ }
26+
27+ # Single pass through the AST to collect all definitions
28+ processed_source . ast . each_descendant ( :def , :block ) do |node |
29+ case node . type
30+ when :def
31+ extract_migrator_method ( node , definitions [ :method_names ] )
32+ when :block
33+ extract_block_definitions ( node , definitions )
34+ end
2635 end
2736
28- processed_source . ast . each_descendant ( :block ) do |node |
29- subject_name = extract_migrator_subject_name ( node )
30- subject_names << subject_name if subject_name
37+ definitions
38+ end
3139
32- let_name = extract_migrator_let_name ( node )
33- let_names << let_name if let_name
40+ def extract_migrator_method ( node , method_names )
41+ return unless contains_migrator_run? ( node )
3442
35- before_after_blocks . add ( node . object_id ) if is_before_after_around_with_migrator? ( node )
36- end
43+ method_names << node . method_name
3744 end
3845
39- def count_migrator_calls ( _calls , subjects , methods , lets , before_after_blocks )
40- call_count = count_before_after_migrations ( before_after_blocks )
41- call_count += count_send_node_migrations ( subjects , methods , lets , before_after_blocks )
46+ def extract_block_definitions ( node , definitions )
47+ return unless node . send_node
4248
43- add_offense ( processed_source . ast , message : sprintf ( MSG , call_count ) ) if call_count > MAX_CALLS
44- end
49+ method_name = node . send_node . method_name
4550
46- def count_before_after_migrations ( before_after_blocks )
47- call_count = 0
48- processed_source . ast . each_descendant ( :block ) do |node |
49- call_count += count_direct_migrations_in_node ( node ) if before_after_blocks . include? ( node . object_id )
51+ case method_name
52+ when :subject
53+ extract_named_migrator ( node , definitions [ :subject_names ] )
54+ when :let , :let!
55+ extract_named_migrator ( node , definitions [ :let_names ] )
56+ when :before , :after , :around
57+ definitions [ :before_after_blocks ] . add ( node . object_id ) if contains_migrator_run? ( node )
5058 end
51- call_count
5259 end
5360
54- def count_send_node_migrations ( subjects , methods , lets , before_after_blocks )
61+ def extract_named_migrator ( node , names )
62+ return unless contains_migrator_run? ( node )
63+
64+ first_arg = node . send_node . first_argument
65+ names << first_arg . value if first_arg &.sym_type?
66+ end
67+
68+ def count_migrator_calls ( definitions )
5569 call_count = 0
70+
71+ # Single pass through send nodes to count all migration calls
5672 processed_source . ast . each_descendant ( :send ) do |node |
57- next if node . each_ancestor ( :block ) . any? { | a | before_after_blocks . include? ( a . object_id ) }
73+ next unless migration_call? ( node , definitions )
5874
59- call_count += count_migration_call ( node , subjects , methods , lets )
75+ call_count += 1
6076 end
77+
6178 call_count
6279 end
6380
64- def count_migration_call ( node , subjects , methods , lets )
65- return 1 if direct_migrator_call? ( node )
66- return 1 if helper_migration_call? ( node , subjects , methods , lets )
81+ def migration_call? ( node , definitions )
82+ in_before_after_block = node . each_ancestor ( :block ) . any? do |ancestor |
83+ definitions [ :before_after_blocks ] . include? ( ancestor . object_id )
84+ end
6785
68- 0
86+ if in_before_after_block
87+ # Count direct Migrator.run calls inside before/after/around blocks
88+ migrator_run_call? ( node )
89+ else
90+ # Count direct calls (not in definitions) or helper invocations
91+ direct_migrator_call? ( node ) || helper_migration_call? ( node , definitions )
92+ end
6993 end
7094
7195 def direct_migrator_call? ( node )
72- return false unless node . method_name == :run && node . receiver &. source &. include? ( 'Migrator' )
96+ return false unless migrator_run_call? ( node )
7397
7498 !inside_definition? ( node )
7599 end
76100
77- def helper_migration_call? ( node , subjects , methods , lets )
78- subjects . include? ( node . method_name ) ||
79- lets . include? ( node . method_name ) ||
80- methods . include? ( node . method_name )
81- end
82-
83- private
84-
85- def extract_migrator_method_name ( node )
86- return nil unless node . type == :def
87- return nil unless node . source . include? ( 'Sequel::Migrator.run' )
88-
89- node . method_name
90- end
91-
92- def extract_migrator_subject_name ( node )
93- return nil unless node . send_node . method_name == :subject
94- return nil unless node . source . include? ( 'Sequel::Migrator.run' )
101+ def migrator_run_call? ( node )
102+ return false unless node . method_name == :run
95103
96- first_arg = node . send_node . first_argument
97- first_arg &.sym_type? ? first_arg . value : nil
98- end
104+ receiver = node . receiver
105+ return false unless receiver
99106
100- def extract_migrator_let_name ( node )
101- return nil unless %i[ let let! ] . include? ( node . send_node . method_name )
102- return nil unless node . source . include? ( 'Sequel::Migrator.run' )
107+ # Check for Sequel::Migrator.run or just Migrator.run
108+ if receiver . const_type?
109+ receiver_name = receiver . const_name
110+ return [ 'Migrator' , 'Sequel::Migrator' ] . include? ( receiver_name )
111+ end
103112
104- first_arg = node . send_node . first_argument
105- first_arg &.sym_type? ? first_arg . value : nil
113+ false
106114 end
107115
108- def is_before_after_around_with_migrator ?( node )
109- return false unless node . send_node
110- return false unless %i[ before after around ] . include? ( node . send_node . method_name )
111-
112- node . source . include? ( 'Sequel::Migrator.run' )
116+ def helper_migration_call ?( node , definitions )
117+ method = node . method_name
118+ definitions [ :subject_names ] . include? ( method ) ||
119+ definitions [ :let_names ] . include? ( method ) ||
120+ definitions [ :method_names ] . include? ( method )
113121 end
114122
115- def count_direct_migrations_in_node ( node )
116- count = 0
117- node . each_descendant ( :send ) do |descendant |
118- count += 1 if descendant . method_name == :run && descendant . receiver &.source &.include? ( 'Migrator' )
119- end
120- count
123+ def contains_migrator_run? ( node )
124+ node . each_descendant ( :send ) . any? { |send_node | migrator_run_call? ( send_node ) }
121125 end
122126
123127 def inside_definition? ( node )
124- node . each_ancestor ( :def ) . any? { |a | a . source . include? ( 'Sequel::Migrator.run' ) } ||
125- node . each_ancestor ( :block ) . any? do |a |
126- %i[ subject let let! ] . include? ( a . send_node &.method_name ) && a . source . include? ( 'Sequel::Migrator.run' )
127- end ||
128- node . each_ancestor ( :block ) . any? do |a |
129- %i[ before after around ] . include? ( a . send_node &.method_name )
128+ node . each_ancestor ( :def , :block ) . any? do |ancestor |
129+ case ancestor . type
130+ when :def
131+ contains_migrator_run? ( ancestor )
132+ when :block
133+ next false unless ancestor . send_node
134+
135+ method = ancestor . send_node . method_name
136+ if %i[ subject let let! ] . include? ( method )
137+ contains_migrator_run? ( ancestor )
138+ else
139+ %i[ before after around ] . include? ( method )
140+ end
130141 end
142+ end
131143 end
132144 end
133145 end
0 commit comments