chutney 2.2.1 → 3.0.0.beta.1

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