chutney 3.0.0.beta.1 → 3.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/dependabot.yml +16 -0
- data/.rubocop.yml +4 -3
- data/LICENSE.txt +1 -1
- data/README.md +7 -1
- data/Rakefile +6 -8
- data/chutney.gemspec +9 -6
- data/config/{chutney.yml → chutney_defaults.yml} +2 -0
- data/docs/index.md +1 -1
- data/docs/usage/rules.md +34 -64
- data/exe/chutney +21 -3
- data/lib/chutney.rb +9 -3
- data/lib/chutney/configuration.rb +7 -2
- data/lib/chutney/formatter.rb +4 -5
- data/lib/chutney/formatter/pie_formatter.rb +8 -13
- data/lib/chutney/formatter/rainbow_formatter.rb +11 -13
- data/lib/chutney/linter.rb +50 -43
- data/lib/chutney/linter/avoid_full_stop.rb +1 -3
- data/lib/chutney/linter/avoid_outline_for_single_example.rb +3 -3
- data/lib/chutney/linter/avoid_scripting.rb +2 -2
- data/lib/chutney/linter/background_does_more_than_setup.rb +3 -4
- data/lib/chutney/linter/background_requires_multiple_scenarios.rb +1 -1
- data/lib/chutney/linter/bad_scenario_name.rb +2 -2
- data/lib/chutney/linter/file_name_differs_feature_name.rb +3 -3
- data/lib/chutney/linter/givens_after_background.rb +2 -2
- data/lib/chutney/linter/invalid_file_name.rb +1 -1
- data/lib/chutney/linter/invalid_step_flow.rb +2 -2
- data/lib/chutney/linter/missing_example_name.rb +2 -4
- data/lib/chutney/linter/missing_feature_description.rb +1 -1
- data/lib/chutney/linter/missing_feature_name.rb +2 -2
- data/lib/chutney/linter/missing_scenario_name.rb +1 -2
- data/lib/chutney/linter/missing_test_action.rb +1 -1
- data/lib/chutney/linter/missing_verification.rb +1 -1
- data/lib/chutney/linter/required_tags_starts_with.rb +5 -6
- data/lib/chutney/linter/same_tag_different_case.rb +37 -0
- data/lib/chutney/linter/same_tag_for_all_scenarios.rb +9 -9
- data/lib/chutney/linter/scenario_names_match.rb +2 -3
- data/lib/chutney/linter/tag_used_multiple_times.rb +1 -1
- data/lib/chutney/linter/too_clumsy.rb +1 -1
- data/lib/chutney/linter/too_long_step.rb +3 -3
- data/lib/chutney/linter/too_many_different_tags.rb +6 -6
- data/lib/chutney/linter/too_many_steps.rb +3 -3
- data/lib/chutney/linter/too_many_tags.rb +3 -3
- data/lib/chutney/linter/unique_scenario_names.rb +2 -2
- data/lib/chutney/linter/unknown_variable.rb +3 -3
- data/lib/chutney/linter/unused_variable.rb +3 -4
- data/lib/chutney/linter/use_background.rb +4 -4
- data/lib/chutney/linter/use_outline.rb +7 -7
- data/lib/chutney/version.rb +1 -1
- data/lib/config/locales/en.yml +3 -0
- data/spec/chutney_spec.rb +9 -9
- metadata +17 -33
@@ -5,37 +5,36 @@ require 'pastel'
|
|
5
5
|
module Chutney
|
6
6
|
# pretty formatter
|
7
7
|
class RainbowFormatter < Formatter
|
8
|
-
|
9
8
|
def initialize
|
10
9
|
super
|
11
|
-
|
10
|
+
|
12
11
|
@pastel = Pastel.new
|
13
12
|
end
|
14
|
-
|
15
|
-
def format
|
13
|
+
|
14
|
+
def format
|
16
15
|
files_with_issues.each do |file, linter|
|
17
16
|
put_file(file)
|
18
17
|
linter.filter { |l| !l[:issues].empty? }.each do |linter_with_issues|
|
19
|
-
|
20
18
|
put_linter(linter_with_issues)
|
21
|
-
linter_with_issues[:issues].each { |i| put_issue(i) }
|
19
|
+
linter_with_issues[:issues].each { |i| put_issue(file, i) }
|
22
20
|
end
|
23
21
|
end
|
24
22
|
put_summary
|
25
23
|
end
|
26
|
-
|
24
|
+
|
27
25
|
def put_file(file)
|
28
26
|
puts @pastel.cyan(file.to_s)
|
29
27
|
end
|
30
|
-
|
28
|
+
|
31
29
|
def put_linter(linter)
|
32
30
|
puts @pastel.red(" #{linter[:linter]}")
|
33
31
|
end
|
34
|
-
|
35
|
-
def put_issue(issue)
|
36
|
-
puts " #{
|
32
|
+
|
33
|
+
def put_issue(file, issue)
|
34
|
+
puts " #{issue[:message]}"
|
35
|
+
puts " #{@pastel.dim file.to_s}:#{@pastel.dim(issue.dig(:location, :line))}"
|
37
36
|
end
|
38
|
-
|
37
|
+
|
39
38
|
def put_summary
|
40
39
|
print "#{files.count} features inspected, "
|
41
40
|
if files_with_issues.count.zero?
|
@@ -44,6 +43,5 @@ module Chutney
|
|
44
43
|
puts @pastel.red("#{files_with_issues.count} taste nasty")
|
45
44
|
end
|
46
45
|
end
|
47
|
-
|
48
46
|
end
|
49
47
|
end
|
data/lib/chutney/linter.rb
CHANGED
@@ -7,7 +7,7 @@ module Chutney
|
|
7
7
|
class Linter
|
8
8
|
attr_accessor :issues
|
9
9
|
attr_reader :filename, :configuration
|
10
|
-
|
10
|
+
|
11
11
|
Lint = Struct.new(:message, :gherkin_type, :location, :feature, :scenario, :step, keyword_init: true)
|
12
12
|
|
13
13
|
def self.descendants
|
@@ -19,63 +19,61 @@ module Chutney
|
|
19
19
|
@filename = filename
|
20
20
|
@issues = []
|
21
21
|
@configuration = configuration
|
22
|
-
# language = @content.dig(:feature, :language) || 'en'
|
23
|
-
# @dialect = Gherkin::Dialect.for(language)
|
24
22
|
end
|
25
23
|
|
26
24
|
def lint
|
27
25
|
raise 'not implemented'
|
28
26
|
end
|
29
|
-
|
27
|
+
|
30
28
|
def and_word?(word)
|
31
29
|
dialect_word(:and).include?(word)
|
32
30
|
end
|
33
|
-
|
31
|
+
|
34
32
|
def background_word?(word)
|
35
33
|
dialect_word(:background).include?(word)
|
36
34
|
end
|
37
|
-
|
35
|
+
|
38
36
|
def but_word?(word)
|
39
37
|
dialect_word(:but).include?(word)
|
40
38
|
end
|
41
|
-
|
39
|
+
|
42
40
|
def examples_word?(word)
|
43
41
|
dialect_word(:examples).include?(word)
|
44
42
|
end
|
45
|
-
|
43
|
+
|
46
44
|
def feature_word?(word)
|
47
45
|
dialect_word(:feature).include?(word)
|
48
46
|
end
|
49
|
-
|
47
|
+
|
50
48
|
def given_word?(word)
|
51
49
|
dialect_word(:given).include?(word)
|
52
50
|
end
|
53
|
-
|
51
|
+
|
54
52
|
def scenario_outline_word?(word)
|
55
53
|
dialect_word(:scenarioOutline).include?(word)
|
56
54
|
end
|
57
|
-
|
55
|
+
|
58
56
|
def then_word?(word)
|
59
57
|
dialect_word(:then).include?(word)
|
60
58
|
end
|
61
|
-
|
59
|
+
|
62
60
|
def when_word?(word)
|
63
61
|
dialect_word(:when).include?(word)
|
64
62
|
end
|
65
|
-
|
63
|
+
|
66
64
|
def dialect_word(word)
|
67
65
|
CukeModeler::Parsing.dialects[dialect][word.to_s].map(&:strip)
|
68
66
|
end
|
69
|
-
|
67
|
+
|
70
68
|
def dialect
|
71
69
|
@content.feature&.parsing_data&.dig(:language) || 'en'
|
72
70
|
end
|
73
|
-
|
71
|
+
|
74
72
|
def tags_for(element)
|
75
|
-
element.tags.map { |tag| tag.name[1
|
73
|
+
element.tags.map { |tag| tag.name[1..] }
|
76
74
|
end
|
77
|
-
|
78
|
-
def add_issue(message, feature = nil, scenario = nil, item = nil)
|
75
|
+
|
76
|
+
def add_issue(message, feature = nil, scenario = nil, item = nil)
|
79
77
|
issues << Lint.new(
|
80
78
|
message: message,
|
81
79
|
gherkin_type: type(feature, scenario, item),
|
@@ -85,17 +83,17 @@ module Chutney
|
|
85
83
|
step: item&.parsing_data&.dig(:name)
|
86
84
|
).to_h
|
87
85
|
end
|
88
|
-
|
86
|
+
|
89
87
|
def location(feature, scenario, step)
|
90
88
|
if step
|
91
89
|
step.parsing_data[:location]
|
92
90
|
elsif scenario
|
93
91
|
scenario.parsing_data.dig(:scenario, :location) || scenario.parsing_data.dig(:background, :location)
|
94
|
-
else
|
95
|
-
feature ? feature.parsing_data[:location] : 0
|
92
|
+
else
|
93
|
+
feature ? feature.parsing_data[:location] : { line: 0, column: 0 }
|
96
94
|
end
|
97
95
|
end
|
98
|
-
|
96
|
+
|
99
97
|
def type(_feature, scenario, step)
|
100
98
|
if step
|
101
99
|
:step
|
@@ -111,14 +109,14 @@ module Chutney
|
|
111
109
|
@content.feature
|
112
110
|
end
|
113
111
|
end
|
114
|
-
|
115
|
-
def elements
|
112
|
+
|
113
|
+
def elements
|
116
114
|
return [] unless feature
|
117
|
-
|
118
|
-
if block_given?
|
115
|
+
|
116
|
+
if block_given?
|
119
117
|
feature.children.each do |child|
|
120
118
|
next if off_switch?(child)
|
121
|
-
|
119
|
+
|
122
120
|
yield(feature, child)
|
123
121
|
end
|
124
122
|
else
|
@@ -128,56 +126,65 @@ module Chutney
|
|
128
126
|
|
129
127
|
def off_switch?(element = feature)
|
130
128
|
off_switch = element.tags
|
129
|
+
.map(&:name)
|
131
130
|
.then { |tags| tags || [] }
|
132
|
-
.filter { |
|
133
|
-
.filter { |tag| tag[:name] == "@disable#{linter_name}" }
|
131
|
+
.filter { |tag_name| tag_name == "@disable#{linter_name}" }
|
134
132
|
.count
|
135
133
|
.positive?
|
136
134
|
off_switch ||= off_switch?(feature) unless element == feature
|
137
135
|
off_switch
|
138
136
|
end
|
139
|
-
|
137
|
+
|
140
138
|
def background
|
141
|
-
|
142
|
-
|
139
|
+
return unless feature&.background
|
140
|
+
return if off_switch?(feature)
|
141
|
+
|
142
|
+
if block_given?
|
143
|
+
yield(feature, feature.background)
|
143
144
|
else
|
144
|
-
feature
|
145
|
+
feature.background
|
145
146
|
end
|
146
147
|
end
|
147
|
-
|
148
|
+
|
148
149
|
def scenarios
|
149
150
|
if block_given?
|
150
151
|
feature&.tests&.each do |test|
|
152
|
+
next if off_switch?(test)
|
153
|
+
|
151
154
|
yield(feature, test)
|
152
155
|
end
|
153
|
-
|
156
|
+
|
154
157
|
else
|
155
158
|
feature&.tests
|
156
159
|
end
|
157
160
|
end
|
158
|
-
|
161
|
+
|
159
162
|
def filled_scenarios
|
160
163
|
if block_given?
|
161
164
|
scenarios do |feature, scenario|
|
162
165
|
next if scenario.steps.empty?
|
163
|
-
|
166
|
+
|
164
167
|
yield(feature, scenario)
|
165
168
|
end
|
166
169
|
else
|
167
170
|
scenarios ? scenarios.filter { |s| !s.steps.empty? } : []
|
168
171
|
end
|
169
172
|
end
|
170
|
-
|
173
|
+
|
171
174
|
def steps
|
172
|
-
feature&.tests&.each do |
|
173
|
-
|
175
|
+
feature&.tests&.each do |test|
|
176
|
+
next if off_switch?(test)
|
177
|
+
|
178
|
+
test.steps.each do |step|
|
179
|
+
yield(feature, test, step)
|
180
|
+
end
|
174
181
|
end
|
175
182
|
end
|
176
183
|
|
177
184
|
def self.linter_name
|
178
185
|
name.split('::').last
|
179
186
|
end
|
180
|
-
|
187
|
+
|
181
188
|
def linter_name
|
182
189
|
self.class.linter_name
|
183
190
|
end
|
@@ -187,10 +194,10 @@ module Chutney
|
|
187
194
|
value += render_step_argument(step.block) if step.block
|
188
195
|
value
|
189
196
|
end
|
190
|
-
|
197
|
+
|
191
198
|
def render_step_argument(argument)
|
192
199
|
return "\n#{argument.content}" if argument.is_a?(CukeModeler::DocString)
|
193
|
-
|
200
|
+
|
194
201
|
result = argument.rows.map do |row|
|
195
202
|
"|#{row.cells.map(&:value).join '|'}|"
|
196
203
|
end.join "\n"
|
@@ -2,12 +2,10 @@
|
|
2
2
|
|
3
3
|
module Chutney
|
4
4
|
# service class to lint for avoiding periods
|
5
|
-
class AvoidFullStop < Linter
|
5
|
+
class AvoidFullStop < Linter
|
6
6
|
def lint
|
7
7
|
steps do |feature, child, step|
|
8
|
-
|
9
8
|
add_issue(I18n.t('linters.avoid_full_stop'), feature, child, step) if step.text.strip.end_with? '.'
|
10
|
-
|
11
9
|
end
|
12
10
|
end
|
13
11
|
end
|
@@ -4,13 +4,13 @@ module Chutney
|
|
4
4
|
# service class to lint for avoiding outline for single example
|
5
5
|
class AvoidOutlineForSingleExample < Linter
|
6
6
|
def lint
|
7
|
-
scenarios do |feature, scenario|
|
7
|
+
scenarios do |feature, scenario|
|
8
8
|
next unless scenario.is_a? CukeModeler::Outline
|
9
9
|
next unless scenario.examples
|
10
|
-
|
10
|
+
|
11
11
|
next if scenario.examples.length > 1
|
12
12
|
next if scenario.examples.first.rows.length > 2 # first row is the header
|
13
|
-
|
13
|
+
|
14
14
|
add_issue(I18n.t('linters.avoid_outline_for_single_example'), feature, scenario)
|
15
15
|
end
|
16
16
|
end
|
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
module Chutney
|
4
4
|
# service class to lint for avoid scripting
|
5
|
-
class AvoidScripting < Linter
|
5
|
+
class AvoidScripting < Linter
|
6
6
|
def lint
|
7
7
|
scenarios do |feature, scenario|
|
8
8
|
when_steps = filter_when_steps(scenario.steps)
|
@@ -10,7 +10,7 @@ module Chutney
|
|
10
10
|
add_issue(I18n.t('linters.avoid_scripting', count: whens), feature, scenario, when_steps.last) if whens > 1
|
11
11
|
end
|
12
12
|
end
|
13
|
-
|
13
|
+
|
14
14
|
def filter_when_steps(steps)
|
15
15
|
steps
|
16
16
|
.drop_while { |step| !when_word?(step.keyword) }
|
@@ -2,14 +2,13 @@
|
|
2
2
|
|
3
3
|
module Chutney
|
4
4
|
# service class to lint for background that does more than setup
|
5
|
-
class BackgroundDoesMoreThanSetup < Linter
|
5
|
+
class BackgroundDoesMoreThanSetup < Linter
|
6
6
|
def lint
|
7
7
|
background do |feature, background|
|
8
|
-
|
9
|
-
invalid_steps = background&.steps&.select do |step|
|
8
|
+
invalid_steps = background&.steps&.select do |step|
|
10
9
|
when_word?(step.keyword) || then_word?(step.keyword)
|
11
10
|
end
|
12
|
-
|
11
|
+
|
13
12
|
next if invalid_steps.nil? || invalid_steps.empty?
|
14
13
|
|
15
14
|
add_issue(I18n.t('linters.background_does_more_than_setup'), feature, background, invalid_steps.first)
|
@@ -6,7 +6,7 @@ module Chutney
|
|
6
6
|
# service class for check that there are multiple scenarios once a background is used
|
7
7
|
class BackgroundRequiresMultipleScenarios < Linter
|
8
8
|
MESSAGE = 'Avoid using Background steps for just one scenario'
|
9
|
-
|
9
|
+
|
10
10
|
def lint
|
11
11
|
background do |feature, background|
|
12
12
|
next unless background
|
@@ -1,12 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Chutney
|
4
|
-
# service class to lint for bad scenario names
|
4
|
+
# service class to lint for bad scenario names
|
5
5
|
class BadScenarioName < Linter
|
6
6
|
def lint
|
7
7
|
scenarios do |feature, scenario|
|
8
8
|
next if scenario.name.empty?
|
9
|
-
|
9
|
+
|
10
10
|
bad = /\w*(test|verif|check)\w*/i
|
11
11
|
match = scenario.name.match(bad).to_a.first
|
12
12
|
add_issue(I18n.t('linters.bad_scenario_name', word: match), feature, scenario) if match
|
@@ -5,11 +5,11 @@ module Chutney
|
|
5
5
|
class FileNameDiffersFeatureName < Linter
|
6
6
|
def lint
|
7
7
|
return unless feature
|
8
|
-
|
8
|
+
|
9
9
|
expected_feature_name = title_case(filename)
|
10
10
|
return if ignore_whitespaces(feature.name).casecmp(ignore_whitespaces(expected_feature_name)) == 0
|
11
|
-
|
12
|
-
add_issue(I18n.t('linters.file_name_differs_feature_name', expected: expected_feature_name), feature)
|
11
|
+
|
12
|
+
add_issue(I18n.t('linters.file_name_differs_feature_name', expected: expected_feature_name), feature)
|
13
13
|
end
|
14
14
|
|
15
15
|
def title_case(value)
|
@@ -1,11 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Chutney
|
4
|
-
# service class to lint for bad scenario names
|
4
|
+
# service class to lint for bad scenario names
|
5
5
|
class GivensAfterBackground < Linter
|
6
6
|
def lint
|
7
7
|
return unless background
|
8
|
-
|
8
|
+
|
9
9
|
filled_scenarios do |feature, scenario|
|
10
10
|
scenario.steps.each do |step|
|
11
11
|
add_issue(I18n.t('linters.givens_after_background'), feature, scenario, step) if given_word?(step.keyword)
|
@@ -7,7 +7,7 @@ module Chutney
|
|
7
7
|
filled_scenarios do |feature, scenario|
|
8
8
|
steps = scenario.steps.select { |step| !and_word?(step.keyword) && !but_word?(step.keyword) }
|
9
9
|
next if steps.empty?
|
10
|
-
|
10
|
+
|
11
11
|
last_step_is_an_action(feature, scenario, steps)
|
12
12
|
given_after_non_given(feature, scenario, steps)
|
13
13
|
verification_before_action(feature, scenario, steps)
|
@@ -33,7 +33,7 @@ module Chutney
|
|
33
33
|
def verification_before_action(feature, scenario, steps)
|
34
34
|
steps.each do |step|
|
35
35
|
break if when_word?(step.keyword)
|
36
|
-
|
36
|
+
|
37
37
|
add_issue(I18n.t('linters.invalid_step_flow.missing_action'), feature, scenario) if then_word?(step.keyword)
|
38
38
|
end
|
39
39
|
end
|
@@ -3,7 +3,6 @@
|
|
3
3
|
module Chutney
|
4
4
|
# service class to lint for missing example names
|
5
5
|
class MissingExampleName < Linter
|
6
|
-
|
7
6
|
def lint
|
8
7
|
scenarios do |_feature, scenario|
|
9
8
|
next unless scenario.is_a? CukeModeler::Outline
|
@@ -11,17 +10,16 @@ module Chutney
|
|
11
10
|
scenario.examples.each do |example|
|
12
11
|
example_count = scenario.examples&.length || 0
|
13
12
|
next unless example_count > 1
|
14
|
-
|
13
|
+
|
15
14
|
check_example(scenario, example)
|
16
15
|
end
|
17
16
|
end
|
18
|
-
end
|
17
|
+
end
|
19
18
|
|
20
19
|
def check_example(scenario, example)
|
21
20
|
name = example.name.strip
|
22
21
|
duplicate_name_count = scenario.examples.filter { |e| e.name == name }.count
|
23
22
|
add_issue(I18n.t('linters.missing_example_name'), feature, scenario, example) if duplicate_name_count >= 2
|
24
23
|
end
|
25
|
-
|
26
24
|
end
|
27
25
|
end
|