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