chutney 2.2.1 → 3.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/.github/dependabot.yml +16 -0
  3. data/.rubocop.yml +10 -3
  4. data/Gemfile +2 -0
  5. data/LICENSE.txt +1 -1
  6. data/README.md +7 -1
  7. data/Rakefile +8 -8
  8. data/chutney.gemspec +13 -6
  9. data/config/{chutney.yml → chutney_defaults.yml} +2 -0
  10. data/config/cucumber.yml +1 -0
  11. data/docs/index.md +1 -1
  12. data/docs/usage/rules.md +34 -64
  13. data/exe/chutney +23 -3
  14. data/lib/chutney.rb +18 -14
  15. data/lib/chutney/configuration.rb +9 -2
  16. data/lib/chutney/formatter.rb +6 -5
  17. data/lib/chutney/formatter/json_formatter.rb +2 -0
  18. data/lib/chutney/formatter/pie_formatter.rb +10 -13
  19. data/lib/chutney/formatter/rainbow_formatter.rb +13 -13
  20. data/lib/chutney/issue.rb +2 -0
  21. data/lib/chutney/linter.rb +92 -86
  22. data/lib/chutney/linter/avoid_full_stop.rb +4 -4
  23. data/lib/chutney/linter/avoid_outline_for_single_example.rb +7 -5
  24. data/lib/chutney/linter/avoid_scripting.rb +8 -6
  25. data/lib/chutney/linter/avoid_typographers_quotes.rb +16 -14
  26. data/lib/chutney/linter/background_does_more_than_setup.rb +8 -7
  27. data/lib/chutney/linter/background_requires_multiple_scenarios.rb +7 -4
  28. data/lib/chutney/linter/bad_scenario_name.rb +6 -4
  29. data/lib/chutney/linter/empty_feature_file.rb +2 -0
  30. data/lib/chutney/linter/file_name_differs_feature_name.rb +7 -5
  31. data/lib/chutney/linter/givens_after_background.rb +7 -8
  32. data/lib/chutney/linter/invalid_file_name.rb +3 -1
  33. data/lib/chutney/linter/invalid_step_flow.rb +9 -9
  34. data/lib/chutney/linter/missing_example_name.rb +9 -9
  35. data/lib/chutney/linter/missing_feature_description.rb +5 -4
  36. data/lib/chutney/linter/missing_feature_name.rb +5 -4
  37. data/lib/chutney/linter/missing_scenario_name.rb +4 -6
  38. data/lib/chutney/linter/missing_test_action.rb +4 -2
  39. data/lib/chutney/linter/missing_verification.rb +4 -2
  40. data/lib/chutney/linter/required_tags_starts_with.rb +7 -6
  41. data/lib/chutney/linter/same_tag_different_case.rb +37 -0
  42. data/lib/chutney/linter/same_tag_for_all_scenarios.rb +20 -19
  43. data/lib/chutney/linter/scenario_names_match.rb +6 -6
  44. data/lib/chutney/linter/tag_used_multiple_times.rb +3 -1
  45. data/lib/chutney/linter/too_clumsy.rb +4 -2
  46. data/lib/chutney/linter/too_long_step.rb +6 -4
  47. data/lib/chutney/linter/too_many_different_tags.rb +10 -8
  48. data/lib/chutney/linter/too_many_steps.rb +6 -4
  49. data/lib/chutney/linter/too_many_tags.rb +5 -3
  50. data/lib/chutney/linter/unique_scenario_names.rb +5 -5
  51. data/lib/chutney/linter/unknown_variable.rb +15 -15
  52. data/lib/chutney/linter/unused_variable.rb +15 -16
  53. data/lib/chutney/linter/use_background.rb +20 -19
  54. data/lib/chutney/linter/use_outline.rb +15 -14
  55. data/lib/chutney/version.rb +3 -1
  56. data/lib/config/locales/en.yml +3 -0
  57. data/spec/chutney_spec.rb +11 -9
  58. data/spec/spec_helper.rb +2 -0
  59. metadata +21 -16
@@ -1,5 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'delegate'
2
- module Chutney
4
+
5
+ module Chutney
3
6
  # gherkin_lint configuration object
4
7
  class Configuration < SimpleDelegator
5
8
  def initialize(path)
@@ -18,7 +21,11 @@ module Chutney
18
21
  end
19
22
 
20
23
  def load_user_configuration
21
- config_file = Dir.glob(File.join(Dir.pwd, '**', '.chutney.yml')).first
24
+ config_files = ['chutney.yml', '.chutney.yml'].map do |fname|
25
+ Dir.glob(File.join(Dir.pwd, '**', fname))
26
+ end.flatten
27
+
28
+ config_file = config_files.first
22
29
  merge_config(config_file) if !config_file.nil? && File.exist?(config_file)
23
30
  end
24
31
 
@@ -1,19 +1,20 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Chutney
2
4
  # base class for all formatters
3
5
  class Formatter
4
6
  attr_accessor :results
5
-
7
+
6
8
  def initialize
7
9
  @results = {}
8
- end
9
-
10
+ end
11
+
10
12
  def files
11
13
  results.map { |k, _v| k }
12
14
  end
13
-
15
+
14
16
  def files_with_issues
15
17
  results.filter { |_k, v| v.any? { |r| r[:issues].count.positive? } }
16
18
  end
17
-
18
19
  end
19
20
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Chutney
2
4
  # Plain old JSON formatter
3
5
  class JSONFormatter < Formatter
@@ -1,13 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'pastel'
2
4
  require 'tty/pie'
3
5
 
4
6
  module Chutney
5
7
  # format results as pie charts
6
8
  class PieFormatter < Formatter
7
- def initialize
8
- super
9
- end
10
-
11
9
  def format
12
10
  data = top_offences.map do |offence|
13
11
  {
@@ -19,14 +17,14 @@ module Chutney
19
17
  end
20
18
  print_report(data)
21
19
  end
22
-
20
+
23
21
  def print_report(data)
24
22
  return if data.empty?
25
23
 
26
24
  print TTY::Pie.new(data: data, radius: 8, legend: { format: '%<label>s %<name>s %<value>i' })
27
25
  puts
28
26
  end
29
-
27
+
30
28
  def top_offences
31
29
  offence = Hash.new(0)
32
30
  files_with_issues.each do |_file, linter|
@@ -36,9 +34,9 @@ module Chutney
36
34
  end
37
35
  offence.reject { |_k, v| v.zero? }.sort_by { |_linter, count| -count }
38
36
  end
39
-
37
+
40
38
  def char_loop
41
- @char_looper ||= Fiber.new do
39
+ @char_looper ||= Fiber.new do
42
40
  chars = %w[• x + @ * / -]
43
41
  current = 0
44
42
  loop do
@@ -49,10 +47,10 @@ module Chutney
49
47
  end
50
48
  @char_looper.resume
51
49
  end
52
-
50
+
53
51
  def colour_loop
54
- @colour_looper ||= Fiber.new do
55
- colours = %i[bright_cyan bright_magenta bright_yellow bright_green]
52
+ @colour_looper ||= Fiber.new do
53
+ colours = %i[bright_cyan bright_magenta bright_yellow bright_green]
56
54
  current = 0
57
55
  loop do
58
56
  current = 0 if current >= colours.count
@@ -62,7 +60,7 @@ module Chutney
62
60
  end
63
61
  @colour_looper.resume
64
62
  end
65
-
63
+
66
64
  def put_summary
67
65
  pastel = Pastel.new
68
66
  print "#{files.count} features inspected, "
@@ -72,6 +70,5 @@ module Chutney
72
70
  puts pastel.red("#{files_with_issues.count} taste nasty")
73
71
  end
74
72
  end
75
-
76
73
  end
77
74
  end
@@ -1,39 +1,40 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'pastel'
2
4
 
3
5
  module Chutney
4
6
  # pretty formatter
5
7
  class RainbowFormatter < Formatter
6
-
7
8
  def initialize
8
9
  super
9
-
10
+
10
11
  @pastel = Pastel.new
11
12
  end
12
-
13
- def format
13
+
14
+ def format
14
15
  files_with_issues.each do |file, linter|
15
16
  put_file(file)
16
17
  linter.filter { |l| !l[:issues].empty? }.each do |linter_with_issues|
17
-
18
18
  put_linter(linter_with_issues)
19
- linter_with_issues[:issues].each { |i| put_issue(i) }
19
+ linter_with_issues[:issues].each { |i| put_issue(file, i) }
20
20
  end
21
21
  end
22
22
  put_summary
23
23
  end
24
-
24
+
25
25
  def put_file(file)
26
26
  puts @pastel.cyan(file.to_s)
27
27
  end
28
-
28
+
29
29
  def put_linter(linter)
30
30
  puts @pastel.red(" #{linter[:linter]}")
31
31
  end
32
-
33
- def put_issue(issue)
34
- 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))}"
35
36
  end
36
-
37
+
37
38
  def put_summary
38
39
  print "#{files.count} features inspected, "
39
40
  if files_with_issues.count.zero?
@@ -42,6 +43,5 @@ module Chutney
42
43
  puts @pastel.red("#{files_with_issues.count} taste nasty")
43
44
  end
44
45
  end
45
-
46
46
  end
47
47
  end
data/lib/chutney/issue.rb CHANGED
@@ -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,77 +19,81 @@ 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)
23
22
  end
24
23
 
25
24
  def lint
26
25
  raise 'not implemented'
27
26
  end
28
-
27
+
29
28
  def and_word?(word)
30
- @dialect.and_keywords.include?(word)
29
+ dialect_word(:and).include?(word)
31
30
  end
32
-
31
+
33
32
  def background_word?(word)
34
- @dialect.background_keywords.include?(word)
33
+ dialect_word(:background).include?(word)
35
34
  end
36
-
35
+
37
36
  def but_word?(word)
38
- @dialect.but_keywords.include?(word)
37
+ dialect_word(:but).include?(word)
39
38
  end
40
-
39
+
41
40
  def examples_word?(word)
42
- @dialect.example_keywords.include?(word)
41
+ dialect_word(:examples).include?(word)
43
42
  end
44
-
43
+
45
44
  def feature_word?(word)
46
- @dialect.feature_keywords.include?(word)
45
+ dialect_word(:feature).include?(word)
47
46
  end
48
-
47
+
49
48
  def given_word?(word)
50
- @dialect.given_keywords.include?(word)
49
+ dialect_word(:given).include?(word)
51
50
  end
52
-
51
+
53
52
  def scenario_outline_word?(word)
54
- @dialect.scenario_outline_keywords.include?(word)
53
+ dialect_word(:scenarioOutline).include?(word)
55
54
  end
56
-
55
+
57
56
  def then_word?(word)
58
- @dialect.then_keywords.include?(word)
57
+ dialect_word(:then).include?(word)
59
58
  end
60
-
59
+
61
60
  def when_word?(word)
62
- @dialect.when_keywords.include?(word)
61
+ dialect_word(:when).include?(word)
62
+ end
63
+
64
+ def dialect_word(word)
65
+ CukeModeler::Parsing.dialects[dialect][word.to_s].map(&:strip)
66
+ end
67
+
68
+ def dialect
69
+ @content.feature&.parsing_data&.dig(:language) || 'en'
63
70
  end
64
-
65
- def tags_for(element)
66
- return [] unless element.include? :tags
67
71
 
68
- element[:tags].map { |tag| tag[:name][1..-1] }
72
+ def tags_for(element)
73
+ element.tags.map { |tag| tag.name[1..] }
69
74
  end
70
-
71
- def add_issue(message, feature = nil, scenario = nil, step = nil)
75
+
76
+ def add_issue(message, feature = nil, scenario = nil, item = nil)
72
77
  issues << Lint.new(
73
78
  message: message,
74
- gherkin_type: type(feature, scenario, step),
75
- location: location(feature, scenario, step),
76
- feature: feature ? feature[:name] : nil,
77
- scenario: scenario ? scenario[:name] : nil,
78
- step: step ? step[:text] : nil
79
+ gherkin_type: type(feature, scenario, item),
80
+ location: location(feature, scenario, item),
81
+ feature: feature&.name,
82
+ scenario: scenario&.name,
83
+ step: item&.parsing_data&.dig(:name)
79
84
  ).to_h
80
85
  end
81
-
86
+
82
87
  def location(feature, scenario, step)
83
88
  if step
84
- step[:location]
89
+ step.parsing_data[:location]
85
90
  elsif scenario
86
- scenario[:location]
87
- else
88
- feature ? feature[:location] : 0
91
+ scenario.parsing_data.dig(:scenario, :location) || scenario.parsing_data.dig(:background, :location)
92
+ else
93
+ feature ? feature.parsing_data[:location] : { line: 0, column: 0 }
89
94
  end
90
95
  end
91
-
96
+
92
97
  def type(_feature, scenario, step)
93
98
  if step
94
99
  :step
@@ -99,101 +104,102 @@ module Chutney
99
104
 
100
105
  def feature
101
106
  if block_given?
102
- yield(@content[:feature]) if @content[:feature]
107
+ yield(@content.feature) if @content.feature
103
108
  else
104
- @content[:feature]
109
+ @content.feature
105
110
  end
106
111
  end
107
-
108
- def elements
112
+
113
+ def elements
109
114
  return [] unless feature
110
-
111
- if block_given?
112
- feature[:children].each do |child|
115
+
116
+ if block_given?
117
+ feature.children.each do |child|
113
118
  next if off_switch?(child)
114
-
119
+
115
120
  yield(feature, child)
116
121
  end
117
122
  else
118
- feature[:children]
123
+ feature.children
119
124
  end
120
125
  end
121
126
 
122
127
  def off_switch?(element = feature)
123
- off_switch = element[:tags]
124
- .then { |tags| tags || [] }
125
- .filter { |tag| tag[:type] == :Tag }
126
- .filter { |tag| tag[:name] == "@disable#{linter_name}" }
127
- .count
128
- .positive?
128
+ off_switch = element.tags
129
+ .map(&:name)
130
+ .then { |tags| tags || [] }
131
+ .filter { |tag_name| tag_name == "@disable#{linter_name}" }
132
+ .count
133
+ .positive?
129
134
  off_switch ||= off_switch?(feature) unless element == feature
130
135
  off_switch
131
136
  end
132
-
137
+
133
138
  def background
139
+ return unless feature&.background
140
+ return if off_switch?(feature)
141
+
134
142
  if block_given?
135
- elements do |feature, child|
136
- next unless child[:type] == :Background
137
-
138
- yield(feature, child)
139
- end
143
+ yield(feature, feature.background)
140
144
  else
141
- elements.filter { |child| child[:type] == :Background }
145
+ feature.background
142
146
  end
143
147
  end
144
-
148
+
145
149
  def scenarios
146
150
  if block_given?
147
- elements do |feature, child|
148
- next unless %i[ScenarioOutline Scenario].include? child[:type]
149
-
150
- yield(feature, child)
151
+ feature&.tests&.each do |test|
152
+ next if off_switch?(test)
153
+
154
+ yield(feature, test)
151
155
  end
156
+
152
157
  else
153
- elements.filter { |child| %i[ScenarioOutline Scenario].include? child[:type] }
158
+ feature&.tests
154
159
  end
155
160
  end
156
-
161
+
157
162
  def filled_scenarios
158
163
  if block_given?
159
164
  scenarios do |feature, scenario|
160
- next unless scenario.include? :steps
161
- next if scenario[:steps].empty?
162
-
165
+ next if scenario.steps.empty?
166
+
163
167
  yield(feature, scenario)
164
168
  end
165
169
  else
166
- scenarios.filter { |s| !s[:steps].empty? }
170
+ scenarios ? scenarios.filter { |s| !s.steps.empty? } : []
167
171
  end
168
172
  end
169
-
173
+
170
174
  def steps
171
- elements do |feature, child|
172
- next unless child.include? :steps
173
-
174
- child[:steps].each { |step| yield(feature, child, step) }
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
175
181
  end
176
182
  end
177
183
 
178
184
  def self.linter_name
179
185
  name.split('::').last
180
186
  end
181
-
187
+
182
188
  def linter_name
183
189
  self.class.linter_name
184
190
  end
185
191
 
186
192
  def render_step(step)
187
- value = "#{step[:keyword]}#{step[:text]}"
188
- value += render_step_argument step[:argument] if step.include? :argument
193
+ value = "#{step.keyword} #{step.text}"
194
+ value += render_step_argument(step.block) if step.block
189
195
  value
190
196
  end
191
-
197
+
192
198
  def render_step_argument(argument)
193
- return "\n#{argument[:content]}" if argument[:type] == :DocString
194
-
195
- result = argument[:rows].map do |row|
196
- "|#{row[:cells].map { |cell| cell[:value] }.join '|'}|"
199
+ return "\n#{argument.content}" if argument.is_a?(CukeModeler::DocString)
200
+
201
+ result = argument.rows.map do |row|
202
+ "|#{row.cells.map(&:value).join '|'}|"
197
203
  end.join "\n"
198
204
  "\n#{result}"
199
205
  end