/
mixin_grouping.rb
135 lines (111 loc) · 3.92 KB
/
mixin_grouping.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
# frozen_string_literal: true
module RuboCop
module Cop
module Style
# Checks for grouping of mixins in `class` and `module` bodies.
# By default it enforces mixins to be placed in separate declarations,
# but it can be configured to enforce grouping them in one declaration.
#
# @example EnforcedStyle: separated (default)
# # bad
# class Foo
# include Bar, Qox
# end
#
# # good
# class Foo
# include Qox
# include Bar
# end
#
# @example EnforcedStyle: grouped
# # bad
# class Foo
# extend Bar
# extend Qox
# end
#
# # good
# class Foo
# extend Qox, Bar
# end
class MixinGrouping < Base
include ConfigurableEnforcedStyle
extend AutoCorrector
MIXIN_METHODS = %i[extend include prepend].freeze
MSG = 'Put `%<mixin>s` mixins in %<suffix>s.'
def on_class(node)
begin_node = node.child_nodes.find(&:begin_type?) || node
begin_node.each_child_node(:send).select(&:macro?).each do |macro|
next if !MIXIN_METHODS.include?(macro.method_name) || macro.arguments.empty?
check(macro)
end
end
alias on_module on_class
private
def range_to_remove_for_subsequent_mixin(mixins, node)
range = node.source_range
prev_mixin = mixins.each_cons(2) { |m, n| break m if n == node }
between = prev_mixin.source_range.end.join(range.begin)
# if separated from previous mixin with only whitespace?
unless /\S/.match?(between.source)
range = range.join(between) # then remove that too
end
range
end
def check(send_node)
if separated_style?
check_separated_style(send_node)
else
check_grouped_style(send_node)
end
end
def check_grouped_style(send_node)
return if sibling_mixins(send_node).size == 1
message = format(MSG, mixin: send_node.method_name, suffix: 'a single statement')
add_offense(send_node, message: message) do |corrector|
range = send_node.source_range
mixins = sibling_mixins(send_node)
if send_node == mixins.first
correction = group_mixins(send_node, mixins)
else
range = range_to_remove_for_subsequent_mixin(mixins, send_node)
correction = ''
end
corrector.replace(range, correction)
end
end
def check_separated_style(send_node)
return if send_node.arguments.one?
message = format(MSG, mixin: send_node.method_name, suffix: 'separate statements')
add_offense(send_node, message: message) do |corrector|
range = send_node.source_range
correction = separate_mixins(send_node)
corrector.replace(range, correction)
end
end
def sibling_mixins(send_node)
siblings = send_node.parent.each_child_node(:send).select(&:macro?)
siblings.select { |sibling_node| sibling_node.method?(send_node.method_name) }
end
def grouped_style?
style == :grouped
end
def separated_style?
style == :separated
end
def separate_mixins(node)
arguments = node.arguments.reverse
mixins = ["#{node.method_name} #{arguments.first.source}"]
arguments[1..].inject(mixins) do |replacement, arg|
replacement << "#{indent(node)}#{node.method_name} #{arg.source}"
end.join("\n")
end
def group_mixins(node, mixins)
mixin_names = mixins.reverse.flat_map { |mixin| mixin.arguments.map(&:source) }
"#{node.method_name} #{mixin_names.join(', ')}"
end
end
end
end
end