chutney 3.0.0.beta.1 → 3.1.1
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 +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
|