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.
- checksums.yaml +4 -4
- data/.gitignore +4 -0
- data/.rubocop.yml +19 -1
- data/Dockerfile +2 -0
- data/Gemfile +3 -2
- data/README.md +24 -2
- data/Rakefile +3 -3
- data/bin/gherkin_lint +10 -2
- data/config/default.yml +58 -0
- data/features/avoid_outline_for_single_example.feature +2 -1
- data/features/avoid_period.feature +1 -1
- data/features/avoid_scripting.feature +1 -1
- data/features/background_does_more_than_setup.feature +1 -1
- data/features/background_requires_scenario.feature +1 -1
- data/features/bad_scenario_name.feature +1 -1
- data/features/be_declarative.feature +1 -1
- data/features/disable_tags.feature +1 -2
- data/features/file_name_differs_feature_name.feature +2 -2
- data/features/invalid_file_name.feature +1 -1
- data/features/invalid_step_flow.feature +1 -1
- data/features/missing_example_name.feature +1 -2
- data/features/missing_feature_description.feature +1 -1
- data/features/missing_feature_name.feature +1 -1
- data/features/missing_scenario_name.feature +1 -1
- data/features/missing_test_action.feature +1 -2
- data/features/missing_verification.feature +1 -2
- data/features/precedence.feature +72 -0
- data/features/required_tags_starts_with.feature +95 -0
- data/features/same_tag_for_all_scenarios.feature +1 -5
- data/features/support/env.rb +72 -0
- data/features/tag_used_multiple_times.feature +1 -1
- data/features/too_clumsy.feature +1 -1
- data/features/too_long_step.feature +1 -1
- data/features/too_many_different_tags.feature +1 -1
- data/features/too_many_steps.feature +1 -1
- data/features/too_many_tags.feature +1 -1
- data/features/unique_scenario_names.feature +1 -1
- data/features/unknown_variable.feature +1 -1
- data/features/unused_variable.feature +1 -1
- data/features/use_background.feature +1 -2
- data/features/use_outline.feature +1 -2
- data/gherkin_lint.gemspec +3 -3
- data/lib/gherkin_lint.rb +44 -51
- data/lib/gherkin_lint/configuration.rb +32 -0
- data/lib/gherkin_lint/issue.rb +1 -1
- data/lib/gherkin_lint/linter.rb +6 -10
- data/lib/gherkin_lint/linter/avoid_scripting.rb +1 -1
- data/lib/gherkin_lint/linter/background_requires_multiple_scenarios.rb +1 -1
- data/lib/gherkin_lint/linter/bad_scenario_name.rb +1 -1
- data/lib/gherkin_lint/linter/be_declarative.rb +10 -8
- data/lib/gherkin_lint/linter/required_tags_starts_with.rb +16 -0
- data/lib/gherkin_lint/linter/tag_collector.rb +9 -0
- data/lib/gherkin_lint/linter/tag_constraint.rb +32 -0
- data/lib/gherkin_lint/linter/too_many_different_tags.rb +3 -0
- data/lib/gherkin_lint/linter/too_many_tags.rb +3 -0
- data/spec/configuration_spec.rb +58 -0
- data/spec/gherkin_lint_spec.rb +68 -0
- data/spec/required_tags_starts_with_spec.rb +74 -0
- data/spec/shared_contexts/file_exists.rb +12 -0
- data/spec/shared_contexts/gherkin_linter.rb +14 -0
- 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
|
"""
|
data/gherkin_lint.gemspec
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
s.name = 'gherkin_lint'
|
3
|
-
s.version = '0.
|
4
|
-
s.date = '2017-
|
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) }
|
data/lib/gherkin_lint.rb
CHANGED
@@ -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
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
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
|
-
|
48
|
+
@config = Configuration.new path || DEFAULT_CONFIG
|
49
|
+
@verbose = false
|
73
50
|
end
|
74
51
|
|
75
|
-
def
|
76
|
-
|
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(
|
80
|
-
|
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(
|
84
|
-
|
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
|
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
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
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
|
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
|
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
|
data/lib/gherkin_lint/issue.rb
CHANGED
data/lib/gherkin_lint/linter.rb
CHANGED
@@ -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.
|
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.
|
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.
|
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].
|
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
|
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
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
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,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
|