chutney 3.0.0.beta.1 → 3.1.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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/.github/dependabot.yml +16 -0
  3. data/.rubocop.yml +4 -3
  4. data/LICENSE.txt +1 -1
  5. data/README.md +7 -1
  6. data/Rakefile +6 -8
  7. data/chutney.gemspec +9 -6
  8. data/config/{chutney.yml → chutney_defaults.yml} +2 -0
  9. data/docs/index.md +1 -1
  10. data/docs/usage/rules.md +34 -64
  11. data/exe/chutney +21 -3
  12. data/lib/chutney.rb +9 -3
  13. data/lib/chutney/configuration.rb +7 -2
  14. data/lib/chutney/formatter.rb +4 -5
  15. data/lib/chutney/formatter/pie_formatter.rb +8 -13
  16. data/lib/chutney/formatter/rainbow_formatter.rb +11 -13
  17. data/lib/chutney/linter.rb +50 -43
  18. data/lib/chutney/linter/avoid_full_stop.rb +1 -3
  19. data/lib/chutney/linter/avoid_outline_for_single_example.rb +3 -3
  20. data/lib/chutney/linter/avoid_scripting.rb +2 -2
  21. data/lib/chutney/linter/background_does_more_than_setup.rb +3 -4
  22. data/lib/chutney/linter/background_requires_multiple_scenarios.rb +1 -1
  23. data/lib/chutney/linter/bad_scenario_name.rb +2 -2
  24. data/lib/chutney/linter/file_name_differs_feature_name.rb +3 -3
  25. data/lib/chutney/linter/givens_after_background.rb +2 -2
  26. data/lib/chutney/linter/invalid_file_name.rb +1 -1
  27. data/lib/chutney/linter/invalid_step_flow.rb +2 -2
  28. data/lib/chutney/linter/missing_example_name.rb +2 -4
  29. data/lib/chutney/linter/missing_feature_description.rb +1 -1
  30. data/lib/chutney/linter/missing_feature_name.rb +2 -2
  31. data/lib/chutney/linter/missing_scenario_name.rb +1 -2
  32. data/lib/chutney/linter/missing_test_action.rb +1 -1
  33. data/lib/chutney/linter/missing_verification.rb +1 -1
  34. data/lib/chutney/linter/required_tags_starts_with.rb +5 -6
  35. data/lib/chutney/linter/same_tag_different_case.rb +37 -0
  36. data/lib/chutney/linter/same_tag_for_all_scenarios.rb +9 -9
  37. data/lib/chutney/linter/scenario_names_match.rb +2 -3
  38. data/lib/chutney/linter/tag_used_multiple_times.rb +1 -1
  39. data/lib/chutney/linter/too_clumsy.rb +1 -1
  40. data/lib/chutney/linter/too_long_step.rb +3 -3
  41. data/lib/chutney/linter/too_many_different_tags.rb +6 -6
  42. data/lib/chutney/linter/too_many_steps.rb +3 -3
  43. data/lib/chutney/linter/too_many_tags.rb +3 -3
  44. data/lib/chutney/linter/unique_scenario_names.rb +2 -2
  45. data/lib/chutney/linter/unknown_variable.rb +3 -3
  46. data/lib/chutney/linter/unused_variable.rb +3 -4
  47. data/lib/chutney/linter/use_background.rb +4 -4
  48. data/lib/chutney/linter/use_outline.rb +7 -7
  49. data/lib/chutney/version.rb +1 -1
  50. data/lib/config/locales/en.yml +3 -0
  51. data/spec/chutney_spec.rb +9 -9
  52. metadata +17 -33
@@ -5,37 +5,36 @@ require 'pastel'
5
5
  module Chutney
6
6
  # pretty formatter
7
7
  class RainbowFormatter < Formatter
8
-
9
8
  def initialize
10
9
  super
11
-
10
+
12
11
  @pastel = Pastel.new
13
12
  end
14
-
15
- def format
13
+
14
+ def format
16
15
  files_with_issues.each do |file, linter|
17
16
  put_file(file)
18
17
  linter.filter { |l| !l[:issues].empty? }.each do |linter_with_issues|
19
-
20
18
  put_linter(linter_with_issues)
21
- linter_with_issues[:issues].each { |i| put_issue(i) }
19
+ linter_with_issues[:issues].each { |i| put_issue(file, i) }
22
20
  end
23
21
  end
24
22
  put_summary
25
23
  end
26
-
24
+
27
25
  def put_file(file)
28
26
  puts @pastel.cyan(file.to_s)
29
27
  end
30
-
28
+
31
29
  def put_linter(linter)
32
30
  puts @pastel.red(" #{linter[:linter]}")
33
31
  end
34
-
35
- def put_issue(issue)
36
- puts " #{@pastel.dim(issue.dig(:location, :line))} #{issue[:message]}"
32
+
33
+ def put_issue(file, issue)
34
+ puts " #{issue[:message]}"
35
+ puts " #{@pastel.dim file.to_s}:#{@pastel.dim(issue.dig(:location, :line))}"
37
36
  end
38
-
37
+
39
38
  def put_summary
40
39
  print "#{files.count} features inspected, "
41
40
  if files_with_issues.count.zero?
@@ -44,6 +43,5 @@ module Chutney
44
43
  puts @pastel.red("#{files_with_issues.count} taste nasty")
45
44
  end
46
45
  end
47
-
48
46
  end
49
47
  end
@@ -7,7 +7,7 @@ module Chutney
7
7
  class Linter
8
8
  attr_accessor :issues
9
9
  attr_reader :filename, :configuration
10
-
10
+
11
11
  Lint = Struct.new(:message, :gherkin_type, :location, :feature, :scenario, :step, keyword_init: true)
12
12
 
13
13
  def self.descendants
@@ -19,63 +19,61 @@ module Chutney
19
19
  @filename = filename
20
20
  @issues = []
21
21
  @configuration = configuration
22
- # language = @content.dig(:feature, :language) || 'en'
23
- # @dialect = Gherkin::Dialect.for(language)
24
22
  end
25
23
 
26
24
  def lint
27
25
  raise 'not implemented'
28
26
  end
29
-
27
+
30
28
  def and_word?(word)
31
29
  dialect_word(:and).include?(word)
32
30
  end
33
-
31
+
34
32
  def background_word?(word)
35
33
  dialect_word(:background).include?(word)
36
34
  end
37
-
35
+
38
36
  def but_word?(word)
39
37
  dialect_word(:but).include?(word)
40
38
  end
41
-
39
+
42
40
  def examples_word?(word)
43
41
  dialect_word(:examples).include?(word)
44
42
  end
45
-
43
+
46
44
  def feature_word?(word)
47
45
  dialect_word(:feature).include?(word)
48
46
  end
49
-
47
+
50
48
  def given_word?(word)
51
49
  dialect_word(:given).include?(word)
52
50
  end
53
-
51
+
54
52
  def scenario_outline_word?(word)
55
53
  dialect_word(:scenarioOutline).include?(word)
56
54
  end
57
-
55
+
58
56
  def then_word?(word)
59
57
  dialect_word(:then).include?(word)
60
58
  end
61
-
59
+
62
60
  def when_word?(word)
63
61
  dialect_word(:when).include?(word)
64
62
  end
65
-
63
+
66
64
  def dialect_word(word)
67
65
  CukeModeler::Parsing.dialects[dialect][word.to_s].map(&:strip)
68
66
  end
69
-
67
+
70
68
  def dialect
71
69
  @content.feature&.parsing_data&.dig(:language) || 'en'
72
70
  end
73
-
71
+
74
72
  def tags_for(element)
75
- element.tags.map { |tag| tag.name[1..-1] }
73
+ element.tags.map { |tag| tag.name[1..] }
76
74
  end
77
-
78
- def add_issue(message, feature = nil, scenario = nil, item = nil)
75
+
76
+ def add_issue(message, feature = nil, scenario = nil, item = nil)
79
77
  issues << Lint.new(
80
78
  message: message,
81
79
  gherkin_type: type(feature, scenario, item),
@@ -85,17 +83,17 @@ module Chutney
85
83
  step: item&.parsing_data&.dig(:name)
86
84
  ).to_h
87
85
  end
88
-
86
+
89
87
  def location(feature, scenario, step)
90
88
  if step
91
89
  step.parsing_data[:location]
92
90
  elsif scenario
93
91
  scenario.parsing_data.dig(:scenario, :location) || scenario.parsing_data.dig(:background, :location)
94
- else
95
- feature ? feature.parsing_data[:location] : 0
92
+ else
93
+ feature ? feature.parsing_data[:location] : { line: 0, column: 0 }
96
94
  end
97
95
  end
98
-
96
+
99
97
  def type(_feature, scenario, step)
100
98
  if step
101
99
  :step
@@ -111,14 +109,14 @@ module Chutney
111
109
  @content.feature
112
110
  end
113
111
  end
114
-
115
- def elements
112
+
113
+ def elements
116
114
  return [] unless feature
117
-
118
- if block_given?
115
+
116
+ if block_given?
119
117
  feature.children.each do |child|
120
118
  next if off_switch?(child)
121
-
119
+
122
120
  yield(feature, child)
123
121
  end
124
122
  else
@@ -128,56 +126,65 @@ module Chutney
128
126
 
129
127
  def off_switch?(element = feature)
130
128
  off_switch = element.tags
129
+ .map(&:name)
131
130
  .then { |tags| tags || [] }
132
- .filter { |tag| tag[:type] == :Tag }
133
- .filter { |tag| tag[:name] == "@disable#{linter_name}" }
131
+ .filter { |tag_name| tag_name == "@disable#{linter_name}" }
134
132
  .count
135
133
  .positive?
136
134
  off_switch ||= off_switch?(feature) unless element == feature
137
135
  off_switch
138
136
  end
139
-
137
+
140
138
  def background
141
- if block_given?
142
- yield(feature, feature&.background)
139
+ return unless feature&.background
140
+ return if off_switch?(feature)
141
+
142
+ if block_given?
143
+ yield(feature, feature.background)
143
144
  else
144
- feature&.background
145
+ feature.background
145
146
  end
146
147
  end
147
-
148
+
148
149
  def scenarios
149
150
  if block_given?
150
151
  feature&.tests&.each do |test|
152
+ next if off_switch?(test)
153
+
151
154
  yield(feature, test)
152
155
  end
153
-
156
+
154
157
  else
155
158
  feature&.tests
156
159
  end
157
160
  end
158
-
161
+
159
162
  def filled_scenarios
160
163
  if block_given?
161
164
  scenarios do |feature, scenario|
162
165
  next if scenario.steps.empty?
163
-
166
+
164
167
  yield(feature, scenario)
165
168
  end
166
169
  else
167
170
  scenarios ? scenarios.filter { |s| !s.steps.empty? } : []
168
171
  end
169
172
  end
170
-
173
+
171
174
  def steps
172
- feature&.tests&.each do |t|
173
- t.steps.each { |s| yield(feature, t, s) }
175
+ feature&.tests&.each do |test|
176
+ next if off_switch?(test)
177
+
178
+ test.steps.each do |step|
179
+ yield(feature, test, step)
180
+ end
174
181
  end
175
182
  end
176
183
 
177
184
  def self.linter_name
178
185
  name.split('::').last
179
186
  end
180
-
187
+
181
188
  def linter_name
182
189
  self.class.linter_name
183
190
  end
@@ -187,10 +194,10 @@ module Chutney
187
194
  value += render_step_argument(step.block) if step.block
188
195
  value
189
196
  end
190
-
197
+
191
198
  def render_step_argument(argument)
192
199
  return "\n#{argument.content}" if argument.is_a?(CukeModeler::DocString)
193
-
200
+
194
201
  result = argument.rows.map do |row|
195
202
  "|#{row.cells.map(&:value).join '|'}|"
196
203
  end.join "\n"
@@ -2,12 +2,10 @@
2
2
 
3
3
  module Chutney
4
4
  # service class to lint for avoiding periods
5
- class AvoidFullStop < Linter
5
+ class AvoidFullStop < Linter
6
6
  def lint
7
7
  steps do |feature, child, step|
8
-
9
8
  add_issue(I18n.t('linters.avoid_full_stop'), feature, child, step) if step.text.strip.end_with? '.'
10
-
11
9
  end
12
10
  end
13
11
  end
@@ -4,13 +4,13 @@ module Chutney
4
4
  # service class to lint for avoiding outline for single example
5
5
  class AvoidOutlineForSingleExample < Linter
6
6
  def lint
7
- scenarios do |feature, scenario|
7
+ scenarios do |feature, scenario|
8
8
  next unless scenario.is_a? CukeModeler::Outline
9
9
  next unless scenario.examples
10
-
10
+
11
11
  next if scenario.examples.length > 1
12
12
  next if scenario.examples.first.rows.length > 2 # first row is the header
13
-
13
+
14
14
  add_issue(I18n.t('linters.avoid_outline_for_single_example'), feature, scenario)
15
15
  end
16
16
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Chutney
4
4
  # service class to lint for avoid scripting
5
- class AvoidScripting < Linter
5
+ class AvoidScripting < Linter
6
6
  def lint
7
7
  scenarios do |feature, scenario|
8
8
  when_steps = filter_when_steps(scenario.steps)
@@ -10,7 +10,7 @@ module Chutney
10
10
  add_issue(I18n.t('linters.avoid_scripting', count: whens), feature, scenario, when_steps.last) if whens > 1
11
11
  end
12
12
  end
13
-
13
+
14
14
  def filter_when_steps(steps)
15
15
  steps
16
16
  .drop_while { |step| !when_word?(step.keyword) }
@@ -2,14 +2,13 @@
2
2
 
3
3
  module Chutney
4
4
  # service class to lint for background that does more than setup
5
- class BackgroundDoesMoreThanSetup < Linter
5
+ class BackgroundDoesMoreThanSetup < Linter
6
6
  def lint
7
7
  background do |feature, background|
8
-
9
- invalid_steps = background&.steps&.select do |step|
8
+ invalid_steps = background&.steps&.select do |step|
10
9
  when_word?(step.keyword) || then_word?(step.keyword)
11
10
  end
12
-
11
+
13
12
  next if invalid_steps.nil? || invalid_steps.empty?
14
13
 
15
14
  add_issue(I18n.t('linters.background_does_more_than_setup'), feature, background, invalid_steps.first)
@@ -6,7 +6,7 @@ module Chutney
6
6
  # service class for check that there are multiple scenarios once a background is used
7
7
  class BackgroundRequiresMultipleScenarios < Linter
8
8
  MESSAGE = 'Avoid using Background steps for just one scenario'
9
-
9
+
10
10
  def lint
11
11
  background do |feature, background|
12
12
  next unless background
@@ -1,12 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Chutney
4
- # service class to lint for bad scenario names
4
+ # service class to lint for bad scenario names
5
5
  class BadScenarioName < Linter
6
6
  def lint
7
7
  scenarios do |feature, scenario|
8
8
  next if scenario.name.empty?
9
-
9
+
10
10
  bad = /\w*(test|verif|check)\w*/i
11
11
  match = scenario.name.match(bad).to_a.first
12
12
  add_issue(I18n.t('linters.bad_scenario_name', word: match), feature, scenario) if match
@@ -5,11 +5,11 @@ module Chutney
5
5
  class FileNameDiffersFeatureName < Linter
6
6
  def lint
7
7
  return unless feature
8
-
8
+
9
9
  expected_feature_name = title_case(filename)
10
10
  return if ignore_whitespaces(feature.name).casecmp(ignore_whitespaces(expected_feature_name)) == 0
11
-
12
- add_issue(I18n.t('linters.file_name_differs_feature_name', expected: expected_feature_name), feature)
11
+
12
+ add_issue(I18n.t('linters.file_name_differs_feature_name', expected: expected_feature_name), feature)
13
13
  end
14
14
 
15
15
  def title_case(value)
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Chutney
4
- # service class to lint for bad scenario names
4
+ # service class to lint for bad scenario names
5
5
  class GivensAfterBackground < Linter
6
6
  def lint
7
7
  return unless background
8
-
8
+
9
9
  filled_scenarios do |feature, scenario|
10
10
  scenario.steps.each do |step|
11
11
  add_issue(I18n.t('linters.givens_after_background'), feature, scenario, step) if given_word?(step.keyword)
@@ -13,7 +13,7 @@ module Chutney
13
13
  end
14
14
  end
15
15
  end
16
-
16
+
17
17
  def recommend(filename)
18
18
  File.basename(filename, '.*').gsub(/::/, '/')
19
19
  .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
@@ -7,7 +7,7 @@ module Chutney
7
7
  filled_scenarios do |feature, scenario|
8
8
  steps = scenario.steps.select { |step| !and_word?(step.keyword) && !but_word?(step.keyword) }
9
9
  next if steps.empty?
10
-
10
+
11
11
  last_step_is_an_action(feature, scenario, steps)
12
12
  given_after_non_given(feature, scenario, steps)
13
13
  verification_before_action(feature, scenario, steps)
@@ -33,7 +33,7 @@ module Chutney
33
33
  def verification_before_action(feature, scenario, steps)
34
34
  steps.each do |step|
35
35
  break if when_word?(step.keyword)
36
-
36
+
37
37
  add_issue(I18n.t('linters.invalid_step_flow.missing_action'), feature, scenario) if then_word?(step.keyword)
38
38
  end
39
39
  end
@@ -3,7 +3,6 @@
3
3
  module Chutney
4
4
  # service class to lint for missing example names
5
5
  class MissingExampleName < Linter
6
-
7
6
  def lint
8
7
  scenarios do |_feature, scenario|
9
8
  next unless scenario.is_a? CukeModeler::Outline
@@ -11,17 +10,16 @@ module Chutney
11
10
  scenario.examples.each do |example|
12
11
  example_count = scenario.examples&.length || 0
13
12
  next unless example_count > 1
14
-
13
+
15
14
  check_example(scenario, example)
16
15
  end
17
16
  end
18
- end
17
+ end
19
18
 
20
19
  def check_example(scenario, example)
21
20
  name = example.name.strip
22
21
  duplicate_name_count = scenario.examples.filter { |e| e.name == name }.count
23
22
  add_issue(I18n.t('linters.missing_example_name'), feature, scenario, example) if duplicate_name_count >= 2
24
23
  end
25
-
26
24
  end
27
25
  end