chutney 2.2.1 → 3.0.0.beta.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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +7 -1
  3. data/Gemfile +2 -0
  4. data/Rakefile +2 -0
  5. data/chutney.gemspec +8 -4
  6. data/config/cucumber.yml +1 -0
  7. data/exe/chutney +2 -0
  8. data/lib/chutney.rb +9 -11
  9. data/lib/chutney/configuration.rb +2 -0
  10. data/lib/chutney/formatter.rb +2 -0
  11. data/lib/chutney/formatter/json_formatter.rb +2 -0
  12. data/lib/chutney/formatter/pie_formatter.rb +2 -0
  13. data/lib/chutney/formatter/rainbow_formatter.rb +2 -0
  14. data/lib/chutney/issue.rb +2 -0
  15. data/lib/chutney/linter.rb +58 -59
  16. data/lib/chutney/linter/avoid_full_stop.rb +3 -1
  17. data/lib/chutney/linter/avoid_outline_for_single_example.rb +8 -6
  18. data/lib/chutney/linter/avoid_scripting.rb +6 -4
  19. data/lib/chutney/linter/avoid_typographers_quotes.rb +16 -14
  20. data/lib/chutney/linter/background_does_more_than_setup.rb +8 -6
  21. data/lib/chutney/linter/background_requires_multiple_scenarios.rb +6 -3
  22. data/lib/chutney/linter/bad_scenario_name.rb +4 -2
  23. data/lib/chutney/linter/empty_feature_file.rb +2 -0
  24. data/lib/chutney/linter/file_name_differs_feature_name.rb +4 -2
  25. data/lib/chutney/linter/givens_after_background.rb +5 -6
  26. data/lib/chutney/linter/invalid_file_name.rb +2 -0
  27. data/lib/chutney/linter/invalid_step_flow.rb +7 -7
  28. data/lib/chutney/linter/missing_example_name.rb +7 -5
  29. data/lib/chutney/linter/missing_feature_description.rb +4 -3
  30. data/lib/chutney/linter/missing_feature_name.rb +3 -2
  31. data/lib/chutney/linter/missing_scenario_name.rb +3 -4
  32. data/lib/chutney/linter/missing_test_action.rb +3 -1
  33. data/lib/chutney/linter/missing_verification.rb +3 -1
  34. data/lib/chutney/linter/required_tags_starts_with.rb +2 -0
  35. data/lib/chutney/linter/same_tag_for_all_scenarios.rb +11 -10
  36. data/lib/chutney/linter/scenario_names_match.rb +4 -3
  37. data/lib/chutney/linter/tag_used_multiple_times.rb +2 -0
  38. data/lib/chutney/linter/too_clumsy.rb +3 -1
  39. data/lib/chutney/linter/too_long_step.rb +4 -2
  40. data/lib/chutney/linter/too_many_different_tags.rb +4 -2
  41. data/lib/chutney/linter/too_many_steps.rb +4 -2
  42. data/lib/chutney/linter/too_many_tags.rb +2 -0
  43. data/lib/chutney/linter/unique_scenario_names.rb +3 -3
  44. data/lib/chutney/linter/unknown_variable.rb +13 -13
  45. data/lib/chutney/linter/unused_variable.rb +13 -13
  46. data/lib/chutney/linter/use_background.rb +17 -16
  47. data/lib/chutney/linter/use_outline.rb +8 -7
  48. data/lib/chutney/version.rb +3 -1
  49. data/spec/chutney_spec.rb +2 -0
  50. data/spec/spec_helper.rb +2 -0
  51. metadata +33 -12
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ad43c930f710e8cac21960ff14f31da2fb4fe373ed2153fa3ad238b317ebaedd
4
- data.tar.gz: 84e96dd0222e18b3ba9f5235e81598accc0e254f1997211c69d7b6fb19532957
3
+ metadata.gz: 3eb2bbfa506f0fde529c0365064ffe21e6f102e37d3790e138e09c972c07319d
4
+ data.tar.gz: 8951672afdbf61863d949ebce24862f203395a931228bd98a0fe1d401ad1544f
5
5
  SHA512:
6
- metadata.gz: 7d26227a05a2087610a64557529f85b2b26afca8b3c273a63203710a65b8640497f7bf5f1aeb19c7572af2b2d4f2487b3e1eb6cf372c7f53bf00a526acd35260
7
- data.tar.gz: a1f32beace478affe4d3ce7a270f9bd411567b9adb421aef0279b495824cdb35236915f7ea8f388b91d826a62a4cb0040041847694679653d4ebc9e0cb7bb665
6
+ metadata.gz: d41e8f3694940cb023fd03192b973a3778366e2745b544e8003f8d2f02c65285de6c7e9b8e254e89ce714732474c0837db8487da0eefbaba77ed405a02cf4f57
7
+ data.tar.gz: 1a3f433eb45190171bad88ca533e65f533f59ff5d2c2654a3da41b4f585b3abab3a6814824b0925ea0973ba76e331ce2bb9ab9345cd2f73ae1d242f64641eaff
@@ -8,7 +8,7 @@
8
8
 
9
9
  # Offense count: 1
10
10
  Metrics/AbcSize:
11
- Max: 16
11
+ Enabled: false
12
12
 
13
13
  # Offense count: 1
14
14
  # Configuration parameters: CountComments.
@@ -55,4 +55,10 @@ Layout/TrailingWhitespace:
55
55
  Enabled: false
56
56
 
57
57
  Style/FrozenStringLiteralComment:
58
+ Enabled: true
59
+
60
+ Style/StringConcatenation:
58
61
  Enabled: false
62
+
63
+ AllCops:
64
+ NewCops: enable
data/Gemfile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source 'https://rubygems.org'
2
4
 
3
5
  gemspec
data/Rakefile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rake/testtask'
2
4
 
3
5
  task default: :build
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Disable rubocop checks for the .gemspec
2
4
  # I'll take the output from 'bundle gem new' to be authoritative
3
5
  # rubocop:disable all
@@ -12,9 +14,9 @@ Gem::Specification.new do |spec|
12
14
  spec.authors = ['Nigel Brookes-Thomas', 'Stefan Rohe', 'Nishtha Argawal', 'John Gluck']
13
15
  spec.email = ['nigel@brookes-thomas.co.uk']
14
16
 
15
- spec.summary = 'A linter for English language Gherkin'
17
+ spec.summary = 'A linter for multi-lingual Gherkin'
16
18
  spec.description = 'A linter for your Cucumber features. ' \
17
- 'It supports any spoken language Cucumber v3 supports.'
19
+ 'It supports any spoken language Cucumber supports.'
18
20
 
19
21
  spec.homepage = 'https://billyruffian.github.io/chutney/'
20
22
  spec.license = 'MIT'
@@ -43,14 +45,15 @@ Gem::Specification.new do |spec|
43
45
  spec.require_paths = ['lib']
44
46
 
45
47
  spec.add_runtime_dependency 'amatch', '~> 0.4.0'
46
- spec.add_runtime_dependency 'gherkin', '~> 5.1.0'
48
+ spec.add_runtime_dependency 'cuke_modeler', '~> 3.3'
49
+ spec.add_runtime_dependency 'gherkin', '>= 5.1.0', '< 9.1'
47
50
  spec.add_runtime_dependency 'i18n', '~> 1.8.2'
48
51
  spec.add_runtime_dependency 'pastel', '~> 0.7'
49
52
  spec.add_runtime_dependency 'tty-pie', '~> 0.3'
50
53
 
51
54
 
52
55
  spec.add_development_dependency 'coveralls', '~> 0.8'
53
- spec.add_development_dependency 'cucumber', '~> 3.0'
56
+ spec.add_development_dependency 'cucumber', '~> 5.1'
54
57
  spec.add_development_dependency 'pry-byebug', '~> 3.0'
55
58
  spec.add_development_dependency 'rake', '~> 13.0'
56
59
  spec.add_development_dependency 'rerun', '~> 0.13'
@@ -58,4 +61,5 @@ Gem::Specification.new do |spec|
58
61
  spec.add_development_dependency 'rubocop', '~> 0.89.0'
59
62
  spec.add_development_dependency 'rspec', '~> 3.8'
60
63
 
64
+ spec.required_ruby_version = '~> 2.6'
61
65
  end
@@ -0,0 +1 @@
1
+ default: --publish-quiet
@@ -1,4 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
2
4
  require 'chutney'
3
5
  require 'chutney/formatter'
4
6
  require 'chutney/formatter/json_formatter'
@@ -1,4 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'amatch'
4
+
2
5
  require 'chutney/configuration'
3
6
  require 'chutney/linter'
4
7
  require 'chutney/linter/avoid_full_stop'
@@ -33,9 +36,11 @@ require 'chutney/linter/unknown_variable'
33
36
  require 'chutney/linter/unused_variable'
34
37
  require 'chutney/linter/use_background'
35
38
  require 'chutney/linter/use_outline'
39
+
40
+ require 'cuke_modeler'
36
41
  require 'forwardable'
37
- require 'gherkin/dialect'
38
- require 'gherkin/parser'
42
+ # require 'gherkin/dialect'
43
+ # require 'gherkin/parser'
39
44
  require 'i18n'
40
45
  require 'set'
41
46
  require 'yaml'
@@ -45,8 +50,7 @@ module Chutney
45
50
  class ChutneyLint
46
51
  extend Forwardable
47
52
  attr_accessor :verbose
48
- attr_reader :files
49
- attr_reader :results
53
+ attr_reader :files, :results
50
54
 
51
55
  def_delegators :@files, :<<, :clear, :delete, :include?
52
56
 
@@ -92,14 +96,8 @@ module Chutney
92
96
 
93
97
  private
94
98
 
95
- def parse(text)
96
- @parser ||= Gherkin::Parser.new
97
- scanner = Gherkin::TokenScanner.new(text)
98
- @parser.parse(scanner)
99
- end
100
-
101
99
  def lint(file)
102
- parsed = parse(File.read(file))
100
+ parsed = CukeModeler::FeatureFile.new(file)
103
101
  linters.each do |linter_class|
104
102
  linter = linter_class.new(file, parsed, configuration[linter_class.linter_name])
105
103
  linter.lint
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'delegate'
2
4
  module Chutney
3
5
  # gherkin_lint configuration object
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Chutney
2
4
  # base class for all formatters
3
5
  class Formatter
@@ -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,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'pastel'
2
4
  require 'tty/pie'
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'pastel'
2
4
 
3
5
  module Chutney
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'term/ansicolor'
2
4
 
3
5
  module Chutney
@@ -1,11 +1,12 @@
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
+ attr_reader :filename, :configuration
9
10
 
10
11
  Lint = Struct.new(:message, :gherkin_type, :location, :feature, :scenario, :step, keyword_init: true)
11
12
 
@@ -18,8 +19,8 @@ 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
@@ -27,65 +28,71 @@ module Chutney
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
73
 
65
74
  def tags_for(element)
66
- return [] unless element.include? :tags
67
-
68
- element[:tags].map { |tag| tag[:name][1..-1] }
75
+ element.tags.map { |tag| tag.name[1..-1] }
69
76
  end
70
77
 
71
- def add_issue(message, feature = nil, scenario = nil, step = nil)
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 ? feature[:name] : nil,
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]
85
92
  elsif scenario
86
- scenario[:location]
93
+ scenario.parsing_data.dig(:scenario, :location) || scenario.parsing_data.dig(:background, :location)
87
94
  else
88
- feature ? feature[:location] : 0
95
+ feature ? feature.parsing_data[:location] : 0
89
96
  end
90
97
  end
91
98
 
@@ -99,9 +106,9 @@ module Chutney
99
106
 
100
107
  def feature
101
108
  if block_given?
102
- yield(@content[:feature]) if @content[:feature]
109
+ yield(@content.feature) if @content.feature
103
110
  else
104
- @content[:feature]
111
+ @content.feature
105
112
  end
106
113
  end
107
114
 
@@ -109,69 +116,61 @@ module Chutney
109
116
  return [] unless feature
110
117
 
111
118
  if block_given?
112
- feature[:children].each do |child|
119
+ feature.children.each do |child|
113
120
  next if off_switch?(child)
114
121
 
115
122
  yield(feature, child)
116
123
  end
117
124
  else
118
- feature[:children]
125
+ feature.children
119
126
  end
120
127
  end
121
128
 
122
129
  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?
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?
129
136
  off_switch ||= off_switch?(feature) unless element == feature
130
137
  off_switch
131
138
  end
132
139
 
133
140
  def background
134
- if block_given?
135
- elements do |feature, child|
136
- next unless child[:type] == :Background
137
-
138
- yield(feature, child)
139
- end
141
+ if block_given?
142
+ yield(feature, feature&.background)
140
143
  else
141
- elements.filter { |child| child[:type] == :Background }
144
+ feature&.background
142
145
  end
143
146
  end
144
147
 
145
148
  def scenarios
146
149
  if block_given?
147
- elements do |feature, child|
148
- next unless %i[ScenarioOutline Scenario].include? child[:type]
149
-
150
- yield(feature, child)
150
+ feature&.tests&.each do |test|
151
+ yield(feature, test)
151
152
  end
153
+
152
154
  else
153
- elements.filter { |child| %i[ScenarioOutline Scenario].include? child[:type] }
155
+ feature&.tests
154
156
  end
155
157
  end
156
158
 
157
159
  def filled_scenarios
158
160
  if block_given?
159
161
  scenarios do |feature, scenario|
160
- next unless scenario.include? :steps
161
- next if scenario[:steps].empty?
162
+ next if scenario.steps.empty?
162
163
 
163
164
  yield(feature, scenario)
164
165
  end
165
166
  else
166
- scenarios.filter { |s| !s[:steps].empty? }
167
+ scenarios ? scenarios.filter { |s| !s.steps.empty? } : []
167
168
  end
168
169
  end
169
170
 
170
171
  def steps
171
- elements do |feature, child|
172
- next unless child.include? :steps
173
-
174
- child[:steps].each { |step| yield(feature, child, step) }
172
+ feature&.tests&.each do |t|
173
+ t.steps.each { |s| yield(feature, t, s) }
175
174
  end
176
175
  end
177
176
 
@@ -184,16 +183,16 @@ module Chutney
184
183
  end
185
184
 
186
185
  def render_step(step)
187
- value = "#{step[:keyword]}#{step[:text]}"
188
- 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
189
188
  value
190
189
  end
191
190
 
192
191
  def render_step_argument(argument)
193
- return "\n#{argument[:content]}" if argument[:type] == :DocString
192
+ return "\n#{argument.content}" if argument.is_a?(CukeModeler::DocString)
194
193
 
195
- result = argument[:rows].map do |row|
196
- "|#{row[:cells].map { |cell| cell[:value] }.join '|'}|"
194
+ result = argument.rows.map do |row|
195
+ "|#{row.cells.map(&:value).join '|'}|"
197
196
  end.join "\n"
198
197
  "\n#{result}"
199
198
  end
@@ -1,10 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Chutney
2
4
  # service class to lint for avoiding periods
3
5
  class AvoidFullStop < Linter
4
6
  def lint
5
7
  steps do |feature, child, step|
6
8
 
7
- add_issue(I18n.t('linters.avoid_full_stop'), feature, child, step) if step[:text].strip.end_with? '.'
9
+ add_issue(I18n.t('linters.avoid_full_stop'), feature, child, step) if step.text.strip.end_with? '.'
8
10
 
9
11
  end
10
12
  end
@@ -1,13 +1,15 @@
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
- scenarios do |feature, scenario|
6
- next unless scenario[:type] == :ScenarioOutline
7
-
8
- next unless scenario.key? :examples
9
- next if scenario[:examples].length > 1
10
- next if scenario[:examples].first[:tableBody].length > 1
7
+ scenarios do |feature, scenario|
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
11
13
 
12
14
  add_issue(I18n.t('linters.avoid_outline_for_single_example'), feature, scenario)
13
15
  end