chutney 2.1.1 → 3.0.1
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 +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
|