chutney 3.0.0.beta.1 → 3.1.1

Sign up to get free protection for your applications and to get access to all the features.
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