chutney 2.1.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
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 +6 -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 +7 -1
  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 +11 -10
  16. data/lib/chutney/formatter/rainbow_formatter.rb +13 -13
  17. data/lib/chutney/issue.rb +2 -0
  18. data/lib/chutney/linter.rb +87 -83
  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 +6 -3
  33. data/lib/chutney/linter/missing_feature_name.rb +6 -3
  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,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
- attr_reader :configuration
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
- @dialect.and_keywords.include?(word)
31
+ dialect_word(:and).include?(word)
30
32
  end
31
-
33
+
32
34
  def background_word?(word)
33
- @dialect.background_keywords.include?(word)
35
+ dialect_word(:background).include?(word)
34
36
  end
35
-
37
+
36
38
  def but_word?(word)
37
- @dialect.but_keywords.include?(word)
39
+ dialect_word(:but).include?(word)
38
40
  end
39
-
41
+
40
42
  def examples_word?(word)
41
- @dialect.example_keywords.include?(word)
43
+ dialect_word(:examples).include?(word)
42
44
  end
43
-
45
+
44
46
  def feature_word?(word)
45
- @dialect.feature_keywords.include?(word)
47
+ dialect_word(:feature).include?(word)
46
48
  end
47
-
49
+
48
50
  def given_word?(word)
49
- @dialect.given_keywords.include?(word)
51
+ dialect_word(:given).include?(word)
50
52
  end
51
-
53
+
52
54
  def scenario_outline_word?(word)
53
- @dialect.scenario_outline_keywords.include?(word)
55
+ dialect_word(:scenarioOutline).include?(word)
54
56
  end
55
-
57
+
56
58
  def then_word?(word)
57
- @dialect.then_keywords.include?(word)
59
+ dialect_word(:then).include?(word)
58
60
  end
59
-
61
+
60
62
  def when_word?(word)
61
- @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)
62
68
  end
63
-
64
- def tags_for(element)
65
- return [] unless element.include? :tags
66
69
 
67
- element[:tags].map { |tag| tag[:name][1..-1] }
70
+ def dialect
71
+ @content.feature&.parsing_data&.dig(:language) || 'en'
68
72
  end
69
-
70
- def add_issue(message, feature, scenario = nil, step = nil)
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, step),
74
- location: location(feature, scenario, step),
75
- feature: feature[:name],
76
- scenario: scenario ? scenario[:name] : nil,
77
- 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)
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
- scenario ? scenario[:location] : feature[:location]
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[:feature]) if @content[:feature]
109
+ yield(@content.feature) if @content.feature
100
110
  else
101
- @content[:feature]
111
+ @content.feature
102
112
  end
103
113
  end
104
-
114
+
105
115
  def elements
106
- if block_given?
107
- feature[:children].each do |child|
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[:children]
125
+ feature.children
114
126
  end
115
127
  end
116
-
128
+
117
129
  def off_switch?(element = feature)
118
- off_switch = element[:tags]
119
- .then { |tags| tags || [] }
120
- .filter { |tag| tag[:type] == :Tag }
121
- .filter { |tag| tag[:name] == "@disable#{linter_name}" }
122
- .count
123
- .positive?
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
- elements do |feature, child|
131
- next unless child[:type] == :Background
132
-
133
- yield(feature, child)
134
- end
142
+ yield(feature, feature&.background)
135
143
  else
136
- elements.filter { |child| child[:type] == :Background }
144
+ feature&.background
137
145
  end
138
146
  end
139
-
147
+
140
148
  def scenarios
141
149
  if block_given?
142
- elements do |feature, child|
143
- next unless %i[ScenarioOutline Scenario].include? child[:type]
144
-
145
- yield(feature, child)
150
+ feature&.tests&.each do |test|
151
+ yield(feature, test)
146
152
  end
153
+
147
154
  else
148
- elements.filter { |child| %i[ScenarioOutline Scenario].include? child[:type] }
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 unless scenario.include? :steps
156
- next if scenario[:steps].empty?
157
-
162
+ next if scenario.steps.empty?
163
+
158
164
  yield(feature, scenario)
159
165
  end
160
166
  else
161
- scenarios.filter { |s| !s[:steps].empty? }
167
+ scenarios ? scenarios.filter { |s| !s.steps.empty? } : []
162
168
  end
163
169
  end
164
-
170
+
165
171
  def steps
166
- elements do |feature, child|
167
- next unless child.include? :steps
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[:keyword]}#{step[:text]}"
183
- value += render_step_argument step[:argument] if step.include? :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[:content]}" if argument[:type] == :DocString
189
-
190
- result = argument[:rows].map do |row|
191
- "|#{row[:cells].map { |cell| cell[:value] }.join '|'}|"
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[: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