rubocop-sorted_methods_by_call 1.0.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.
@@ -0,0 +1,491 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module SortedMethodsByCall
6
+ # +RuboCop::Cop::SortedMethodsByCall::Waterfall+ enforces "waterfall" ordering:
7
+ # define a method after any method that calls it (within the same scope).
8
+ #
9
+ # - Scopes: class/module/sclass (top-level can be enabled in config)
10
+ # - Offense: when a callee is defined above its caller
11
+ # - Autocorrect: UNSAFE; reorders methods within a contiguous visibility section
12
+ #
13
+ # Example (good):
14
+ # def foo
15
+ # bar
16
+ # end
17
+ #
18
+ # def bar
19
+ # 123
20
+ # end
21
+ #
22
+ # Example (bad):
23
+ # def bar
24
+ # 123
25
+ # end
26
+ #
27
+ # def foo
28
+ # bar
29
+ # end
30
+ #
31
+ # Autocorrect (unsafe, opt-in via SafeAutoCorrect: false): topologically sorts the contiguous
32
+ # block of defs to satisfy edges (caller -> callee). Skips cycles and non-contiguous groups.
33
+ class Waterfall < ::RuboCop::Cop::Base # rubocop:disable Metrics/ClassLength
34
+ include ::RuboCop::Cop::RangeHelp
35
+ extend ::RuboCop::Cop::AutoCorrector
36
+
37
+ # +RuboCop::Cop::SortedMethodsByCall::Waterfall::MSG+ -> String
38
+ #
39
+ # Template message for offenses.
40
+ MSG = 'Define %<callee>s after its caller %<caller>s (waterfall order).'
41
+
42
+ # +RuboCop::Cop::SortedMethodsByCall::Waterfall#on_begin+ -> void
43
+ #
44
+ # Entry point for root :begin nodes (top-level). Whether it is analyzed
45
+ # depends on configuration (e.g., CheckTopLevel). By default, only class/module scopes are analyzed.
46
+ #
47
+ # @param [RuboCop::AST::Node] node
48
+ # @return [void]
49
+ def on_begin(node)
50
+ analyze_scope(node)
51
+ end
52
+
53
+ # +RuboCop::Cop::SortedMethodsByCall::Waterfall#on_class+ -> void
54
+ #
55
+ # Entry point for class scopes.
56
+ #
57
+ # @param [RuboCop::AST::Node] node
58
+ # @return [void]
59
+ def on_class(node)
60
+ analyze_scope(node)
61
+ end
62
+
63
+ # +RuboCop::Cop::SortedMethodsByCall::Waterfall#on_module+ -> void
64
+ #
65
+ # Entry point for module scopes.
66
+ #
67
+ # @param [RuboCop::AST::Node] node
68
+ # @return [void]
69
+ def on_module(node)
70
+ analyze_scope(node)
71
+ end
72
+
73
+ # +RuboCop::Cop::SortedMethodsByCall::Waterfall#on_sclass+ -> void
74
+ #
75
+ # Entry point for singleton class scopes (class << self).
76
+ #
77
+ # @param [RuboCop::AST::Node] node
78
+ # @return [void]
79
+ def on_sclass(node)
80
+ analyze_scope(node)
81
+ end
82
+
83
+ private
84
+
85
+ # +RuboCop::Cop::SortedMethodsByCall::Waterfall#analyze_scope+ -> void
86
+ #
87
+ # Collects defs in the current scope, builds caller->callee edges
88
+ # for local sends, finds the first backward edge (callee defined before caller),
89
+ # and registers an offense. If autocorrection is requested, attempts to reorder
90
+ # methods within the same visibility section.
91
+ #
92
+ # @param [RuboCop::AST::Node] scope_node
93
+ # @return [void]
94
+ def analyze_scope(scope_node)
95
+ body_nodes = scope_body_nodes(scope_node)
96
+ return if body_nodes.empty?
97
+
98
+ def_nodes = body_nodes.select { |n| %i[def defs].include?(n.type) }
99
+ return if def_nodes.size <= 1
100
+
101
+ names = def_nodes.map(&:method_name)
102
+ names_set = names.to_set
103
+ index_of = names.each_with_index.to_h
104
+
105
+ edges = []
106
+ def_nodes.each do |def_node|
107
+ local_calls(def_node, names_set).each do |callee|
108
+ next if callee == def_node.method_name # self-recursion
109
+
110
+ edges << [def_node.method_name, callee]
111
+ end
112
+ end
113
+
114
+ allow_recursion = cop_config.fetch('AllowedRecursion') { true }
115
+ adj = build_adj(names, edges)
116
+
117
+ violation = first_backward_edge(edges, index_of, adj, allow_recursion)
118
+ return unless violation
119
+
120
+ caller_name, callee_name = violation
121
+ callee_node = def_nodes[index_of[callee_name]]
122
+
123
+ add_offense(callee_node,
124
+ message: format(MSG, callee: "##{callee_name}", caller: "##{caller_name}")) do |corrector|
125
+ try_autocorrect(corrector, body_nodes, def_nodes, edges)
126
+ end
127
+
128
+ # Recurse into nested scopes
129
+ body_nodes.each { |n| analyze_scope(n) if n.class_type? || n.module_type? || n.sclass_type? }
130
+ end
131
+
132
+ # +RuboCop::Cop::SortedMethodsByCall::Waterfall#scope_body_nodes+ -> Array<RuboCop::AST::Node>
133
+ #
134
+ # Normalizes a scope node to its immediate "body" items we iterate over.
135
+ #
136
+ # @param [RuboCop::AST::Node] node
137
+ # @return [Array<RuboCop::AST::Node>]
138
+ def scope_body_nodes(node)
139
+ case node.type
140
+ when :begin
141
+ node.children
142
+ when :class, :module, :sclass
143
+ body = node.body
144
+ return [] unless body
145
+
146
+ body.begin_type? ? body.children : [body]
147
+ else
148
+ []
149
+ end
150
+ end
151
+
152
+ # +RuboCop::Cop::SortedMethodsByCall::Waterfall#local_calls+ -> Array<Symbol>
153
+ #
154
+ # Returns the set of local method names (receiver is nil/self) invoked inside
155
+ # a given def node whose names exist in the provided name set.
156
+ #
157
+ # @param [RuboCop::AST::Node] def_node
158
+ # @param [Set<Symbol>] names_set
159
+ # @return [Array<Symbol>]
160
+ def local_calls(def_node, names_set)
161
+ body = def_node.body
162
+ return [] unless body
163
+
164
+ res = []
165
+ body.each_node(:send) do |send|
166
+ recv = send.receiver
167
+ next unless recv.nil? || recv&.self_type?
168
+
169
+ mname = send.method_name
170
+ res << mname if names_set.include?(mname)
171
+ end
172
+ res.uniq
173
+ end
174
+
175
+ # +RuboCop::Cop::SortedMethodsByCall::Waterfall#find_violation+ -> [Symbol, Symbol], nil
176
+ #
177
+ # Finds the first backward edge (caller->callee where callee is defined above caller)
178
+ # using the provided index map.
179
+ #
180
+ # @param [Array<Array(Symbol, Symbol)>] edges
181
+ # @param [Hash{Symbol=>Integer}] index_of
182
+ # @return [[Symbol, Symbol], nil] tuple [caller, callee] or nil if none found
183
+ def find_violation(edges, index_of)
184
+ edges.find do |caller, callee|
185
+ index_of.key?(caller) && index_of.key?(callee) && index_of[callee] < index_of[caller]
186
+ end
187
+ end
188
+
189
+ # +RuboCop::Cop::SortedMethodsByCall::Waterfall#try_autocorrect+ -> void
190
+ #
191
+ # UNSAFE: Reorders method definitions inside the target visibility section only
192
+ # (does not cross private/protected/public boundaries). Skips if defs are not
193
+ # contiguous within the section or if a cycle prevents a consistent topo order.
194
+ #
195
+ # @param [RuboCop::Cop::Corrector] corrector
196
+ # @param [Array<RuboCop::AST::Node>] body_nodes
197
+ # @param [Array<RuboCop::AST::Node>] def_nodes
198
+ # @param [Array<Array(Symbol, Symbol)>] edges
199
+ # @return [void]
200
+ #
201
+ # @note Applied only when user asked for autocorrections; with SafeAutoCorrect: false, this runs under -A.
202
+ # @note Also preserves contiguous leading doc comments above each method.
203
+ def try_autocorrect(corrector, body_nodes, def_nodes, edges)
204
+ # Group method definitions into visibility sections
205
+ sections = extract_visibility_sections(body_nodes)
206
+
207
+ # Find the section that contains our violating methods
208
+ caller_name, callee_name = first_backward_edge(
209
+ edges,
210
+ def_nodes.map(&:method_name).each_with_index.to_h,
211
+ build_adj(def_nodes.map(&:method_name), edges),
212
+ cop_config.fetch('AllowedRecursion') { true }
213
+ )
214
+
215
+ # No violation -> nothing to do
216
+ return unless caller_name && callee_name
217
+
218
+ # Find a visibility section that contains both names
219
+ target_section = sections.find do |section|
220
+ names_in_section = section[:defs].to_set(&:method_name)
221
+ names_in_section.include?(caller_name) && names_in_section.include?(callee_name)
222
+ end
223
+
224
+ # If violation spans multiple sections, skip autocorrect
225
+ return unless target_section
226
+
227
+ defs = target_section[:defs]
228
+ return unless defs.size > 1
229
+
230
+ # Apply topological sort only within this visibility section
231
+ defs = target_section[:defs]
232
+ names = defs.map(&:method_name)
233
+ idx_of = names.each_with_index.to_h
234
+
235
+ # Filter edges to only those within this section
236
+ # Filter edges to only those within this section
237
+ section_names = names.to_set
238
+ section_edges = edges.select { |u, v| section_names.include?(u) && section_names.include?(v) }
239
+
240
+ sorted_names = topo_sort(names, section_edges, idx_of)
241
+ return unless sorted_names
242
+
243
+ # Capture each def with its leading contiguous comment block
244
+ ranges_by_name = defs.to_h { |d| [d.method_name, range_with_leading_comments(d)] }
245
+ sorted_def_sources = sorted_names.map { |name| ranges_by_name[name].source }
246
+
247
+ # Reconstruct the section: keep the visibility modifier (if any) above the first def
248
+ visibility_node = target_section[:visibility]
249
+ visibility_source = visibility_node&.source.to_s
250
+
251
+ new_content = if visibility_source.empty?
252
+ sorted_def_sources.join("\n\n")
253
+ else
254
+ "#{visibility_source}\n\n#{sorted_def_sources.join("\n\n")}"
255
+ end
256
+
257
+ # Expand the replaced region:
258
+ # - if a visibility node exists, start from its begin_pos (so we replace it)
259
+ # - otherwise, start from the earliest leading doc-comment of the defs
260
+ section_begin =
261
+ if visibility_node
262
+ visibility_node.source_range.begin_pos
263
+ else
264
+ defs.map { |d| range_with_leading_comments(d).begin_pos }.min
265
+ end
266
+
267
+ # Always end at the end of the last def
268
+ section_end = defs.last.source_range.end_pos
269
+
270
+ region = Parser::Source::Range.new(processed_source.buffer, section_begin, section_end)
271
+ corrector.replace(region, new_content)
272
+ end
273
+
274
+ # +RuboCop::Cop::SortedMethodsByCall::Waterfall#build_adj+ -> Hash{Symbol=>Array<Symbol>}
275
+ #
276
+ # Builds an adjacency list for edges restricted to known names.
277
+ #
278
+ # @param [Array<Symbol>] names
279
+ # @param [Array<Array(Symbol, Symbol)>] edges
280
+ # @return [Hash{Symbol=>Array<Symbol>}]
281
+ def build_adj(names, edges)
282
+ allowed = names.to_set
283
+ adj = Hash.new { |h, k| h[k] = [] }
284
+ edges.each do |u, v|
285
+ next unless allowed.include?(u) && allowed.include?(v)
286
+ next if u == v
287
+
288
+ adj[u] << v
289
+ end
290
+ adj
291
+ end
292
+
293
+ # +RuboCop::Cop::SortedMethodsByCall::Waterfall#first_backward_edge+ -> [Symbol, Symbol], nil
294
+ #
295
+ # Returns the first backward edge found, optionally skipping mutual recursion
296
+ # if so configured.
297
+ #
298
+ # @param [Array<Array(Symbol, Symbol)>] edges
299
+ # @param [Hash{Symbol=>Integer}] index_of
300
+ # @param [Hash{Symbol=>Array<Symbol>}] adj
301
+ # @param [Boolean] allow_recursion whether to ignore cycles
302
+ # @return [[Symbol, Symbol], nil]
303
+ def first_backward_edge(edges, index_of, adj, allow_recursion)
304
+ edges.find do |caller, callee|
305
+ next unless index_of.key?(caller) && index_of.key?(callee)
306
+ # If mutual recursion allowed and there is a path callee -> caller, skip
307
+ next if allow_recursion && path_exists?(callee, caller, adj)
308
+
309
+ index_of[callee] < index_of[caller]
310
+ end
311
+ end
312
+
313
+ # +RuboCop::Cop::SortedMethodsByCall::Waterfall#path_exists?+ -> Boolean
314
+ #
315
+ # Tests whether a path exists in the adjacency graph from +src+ to +dst+ (BFS).
316
+ #
317
+ # @param [Symbol] src
318
+ # @param [Symbol] dst
319
+ # @param [Hash{Symbol=>Array<Symbol>}] adj
320
+ # @param [Integer] limit traversal step limit (guard)
321
+ # @return [Boolean]
322
+ def path_exists?(src, dst, adj, limit = 200)
323
+ return true if src == dst
324
+
325
+ visited = {}
326
+ q = [src]
327
+ steps = 0
328
+ until q.empty?
329
+ steps += 1
330
+ return false if steps > limit
331
+
332
+ u = q.shift
333
+ next if visited[u]
334
+
335
+ visited[u] = true
336
+ return true if u == dst
337
+
338
+ adj[u].each { |v| q << v unless visited[v] }
339
+ end
340
+ false
341
+ end
342
+
343
+ # +RuboCop::Cop::SortedMethodsByCall::Waterfall#extract_visibility_sections+ -> Array<Hash>
344
+ #
345
+ # Splits the body into contiguous sections of defs grouped by visibility modifier
346
+ # (private/protected/public). Returns metadata for each section including:
347
+ # :visibility -> visibility modifier node or nil
348
+ # :defs -> array of def/defs nodes
349
+ # :start_pos -> Integer (begin_pos)
350
+ # :end_pos -> Integer (end_pos)
351
+ #
352
+ # @param [Array<RuboCop::AST::Node>] body_nodes
353
+ # @return [Array<Hash{Symbol=>untyped}>]
354
+ def extract_visibility_sections(body_nodes)
355
+ sections = []
356
+ current_visibility = nil
357
+ current_defs = []
358
+ section_start = nil
359
+
360
+ body_nodes.each_with_index do |node, idx|
361
+ case node.type
362
+ when :def, :defs
363
+ current_defs << node
364
+ section_start ||= node.source_range.begin_pos
365
+ when :send
366
+ # Check if this is a visibility modifier (private/protected/public)
367
+ if node.receiver.nil? && %i[private protected public].include?(node.method_name) && node.arguments.empty?
368
+ # End current section if it has defs
369
+ unless current_defs.empty?
370
+ sections << {
371
+ visibility: current_visibility,
372
+ defs: current_defs.dup,
373
+ start_pos: section_start,
374
+ end_pos: body_nodes[idx - 1].source_range.end_pos
375
+ }
376
+ current_defs = []
377
+ section_start = nil
378
+ end
379
+ current_visibility = node
380
+ else
381
+ # Non-visibility send - breaks contiguity
382
+ unless current_defs.empty?
383
+ sections << {
384
+ visibility: current_visibility,
385
+ defs: current_defs.dup,
386
+ start_pos: section_start,
387
+ end_pos: body_nodes[idx - 1].source_range.end_pos
388
+ }
389
+ current_defs = []
390
+ section_start = nil
391
+ current_visibility = nil
392
+ end
393
+ end
394
+ else
395
+ # Any other node type breaks contiguity
396
+ unless current_defs.empty?
397
+ sections << {
398
+ visibility: current_visibility,
399
+ defs: current_defs.dup,
400
+ start_pos: section_start,
401
+ end_pos: body_nodes[idx - 1].source_range.end_pos
402
+ }
403
+ current_defs = []
404
+ section_start = nil
405
+ current_visibility = nil
406
+ end
407
+ end
408
+ end
409
+
410
+ # Handle trailing defs
411
+ unless current_defs.empty?
412
+ sections << {
413
+ visibility: current_visibility,
414
+ defs: current_defs,
415
+ start_pos: section_start,
416
+ end_pos: current_defs.last.source_range.end_pos
417
+ }
418
+ end
419
+
420
+ sections
421
+ end
422
+
423
+ # +RuboCop::Cop::SortedMethodsByCall::Waterfall#topo_sort+ -> Array<Symbol>, nil
424
+ #
425
+ # Performs a stable topological sort using current order as a tie-breaker.
426
+ #
427
+ # @param [Array<Symbol>] names
428
+ # @param [Array<Array(Symbol, Symbol)>] edges
429
+ # @param [Hash{Symbol=>Integer}] idx_of
430
+ # @return [Array<Symbol>, nil] sorted names or nil if a cycle prevents a full order
431
+ def topo_sort(names, edges, idx_of)
432
+ indegree = Hash.new(0)
433
+ adj = Hash.new { |h, k| h[k] = [] }
434
+
435
+ edges.each do |caller, callee|
436
+ next unless names.include?(caller) && names.include?(callee)
437
+ next if caller == callee
438
+
439
+ adj[caller] << callee
440
+ indegree[callee] += 1
441
+ indegree[caller] ||= 0
442
+ end
443
+ names.each { |n| indegree[n] ||= 0 }
444
+
445
+ queue = names.select { |n| indegree[n].zero? }.sort_by { |n| idx_of[n] }
446
+ result = []
447
+
448
+ until queue.empty?
449
+ n = queue.shift
450
+ result << n
451
+ adj[n].each do |m|
452
+ indegree[m] -= 1
453
+ queue << m if indegree[m].zero?
454
+ end
455
+ queue.sort_by! { |x| idx_of[x] }
456
+ end
457
+
458
+ return nil unless result.size == names.size
459
+
460
+ result
461
+ end
462
+
463
+ # +RuboCop::Cop::SortedMethodsByCall::Waterfall#range_with_leading_comments+ -> Parser::Source::Range
464
+ #
465
+ # Returns a range that starts at the first contiguous comment line immediately
466
+ # above the def/defs node, and ends at the end of the def. This preserves
467
+ # YARD/RDoc doc comments when methods are moved during autocorrect.
468
+ #
469
+ # @param [RuboCop::AST::Node] node The def/defs node.
470
+ # @return [Parser::Source::Range] Range covering leading comments + method body.
471
+ def range_with_leading_comments(node)
472
+ buffer = processed_source.buffer
473
+ expr = node.source_range
474
+
475
+ start_line = expr.line
476
+ lineno = start_line - 1
477
+ while lineno >= 1
478
+ line = buffer.source_line(lineno)
479
+ break unless line =~ /\A\s*#/
480
+
481
+ start_line = lineno
482
+ lineno -= 1
483
+ end
484
+
485
+ start_pos = buffer.line_range(start_line).begin_pos
486
+ Parser::Source::Range.new(buffer, start_pos, expr.end_pos)
487
+ end
488
+ end
489
+ end
490
+ end
491
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module SortedMethodsByCall
5
+ # +RuboCop::SortedMethodsByCall::Compare+ provides helpers to compare
6
+ # definition orders and call orders using “ordered subsequence” semantics.
7
+ # It’s used by the cop to check that called methods appear in the same
8
+ # relative order as they are defined (not necessarily contiguously).
9
+ module Compare
10
+ class << self
11
+ # +RuboCop::SortedMethodsByCall::Compare.hashes_ordered_equal?(actual, expected)+ -> Bool
12
+ #
13
+ # For each scope key, checks that every call in +expected[k]+ exists in +actual[k]+ and
14
+ # appears in the same relative order (i.e., +expected[k]+ is a subsequence of +actual[k]+).
15
+ # Returns false if a call is unknown (not present in +actual[k]+) or out of order.
16
+ #
17
+ # @example
18
+ # defs = { main: %i[abc foo bar a hello] }
19
+ # calls = { main: %i[foo bar hello] }
20
+ # RuboCop::SortedMethodsByCall::Compare.hashes_ordered_equal?(defs, calls) #=> true
21
+ #
22
+ # calls2 = { main: %i[bar foo] }
23
+ # RuboCop::SortedMethodsByCall::Compare.hashes_ordered_equal?(defs, calls2) #=> false
24
+ #
25
+ # @param [Hash{Object=>Array<Symbol>}] actual Actual definitions per scope.
26
+ # @param [Hash{Object=>Array<Symbol>}] expected Expected calls per scope.
27
+ # @return [Bool] true if for every scope +k+, +expected[k]+ is a subsequence of +actual[k]+
28
+ # and contains no unknown methods.
29
+ def hashes_ordered_equal?(actual, expected)
30
+ return false unless actual.is_a?(Hash) && expected.is_a?(Hash)
31
+
32
+ (actual.keys | expected.keys).all? do |k|
33
+ defs = Array(actual[k])
34
+ calls = Array(expected[k])
35
+ (calls - defs).empty? && subsequence?(defs, calls)
36
+ end
37
+ end
38
+
39
+ # +RuboCop::SortedMethodsByCall::Compare.subsequence?(arr, sub)+ -> Bool
40
+ #
41
+ # Returns true if +sub+ is a subsequence of +arr+ (order preserved),
42
+ # not necessarily contiguous. An empty +sub+ returns true.
43
+ #
44
+ # @example
45
+ # arr = %i[abc foo bar a hello]
46
+ # RuboCop::SortedMethodsByCall::Compare.subsequence?(arr, %i[foo bar hello]) #=> true
47
+ # RuboCop::SortedMethodsByCall::Compare.subsequence?(arr, %i[bar foo]) #=> false
48
+ #
49
+ # @param [Array<#==>] arr Base sequence (typically Array<Symbol>).
50
+ # @param [Array<#==>, nil] sub Candidate subsequence (typically Array<Symbol>).
51
+ # @return [Bool] true if +sub+ appears in +arr+ in order.
52
+ def subsequence?(arr, sub)
53
+ return true if sub.nil? || sub.empty?
54
+
55
+ i = 0
56
+ sub.each do |el|
57
+ found = false
58
+ while i < arr.length
59
+ if arr[i] == el
60
+ found = true
61
+ i += 1
62
+ break
63
+ end
64
+ i += 1
65
+ end
66
+ return false unless found
67
+ end
68
+ true
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module SortedMethodsByCall
5
+ module Util # :nodoc:
6
+ # +RuboCop::SortedMethodsByCall::Util.deep_merge(hash, other)+ -> Hash
7
+ #
8
+ # Merges two hashes without overwriting values that share the same key.
9
+ # When a key exists in both hashes, values are accumulated into an array
10
+ # ("buckets"). Scalars are wrapped to arrays automatically.
11
+ #
12
+ # - Non-destructive: returns a new Hash; does not mutate +hash+.
13
+ # - If +other+ is not a Hash, the original +hash+ is returned as-is.
14
+ #
15
+ # @example Accumulate values into buckets
16
+ # base = { main: :abc, class_T: :hi }
17
+ # other = { class_T: :h1 }
18
+ # RuboCop::SortedMethodsByCall::Util.deep_merge(base, other)
19
+ # #=> { main: [:abc], class_T: [:hi, :h1] }
20
+ #
21
+ # @param [Hash] hash The base hash to merge from.
22
+ # @param [Hash] other The hash to merge into +hash+.
23
+ # @return [Hash] A new hash with accumulated values per key.
24
+ # @see Hash#merge
25
+ def self.deep_merge(h, other)
26
+ return h unless other.is_a?(Hash)
27
+
28
+ h.merge(other) { |_, a, b| Array(a) + Array(b) }
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'lint_roller'
4
+ require_relative 'version'
5
+
6
+ module RuboCop
7
+ module SortedMethodsByCall
8
+ # +RuboCop::SortedMethodsByCall::Plugin+ integrates this extension with RuboCop's
9
+ # plugin system via lint_roller. It declares metadata and tells RuboCop where
10
+ # to find the plugin's default configuration.
11
+ #
12
+ # The plugin is discovered by RuboCop when you configure:
13
+ # plugins:
14
+ # - rubocop-sorted_methods_by_call
15
+ #
16
+ # It will automatically apply rules (config/default.yml) and make the cops
17
+ # available to the engine.
18
+ class Plugin < LintRoller::Plugin
19
+ # +RuboCop::SortedMethodsByCall::Plugin#about+ -> LintRoller::About
20
+ #
21
+ # Declares plugin metadata (name, version, homepage, description).
22
+ #
23
+ # @return [LintRoller::About] Metadata describing this plugin.
24
+ def about
25
+ LintRoller::About.new(
26
+ name: 'rubocop-sorted_methods_by_call',
27
+ version: VERSION,
28
+ homepage: 'https://github.com/unurgunite/rubocop-sorted_methods_by_call',
29
+ description: 'Enforces waterfall ordering: define methods after the methods that call them.'
30
+ )
31
+ end
32
+
33
+ # +RuboCop::SortedMethodsByCall::Plugin#supported?+ -> Boolean
34
+ #
35
+ # Indicates that this plugin supports RuboCop as the lint engine.
36
+ #
37
+ # @param [Object] context LintRoller context (engine, versions, etc.).
38
+ # @return [Boolean] true for RuboCop engine; false otherwise.
39
+ def supported?(context)
40
+ context.engine == :rubocop
41
+ end
42
+
43
+ # +RuboCop::SortedMethodsByCall::Plugin#rules+ -> LintRoller::Rules
44
+ #
45
+ # Returns the plugin rules for RuboCop. This points RuboCop to the default
46
+ # configuration file shipped with the gem (config/default.yml).
47
+ #
48
+ # @param [Object] _context LintRoller context (unused).
49
+ # @return [LintRoller::Rules] Rule declaration for RuboCop to load.
50
+ #
51
+ # @see config/default.yml
52
+ def rules(_context)
53
+ LintRoller::Rules.new(
54
+ type: :path,
55
+ config_format: :rubocop,
56
+ value: Pathname.new(__dir__).join('../../../config/default.yml')
57
+ )
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module SortedMethodsByCall
5
+ VERSION = '1.0.0'
6
+ end
7
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'rubocop/sorted_methods_by_call/plugin'
4
+ require_relative 'rubocop/sorted_methods_by_call/version'
5
+ require_relative 'rubocop/cop/sorted_methods_by_call/waterfall'
@@ -0,0 +1,13 @@
1
+ plugins:
2
+ - rubocop-sorted_methods_by_call
3
+
4
+ inherit_from: ../.rubocop.yml
5
+
6
+ SortedMethodsByCall/Waterfall:
7
+ Enabled: true
8
+
9
+ AllCops:
10
+ Exclude:
11
+ - 'rubocop-sorted_methods_by_call.gemspec'
12
+ - 'Gemfile'
13
+ - 'Gemfile.lock'
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gem 'rubocop-sorted_methods_by_call', path: '../rubocop-sorted_methods_by_call'