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