gherkin_lint 0.6.3 → 1.0.0

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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +4 -0
  3. data/.rubocop.yml +19 -1
  4. data/Dockerfile +2 -0
  5. data/Gemfile +3 -2
  6. data/README.md +24 -2
  7. data/Rakefile +3 -3
  8. data/bin/gherkin_lint +10 -2
  9. data/config/default.yml +58 -0
  10. data/features/avoid_outline_for_single_example.feature +2 -1
  11. data/features/avoid_period.feature +1 -1
  12. data/features/avoid_scripting.feature +1 -1
  13. data/features/background_does_more_than_setup.feature +1 -1
  14. data/features/background_requires_scenario.feature +1 -1
  15. data/features/bad_scenario_name.feature +1 -1
  16. data/features/be_declarative.feature +1 -1
  17. data/features/disable_tags.feature +1 -2
  18. data/features/file_name_differs_feature_name.feature +2 -2
  19. data/features/invalid_file_name.feature +1 -1
  20. data/features/invalid_step_flow.feature +1 -1
  21. data/features/missing_example_name.feature +1 -2
  22. data/features/missing_feature_description.feature +1 -1
  23. data/features/missing_feature_name.feature +1 -1
  24. data/features/missing_scenario_name.feature +1 -1
  25. data/features/missing_test_action.feature +1 -2
  26. data/features/missing_verification.feature +1 -2
  27. data/features/precedence.feature +72 -0
  28. data/features/required_tags_starts_with.feature +95 -0
  29. data/features/same_tag_for_all_scenarios.feature +1 -5
  30. data/features/support/env.rb +72 -0
  31. data/features/tag_used_multiple_times.feature +1 -1
  32. data/features/too_clumsy.feature +1 -1
  33. data/features/too_long_step.feature +1 -1
  34. data/features/too_many_different_tags.feature +1 -1
  35. data/features/too_many_steps.feature +1 -1
  36. data/features/too_many_tags.feature +1 -1
  37. data/features/unique_scenario_names.feature +1 -1
  38. data/features/unknown_variable.feature +1 -1
  39. data/features/unused_variable.feature +1 -1
  40. data/features/use_background.feature +1 -2
  41. data/features/use_outline.feature +1 -2
  42. data/gherkin_lint.gemspec +3 -3
  43. data/lib/gherkin_lint.rb +44 -51
  44. data/lib/gherkin_lint/configuration.rb +32 -0
  45. data/lib/gherkin_lint/issue.rb +1 -1
  46. data/lib/gherkin_lint/linter.rb +6 -10
  47. data/lib/gherkin_lint/linter/avoid_scripting.rb +1 -1
  48. data/lib/gherkin_lint/linter/background_requires_multiple_scenarios.rb +1 -1
  49. data/lib/gherkin_lint/linter/bad_scenario_name.rb +1 -1
  50. data/lib/gherkin_lint/linter/be_declarative.rb +10 -8
  51. data/lib/gherkin_lint/linter/required_tags_starts_with.rb +16 -0
  52. data/lib/gherkin_lint/linter/tag_collector.rb +9 -0
  53. data/lib/gherkin_lint/linter/tag_constraint.rb +32 -0
  54. data/lib/gherkin_lint/linter/too_many_different_tags.rb +3 -0
  55. data/lib/gherkin_lint/linter/too_many_tags.rb +3 -0
  56. data/spec/configuration_spec.rb +58 -0
  57. data/spec/gherkin_lint_spec.rb +68 -0
  58. data/spec/required_tags_starts_with_spec.rb +74 -0
  59. data/spec/shared_contexts/file_exists.rb +12 -0
  60. data/spec/shared_contexts/gherkin_linter.rb +14 -0
  61. metadata +18 -3
@@ -11,6 +11,7 @@ Feature: Unique Scenario Names
11
11
 
12
12
  linter = GherkinLint::GherkinLint.new
13
13
  linter.enable %w(UniqueScenarioNames)
14
+ linter.set_linter
14
15
  linter.analyze 'lint.feature'
15
16
  exit linter.report
16
17
 
@@ -42,5 +43,4 @@ Feature: Unique Scenario Names
42
43
  When I run `ruby lint.rb`
43
44
  Then it should pass with exactly:
44
45
  """
45
-
46
46
  """
@@ -11,6 +11,7 @@ Feature: Unknown Variable
11
11
 
12
12
  linter = GherkinLint::GherkinLint.new
13
13
  linter.enable %w(UnknownVariable)
14
+ linter.set_linter
14
15
  linter.analyze 'lint.feature'
15
16
  exit linter.report
16
17
 
@@ -115,5 +116,4 @@ Feature: Unknown Variable
115
116
  When I run `ruby lint.rb`
116
117
  Then it should pass with exactly:
117
118
  """
118
-
119
119
  """
@@ -11,6 +11,7 @@ Feature: Unused Variable
11
11
 
12
12
  linter = GherkinLint::GherkinLint.new
13
13
  linter.enable %w(UnusedVariable)
14
+ linter.set_linter
14
15
  linter.analyze 'lint.feature'
15
16
  exit linter.report
16
17
 
@@ -98,5 +99,4 @@ Feature: Unused Variable
98
99
  When I run `ruby lint.rb`
99
100
  Then it should pass with exactly:
100
101
  """
101
-
102
102
  """
@@ -11,6 +11,7 @@ Feature: Use Background
11
11
 
12
12
  linter = GherkinLint::GherkinLint.new
13
13
  linter.enable %w(UseBackground)
14
+ linter.set_linter
14
15
  linter.analyze 'lint.feature'
15
16
  exit linter.report
16
17
 
@@ -85,7 +86,6 @@ Feature: Use Background
85
86
  When I run `ruby lint.rb`
86
87
  Then it should pass with exactly:
87
88
  """
88
-
89
89
  """
90
90
 
91
91
  Scenario: Valid Single Scenario
@@ -105,5 +105,4 @@ Feature: Use Background
105
105
  When I run `ruby lint.rb`
106
106
  Then it should pass with exactly:
107
107
  """
108
-
109
108
  """
@@ -11,6 +11,7 @@ Feature: Use Outline
11
11
 
12
12
  linter = GherkinLint::GherkinLint.new
13
13
  linter.enable %w(UseOutline)
14
+ linter.set_linter
14
15
  linter.analyze 'lint.feature'
15
16
  exit linter.report
16
17
 
@@ -54,7 +55,6 @@ Feature: Use Outline
54
55
  When I run `ruby lint.rb`
55
56
  Then it should pass with exactly:
56
57
  """
57
-
58
58
  """
59
59
 
60
60
  Scenario: Valid Example
@@ -75,5 +75,4 @@ Feature: Use Outline
75
75
  When I run `ruby lint.rb`
76
76
  Then it should pass with exactly:
77
77
  """
78
-
79
78
  """
@@ -1,10 +1,10 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = 'gherkin_lint'
3
- s.version = '0.6.3'
4
- s.date = '2017-03-12'
3
+ s.version = '1.0.0'
4
+ s.date = '2017-08-25'
5
5
  s.summary = 'Gherkin Lint'
6
6
  s.description = 'Lint Gherkin Files'
7
- s.authors = ['Stefan Rohe']
7
+ s.authors = ['Stefan Rohe', 'Nishtha Argawal', 'John Gluck']
8
8
  s.homepage = 'http://github.com/funkwerk/gherkin_lint/'
9
9
  s.files = `git ls-files`.split("\n")
10
10
  s.executables = s.files.grep(%r{^bin/}) { |file| File.basename(file) }
@@ -1,6 +1,7 @@
1
1
  gem 'gherkin', '>=4.0.0'
2
2
 
3
3
  require 'gherkin/parser'
4
+ require 'gherkin_lint/linter'
4
5
  require 'gherkin_lint/linter/avoid_outline_for_single_example'
5
6
  require 'gherkin_lint/linter/avoid_period'
6
7
  require 'gherkin_lint/linter/avoid_scripting'
@@ -17,6 +18,7 @@ require 'gherkin_lint/linter/missing_feature_name'
17
18
  require 'gherkin_lint/linter/missing_scenario_name'
18
19
  require 'gherkin_lint/linter/missing_test_action'
19
20
  require 'gherkin_lint/linter/missing_verification'
21
+ require 'gherkin_lint/linter/required_tags_starts_with'
20
22
  require 'gherkin_lint/linter/same_tag_for_all_scenarios'
21
23
  require 'gherkin_lint/linter/tag_used_multiple_times'
22
24
  require 'gherkin_lint/linter/too_clumsy'
@@ -31,69 +33,61 @@ require 'gherkin_lint/linter/use_background'
31
33
  require 'gherkin_lint/linter/use_outline'
32
34
  require 'multi_json'
33
35
  require 'set'
34
-
36
+ require 'gherkin_lint/configuration'
35
37
  module GherkinLint
36
38
  # gherkin linter
37
39
  class GherkinLint
38
- LINTER = [
39
- AvoidOutlineForSingleExample,
40
- AvoidPeriod,
41
- AvoidScripting,
42
- BackgroundDoesMoreThanSetup,
43
- BackgroundRequiresMultipleScenarios,
44
- BadScenarioName,
45
- BeDeclarative,
46
- FileNameDiffersFeatureName,
47
- MissingExampleName,
48
- MissingFeatureDescription,
49
- MissingFeatureName,
50
- MissingScenarioName,
51
- MissingTestAction,
52
- MissingVerification,
53
- InvalidFileName,
54
- InvalidStepFlow,
55
- SameTagForAllScenarios,
56
- TagUsedMultipleTimes,
57
- TooClumsy,
58
- TooManyDifferentTags,
59
- TooManySteps,
60
- TooManyTags,
61
- TooLongStep,
62
- UniqueScenarioNames,
63
- UnknownVariable,
64
- UnusedVariable,
65
- UseBackground,
66
- UseOutline
67
- ].freeze
68
-
69
- def initialize
40
+ attr_accessor :verbose
41
+ default_file = File.expand_path('../../', __FILE__), '**/config', 'default.yml'
42
+ DEFAULT_CONFIG = Dir.glob(File.join(default_file)).first.freeze
43
+ LINTER = Linter.descendants
44
+
45
+ def initialize(path = nil)
70
46
  @files = {}
71
47
  @linter = []
72
- enable_all
48
+ @config = Configuration.new path || DEFAULT_CONFIG
49
+ @verbose = false
73
50
  end
74
51
 
75
- def enable_all
76
- disable []
52
+ def enabled(linter_name, value)
53
+ @config.config[linter_name]['Enabled'] = value if @config.config.key? linter_name
77
54
  end
78
55
 
79
- def enable(enabled_linter)
80
- set_linter(enabled_linter)
56
+ def enable(enabled_linters)
57
+ enabled_linters.each do |linter|
58
+ enabled linter, true
59
+ end
81
60
  end
82
61
 
83
- def disable(disabled_linter)
84
- set_linter(LINTER.map { |linter| linter.new.name.split('::').last }, disabled_linter)
62
+ def disable(disabled_linters)
63
+ disabled_linters.each do |linter|
64
+ enabled linter, false
65
+ end
66
+ end
67
+
68
+ # Testing feature
69
+ def disable_all
70
+ @config.config.each do |member|
71
+ @config.config[member[0]]['Enabled'] = false
72
+ end
85
73
  end
86
74
 
87
- def set_linter(enabled_linter, disabled_linter = [])
75
+ def set_linter
88
76
  @linter = []
89
- enabled_linter = Set.new enabled_linter
90
- disabled_linter = Set.new disabled_linter
91
77
  LINTER.each do |linter|
92
78
  new_linter = linter.new
93
- name = new_linter.class.name.split('::').last
94
- next unless enabled_linter.include? name
95
- next if disabled_linter.include? name
96
- @linter.push new_linter
79
+ linter_enabled = @config.config[new_linter.class.name.split('::').last]['Enabled']
80
+ evaluate_members(new_linter) if linter_enabled
81
+ @linter.push new_linter if linter_enabled
82
+ end
83
+ end
84
+
85
+ def evaluate_members(linter)
86
+ @config.config[linter.class.name.split('::').last].each do |member, value|
87
+ next if member.downcase.casecmp('enabled').zero?
88
+ member = member.downcase.to_sym
89
+ raise 'Member not found! Check the YAML' unless linter.respond_to? member
90
+ linter.public_send(member, value)
97
91
  end
98
92
  end
99
93
 
@@ -111,8 +105,7 @@ module GherkinLint
111
105
  linter.issues
112
106
  end.flatten
113
107
 
114
- issues.each { |issue| puts issue.render }
115
-
108
+ print issues
116
109
  return 0 if issues.select { |issue| issue.class == Error }.empty?
117
110
  -1
118
111
  end
@@ -129,8 +122,8 @@ module GherkinLint
129
122
  end
130
123
 
131
124
  def print(issues)
132
- puts "There are #{issues.length} Issues" unless issues.empty?
133
- issues.each { |issue| puts issue }
125
+ puts 'There are no issues' if issues.empty? && @verbose
126
+ issues.each { |issue| puts issue.render }
134
127
  end
135
128
  end
136
129
  end
@@ -0,0 +1,32 @@
1
+ require 'yaml'
2
+ module GherkinLint
3
+ # gherkin_lint configuration object
4
+ class Configuration
5
+ attr_reader :config
6
+
7
+ def initialize(path)
8
+ @path = path
9
+ @config = load_configuration || ''
10
+ load_user_configuration
11
+ end
12
+
13
+ def configuration_path
14
+ @path
15
+ end
16
+
17
+ def load_configuration
18
+ YAML.load_file configuration_path || '' if File.exist? configuration_path
19
+ end
20
+
21
+ def load_user_configuration
22
+ config_file = Dir.glob(File.join(Dir.pwd, '**', '.gherkin_lint.yml')).first
23
+ merge_config(config_file) if !config_file.nil? && File.exist?(config_file)
24
+ end
25
+
26
+ private
27
+
28
+ def merge_config(config_file)
29
+ @config.merge!(YAML.load_file(config_file)) { |_k, old, new| old.merge!(new) } unless @config.empty?
30
+ end
31
+ end
32
+ end
@@ -1,9 +1,9 @@
1
1
  require 'term/ansicolor'
2
- include Term::ANSIColor
3
2
 
4
3
  module GherkinLint
5
4
  # entity value class for issues
6
5
  class Issue
6
+ include Term::ANSIColor
7
7
  attr_reader :name, :references, :description
8
8
 
9
9
  def initialize(name, references, description = nil)
@@ -6,6 +6,10 @@ module GherkinLint
6
6
  class Linter
7
7
  attr_reader :issues
8
8
 
9
+ def self.descendants
10
+ ObjectSpace.each_object(::Class).select { |klass| klass < self }
11
+ end
12
+
9
13
  def initialize
10
14
  @issues = []
11
15
  @files = {}
@@ -55,7 +59,6 @@ module GherkinLint
55
59
  def elements
56
60
  @files.each do |file, content|
57
61
  feature = content[:feature]
58
-
59
62
  next if feature.nil?
60
63
  next unless feature.key? :children
61
64
  feature[:children].each do |scenario|
@@ -76,11 +79,10 @@ module GherkinLint
76
79
  end
77
80
 
78
81
  def filter_tag(data, tag)
79
- return data.select { |item| !tag?(item, tag) }.map { |item| filter_tag(item, tag) } if data.class == Array
82
+ return data.reject { |item| tag?(item, tag) }.map { |item| filter_tag(item, tag) } if data.class == Array
80
83
  return {} if (data.class == Hash) && (data.include? :feature) && tag?(data[:feature], tag)
81
84
  return data unless data.respond_to? :each_pair
82
85
  result = {}
83
-
84
86
  data.each_pair { |key, value| result[key] = filter_tag(value, tag) }
85
87
  result
86
88
  end
@@ -98,14 +100,13 @@ module GherkinLint
98
100
 
99
101
  data.each_pair do |key, value|
100
102
  value = suppress(value, tags) if key == :tags
101
-
102
103
  result[key] = suppress_tags(value, tags)
103
104
  end
104
105
  result
105
106
  end
106
107
 
107
108
  def suppress(data, tags)
108
- data.select { |item| !tags.map { |tag| "@#{tag}" }.include? item[:name] }
109
+ data.reject { |item| tags.map { |tag| "@#{tag}" }.include? item[:name] }
109
110
  end
110
111
 
111
112
  def lint
@@ -135,11 +136,6 @@ module GherkinLint
135
136
  @issues.push Warning.new(name, references, description)
136
137
  end
137
138
 
138
- def gather_tags(element)
139
- return [] unless element.include? :tags
140
- element[:tags].map { |tag| tag[:name][1..-1] }
141
- end
142
-
143
139
  def render_step(step)
144
140
  value = "#{step[:keyword]}#{step[:text]}"
145
141
  value += render_step_argument step[:argument] if step.include? :argument
@@ -16,7 +16,7 @@ module GherkinLint
16
16
  def filter_when_steps(steps)
17
17
  steps = steps.drop_while { |step| step[:keyword] != 'When ' }
18
18
  steps = steps.reverse.drop_while { |step| step[:keyword] != 'Then ' }.reverse
19
- steps.select { |step| step[:keyword] != 'Then ' }
19
+ steps.reject { |step| step[:keyword] == 'Then ' }
20
20
  end
21
21
  end
22
22
  end
@@ -5,7 +5,7 @@ module GherkinLint
5
5
  class BackgroundRequiresMultipleScenarios < Linter
6
6
  def lint
7
7
  backgrounds do |file, feature, background|
8
- scenarios = feature[:children].select { |element| element[:type] != :Background }
8
+ scenarios = feature[:children].reject { |element| element[:type] == :Background }
9
9
  next if scenarios.length >= 2
10
10
 
11
11
  references = [reference(file, feature, background)]
@@ -8,7 +8,7 @@ module GherkinLint
8
8
  next if scenario[:name].empty?
9
9
  references = [reference(file, feature, scenario)]
10
10
  description = 'Prefer to rely just on Given and When steps when name your scenario to keep it stable'
11
- bad_words = %w(test verif check)
11
+ bad_words = %w[test verif check]
12
12
  bad_words.each do |bad_word|
13
13
  add_error(references, description) if scenario[:name].downcase.include? bad_word
14
14
  end
@@ -25,14 +25,16 @@ module GherkinLint
25
25
  end
26
26
 
27
27
  def verbs(tagged_text)
28
- verbs = [
29
- :get_infinitive_verbs,
30
- :get_past_tense_verbs,
31
- :get_gerund_verbs,
32
- :get_passive_verbs,
33
- :get_present_verbs,
34
- :get_base_present_verbs
35
- ]
28
+ verbs =
29
+ %i[
30
+ get_infinitive_verbs
31
+ get_past_tense_verbs
32
+ get_gerund_verbs
33
+ get_passive_verbs
34
+ get_present_verbs
35
+ get_base_present_verbs
36
+ ]
37
+
36
38
  verbs.map { |verb| tagger.send(verb, tagged_text).keys }.flatten
37
39
  end
38
40
 
@@ -0,0 +1,16 @@
1
+ require 'gherkin_lint/linter/tag_constraint'
2
+ require 'gherkin_lint/linter'
3
+
4
+ module GherkinLint
5
+ # service class to lint for tags used multiple times
6
+ class RequiredTagsStartsWith < Linter
7
+ include TagConstraint
8
+
9
+ def match_pattern?(target)
10
+ target.each do |t|
11
+ return true if t.delete!('@').start_with?(*@pattern)
12
+ end
13
+ false
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,9 @@
1
+ module GherkinLint
2
+ # Mixin to lint for tags based on their relationship to eachother
3
+ module TagCollector
4
+ def gather_tags(element)
5
+ return [] unless element.include? :tags
6
+ element[:tags].map { |tag| tag[:name][1..-1] }
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,32 @@
1
+ module GherkinLint
2
+ # Mixin to lint for tags that have certain string contraints
3
+ module TagConstraint
4
+ def lint
5
+ scenarios do |file, feature, scenario|
6
+ next if match_pattern? tags(feature)
7
+ next if match_pattern? tags(scenario)
8
+ references = [reference(file, feature, scenario)]
9
+ add_error(references, 'Required Tag not found')
10
+ end
11
+ end
12
+
13
+ def tags(element)
14
+ return [] unless element.include? :tags
15
+ element[:tags].map { |a| a[:name] }
16
+ end
17
+
18
+ def matcher(pattern)
19
+ @pattern = pattern
20
+ validate_input
21
+ end
22
+
23
+ def match_pattern?(_tags)
24
+ raise NoMethodError, 'This is an abstraction that must be implemented by the includer'
25
+ end
26
+
27
+ def validate_input
28
+ raise 'No Tags provided in the YAML' if @pattern.nil?
29
+ warn 'Required Tags matcher has no value' if @pattern.empty?
30
+ end
31
+ end
32
+ end