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.
- 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
|