Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Gherkin abstraction layer #20

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ gemspec

gem 'amatch'
gem 'aruba', '1.0.0.pre.alpha.2'
gem 'cuke_modeler', '~> 1.3'
gem 'engtagger', '>=0.2.1'
gem 'gherkin', '>=4.0.0'
# TODO: update to gherkin4 gem 'gherkin_format'
# TODO: update to gherkin4 gem 'gherkin_language'
# TODO: update to cuke_modeler gem 'gherkin_format'
# TODO: update to cuke_modeler gem 'gherkin_language'
gem 'rake'
gem 'rspec'
gem 'rubocop'
Expand Down
2 changes: 1 addition & 1 deletion gherkin_lint.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ Gem::Specification.new do |s|
s.files = `git ls-files`.split("\n")
s.executables = s.files.grep(%r{^bin/}) { |file| File.basename(file) }
s.add_runtime_dependency 'amatch', ['~> 0.3', '>= 0.3.0']
s.add_runtime_dependency 'cuke_modeler', ['~> 1.3']
s.add_runtime_dependency 'engtagger', ['~> 0.2', '>= 0.2.0']
s.add_runtime_dependency 'gherkin', ['>= 4.0.0', '< 6.0']
s.add_runtime_dependency 'multi_json', ['~> 1.12', '>= 1.12.1']
s.add_runtime_dependency 'term-ansicolor', ['~> 1.3', '>= 1.3.2']
s.add_development_dependency 'aruba', ['~> 0.6', '>= 0.6.2']
Expand Down
14 changes: 12 additions & 2 deletions lib/gherkin_lint.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
gem 'gherkin', '>=4.0.0'

require 'gherkin/parser'
require 'cuke_modeler'
require 'gherkin_lint/linter'
require 'gherkin_lint/linter/avoid_outline_for_single_example'
require 'gherkin_lint/linter/avoid_period'
Expand Down Expand Up @@ -92,9 +92,11 @@ def evaluate_members(linter)
end

def analyze(file)
@files[file] = parse file
@files[file] = model file
end

# TODO: remove this method
# Deprecated
def parse(file)
to_json File.read(file)
end
Expand All @@ -114,6 +116,8 @@ def disable_tags
LINTER.map { |lint| "disable#{lint.new.class.name.split('::').last}" }
end

# TODO: remove this method
# Deprecated
def to_json(input)
parser = Gherkin::Parser.new
scanner = Gherkin::TokenScanner.new input
Expand All @@ -125,5 +129,11 @@ def print(issues)
puts 'There are no issues' if issues.empty? && @verbose
issues.each { |issue| puts issue.render }
end

private

def model(file)
CukeModeler::FeatureFile.new(file)
end
end
end
89 changes: 31 additions & 58 deletions lib/gherkin_lint/linter.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
require 'gherkin_lint/issue'
require 'gherkin_lint/model_filter'

# gherkin utilities
module GherkinLint
# base class for all linters
class Linter
include ModelFilter
attr_reader :issues

def self.descendants
Expand All @@ -16,8 +18,8 @@ def initialize
end

def features
@files.each do |file, content|
feature = content[:feature]
@files.each do |file, model|
feature = model.feature
next if feature.nil?
yield(file, feature)
end
Expand All @@ -29,39 +31,42 @@ def files

def scenarios
elements do |file, feature, scenario|
next if scenario[:type] == :Background
next if scenario.is_a?(CukeModeler::Background)
yield(file, feature, scenario)
end
end

def filled_scenarios
scenarios do |file, feature, scenario|
next unless scenario.include? :steps
next if scenario[:steps].empty?
next unless scenario.respond_to? :steps
next if scenario.steps.empty?
yield(file, feature, scenario)
end
end

def steps
elements do |file, feature, scenario|
next unless scenario.include? :steps
scenario[:steps].each { |step| yield(file, feature, scenario, step) }
next unless scenario.steps.any?
scenario.steps.each { |step| yield(file, feature, scenario, step) }
end
end

def backgrounds
elements do |file, feature, scenario|
next unless scenario[:type] == :Background
next unless scenario.is_a?(CukeModeler::Background)
yield(file, feature, scenario)
end
end

def elements
@files.each do |file, content|
feature = content[:feature]
@files.each do |file, model|
feature = model.feature
next if feature.nil?
next unless feature.key? :children
feature[:children].each do |scenario|
next unless feature.background || feature.tests.any?

everything = [feature.background].compact + feature.tests

everything.each do |scenario|
yield(file, feature, scenario)
end
end
Expand All @@ -71,60 +76,28 @@ def name
self.class.name.split('::').last
end

def lint_files(files, tags_to_suppress)
def lint_files(files, _tags_to_suppress)
@files = files
@files = filter_tag(@files, "disable#{name}")
@files = suppress_tags(@files, tags_to_suppress)
filter_guarded_models
lint
end

def filter_tag(data, tag)
return data.reject { |item| tag?(item, tag) }.map { |item| filter_tag(item, tag) } if data.class == Array
return {} if (data.class == Hash) && (data.include? :feature) && tag?(data[:feature], tag)
return data unless data.respond_to? :each_pair
result = {}
data.each_pair { |key, value| result[key] = filter_tag(value, tag) }
result
end

def tag?(data, tag)
return false if data.class != Hash
return false unless data.include? :tags
data[:tags].map { |item| item[:name] }.include? "@#{tag}"
end

def suppress_tags(data, tags)
return data.map { |item| suppress_tags(item, tags) } if data.class == Array
return data unless data.class == Hash
result = {}

data.each_pair do |key, value|
value = suppress(value, tags) if key == :tags
result[key] = suppress_tags(value, tags)
end
result
end

def suppress(data, tags)
data.reject { |item| tags.map { |tag| "@#{tag}" }.include? item[:name] }
end

def lint
raise 'not implemented'
end

def reference(file, feature = nil, scenario = nil, step = nil)
return file if feature.nil? || feature[:name].empty?
result = "#{file} (#{line(feature, scenario, step)}): #{feature[:name]}"
result += ".#{scenario[:name]}" unless scenario.nil? || scenario[:name].empty?
result += " step: #{step[:text]}" unless step.nil?
return file if feature.nil? || feature.name.empty?
result = "#{file} (#{line(feature, scenario, step)}): #{feature.name}"
result += ".#{scenario.name}" unless scenario.nil? || scenario.name.empty?
result += " step: #{step.text}" unless step.nil?
result
end

def line(feature, scenario, step)
line = feature.nil? ? nil : feature[:location][:line]
line = scenario[:location][:line] unless scenario.nil?
line = step[:location][:line] unless step.nil?
line = feature.nil? ? nil : feature.source_line
line = scenario.source_line unless scenario.nil?
line = step.source_line unless step.nil?
line
end

Expand All @@ -137,15 +110,15 @@ def add_warning(references, description = nil)
end

def render_step(step)
value = "#{step[:keyword]}#{step[:text]}"
value += render_step_argument step[:argument] if step.include? :argument
value = "#{step.keyword} #{step.text}"
value += render_step_argument step.block if step.block
value
end

def render_step_argument(argument)
return "\n#{argument[:content]}" if argument[:type] == :DocString
result = argument[:rows].map do |row|
"|#{row[:cells].map { |cell| cell[:value] }.join '|'}|"
return "\n#{argument.content}" if argument.is_a?(CukeModeler::DocString)
result = argument.rows.map do |row|
"|#{row.cells.map(&:value).join '|'}|"
end.join "\n"
"\n#{result}"
end
Expand Down
8 changes: 4 additions & 4 deletions lib/gherkin_lint/linter/avoid_outline_for_single_example.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ module GherkinLint
class AvoidOutlineForSingleExample < Linter
def lint
scenarios do |file, feature, scenario|
next unless scenario[:type] == :ScenarioOutline
next unless scenario.is_a?(CukeModeler::Outline)

next unless scenario.key? :examples
next if scenario[:examples].length > 1
next if scenario[:examples].first[:tableBody].length > 1
next unless scenario.examples.any?
next if scenario.examples.length > 1
next if scenario.examples.first.argument_rows.length > 1

references = [reference(file, feature, scenario)]
add_error(references, 'Better write a scenario')
Expand Down
6 changes: 3 additions & 3 deletions lib/gherkin_lint/linter/avoid_period.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ module GherkinLint
class AvoidPeriod < Linter
def lint
scenarios do |file, feature, scenario|
next unless scenario.key? :steps
next unless scenario.respond_to? :steps

scenario[:steps].each do |step|
scenario.steps.each do |step|
references = [reference(file, feature, scenario, step)]
add_error(references) if step[:text].strip.end_with? '.'
add_error(references) if step.text.strip.end_with? '.'
end
end
end
Expand Down
8 changes: 4 additions & 4 deletions lib/gherkin_lint/linter/avoid_scripting.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ module GherkinLint
class AvoidScripting < Linter
def lint
filled_scenarios do |file, feature, scenario|
steps = filter_when_steps scenario[:steps]
steps = filter_when_steps scenario.steps

next if steps.length <= 1
references = [reference(file, feature, scenario)]
Expand All @@ -14,9 +14,9 @@ def lint
end

def filter_when_steps(steps)
steps = steps.drop_while { |step| step[:keyword] != 'When ' }
steps = steps.reverse.drop_while { |step| step[:keyword] != 'Then ' }.reverse
steps.reject { |step| step[:keyword] == 'Then ' }
steps = steps.drop_while { |step| step.keyword != 'When' }
steps = steps.reverse.drop_while { |step| step.keyword != 'Then' }.reverse
steps.reject { |step| step.keyword == 'Then' }
end
end
end
4 changes: 2 additions & 2 deletions lib/gherkin_lint/linter/background_does_more_than_setup.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ module GherkinLint
class BackgroundDoesMoreThanSetup < Linter
def lint
backgrounds do |file, feature, background|
next unless background.key? :steps
invalid_steps = background[:steps].select { |step| step[:keyword] == 'When ' || step[:keyword] == 'Then ' }
next unless background.steps.any?
invalid_steps = background.steps.select { |step| step.keyword == 'When' || step.keyword == 'Then' }
next if invalid_steps.empty?
references = [reference(file, feature, background, invalid_steps[0])]
add_error(references, 'Just Given Steps allowed')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ module GherkinLint
class BackgroundRequiresMultipleScenarios < Linter
def lint
backgrounds do |file, feature, background|
scenarios = feature[:children].reject { |element| element[:type] == :Background }
scenarios = feature.children.reject { |element| element.is_a?(CukeModeler::Background) }
next if scenarios.length >= 2

references = [reference(file, feature, background)]
Expand Down
4 changes: 2 additions & 2 deletions lib/gherkin_lint/linter/bad_scenario_name.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ module GherkinLint
class BadScenarioName < Linter
def lint
scenarios do |file, feature, scenario|
next if scenario[:name].empty?
next if scenario.name.empty?
references = [reference(file, feature, scenario)]
description = 'Prefer to rely just on Given and When steps when name your scenario to keep it stable'
bad_words = %w[test verif check]
bad_words.each do |bad_word|
add_error(references, description) if scenario[:name].downcase.include? bad_word
add_error(references, description) if scenario.name.downcase.include? bad_word
end
end
end
Expand Down
4 changes: 2 additions & 2 deletions lib/gherkin_lint/linter/be_declarative.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@ def initialize

def lint
filled_scenarios do |file, feature, scenario|
scenario[:steps].each do |step|
scenario.steps.each do |step|
references = [reference(file, feature, scenario, step)]
add_warning(references, 'no verb') unless verb? step
end
end
end

def verb?(step)
tagged = tagger.add_tags step[:text]
tagged = tagger.add_tags step.text
step_verbs = verbs tagged

!step_verbs.empty?
Expand Down
4 changes: 2 additions & 2 deletions lib/gherkin_lint/linter/file_name_differs_feature_name.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ module GherkinLint
class FileNameDiffersFeatureName < Linter
def lint
features do |file, feature|
next unless feature.include? :name
next if feature.name.empty?
expected_feature_name = title_case file
next if ignore_whitespaces(feature[:name]).casecmp(ignore_whitespaces(expected_feature_name)) == 0
next if ignore_whitespaces(feature.name).casecmp(ignore_whitespaces(expected_feature_name)) == 0
references = [reference(file, feature)]
add_error(references, "Feature name should be '#{expected_feature_name}'")
end
Expand Down
10 changes: 5 additions & 5 deletions lib/gherkin_lint/linter/invalid_step_flow.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ module GherkinLint
class InvalidStepFlow < Linter
def lint
filled_scenarios do |file, feature, scenario|
steps = scenario[:steps].select { |step| step[:keyword] != 'And ' && step[:keyword] != 'But ' }
steps = scenario.steps.select { |step| step.keyword != 'And' && step.keyword != 'But' }
next if steps.empty?
last_step_is_an_action(file, feature, scenario, steps)
given_after_non_given(file, feature, scenario, steps)
Expand All @@ -15,24 +15,24 @@ def lint

def last_step_is_an_action(file, feature, scenario, steps)
references = [reference(file, feature, scenario, steps.last)]
add_error(references, 'Last step is an action') if steps.last[:keyword] == 'When '
add_error(references, 'Last step is an action') if steps.last.keyword == 'When'
end

def given_after_non_given(file, feature, scenario, steps)
last_step = steps.first
steps.each do |step|
references = [reference(file, feature, scenario, step)]
description = 'Given after Action or Verification'
add_error(references, description) if step[:keyword] == 'Given ' && last_step[:keyword] != 'Given '
add_error(references, description) if step.keyword == 'Given' && last_step.keyword != 'Given'
last_step = step
end
end

def verification_before_action(file, feature, scenario, steps)
steps.each do |step|
break if step[:keyword] == 'When '
break if step.keyword == 'When'
references = [reference(file, feature, scenario, step)]
add_error(references, 'Missing Action') if step[:keyword] == 'Then '
add_error(references, 'Missing Action') if step.keyword == 'Then'
end
end
end
Expand Down
Loading