gherkin_lint 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (4) hide show
  1. checksums.yaml +7 -0
  2. data/bin/gherkin_lint +26 -0
  3. data/lib/gherkin_lint.rb +414 -0
  4. 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
@@ -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: []