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.
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: []