-
-
Notifications
You must be signed in to change notification settings - Fork 269
/
spec_file_path_format.rb
133 lines (111 loc) · 4.08 KB
/
spec_file_path_format.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
# frozen_string_literal: true
module RuboCop
module Cop
module RSpec
# Checks that spec file paths are consistent and well-formed.
#
# @example
# # bad
# whatever_spec.rb # describe MyClass
# my_class_spec.rb # describe MyClass, '#method'
#
# # good
# my_class_spec.rb # describe MyClass
# my_class_method_spec.rb # describe MyClass, '#method'
# my_class/method_spec.rb # describe MyClass, '#method'
#
# @example `CustomTransform: {RuboCop=>rubocop, RSpec=>rspec}` (default)
# # good
# rubocop_spec.rb # describe RuboCop
# rspec_spec.rb # describe RSpec
#
# @example `IgnoreMethods: false` (default)
# # bad
# my_class_spec.rb # describe MyClass, '#method'
#
# @example `IgnoreMethods: true`
# # good
# my_class_spec.rb # describe MyClass, '#method'
#
# @example `IgnoreMetadata: {type=>routing}` (default)
# # good
# whatever_spec.rb # describe MyClass, type: :routing do; end
#
class SpecFilePathFormat < Base
include TopLevelGroup
include Namespace
include FileHelp
MSG = 'Spec path should end with `%<suffix>s`.'
# @!method example_group_arguments(node)
def_node_matcher :example_group_arguments, <<~PATTERN
(block $(send #rspec? #ExampleGroups.all $_ $...) ...)
PATTERN
# @!method metadata_key_value(node)
def_node_search :metadata_key_value, '(pair (sym $_key) (sym $_value))'
def on_top_level_example_group(node)
return unless top_level_groups.one?
example_group_arguments(node) do |send_node, class_name, arguments|
next if !class_name.const_type? || ignore_metadata?(arguments)
ensure_correct_file_path(send_node, class_name, arguments)
end
end
private
def ensure_correct_file_path(send_node, class_name, arguments)
pattern = correct_path_pattern(class_name, arguments)
return if filename_ends_with?(pattern)
# For the suffix shown in the offense message, modify the regular
# expression pattern to resemble a glob pattern for clearer error
# messages.
suffix = pattern.sub('.*', '*').sub('[^/]*', '*').sub('\.', '.')
add_offense(send_node, message: format(MSG, suffix: suffix))
end
def ignore_metadata?(arguments)
arguments.any? do |argument|
metadata_key_value(argument).any? do |key, value|
ignore_metadata.values_at(key.to_s).include?(value.to_s)
end
end
end
def correct_path_pattern(class_name, arguments)
path = [expected_path(class_name)]
path << '.*' unless ignore?(arguments.first)
path << [name_pattern(arguments.first), '[^/]*_spec\.rb']
path.join
end
def name_pattern(method_name)
return if ignore?(method_name)
method_name.str_content.gsub(/\s/, '_').gsub(/\W/, '')
end
def ignore?(method_name)
!method_name&.str_type? || ignore_methods?
end
def expected_path(constant)
constants = namespace(constant) + constant.const_name.split('::')
File.join(
constants.map do |name|
custom_transform.fetch(name) { camel_to_snake_case(name) }
end
)
end
def camel_to_snake_case(string)
string
.gsub(/([^A-Z])([A-Z]+)/, '\1_\2')
.gsub(/([A-Z])([A-Z][^A-Z\d]+)/, '\1_\2')
.downcase
end
def custom_transform
cop_config.fetch('CustomTransform', {})
end
def ignore_methods?
cop_config['IgnoreMethods']
end
def ignore_metadata
cop_config.fetch('IgnoreMetadata', {})
end
def filename_ends_with?(pattern)
expanded_file_path.match?("#{pattern}$")
end
end
end
end
end