gherkin_lint 0.6.3 → 1.0.0

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