chutney 2.1.1 → 3.0.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/.rubocop.yml +8 -2
- data/Gemfile +2 -0
- data/README.md +5 -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 +3 -0
- 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 +10 -13
- data/lib/chutney/formatter/rainbow_formatter.rb +13 -13
- data/lib/chutney/issue.rb +2 -0
- data/lib/chutney/linter.rb +95 -84
- 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 +5 -4
- data/lib/chutney/linter/missing_feature_name.rb +5 -4
- 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,12 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
# gherkin utilities
|
2
4
|
|
3
5
|
module Chutney
|
4
6
|
# base class for all linters
|
5
7
|
class Linter
|
6
8
|
attr_accessor :issues
|
7
|
-
attr_reader :filename
|
8
|
-
|
9
|
-
|
9
|
+
attr_reader :filename, :configuration
|
10
|
+
|
10
11
|
Lint = Struct.new(:message, :gherkin_type, :location, :feature, :scenario, :step, keyword_init: true)
|
11
12
|
|
12
13
|
def self.descendants
|
@@ -18,75 +19,83 @@ module Chutney
|
|
18
19
|
@filename = filename
|
19
20
|
@issues = []
|
20
21
|
@configuration = configuration
|
21
|
-
language = @content.dig(:feature, :language) || 'en'
|
22
|
-
@dialect = Gherkin::Dialect.for(language)
|
22
|
+
# language = @content.dig(:feature, :language) || 'en'
|
23
|
+
# @dialect = Gherkin::Dialect.for(language)
|
23
24
|
end
|
24
25
|
|
25
26
|
def lint
|
26
27
|
raise 'not implemented'
|
27
28
|
end
|
28
|
-
|
29
|
+
|
29
30
|
def and_word?(word)
|
30
|
-
|
31
|
+
dialect_word(:and).include?(word)
|
31
32
|
end
|
32
|
-
|
33
|
+
|
33
34
|
def background_word?(word)
|
34
|
-
|
35
|
+
dialect_word(:background).include?(word)
|
35
36
|
end
|
36
|
-
|
37
|
+
|
37
38
|
def but_word?(word)
|
38
|
-
|
39
|
+
dialect_word(:but).include?(word)
|
39
40
|
end
|
40
|
-
|
41
|
+
|
41
42
|
def examples_word?(word)
|
42
|
-
|
43
|
+
dialect_word(:examples).include?(word)
|
43
44
|
end
|
44
|
-
|
45
|
+
|
45
46
|
def feature_word?(word)
|
46
|
-
|
47
|
+
dialect_word(:feature).include?(word)
|
47
48
|
end
|
48
|
-
|
49
|
+
|
49
50
|
def given_word?(word)
|
50
|
-
|
51
|
+
dialect_word(:given).include?(word)
|
51
52
|
end
|
52
|
-
|
53
|
+
|
53
54
|
def scenario_outline_word?(word)
|
54
|
-
|
55
|
+
dialect_word(:scenarioOutline).include?(word)
|
55
56
|
end
|
56
|
-
|
57
|
+
|
57
58
|
def then_word?(word)
|
58
|
-
|
59
|
+
dialect_word(:then).include?(word)
|
59
60
|
end
|
60
|
-
|
61
|
+
|
61
62
|
def when_word?(word)
|
62
|
-
|
63
|
+
dialect_word(:when).include?(word)
|
64
|
+
end
|
65
|
+
|
66
|
+
def dialect_word(word)
|
67
|
+
CukeModeler::Parsing.dialects[dialect][word.to_s].map(&:strip)
|
68
|
+
end
|
69
|
+
|
70
|
+
def dialect
|
71
|
+
@content.feature&.parsing_data&.dig(:language) || 'en'
|
63
72
|
end
|
64
|
-
|
65
|
-
def tags_for(element)
|
66
|
-
return [] unless element.include? :tags
|
67
73
|
|
68
|
-
|
74
|
+
def tags_for(element)
|
75
|
+
element.tags.map { |tag| tag.name[1..-1] }
|
69
76
|
end
|
70
|
-
|
71
|
-
def add_issue(message, feature, scenario = nil,
|
77
|
+
|
78
|
+
def add_issue(message, feature = nil, scenario = nil, item = nil)
|
72
79
|
issues << Lint.new(
|
73
80
|
message: message,
|
74
|
-
gherkin_type: type(feature, scenario,
|
75
|
-
location: location(feature, scenario,
|
76
|
-
feature: feature
|
77
|
-
scenario: scenario
|
78
|
-
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)
|
79
86
|
).to_h
|
80
87
|
end
|
81
|
-
|
88
|
+
|
82
89
|
def location(feature, scenario, step)
|
83
90
|
if step
|
84
|
-
step[:location]
|
91
|
+
step.parsing_data[:location]
|
92
|
+
elsif scenario
|
93
|
+
scenario.parsing_data.dig(:scenario, :location) || scenario.parsing_data.dig(:background, :location)
|
85
94
|
else
|
86
|
-
|
95
|
+
feature ? feature.parsing_data[:location] : { line: 0, column: 0 }
|
87
96
|
end
|
88
97
|
end
|
89
|
-
|
98
|
+
|
90
99
|
def type(_feature, scenario, step)
|
91
100
|
if step
|
92
101
|
:step
|
@@ -97,101 +106,103 @@ module Chutney
|
|
97
106
|
|
98
107
|
def feature
|
99
108
|
if block_given?
|
100
|
-
yield(@content
|
109
|
+
yield(@content.feature) if @content.feature
|
101
110
|
else
|
102
|
-
@content
|
111
|
+
@content.feature
|
103
112
|
end
|
104
113
|
end
|
105
|
-
|
106
|
-
def elements
|
114
|
+
|
115
|
+
def elements
|
107
116
|
return [] unless feature
|
108
|
-
|
109
|
-
if block_given?
|
110
|
-
feature
|
117
|
+
|
118
|
+
if block_given?
|
119
|
+
feature.children.each do |child|
|
111
120
|
next if off_switch?(child)
|
112
|
-
|
121
|
+
|
113
122
|
yield(feature, child)
|
114
123
|
end
|
115
124
|
else
|
116
|
-
feature
|
125
|
+
feature.children
|
117
126
|
end
|
118
127
|
end
|
119
128
|
|
120
129
|
def off_switch?(element = feature)
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
130
|
+
# require 'pry'; binding.pry
|
131
|
+
off_switch = element.tags
|
132
|
+
.map(&:name)
|
133
|
+
.then { |tags| tags || [] }
|
134
|
+
.filter { |tag_name| tag_name == "@disable#{linter_name}" }
|
135
|
+
.count
|
136
|
+
.positive?
|
127
137
|
off_switch ||= off_switch?(feature) unless element == feature
|
128
138
|
off_switch
|
129
139
|
end
|
130
|
-
|
140
|
+
|
131
141
|
def background
|
142
|
+
return unless feature&.background
|
143
|
+
return if off_switch?(feature)
|
144
|
+
|
132
145
|
if block_given?
|
133
|
-
|
134
|
-
next unless child[:type] == :Background
|
135
|
-
|
136
|
-
yield(feature, child)
|
137
|
-
end
|
146
|
+
yield(feature, feature.background)
|
138
147
|
else
|
139
|
-
|
148
|
+
feature.background
|
140
149
|
end
|
141
150
|
end
|
142
|
-
|
151
|
+
|
143
152
|
def scenarios
|
144
153
|
if block_given?
|
145
|
-
|
146
|
-
next
|
147
|
-
|
148
|
-
yield(feature,
|
154
|
+
feature&.tests&.each do |test|
|
155
|
+
next if off_switch?(test)
|
156
|
+
|
157
|
+
yield(feature, test)
|
149
158
|
end
|
159
|
+
|
150
160
|
else
|
151
|
-
|
161
|
+
feature&.tests
|
152
162
|
end
|
153
163
|
end
|
154
|
-
|
164
|
+
|
155
165
|
def filled_scenarios
|
156
166
|
if block_given?
|
157
167
|
scenarios do |feature, scenario|
|
158
|
-
next
|
159
|
-
|
160
|
-
|
168
|
+
next if scenario.steps.empty?
|
169
|
+
|
161
170
|
yield(feature, scenario)
|
162
171
|
end
|
163
172
|
else
|
164
|
-
scenarios.filter { |s| !s
|
173
|
+
scenarios ? scenarios.filter { |s| !s.steps.empty? } : []
|
165
174
|
end
|
166
175
|
end
|
167
|
-
|
176
|
+
|
168
177
|
def steps
|
169
|
-
|
170
|
-
next
|
171
|
-
|
172
|
-
|
178
|
+
feature&.tests&.each do |test|
|
179
|
+
next if off_switch?(test)
|
180
|
+
|
181
|
+
test.steps.each do |step|
|
182
|
+
yield(feature, test, step)
|
183
|
+
end
|
173
184
|
end
|
174
185
|
end
|
175
186
|
|
176
187
|
def self.linter_name
|
177
188
|
name.split('::').last
|
178
189
|
end
|
179
|
-
|
190
|
+
|
180
191
|
def linter_name
|
181
192
|
self.class.linter_name
|
182
193
|
end
|
183
194
|
|
184
195
|
def render_step(step)
|
185
|
-
value = "#{step
|
186
|
-
value += render_step_argument
|
196
|
+
value = "#{step.keyword} #{step.text}"
|
197
|
+
value += render_step_argument(step.block) if step.block
|
187
198
|
value
|
188
199
|
end
|
189
|
-
|
200
|
+
|
190
201
|
def render_step_argument(argument)
|
191
|
-
return "\n#{argument
|
192
|
-
|
193
|
-
result = argument
|
194
|
-
"|#{row
|
202
|
+
return "\n#{argument.content}" if argument.is_a?(CukeModeler::DocString)
|
203
|
+
|
204
|
+
result = argument.rows.map do |row|
|
205
|
+
"|#{row.cells.map(&:value).join '|'}|"
|
195
206
|
end.join "\n"
|
196
207
|
"\n#{result}"
|
197
208
|
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
|