chutney 2.1.1 → 3.0.1

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 +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