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.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +8 -2
  3. data/Gemfile +2 -0
  4. data/README.md +5 -1
  5. data/Rakefile +2 -0
  6. data/chutney.gemspec +13 -6
  7. data/config/{chutney.yml → chutney_defaults.yml} +2 -0
  8. data/config/cucumber.yml +1 -0
  9. data/docs/usage/rules.md +3 -0
  10. data/exe/chutney +23 -3
  11. data/lib/chutney.rb +26 -22
  12. data/lib/chutney/configuration.rb +9 -2
  13. data/lib/chutney/formatter.rb +6 -5
  14. data/lib/chutney/formatter/json_formatter.rb +2 -0
  15. data/lib/chutney/formatter/pie_formatter.rb +10 -13
  16. data/lib/chutney/formatter/rainbow_formatter.rb +13 -13
  17. data/lib/chutney/issue.rb +2 -0
  18. data/lib/chutney/linter.rb +95 -84
  19. data/lib/chutney/linter/avoid_full_stop.rb +4 -4
  20. data/lib/chutney/linter/avoid_outline_for_single_example.rb +7 -5
  21. data/lib/chutney/linter/avoid_scripting.rb +8 -6
  22. data/lib/chutney/linter/avoid_typographers_quotes.rb +16 -14
  23. data/lib/chutney/linter/background_does_more_than_setup.rb +8 -7
  24. data/lib/chutney/linter/background_requires_multiple_scenarios.rb +7 -4
  25. data/lib/chutney/linter/bad_scenario_name.rb +6 -4
  26. data/lib/chutney/linter/empty_feature_file.rb +10 -0
  27. data/lib/chutney/linter/file_name_differs_feature_name.rb +7 -5
  28. data/lib/chutney/linter/givens_after_background.rb +7 -8
  29. data/lib/chutney/linter/invalid_file_name.rb +3 -1
  30. data/lib/chutney/linter/invalid_step_flow.rb +9 -9
  31. data/lib/chutney/linter/missing_example_name.rb +9 -9
  32. data/lib/chutney/linter/missing_feature_description.rb +5 -4
  33. data/lib/chutney/linter/missing_feature_name.rb +5 -4
  34. data/lib/chutney/linter/missing_scenario_name.rb +4 -6
  35. data/lib/chutney/linter/missing_test_action.rb +4 -2
  36. data/lib/chutney/linter/missing_verification.rb +4 -2
  37. data/lib/chutney/linter/required_tags_starts_with.rb +7 -6
  38. data/lib/chutney/linter/same_tag_for_all_scenarios.rb +20 -19
  39. data/lib/chutney/linter/scenario_names_match.rb +6 -6
  40. data/lib/chutney/linter/tag_used_multiple_times.rb +3 -1
  41. data/lib/chutney/linter/too_clumsy.rb +4 -2
  42. data/lib/chutney/linter/too_long_step.rb +6 -4
  43. data/lib/chutney/linter/too_many_different_tags.rb +10 -8
  44. data/lib/chutney/linter/too_many_steps.rb +6 -4
  45. data/lib/chutney/linter/too_many_tags.rb +5 -3
  46. data/lib/chutney/linter/unique_scenario_names.rb +5 -5
  47. data/lib/chutney/linter/unknown_variable.rb +15 -15
  48. data/lib/chutney/linter/unused_variable.rb +15 -16
  49. data/lib/chutney/linter/use_background.rb +20 -19
  50. data/lib/chutney/linter/use_outline.rb +15 -14
  51. data/lib/chutney/version.rb +3 -1
  52. data/lib/config/locales/en.yml +2 -0
  53. data/spec/chutney_spec.rb +11 -9
  54. data/spec/spec_helper.rb +2 -0
  55. metadata +19 -15
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'term/ansicolor'
2
4
 
3
5
  module Chutney
@@ -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
- attr_reader :configuration
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
- @dialect.and_keywords.include?(word)
31
+ dialect_word(:and).include?(word)
31
32
  end
32
-
33
+
33
34
  def background_word?(word)
34
- @dialect.background_keywords.include?(word)
35
+ dialect_word(:background).include?(word)
35
36
  end
36
-
37
+
37
38
  def but_word?(word)
38
- @dialect.but_keywords.include?(word)
39
+ dialect_word(:but).include?(word)
39
40
  end
40
-
41
+
41
42
  def examples_word?(word)
42
- @dialect.example_keywords.include?(word)
43
+ dialect_word(:examples).include?(word)
43
44
  end
44
-
45
+
45
46
  def feature_word?(word)
46
- @dialect.feature_keywords.include?(word)
47
+ dialect_word(:feature).include?(word)
47
48
  end
48
-
49
+
49
50
  def given_word?(word)
50
- @dialect.given_keywords.include?(word)
51
+ dialect_word(:given).include?(word)
51
52
  end
52
-
53
+
53
54
  def scenario_outline_word?(word)
54
- @dialect.scenario_outline_keywords.include?(word)
55
+ dialect_word(:scenarioOutline).include?(word)
55
56
  end
56
-
57
+
57
58
  def then_word?(word)
58
- @dialect.then_keywords.include?(word)
59
+ dialect_word(:then).include?(word)
59
60
  end
60
-
61
+
61
62
  def when_word?(word)
62
- @dialect.when_keywords.include?(word)
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
- element[:tags].map { |tag| tag[:name][1..-1] }
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, step = 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, step),
75
- location: location(feature, scenario, step),
76
- feature: feature[:name],
77
- scenario: scenario ? scenario[:name] : nil,
78
- step: step ? step[:text] : nil
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
- scenario ? scenario[:location] : feature[:location]
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[:feature]) if @content[:feature]
109
+ yield(@content.feature) if @content.feature
101
110
  else
102
- @content[:feature]
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[:children].each do |child|
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[:children]
125
+ feature.children
117
126
  end
118
127
  end
119
128
 
120
129
  def off_switch?(element = feature)
121
- off_switch = element[:tags]
122
- .then { |tags| tags || [] }
123
- .filter { |tag| tag[:type] == :Tag }
124
- .filter { |tag| tag[:name] == "@disable#{linter_name}" }
125
- .count
126
- .positive?
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
- elements do |feature, child|
134
- next unless child[:type] == :Background
135
-
136
- yield(feature, child)
137
- end
146
+ yield(feature, feature.background)
138
147
  else
139
- elements.filter { |child| child[:type] == :Background }
148
+ feature.background
140
149
  end
141
150
  end
142
-
151
+
143
152
  def scenarios
144
153
  if block_given?
145
- elements do |feature, child|
146
- next unless %i[ScenarioOutline Scenario].include? child[:type]
147
-
148
- yield(feature, child)
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
- elements.filter { |child| %i[ScenarioOutline Scenario].include? child[:type] }
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 unless scenario.include? :steps
159
- next if scenario[:steps].empty?
160
-
168
+ next if scenario.steps.empty?
169
+
161
170
  yield(feature, scenario)
162
171
  end
163
172
  else
164
- scenarios.filter { |s| !s[:steps].empty? }
173
+ scenarios ? scenarios.filter { |s| !s.steps.empty? } : []
165
174
  end
166
175
  end
167
-
176
+
168
177
  def steps
169
- elements do |feature, child|
170
- next unless child.include? :steps
171
-
172
- child[:steps].each { |step| yield(feature, child, step) }
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[:keyword]}#{step[:text]}"
186
- value += render_step_argument step[:argument] if step.include? :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[:content]}" if argument[:type] == :DocString
192
-
193
- result = argument[:rows].map do |row|
194
- "|#{row[:cells].map { |cell| cell[:value] }.join '|'}|"
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[:type] == :ScenarioOutline
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[:steps])
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[:keyword]) }
15
- .then { |s| s.reverse.drop_while { |step| !then_word?(step[:keyword]) }.reverse }
16
- .then { |s| s.reject { |step| then_word?(step[:keyword]) } }
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 |_feature, scenario|
8
- lint_steps(scenario[:steps])
9
+ scenarios do |feature, scenario|
10
+ lint_steps(feature, scenario)
9
11
 
10
- example_count = scenario[:examples]&.length || 0
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[:examples])
15
+ lint_examples(feature, scenario)
14
16
  end
15
17
  end
16
18
 
17
- def lint_steps(steps)
18
- steps.each do |step|
19
- issue(step) if TYPOGRAPHER_QUOTES.any? { |tq| step[:text].include? tq }
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(examples)
24
- examples.each do |example|
25
- example[:tableBody].each do |body|
26
- body[:cells].each do |cell|
27
- issue(cell) if TYPOGRAPHER_QUOTES.any? { |tq| cell[:value].include? tq }
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
- next unless background.key? :steps
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
- add_issue(I18n.t('linters.background_does_more_than_setup'), feature, background)
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'.freeze
7
-
8
+ MESSAGE = 'Avoid using Background steps for just one scenario'
9
+
8
10
  def lint
9
11
  background do |feature, background|
10
- scenarios = feature[:children].reject { |element| element[:type] == :Background }
11
- next if scenarios.length >= 2
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