gherkin_lint 0.0.1
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 +7 -0
- data/bin/gherkin_lint +26 -0
- data/lib/gherkin_lint.rb +414 -0
- metadata +87 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 0ada6bfa04c433aad9b3d227c5ae836b79d0dc7c
|
4
|
+
data.tar.gz: 22345665cd0d4f7985f22ba857ae8a6d61cd3713
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: dce80fc451a1cecbd88da1a7efdc27d8e1d35fd3674a10865082f1d42b6156ecea8827132dd8772d0afba59237e969e5974ebb4e19c89639d9731370e0483739
|
7
|
+
data.tar.gz: 7814679c1fbefbfdf15870578adeffd530a849fb62c01c94ebb36dea68a26c395a91eea505cbf1aef86f44f9301324bfb38e616eb50f2e3d71609c7e624da5cc
|
data/bin/gherkin_lint
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'gherkin_lint'
|
3
|
+
require 'optparse'
|
4
|
+
|
5
|
+
options = {}
|
6
|
+
OptionParser.new do |opts|
|
7
|
+
opts.banner = 'Usage: gherkin_language [files]'
|
8
|
+
opts.on('-v', '--[no-]verbose', 'Run verbosely') do |verbose|
|
9
|
+
options[:verbose] = verbose
|
10
|
+
end
|
11
|
+
opts.on('--enable [CHECKS]', 'Enabled checks. Separated by ","') do |checks|
|
12
|
+
options[:enable] = checks.split(',')
|
13
|
+
end
|
14
|
+
opts.on('--disable [CHECKS]', 'Disabled checks. Separated by ","') do |checks|
|
15
|
+
options[:disable] = checks.split(',')
|
16
|
+
end
|
17
|
+
end.parse!
|
18
|
+
|
19
|
+
linter = GherkinLint.new
|
20
|
+
|
21
|
+
linter.disable options[:disable] if options.key? :disable
|
22
|
+
linter.enable options[:enable] if options.key? :enable
|
23
|
+
|
24
|
+
ARGV.each { |file| linter.analyze file }
|
25
|
+
|
26
|
+
exit linter.report
|
data/lib/gherkin_lint.rb
ADDED
@@ -0,0 +1,414 @@
|
|
1
|
+
require 'gherkin/formatter/json_formatter'
|
2
|
+
require 'gherkin/parser/parser'
|
3
|
+
require 'stringio'
|
4
|
+
require 'multi_json'
|
5
|
+
require 'term/ansicolor'
|
6
|
+
include Term::ANSIColor
|
7
|
+
require 'set'
|
8
|
+
|
9
|
+
# gherkin utilities
|
10
|
+
class GherkinLint
|
11
|
+
# entity value class for issues
|
12
|
+
class Issue
|
13
|
+
attr_reader :name, :references, :description
|
14
|
+
|
15
|
+
def initialize(name, references, description = nil)
|
16
|
+
@name = name
|
17
|
+
@references = references
|
18
|
+
@description = description
|
19
|
+
end
|
20
|
+
|
21
|
+
def render
|
22
|
+
result = red(@name)
|
23
|
+
result += " - #{@description}" unless @description.nil?
|
24
|
+
result += "\n " + green(@references.uniq * "\n ")
|
25
|
+
result
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# base class for all linters
|
30
|
+
class Linter
|
31
|
+
attr_reader :issues
|
32
|
+
|
33
|
+
def initialize
|
34
|
+
@issues = []
|
35
|
+
@files = {}
|
36
|
+
end
|
37
|
+
|
38
|
+
def features
|
39
|
+
@files.each do |file, content|
|
40
|
+
content.each do |feature|
|
41
|
+
yield(file, feature)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def files
|
47
|
+
@files.keys.each { |file| yield file }
|
48
|
+
end
|
49
|
+
|
50
|
+
def scenarios
|
51
|
+
@files.each do |file, content|
|
52
|
+
content.each do |feature|
|
53
|
+
next unless feature.key? 'elements'
|
54
|
+
feature['elements'].each do |scenario|
|
55
|
+
next if scenario['keyword'] == 'Background'
|
56
|
+
yield(file, feature, scenario)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def backgrounds
|
63
|
+
@files.each do |file, content|
|
64
|
+
content.each do |feature|
|
65
|
+
next unless feature.key? 'elements'
|
66
|
+
feature['elements'].each do |scenario|
|
67
|
+
next unless scenario['keyword'] == 'Background'
|
68
|
+
yield(file, feature, scenario)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def name
|
75
|
+
self.class.name.split('::').last
|
76
|
+
end
|
77
|
+
|
78
|
+
def lint_files(files)
|
79
|
+
@files = files
|
80
|
+
lint
|
81
|
+
end
|
82
|
+
|
83
|
+
def lint
|
84
|
+
fail 'not implemented'
|
85
|
+
end
|
86
|
+
|
87
|
+
def reference(file, feature = nil, scenario = nil, step = nil)
|
88
|
+
return file if feature.nil? || feature['name'].empty?
|
89
|
+
result = "#{file} (#{line(feature, scenario, step)}): #{feature['name']}"
|
90
|
+
result += ".#{scenario['name']}" unless scenario.nil? || scenario['name'].empty?
|
91
|
+
result += " step: #{step['name']}" unless step.nil?
|
92
|
+
result
|
93
|
+
end
|
94
|
+
|
95
|
+
def line(feature, scenario, step)
|
96
|
+
line = feature.nil? ? nil : feature['line']
|
97
|
+
line = scenario['line'] unless scenario.nil?
|
98
|
+
line = step['line'] unless step.nil?
|
99
|
+
line
|
100
|
+
end
|
101
|
+
|
102
|
+
def add_issue(references, description = nil)
|
103
|
+
@issues.push Issue.new(name, references, description)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# service class to lint for unique scenario names
|
108
|
+
class UniqueScenarioNames < Linter
|
109
|
+
def lint
|
110
|
+
references_by_name = Hash.new []
|
111
|
+
scenarios do |file, feature, scenario|
|
112
|
+
next unless scenario.key? 'name'
|
113
|
+
scenario_name = "#{feature['name']}.#{scenario['name']}"
|
114
|
+
references_by_name[scenario_name] = references_by_name[scenario_name] + [reference(file, feature, scenario)]
|
115
|
+
end
|
116
|
+
references_by_name.each do |name, references|
|
117
|
+
next if references.length <= 1
|
118
|
+
add_issue(references, "'#{name}' used #{references.length} times")
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# service class to lint for missing test actions
|
124
|
+
class MissingTestAction < Linter
|
125
|
+
def lint
|
126
|
+
scenarios do |file, feature, scenario|
|
127
|
+
next unless scenario.key? 'steps'
|
128
|
+
when_steps = scenario['steps'].select { |step| step['keyword'] == 'When ' }
|
129
|
+
next if when_steps.length > 0
|
130
|
+
references = [reference(file, feature, scenario)]
|
131
|
+
add_issue(references, 'No \'When\'-Step')
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
# service class to lint for missing verifications
|
137
|
+
class MissingVerification < Linter
|
138
|
+
def lint
|
139
|
+
scenarios do |file, feature, scenario|
|
140
|
+
next unless scenario.key? 'steps'
|
141
|
+
then_steps = scenario['steps'].select { |step| step['keyword'] == 'Then ' }
|
142
|
+
next if then_steps.length > 0
|
143
|
+
references = [reference(file, feature, scenario)]
|
144
|
+
add_issue(references, 'No verification step')
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
# service class to lint for backgrond that does more than setup
|
150
|
+
class BackgroundDoesMoreThanSetup < Linter
|
151
|
+
def lint
|
152
|
+
backgrounds do |file, feature, background|
|
153
|
+
next unless background.key? 'steps'
|
154
|
+
invalid_steps = background['steps'].select { |step| step['keyword'] == 'When ' || step['keyword'] == 'Then ' }
|
155
|
+
next if invalid_steps.empty?
|
156
|
+
references = [reference(file, feature, background, invalid_steps[0])]
|
157
|
+
add_issue(references, 'Just Given Steps allowed')
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
# service class to lint for missing feature names
|
163
|
+
class MissingFeatureName < Linter
|
164
|
+
def lint
|
165
|
+
features do |file, feature|
|
166
|
+
name = feature.key?('name') ? feature['name'].strip : ''
|
167
|
+
next unless name.empty?
|
168
|
+
references = [reference(file, feature)]
|
169
|
+
add_issue(references, 'No Feature Name')
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
# service class to lint for missing feature descriptions
|
175
|
+
class MissingFeatureDescription < Linter
|
176
|
+
def lint
|
177
|
+
features do |file, feature|
|
178
|
+
name = feature.key?('description') ? feature['description'].strip : ''
|
179
|
+
next unless name.empty?
|
180
|
+
references = [reference(file, feature)]
|
181
|
+
add_issue(references, 'Favor a user story as description')
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
# service class to lint for missing scenario names
|
187
|
+
class MissingScenarioName < Linter
|
188
|
+
def lint
|
189
|
+
scenarios do |file, feature, scenario|
|
190
|
+
name = scenario.key?('name') ? scenario['name'].strip : ''
|
191
|
+
references = [reference(file, feature, scenario)]
|
192
|
+
next unless name.empty?
|
193
|
+
add_issue(references, 'No Scenario Name')
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
# service class to lint for missing example names
|
199
|
+
class MissingExampleName < Linter
|
200
|
+
def lint
|
201
|
+
scenarios do |file, feature, scenario|
|
202
|
+
next unless scenario.key? 'examples'
|
203
|
+
scenario['examples'].each do |example|
|
204
|
+
name = example.key?('name') ? example['name'].strip : ''
|
205
|
+
next unless name.empty?
|
206
|
+
references = [reference(file, feature, scenario)]
|
207
|
+
add_issue(references, 'No Example Name')
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
# service class to lint for invalid step flow
|
214
|
+
class InvalidStepFlow < Linter
|
215
|
+
def lint
|
216
|
+
scenarios do |file, feature, scenario|
|
217
|
+
next unless scenario.key? 'steps'
|
218
|
+
steps = scenario['steps'].select { |step| step['keyword'] != 'And ' && step['keyword'] != 'But ' }
|
219
|
+
last_step_is_an_action(file, feature, scenario, steps)
|
220
|
+
given_after_non_given(file, feature, scenario, steps)
|
221
|
+
verification_before_action(file, feature, scenario, steps)
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
def last_step_is_an_action(file, feature, scenario, steps)
|
226
|
+
references = [reference(file, feature, scenario, steps.last)]
|
227
|
+
add_issue(references, 'Last step is an action') if steps.last['keyword'] == 'When '
|
228
|
+
end
|
229
|
+
|
230
|
+
def given_after_non_given(file, feature, scenario, steps)
|
231
|
+
last_step = steps.first
|
232
|
+
steps.each do |step|
|
233
|
+
references = [reference(file, feature, scenario, step)]
|
234
|
+
description = 'Given after Action or Verification'
|
235
|
+
add_issue(references, description) if step['keyword'] == 'Given ' && last_step['keyword'] != 'Given '
|
236
|
+
last_step = step
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
def verification_before_action(file, feature, scenario, steps)
|
241
|
+
steps.each do |step|
|
242
|
+
break if step['keyword'] == 'When '
|
243
|
+
references = [reference(file, feature, scenario, step)]
|
244
|
+
description = 'Verification before action'
|
245
|
+
add_issue(references, description) if step['keyword'] == 'Then '
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
# service class to lint for invalid scenario names
|
251
|
+
class InvalidScenarioName < Linter
|
252
|
+
def lint
|
253
|
+
scenarios do |file, feature, scenario|
|
254
|
+
next if scenario['name'].empty?
|
255
|
+
references = [reference(file, feature, scenario)]
|
256
|
+
description = 'Prefer to rely just on Given and When steps when name your scenario to keep it stable'
|
257
|
+
bad_words = %w(test verif check)
|
258
|
+
bad_words.each do |bad_word|
|
259
|
+
add_issue(references, description) if scenario['name'].downcase.include? bad_word
|
260
|
+
end
|
261
|
+
end
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
# service class to lint for invalid file names
|
266
|
+
class InvalidFileName < Linter
|
267
|
+
def lint
|
268
|
+
files do |file|
|
269
|
+
base = File.basename file
|
270
|
+
next if base == base.downcase
|
271
|
+
references = [reference(file)]
|
272
|
+
add_issue(references, 'Feature files should be snake_cased')
|
273
|
+
end
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
# service class to lint for unused variables
|
278
|
+
class UnusedVariable < Linter
|
279
|
+
def lint
|
280
|
+
scenarios do |file, feature, scenario|
|
281
|
+
next unless scenario.key? 'examples'
|
282
|
+
scenario['examples'].each do |example|
|
283
|
+
next unless example.key? 'rows'
|
284
|
+
example['rows'].first['cells'].each do |variable|
|
285
|
+
references = [reference(file, feature, scenario)]
|
286
|
+
add_issue(references, "'<#{variable}>' is unused") unless used?(variable, scenario)
|
287
|
+
end
|
288
|
+
end
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
def used?(variable, scenario)
|
293
|
+
variable = "<#{variable}>"
|
294
|
+
return false unless scenario.key? 'steps'
|
295
|
+
scenario['steps'].each do |step|
|
296
|
+
return true if step['name'].include? variable
|
297
|
+
return true if used_in_docstring?(variable, step)
|
298
|
+
return true if used_in_table?(variable, step)
|
299
|
+
end
|
300
|
+
false
|
301
|
+
end
|
302
|
+
|
303
|
+
def used_in_docstring?(variable, step)
|
304
|
+
step.key?('doc_string') && step['doc_string']['value'].include?(variable)
|
305
|
+
end
|
306
|
+
|
307
|
+
def used_in_table?(variable, step)
|
308
|
+
return false unless step.key? 'rows'
|
309
|
+
step['rows'].each do |row|
|
310
|
+
row['cells'].each { |value| return true if value.include?(variable) }
|
311
|
+
end
|
312
|
+
false
|
313
|
+
end
|
314
|
+
end
|
315
|
+
|
316
|
+
# service class to lint for avoiding colons
|
317
|
+
class AvoidColon < Linter
|
318
|
+
def lint
|
319
|
+
scenarios do |file, feature, scenario|
|
320
|
+
next unless scenario.key? 'steps'
|
321
|
+
|
322
|
+
scenario['steps'].each do |step|
|
323
|
+
references = [reference(file, feature, scenario, step)]
|
324
|
+
add_issue(references) if step['name'].strip.end_with? '.'
|
325
|
+
end
|
326
|
+
end
|
327
|
+
end
|
328
|
+
end
|
329
|
+
|
330
|
+
LINTER = [
|
331
|
+
AvoidColon,
|
332
|
+
BackgroundDoesMoreThanSetup,
|
333
|
+
MissingExampleName,
|
334
|
+
MissingFeatureDescription,
|
335
|
+
MissingFeatureName,
|
336
|
+
MissingScenarioName,
|
337
|
+
MissingTestAction,
|
338
|
+
MissingVerification,
|
339
|
+
InvalidFileName,
|
340
|
+
InvalidScenarioName,
|
341
|
+
InvalidStepFlow,
|
342
|
+
UniqueScenarioNames,
|
343
|
+
UnusedVariable
|
344
|
+
]
|
345
|
+
|
346
|
+
def initialize
|
347
|
+
@files = {}
|
348
|
+
@linter = []
|
349
|
+
enable_all
|
350
|
+
end
|
351
|
+
|
352
|
+
def enable_all
|
353
|
+
disable []
|
354
|
+
end
|
355
|
+
|
356
|
+
def enable(enabled_linter)
|
357
|
+
@linter = []
|
358
|
+
enabled_linter = Set.new enabled_linter
|
359
|
+
LINTER.each do |linter|
|
360
|
+
new_linter = linter.new
|
361
|
+
next unless enabled_linter.include? new_linter.class.name.split('::').last
|
362
|
+
register_linter new_linter
|
363
|
+
end
|
364
|
+
end
|
365
|
+
|
366
|
+
def disable(disabled_linter)
|
367
|
+
@linter = []
|
368
|
+
disabled_linter = Set.new disabled_linter
|
369
|
+
LINTER.each do |linter|
|
370
|
+
new_linter = linter.new
|
371
|
+
next if disabled_linter.include? new_linter.class.name.split('::').last
|
372
|
+
register_linter new_linter
|
373
|
+
end
|
374
|
+
end
|
375
|
+
|
376
|
+
def register_linter(linter)
|
377
|
+
@linter.push linter
|
378
|
+
end
|
379
|
+
|
380
|
+
def analyze(file)
|
381
|
+
@files[file] = parse file
|
382
|
+
end
|
383
|
+
|
384
|
+
def parse(file)
|
385
|
+
content = File.read file
|
386
|
+
# puts to_json(content, file)
|
387
|
+
to_json(content, file)
|
388
|
+
end
|
389
|
+
|
390
|
+
def report
|
391
|
+
issues = @linter.map do |linter|
|
392
|
+
linter.lint_files @files
|
393
|
+
linter.issues
|
394
|
+
end.flatten
|
395
|
+
|
396
|
+
issues.each { |issue| puts issue.render }
|
397
|
+
return 0 if issues.length == 0
|
398
|
+
-1
|
399
|
+
end
|
400
|
+
|
401
|
+
def to_json(input, file = 'generated.feature')
|
402
|
+
io = StringIO.new
|
403
|
+
formatter = Gherkin::Formatter::JSONFormatter.new(io)
|
404
|
+
parser = Gherkin::Parser::Parser.new(formatter, true)
|
405
|
+
parser.parse(input, file, 0)
|
406
|
+
formatter.done
|
407
|
+
MultiJson.load io.string
|
408
|
+
end
|
409
|
+
|
410
|
+
def print(issues)
|
411
|
+
puts "There are #{issues.length} Issues" unless issues.empty?
|
412
|
+
issues.each { |issue| puts issue }
|
413
|
+
end
|
414
|
+
end
|
metadata
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: gherkin_lint
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Stefan Rohe
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-07-02 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: gherkin
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 2.12.2
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 2.12.2
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: term-ansicolor
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 1.3.2
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 1.3.2
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: aruba
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 0.6.2
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 0.6.2
|
55
|
+
description: Lint Gherkin Files
|
56
|
+
email:
|
57
|
+
executables:
|
58
|
+
- gherkin_lint
|
59
|
+
extensions: []
|
60
|
+
extra_rdoc_files: []
|
61
|
+
files:
|
62
|
+
- bin/gherkin_lint
|
63
|
+
- lib/gherkin_lint.rb
|
64
|
+
homepage: http://github.com/funkwerk/gherkin_lint/
|
65
|
+
licenses: []
|
66
|
+
metadata: {}
|
67
|
+
post_install_message:
|
68
|
+
rdoc_options: []
|
69
|
+
require_paths:
|
70
|
+
- lib
|
71
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
77
|
+
requirements:
|
78
|
+
- - ">="
|
79
|
+
- !ruby/object:Gem::Version
|
80
|
+
version: '0'
|
81
|
+
requirements: []
|
82
|
+
rubyforge_project:
|
83
|
+
rubygems_version: 2.2.3
|
84
|
+
signing_key:
|
85
|
+
specification_version: 4
|
86
|
+
summary: Gherkin Lint
|
87
|
+
test_files: []
|