rubocop-nueca 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.
- checksums.yaml +7 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/README.md +26 -0
- data/config/default.yml +106 -0
- data/lib/rubocop/cop/rails/date_time_conversion.rb +22 -0
- data/lib/rubocop/cop/rails/date_time_current.rb +23 -0
- data/lib/rubocop/cop/rails/migration_table_variable.rb +40 -0
- data/lib/rubocop/cop/rails/model_association_consistent_spacing.rb +71 -0
- data/lib/rubocop/cop/rails/model_association_grouping.rb +114 -0
- data/lib/rubocop/cop/rails/model_association_scattering.rb +97 -0
- data/lib/rubocop/cop/rails/model_association_separation.rb +92 -0
- data/lib/rubocop/cop/rails/model_association_sorting.rb +91 -0
- data/lib/rubocop/cop/rails/postgres_timestamp.rb +33 -0
- data/lib/rubocop/cop/rails/route_consistent_spacing.rb +64 -0
- data/lib/rubocop/cop/rails/route_grouping.rb +88 -0
- data/lib/rubocop/cop/rails/route_root_position.rb +103 -0
- data/lib/rubocop/cop/rails/route_separation.rb +84 -0
- data/lib/rubocop/cop/rails/route_sorting.rb +73 -0
- data/lib/rubocop/cop/rails/time_zone_today.rb +24 -0
- data/lib/rubocop/cop/shared/collection_context.rb +139 -0
- data/lib/rubocop/cop/shared/route_collector.rb +26 -0
- data/lib/rubocop/nueca/plugin.rb +30 -0
- data/lib/rubocop/nueca/version.rb +7 -0
- data/lib/rubocop-nueca.rb +12 -0
- metadata +185 -0
@@ -0,0 +1,92 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Rails
|
6
|
+
class ModelAssociationSeparation < RuboCop::Cop::Base
|
7
|
+
MSG = 'Separate different association types with a blank line.'
|
8
|
+
ASSOCIATION_METHODS = [
|
9
|
+
:belongs_to,
|
10
|
+
:has_one,
|
11
|
+
:has_many,
|
12
|
+
:has_and_belongs_to_many
|
13
|
+
].freeze
|
14
|
+
|
15
|
+
def on_class(node)
|
16
|
+
return unless model_class?(node)
|
17
|
+
|
18
|
+
associations = find_associations(node)
|
19
|
+
return if associations.size < 2
|
20
|
+
|
21
|
+
check_association_separation(associations)
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def model_class?(node)
|
27
|
+
parent_class = node.parent_class
|
28
|
+
return false unless parent_class
|
29
|
+
|
30
|
+
parent_name = parent_class.const_name
|
31
|
+
['ApplicationRecord', 'ActiveRecord::Base'].include?(parent_name)
|
32
|
+
end
|
33
|
+
|
34
|
+
def find_associations(class_node)
|
35
|
+
associations = []
|
36
|
+
|
37
|
+
class_node.body&.each_child_node do |child|
|
38
|
+
next unless child.type == :send
|
39
|
+
next unless association_method?(child)
|
40
|
+
|
41
|
+
source_range = child.source_range
|
42
|
+
associations << {
|
43
|
+
node: child,
|
44
|
+
method: child.method_name,
|
45
|
+
start_line: source_range.line,
|
46
|
+
end_line: source_range.last_line
|
47
|
+
}
|
48
|
+
end
|
49
|
+
|
50
|
+
associations.sort_by { |assoc| assoc[:start_line] }
|
51
|
+
end
|
52
|
+
|
53
|
+
def association_method?(node)
|
54
|
+
return false unless node.receiver.nil?
|
55
|
+
|
56
|
+
ASSOCIATION_METHODS.include?(node.method_name)
|
57
|
+
end
|
58
|
+
|
59
|
+
def check_association_separation(associations)
|
60
|
+
associations.each_with_index do |current, index|
|
61
|
+
next_assoc = associations[index + 1]
|
62
|
+
break unless next_assoc
|
63
|
+
next if same_association_type?(current, next_assoc)
|
64
|
+
next if properly_separated?(current, next_assoc)
|
65
|
+
|
66
|
+
add_offense(next_assoc[:node], message: MSG)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def same_association_type?(current, next_assoc)
|
71
|
+
current[:method] == next_assoc[:method]
|
72
|
+
end
|
73
|
+
|
74
|
+
def properly_separated?(current, next_assoc)
|
75
|
+
current_end_line = current[:end_line]
|
76
|
+
next_start_line = next_assoc[:start_line]
|
77
|
+
lines_between = next_start_line - current_end_line - 1
|
78
|
+
|
79
|
+
return true if lines_between >= 2
|
80
|
+
|
81
|
+
if lines_between == 1
|
82
|
+
between_line = current_end_line
|
83
|
+
line_content = processed_source.lines[between_line].strip
|
84
|
+
return line_content.empty?
|
85
|
+
end
|
86
|
+
|
87
|
+
false
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Rails
|
6
|
+
class ModelAssociationSorting < RuboCop::Cop::Base
|
7
|
+
MSG = 'Sort associations of the same type alphabetically. Expected order: %<expected>s.'
|
8
|
+
ASSOCIATION_METHODS = [
|
9
|
+
:belongs_to,
|
10
|
+
:has_one,
|
11
|
+
:has_many,
|
12
|
+
:has_and_belongs_to_many
|
13
|
+
].freeze
|
14
|
+
|
15
|
+
def on_class(node)
|
16
|
+
return unless model_class?(node)
|
17
|
+
|
18
|
+
associations = find_associations(node)
|
19
|
+
return if associations.size < 2
|
20
|
+
|
21
|
+
check_association_sorting(associations)
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def model_class?(node)
|
27
|
+
parent_class = node.parent_class
|
28
|
+
return false unless parent_class
|
29
|
+
|
30
|
+
parent_name = parent_class.const_name
|
31
|
+
['ApplicationRecord', 'ActiveRecord::Base'].include?(parent_name)
|
32
|
+
end
|
33
|
+
|
34
|
+
def find_associations(class_node)
|
35
|
+
associations = []
|
36
|
+
|
37
|
+
class_node.body&.each_child_node do |child|
|
38
|
+
next unless child.type == :send
|
39
|
+
next unless association_method?(child)
|
40
|
+
|
41
|
+
source_range = child.source_range
|
42
|
+
associations << {
|
43
|
+
node: child,
|
44
|
+
method: child.method_name,
|
45
|
+
name: association_name(child),
|
46
|
+
start_line: source_range.line,
|
47
|
+
end_line: source_range.last_line
|
48
|
+
}
|
49
|
+
end
|
50
|
+
|
51
|
+
associations.sort_by { |assoc| assoc[:start_line] }
|
52
|
+
end
|
53
|
+
|
54
|
+
def association_method?(node)
|
55
|
+
return false unless node.receiver.nil?
|
56
|
+
|
57
|
+
ASSOCIATION_METHODS.include?(node.method_name)
|
58
|
+
end
|
59
|
+
|
60
|
+
def association_name(node)
|
61
|
+
first_arg = node.arguments.first
|
62
|
+
return nil unless first_arg&.sym_type?
|
63
|
+
|
64
|
+
first_arg.value.to_s
|
65
|
+
end
|
66
|
+
|
67
|
+
def check_association_sorting(associations)
|
68
|
+
grouped = associations.group_by { |assoc| assoc[:method] }
|
69
|
+
|
70
|
+
grouped.each_value do |group_associations|
|
71
|
+
next if group_associations.size < 2
|
72
|
+
|
73
|
+
check_group_sorting(group_associations)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def check_group_sorting(group_associations)
|
78
|
+
names = group_associations.map { |assoc| assoc[:name] }
|
79
|
+
sorted_names = names.sort
|
80
|
+
|
81
|
+
return if names == sorted_names
|
82
|
+
|
83
|
+
expected_order = sorted_names.join(', ')
|
84
|
+
message = format(MSG, expected: expected_order)
|
85
|
+
|
86
|
+
add_offense(group_associations.first[:node], message: message)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Rails
|
6
|
+
class PostgresTimestamp < RuboCop::Cop::Base
|
7
|
+
MSG = 'Use t.timestamptz for better timezone support.'
|
8
|
+
RESTRICT_ON_SEND = [:datetime, :timestamps].freeze
|
9
|
+
|
10
|
+
def_node_matcher :table_datetime_usage, <<~PATTERN
|
11
|
+
(send lvar :datetime ...)
|
12
|
+
PATTERN
|
13
|
+
|
14
|
+
def_node_matcher :table_timestamps_usage, <<~PATTERN
|
15
|
+
(send lvar :timestamps ...)
|
16
|
+
PATTERN
|
17
|
+
|
18
|
+
def on_send(node)
|
19
|
+
return unless in_migration_file?
|
20
|
+
return unless node.receiver&.name == :t
|
21
|
+
|
22
|
+
add_offense(node, message: MSG) if table_datetime_usage(node) || table_timestamps_usage(node)
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def in_migration_file?
|
28
|
+
processed_source.file_path&.include?('db/migrate/') || false
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../shared/route_collector'
|
4
|
+
|
5
|
+
module RuboCop
|
6
|
+
module Cop
|
7
|
+
module Rails
|
8
|
+
class RouteConsistentSpacing < RuboCop::Cop::Base
|
9
|
+
MSG = 'Do not leave blank lines between routes of the same type at the same namespace level.'
|
10
|
+
|
11
|
+
def on_block(node)
|
12
|
+
return unless rails_routes_draw_block?(node)
|
13
|
+
|
14
|
+
routes = collect_routes(node)
|
15
|
+
return if routes.size < 2
|
16
|
+
|
17
|
+
check_consistent_spacing(routes)
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def rails_routes_draw_block?(node)
|
23
|
+
return false unless node.block_type?
|
24
|
+
|
25
|
+
send_node = node.send_node
|
26
|
+
receiver = send_node.receiver
|
27
|
+
return false unless receiver
|
28
|
+
|
29
|
+
receiver.source == 'Rails.application.routes' && send_node.method_name == :draw
|
30
|
+
end
|
31
|
+
|
32
|
+
def collect_routes(routes_block)
|
33
|
+
collector = RouteCollector.new
|
34
|
+
body = routes_block.body
|
35
|
+
collector.collect(body) if body
|
36
|
+
collector.routes.sort_by { |route| route[:line] }
|
37
|
+
end
|
38
|
+
|
39
|
+
def check_consistent_spacing(routes)
|
40
|
+
routes.each_with_index do |current_route, index|
|
41
|
+
next_route = routes[index + 1]
|
42
|
+
next unless next_route
|
43
|
+
next unless same_type_and_level?(current_route, next_route)
|
44
|
+
|
45
|
+
add_offense(next_route[:node], message: MSG) if blank_line_between?(current_route, next_route)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def same_type_and_level?(current_route, next_route)
|
50
|
+
current_route[:type] == next_route[:type] &&
|
51
|
+
current_route[:namespace_level] == next_route[:namespace_level]
|
52
|
+
end
|
53
|
+
|
54
|
+
def blank_line_between?(current_route, next_route)
|
55
|
+
current_end_line = current_route[:end_line]
|
56
|
+
next_start_line = next_route[:line]
|
57
|
+
lines_between = next_start_line - current_end_line - 1
|
58
|
+
|
59
|
+
lines_between >= 1
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../shared/route_collector'
|
4
|
+
|
5
|
+
module RuboCop
|
6
|
+
module Cop
|
7
|
+
module Rails
|
8
|
+
class RouteGrouping < RuboCop::Cop::Base
|
9
|
+
MSG = 'Group routes by type. Keep simple routes, resources, and namespaces grouped together.'
|
10
|
+
|
11
|
+
def on_block(node)
|
12
|
+
return unless rails_routes_draw_block?(node)
|
13
|
+
|
14
|
+
routes = collect_routes(node)
|
15
|
+
return if routes.size < 2
|
16
|
+
|
17
|
+
check_for_scattered_routes(routes)
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def rails_routes_draw_block?(node)
|
23
|
+
return false unless node.block_type?
|
24
|
+
|
25
|
+
send_node = node.send_node
|
26
|
+
receiver = send_node.receiver
|
27
|
+
return false unless receiver
|
28
|
+
|
29
|
+
receiver.source == 'Rails.application.routes' && send_node.method_name == :draw
|
30
|
+
end
|
31
|
+
|
32
|
+
def collect_routes(routes_block)
|
33
|
+
collector = RouteCollector.new
|
34
|
+
body = routes_block.body
|
35
|
+
collector.collect(body) if body
|
36
|
+
collector.routes.sort_by { |route| route[:line] }
|
37
|
+
end
|
38
|
+
|
39
|
+
def check_for_scattered_routes(routes)
|
40
|
+
routes_by_type_and_context = routes.group_by do |route|
|
41
|
+
[route[:type], route[:namespace_level], route[:namespace_path]]
|
42
|
+
end
|
43
|
+
|
44
|
+
routes_by_type_and_context.each_value do |type_routes|
|
45
|
+
next if type_routes.size < 2
|
46
|
+
|
47
|
+
find_scattered_routes(type_routes, routes).each do |route|
|
48
|
+
add_offense(route[:node], message: MSG)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def find_scattered_routes(type_routes, all_routes)
|
54
|
+
scattered = []
|
55
|
+
|
56
|
+
type_routes.each_with_index do |current_route, index|
|
57
|
+
next if index.zero?
|
58
|
+
|
59
|
+
if scattered_from_previous?(current_route, type_routes[0...index],
|
60
|
+
all_routes) && !scattered.include?(current_route) # rubocop:disable Rails/NegateInclude
|
61
|
+
scattered << current_route
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
scattered
|
66
|
+
end
|
67
|
+
|
68
|
+
def scattered_from_previous?(current_route, previous_routes, all_routes)
|
69
|
+
current_line = current_route[:line]
|
70
|
+
route_type = current_route[:type]
|
71
|
+
namespace_level = current_route[:namespace_level]
|
72
|
+
|
73
|
+
previous_routes.any? do |prev_route|
|
74
|
+
different_types_between?(all_routes, prev_route[:line], current_line, route_type, namespace_level)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def different_types_between?(all_routes, start_line, end_line, route_type, namespace_level)
|
79
|
+
all_routes.any? do |route|
|
80
|
+
line = route[:line]
|
81
|
+
line > start_line && line < end_line &&
|
82
|
+
route[:namespace_level] == namespace_level && route[:type] != route_type
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../shared/route_collector'
|
4
|
+
|
5
|
+
module RuboCop
|
6
|
+
module Cop
|
7
|
+
module Rails
|
8
|
+
class RouteRootPosition < RuboCop::Cop::Base
|
9
|
+
MSG = 'The root route should be positioned at the top of routes within the same namespace level.'
|
10
|
+
|
11
|
+
def on_block(node)
|
12
|
+
return unless rails_routes_draw_block?(node)
|
13
|
+
|
14
|
+
routes = collect_routes(node)
|
15
|
+
return if routes.empty?
|
16
|
+
|
17
|
+
check_root_position(routes)
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def rails_routes_draw_block?(node)
|
23
|
+
return false unless node.block_type?
|
24
|
+
|
25
|
+
send_node = node.send_node
|
26
|
+
receiver = send_node.receiver
|
27
|
+
return false unless receiver
|
28
|
+
|
29
|
+
receiver.source == 'Rails.application.routes' && send_node.method_name == :draw
|
30
|
+
end
|
31
|
+
|
32
|
+
def collect_routes(routes_block)
|
33
|
+
collector = RouteCollector.new
|
34
|
+
body = routes_block.body
|
35
|
+
collector.collect(body) if body
|
36
|
+
collector.routes.sort_by { |route| route[:line] }
|
37
|
+
end
|
38
|
+
|
39
|
+
def check_root_position(routes)
|
40
|
+
routes_by_context = routes.group_by do |route|
|
41
|
+
[route[:namespace_level], route[:namespace_path]]
|
42
|
+
end
|
43
|
+
|
44
|
+
routes_by_context.each_value do |context_routes|
|
45
|
+
check_root_in_context(context_routes)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def check_root_in_context(context_routes)
|
50
|
+
return if context_routes.size < 2
|
51
|
+
|
52
|
+
root_routes, non_root_routes = partition_routes_by_root(context_routes)
|
53
|
+
return if root_routes.empty?
|
54
|
+
|
55
|
+
non_root_simple_routes = filter_simple_routes(non_root_routes)
|
56
|
+
return if non_root_simple_routes.empty?
|
57
|
+
|
58
|
+
detect_mispositioned_roots(root_routes, non_root_simple_routes)
|
59
|
+
end
|
60
|
+
|
61
|
+
def filter_simple_routes(routes)
|
62
|
+
routes.select { |route| simple_route?(route) }
|
63
|
+
end
|
64
|
+
|
65
|
+
def detect_mispositioned_roots(root_routes, simple_routes)
|
66
|
+
root_routes.each do |root_route|
|
67
|
+
if any_simple_route_before?(root_route, simple_routes)
|
68
|
+
add_offense(root_route[:node], message: MSG)
|
69
|
+
break
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def any_simple_route_before?(root_route, simple_routes)
|
75
|
+
simple_routes.any? { |simple_route| root_route[:line] > simple_route[:line] }
|
76
|
+
end
|
77
|
+
|
78
|
+
def partition_routes_by_root(routes)
|
79
|
+
root_routes = []
|
80
|
+
non_root_routes = []
|
81
|
+
|
82
|
+
routes.each do |route|
|
83
|
+
if root_route?(route)
|
84
|
+
root_routes << route
|
85
|
+
else
|
86
|
+
non_root_routes << route
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
[root_routes, non_root_routes]
|
91
|
+
end
|
92
|
+
|
93
|
+
def root_route?(route)
|
94
|
+
route[:name] == 'root'
|
95
|
+
end
|
96
|
+
|
97
|
+
def simple_route?(route)
|
98
|
+
route[:type] == :simple
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../shared/route_collector'
|
4
|
+
|
5
|
+
module RuboCop
|
6
|
+
module Cop
|
7
|
+
module Rails
|
8
|
+
class RouteSeparation < RuboCop::Cop::Base
|
9
|
+
MSG = 'Separate different route types with a blank line.'
|
10
|
+
|
11
|
+
def on_block(node)
|
12
|
+
return unless rails_routes_draw_block?(node)
|
13
|
+
|
14
|
+
routes = collect_routes(node)
|
15
|
+
return if routes.size < 2
|
16
|
+
|
17
|
+
check_route_separation(routes)
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def rails_routes_draw_block?(node)
|
23
|
+
return false unless node.block_type?
|
24
|
+
|
25
|
+
send_node = node.send_node
|
26
|
+
receiver = send_node.receiver
|
27
|
+
return false unless receiver
|
28
|
+
|
29
|
+
receiver.source == 'Rails.application.routes' && send_node.method_name == :draw
|
30
|
+
end
|
31
|
+
|
32
|
+
def collect_routes(routes_block)
|
33
|
+
collector = RouteCollector.new
|
34
|
+
body = routes_block.body
|
35
|
+
collector.collect(body) if body
|
36
|
+
collector.routes.sort_by { |route| route[:line] }
|
37
|
+
end
|
38
|
+
|
39
|
+
def check_route_separation(routes)
|
40
|
+
routes_by_context = routes.group_by do |route|
|
41
|
+
[route[:namespace_level], route[:namespace_path]]
|
42
|
+
end
|
43
|
+
|
44
|
+
routes_by_context.each_value do |context_routes|
|
45
|
+
next if context_routes.size < 2
|
46
|
+
|
47
|
+
check_separation_within_context(context_routes)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def check_separation_within_context(routes)
|
52
|
+
routes.each_with_index do |current_route, index|
|
53
|
+
next_route = routes[index + 1]
|
54
|
+
break unless next_route
|
55
|
+
next if same_route_type?(current_route, next_route)
|
56
|
+
next if properly_separated?(current_route, next_route)
|
57
|
+
|
58
|
+
add_offense(next_route[:node], message: MSG)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def same_route_type?(current_route, next_route)
|
63
|
+
current_route[:type] == next_route[:type]
|
64
|
+
end
|
65
|
+
|
66
|
+
def properly_separated?(current_route, next_route)
|
67
|
+
current_end_line = current_route[:end_line]
|
68
|
+
next_start_line = next_route[:line]
|
69
|
+
lines_between = next_start_line - current_end_line - 1
|
70
|
+
|
71
|
+
return true if lines_between >= 2
|
72
|
+
|
73
|
+
if lines_between == 1
|
74
|
+
between_line = current_end_line
|
75
|
+
line_content = processed_source.lines[between_line].strip
|
76
|
+
return line_content.empty?
|
77
|
+
end
|
78
|
+
|
79
|
+
false
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../shared/route_collector'
|
4
|
+
|
5
|
+
module RuboCop
|
6
|
+
module Cop
|
7
|
+
module Rails
|
8
|
+
class RouteSorting < RuboCop::Cop::Base
|
9
|
+
MSG = 'Sort routes of the same type alphabetically within the same namespace level. ' \
|
10
|
+
'Expected order: %<expected>s.'
|
11
|
+
|
12
|
+
def on_block(node)
|
13
|
+
return unless rails_routes_draw_block?(node)
|
14
|
+
|
15
|
+
routes = collect_routes(node)
|
16
|
+
return if routes.size < 2
|
17
|
+
|
18
|
+
check_route_sorting(routes)
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def rails_routes_draw_block?(node)
|
24
|
+
return false unless node.block_type?
|
25
|
+
|
26
|
+
send_node = node.send_node
|
27
|
+
receiver = send_node.receiver
|
28
|
+
return false unless receiver
|
29
|
+
|
30
|
+
receiver.source == 'Rails.application.routes' && send_node.method_name == :draw
|
31
|
+
end
|
32
|
+
|
33
|
+
def collect_routes(routes_block)
|
34
|
+
collector = RouteCollector.new
|
35
|
+
body = routes_block.body
|
36
|
+
collector.collect(body) if body
|
37
|
+
collector.routes.sort_by { |route| route[:line] }
|
38
|
+
end
|
39
|
+
|
40
|
+
def check_route_sorting(routes)
|
41
|
+
grouped_routes = routes.group_by do |route|
|
42
|
+
[route[:type], route[:namespace_level], route[:namespace_path]]
|
43
|
+
end
|
44
|
+
|
45
|
+
grouped_routes.each_value do |group_routes|
|
46
|
+
next if group_routes.size < 2
|
47
|
+
|
48
|
+
check_group_sorting(group_routes)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def check_group_sorting(group_routes)
|
53
|
+
non_root_routes = group_routes.reject { |route| root_route?(route) }
|
54
|
+
return if non_root_routes.size < 2
|
55
|
+
|
56
|
+
route_names = non_root_routes.map { |route| route[:name] }
|
57
|
+
sorted_names = route_names.sort
|
58
|
+
|
59
|
+
return if route_names == sorted_names
|
60
|
+
|
61
|
+
expected_order = sorted_names.uniq.join(', ')
|
62
|
+
message = format(MSG, expected: expected_order)
|
63
|
+
|
64
|
+
add_offense(non_root_routes.first[:node], message: message)
|
65
|
+
end
|
66
|
+
|
67
|
+
def root_route?(route)
|
68
|
+
route[:name] == 'root'
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Rails
|
6
|
+
class TimeZoneToday < RuboCop::Cop::Base
|
7
|
+
MSG = 'Do not use Time.zone.today.'
|
8
|
+
RESTRICT_ON_SEND = [:today].freeze
|
9
|
+
|
10
|
+
def_node_matcher :time_zone_today_usage, <<~PATTERN
|
11
|
+
(send
|
12
|
+
(send
|
13
|
+
(const nil? :Time) :zone) :today)
|
14
|
+
PATTERN
|
15
|
+
|
16
|
+
def on_send(node)
|
17
|
+
return unless time_zone_today_usage(node)
|
18
|
+
|
19
|
+
add_offense(node)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|