lib/cuke_sniffer/rule_config.rb
module CukeSniffer
# Contains the rules and various scores used in evaluating objects
module RuleConfig
# Will prevent suite from executing properly
FATAL = 100
# Will cause problem with debugging
ERROR = 25
# Readability/misuse of cucumber
WARNING = 10
# Small improvements that can be made
INFO = 1
fatal_rules = {
:no_examples => {
:enabled => true,
:phrase => "Scenario Outline with no examples.",
:score => FATAL,
:targets => ["Scenario"],
:reason => lambda { |scenario, rule| scenario.outline? and scenario.examples_table.size == 1}
},
:no_examples_table => {
:enabled => true,
:phrase => "Scenario Outline with no examples table.",
:score => FATAL,
:targets => ["Scenario"],
:reason => lambda { |scenario, rule| scenario.outline? and scenario.examples_table.empty?}
},
:recursive_nested_step => {
:enabled => true,
:phrase => "Recursive nested step call.",
:score => FATAL,
:targets => ["StepDefinition"],
:reason => lambda { |step_definition, rule| step_definition.store_rule_many_times(rule, step_definition.recursive_nested_steps.size)}
},
:background_with_tag => {
:enabled => true,
:phrase => "There is a background with a tag. This feature file cannot run!",
:score => FATAL,
:targets => ["Background"],
:reason => lambda { |background, rule| background.tags.size > 0}
},
:comment_after_tag => {
:enabled => true,
:phrase => "Comment comes between tag and properly executing line. This feature file cannot run!",
:score => FATAL,
:targets => ["Feature", "Scenario"],
:reason =>
lambda { |feature_rule_target, rule|
tokens = feature_rule_target.tags.collect { |line| line.split }.flatten
return_value = nil
tokens.each_with_index do |token, index|
if feature_rule_target.is_comment?(token) && tokens[0...index].any? { |x| x =~ /\A@/ }
return_value = feature_rule_target.store_rule(rule)
break
end
end
return_value
}
},
:universal_nested_step => {
:enabled => true,
:phrase => "A nested step should not universally match all step definitions. Dead steps cannot be correctly cataloged.",
:score => FATAL,
:targets => ["StepDefinition"],
:reason => lambda { |step_definition, rule| step_definition.nested_steps.each_value do | step_value |
modified_step = step_value.gsub(/\#{[^}]*}/, '.*')
step_definition.store_rule(rule) if modified_step == '.*'
end}
}
}
error_rules = {
:no_description => {
:enabled => true,
:phrase => "{class} has no description.",
:score => ERROR,
:targets => ["Feature", "Scenario"],
:reason => lambda { |feature_rule_target, rule| feature_rule_target.name.empty?}
},
:no_scenarios => {
:enabled => true,
:phrase => "Feature with no scenarios.",
:score => ERROR,
:targets => ["Feature"],
:reason => lambda { |feature, rule| feature.scenarios.empty?}
},
:commented_step => {
:enabled => true,
:phrase => "Commented step.",
:score => ERROR,
:targets => ["Scenario", "Background"],
:reason => lambda { |scenario, rule| scenario.steps.each do |step|
scenario.store_rule(rule) if scenario.is_comment?(step)
end}
},
:commented_example => {
:enabled => true,
:phrase => "Commented example.",
:score => ERROR,
:targets => ["Scenario"],
:reason => lambda { |scenario, rule| scenario.store_rule_many_times(rule, scenario.commented_examples.size) }
},
:no_steps => {
:enabled => true,
:phrase => "No steps in Scenario.",
:score => ERROR,
:targets => ["Scenario", "Background"],
:reason => lambda { |scenario, rule| scenario.steps.empty?}
},
:one_word_step => {
:enabled => true,
:phrase => "Step that is only one word long.",
:score => ERROR,
:targets => ["Scenario", "Background"],
:reason => lambda { |scenario, rule| scenario.steps.each {|step| scenario.store_rule(rule) if step.split.count == 2}}
},
:no_code => {
:enabled => true,
:phrase => "No code in Step Definition.",
:score => ERROR,
:targets => ["StepDefinition"],
:reason => lambda { |step_definition, rule| step_definition.code.empty?}
},
:around_hook_without_2_parameters => {
:enabled => true,
:phrase => "Around hook without 2 parameters for Scenario and Block.",
:score => ERROR,
:targets => ["Hook"],
:reason => lambda { |hook, rule| hook.around? and hook.parameters.count != 2}
},
:around_hook_no_block_call => {
:enabled => true,
:phrase => "Around hook does not call its block.",
:score => ERROR,
:targets => ["Hook"],
:reason => lambda { |hook, rule| hook.around? and !hook.calls_block?}
},
:hook_no_debugging => {
:enabled => true,
:phrase => "Hook without a begin/rescue. Reduced visibility when debugging.",
:score => ERROR,
:targets => ["Hook"],
:reason => lambda { |hook, rule| !hook.rescues?}
},
:hook_conflicting_tags => {
:enabled => true,
:phrase => "Hook that both expects and ignores the same tag. This hook will not function as expected.",
:score => ERROR,
:targets => ["Hook"],
:reason => lambda { |hook, rule| hook.conflicting_tags? }
},
}
warning_rules = {
:numbers_in_description => {
:enabled => true,
:phrase => "{class} has numbers in the description.",
:score => WARNING,
:targets => ["Feature", "Scenario", "Background"],
:reason => lambda { |feature_rule_target, rule| !(feature_rule_target.name =~ /\d+/).nil?}
},
:empty_feature => {
:enabled => true,
:phrase => "Feature file has no content.",
:score => WARNING,
:targets => ["Feature"],
:reason => lambda { |feature, rule| feature.feature_lines == []}
},
:background_with_no_scenarios => {
:enabled => true,
:phrase => "Feature has a background with no scenarios.",
:score => WARNING,
:targets => ["Feature"],
:reason => lambda { |feature, rule| feature.scenarios.empty? and !feature.background.nil?}
},
:background_with_one_scenario => {
:enabled => true,
:phrase => "Feature has a background with one scenario.",
:score => WARNING,
:targets => ["Feature"],
:reason => lambda { |feature, rule| feature.scenarios.size == 1 and !feature.background.nil?}
},
:too_many_steps => {
:enabled => true,
:phrase => "{class} with too many steps.",
:score => WARNING,
:max => 7,
:targets => ["Scenario", "Background"],
:reason => lambda { |scenario, rule| scenario.steps.count > rule.conditions[:max]}
},
:out_of_order_steps => {
:enabled => true,
:phrase => "Scenario steps out of Given/When/Then order.",
:score => WARNING,
:targets => ["Scenario"],
:reason => lambda { |scenario, rule| step_order = scenario.get_step_order
if !step_order.empty?
["But", "*", "And"].each { |type| step_order.delete(type) }
step_order = step_order.chunk { |keyword| keyword }.map(&:first)
if(step_order != %w(Given When Then) and step_order != %w(When Then))
scenario.store_rule(rule)
end
end
}
},
:invalid_first_step => {
:enabled => true,
:phrase => "Invalid first step. Began with And/But.",
:score => WARNING,
:targets => ["Scenario", "Background"],
:reason => lambda { |scenario, rule| !(scenario.steps.first =~ /^\s*(And|But).*$/).nil?}
},
:asterisk_step => {
:enabled => true,
:phrase => "Step includes a * instead of Given/When/Then/And/But.",
:score => WARNING,
:targets => ["Scenario", "Background"],
:reason => lambda { |scenario, rule| scenario.store_rule_many_times(rule, scenario.get_steps("*").size)
}
},
:one_example => {
:enabled => true,
:phrase => "Scenario Outline with only one example.",
:score => WARNING,
:targets => ["Scenario"],
:reason => lambda { |scenario, rule| scenario.outline? and scenario.examples_table.size == 2}
},
:too_many_examples => {
:enabled => true,
:phrase => "Scenario Outline with too many examples.",
:score => WARNING,
:max => 10,
:targets => ["Scenario"],
:reason => lambda { |scenario, rule| scenario.outline? and (scenario.examples_table.size - 1) >= rule.conditions[:max]}
},
:multiple_given_when_then => {
:enabled => true,
:phrase => "Given/When/Then used multiple times in the same {class}.",
:score => WARNING,
:targets => ["Scenario", "Background"],
:reason => lambda { |scenario, rule|
phrase = rule.phrase.gsub('{class}', scenario.type)
['Given', 'When', 'Then'].each do |step_start|
scenario.store_rule(rule, phrase) if scenario.get_steps(step_start).size > 1
end}
},
:too_many_parameters => {
:enabled => true,
:phrase => "Too many parameters in Step Definition.",
:score => WARNING,
:max => 4,
:targets => ["StepDefinition"],
:reason => lambda { |step_definition, rule| step_definition.parameters.size > rule.conditions[:max]}
},
:lazy_debugging => {
:enabled => true,
:phrase => "Lazy Debugging through puts, p, or print",
:score => WARNING,
:targets => ["StepDefinition"],
:reason => lambda { |step_definition, rule| step_definition.code.each {|line| step_definition.store_rule(rule) if line.strip =~ /^(p|puts)( |\()('|"|%(q|Q)?\{)/}}
},
:pending => {
:enabled => true,
:phrase => "Pending step definition. Implement or remove.",
:score => WARNING,
:targets => ["StepDefinition"],
:reason => lambda { |step_definition, rule| step_definition.code.each {|line|
if line =~ /^\s*pending(\(.*\))?(\s*[#].*)?$/
step_definition.store_rule(rule)
break
end
}}
},
:feature_same_tag => {
:enabled => true,
:phrase => "Same tag appears on both Feature and Scenario",
:score => WARNING,
:targets => ["Feature"],
:reason => lambda { |feature, rule| if(feature.scenarios.count >= 2)
feature.scenarios[1..-1].each do |scenario|
feature.tags.each do |tag|
feature.store_rule(rule) if scenario.tags.include?(tag)
end
end
end}
},
:scenario_same_tag => {
:enabled => true,
:phrase => "Tag appears on all scenarios.",
:score => WARNING,
:targets => ["Feature"],
#TODO really hacky
:reason => lambda { |feature, rule| unless feature.scenarios.empty?
base_tag_list = feature.scenarios.first.tags.clone
feature.scenarios.each do |scenario|
base_tag_list.each do |tag|
base_tag_list.delete(tag) unless scenario.tags.include?(tag)
end
end
base_tag_list.count.times { feature.store_rule(rule) }
end}
},
:commas_in_description => {
:enabled => true,
:phrase => "There are commas in the description, creating possible multirunning scenarios or features.",
:score => WARNING,
:targets => ["Feature", "Scenario"],
:reason => lambda { |rule_target, rule| rule_target.name.include?(",")}
},
:commented_tag => {
:enabled => true,
:phrase => "{class} has a commented out tag",
:score => WARNING,
:targets => ["Feature", "Scenario"],
:reason => lambda { |feature_rule_target, rule| feature_rule_target.tags.each do | tag |
feature_rule_target.store_rule(rule, rule.phrase.gsub("{class}", feature_rule_target.type)) if feature_rule_target.is_comment_and_tag?(tag)
end}
},
:empty_hook => {
:enabled => true,
:phrase => "Hook with no content.",
:score => WARNING,
:targets => ["Hook"],
:reason => lambda { |hook, rule| hook.code == []}
},
:hook_all_comments => {
:enabled => true,
:phrase => "Hook is only comments.",
:score => WARNING,
:targets => ["Hook"],
:reason => lambda { |hook, rule| flag = true
hook.code.each do |line|
flag = false if line.match(/^\s*#.*$/).nil?
end
flag}
},
:hook_duplicate_tags => {
:enabled => true,
:phrase => "Hook has duplicate tags.",
:score => WARNING,
:targets => ["Hook"],
:reason => lambda { |hook, rule|
all_tags = []
hook.tags.each { |single_tag| all_tags << single_tag.split(',') }
all_tags.flatten!
unique_tags = all_tags.uniq
true unless all_tags == unique_tags}
},
:duplicate_scenario_name => {
:enabled => true,
:phrase => "Feature has scenarios with the same name.",
:score => WARNING,
:targets => ["Feature"],
:reason => lambda { |feature, rule|
names = feature.scenarios.collect { |scenario| scenario.name }
return names.length > names.uniq.length
}
}
}
info_rules = {
:too_many_tags => {
:enabled => true,
:phrase => "{class} has too many tags.",
:score => INFO,
:max => 8,
:targets => ["Feature", "Scenario"],
:reason => lambda { |feature_rule_target, rule| feature_rule_target.tags.size >= rule.conditions[:max]}
},
:long_name => {
:enabled => true,
:phrase => "{class} has a long description.",
:score => INFO,
:max => 180,
:targets => ["Feature", "Scenario", "Background"],
:reason => lambda { |feature_rule_target, rule| feature_rule_target.name.length >= rule.conditions[:max]}
},
:implementation_word => {
:enabled => true,
:phrase => "Implementation word used: {word}.",
:score => INFO,
:words => ["page", "site", "url", "drop down", "dropdown", "select list", "click", "text box", "radio button", "check box", "xml", "window", "pop up", "pop-up", "screen", "database", "DB"],
:targets => ["Scenario", "Background"],
:reason => lambda { |scenario, rule| scenario.steps.each do |step|
next if scenario.is_comment?(step)
rule.conditions[:words].each do |word|
new_phrase = rule.phrase.gsub(/{.*}/, word)
scenario.store_rule(rule, new_phrase) if step.include?(word)
end
end}
},
:implementation_word_button => {
:enabled => true,
:phrase => "Implementation word used: button.",
:score => INFO,
:targets => ["Scenario"],
:reason => lambda { |scenario, rule| scenario.steps.each do |step|
matches = step.match(/(?<prefix>\w+)\sbutton/i)
if(!matches.nil? and matches[:prefix].downcase != 'radio')
scenario.store_rule(rule)
end
end}
},
:implementation_word_tab => {
:enabled => true,
:phrase => "Implementation word used: tab.",
:score => INFO,
:targets => ["Scenario"],
:reason => lambda { |scenario, rule| scenario.steps.each do |step|
scenario.store_rule(rule) if (step.split.include?("tab"))
end}
},
:too_many_scenarios => {
:enabled => true,
:phrase => "Feature with too many scenarios.",
:score => INFO,
:max => 10,
:targets => ["Feature"],
:reason => lambda { |feature, rule| feature.scenarios.size >= rule.conditions[:max]}
},
:date_used => {
:enabled => true,
:phrase => "Date used.",
:score => INFO,
:targets => ["Scenario", "Background"],
:reason => lambda { |scenario, rule| scenario.steps.each {|step| scenario.store_rule(rule) if step =~ CukeSniffer::Constants::DATE_REGEX}}
},
:nested_step => {
:enabled => true,
:phrase => "Nested step call.",
:score => INFO,
:targets => ["StepDefinition"],
:reason => lambda { |step_definition, rule| !step_definition.nested_steps.empty?}
},
:commented_code => {
:enabled => true,
:phrase => "Commented code in Step Definition.",
:score => INFO,
:targets => ["StepDefinition"],
:reason => lambda { |step_definition, rule| step_definition.code.each {|line| step_definition.store_rule(rule) if step_definition.is_comment?(line)}}
},
:small_sleep => {
:enabled => true,
:phrase => "Small sleeps used. Use a wait_until like method.",
:score => INFO,
:max => 2,
:targets => ["StepDefinition"],
:reason => lambda { |step_definition, rule| step_definition.code.each do |line|
match_data = line.match /^\s*sleep(\s|\()(?<sleep_time>.*)\)?/
if match_data
sleep_value = match_data[:sleep_time].to_f
step_definition.store_rule(rule) if sleep_value < rule.conditions[:max]
end
end}
},
:large_sleep => {
:enabled => true,
:phrase => "Large sleeps used. Use a wait_until like method.",
:score => INFO,
:min => 2,
:targets => ["StepDefinition"],
:reason => lambda { |step_definition, rule| step_definition.code.each do |line|
match_data = line.match /^\s*sleep(\s|\()(?<sleep_time>.*)\)?/
if match_data
sleep_value = match_data[:sleep_time].to_f
step_definition.store_rule(rule) if sleep_value > rule.conditions[:min]
end
end}
},
:todo => {
:enabled => true,
:phrase => "Todo found. Resolve it.",
:score => INFO,
:targets => ["StepDefinition"],
:reason => lambda { |step_definition, rule| step_definition.store_rule_many_times(rule, step_definition.todo.size)}
},
:hook_not_in_hooks_file => {
:enabled => true,
:phrase => "Hook found outside of the designated hooks file",
:score => INFO,
:file => "hooks.rb",
:targets => ["Hook"],
:reason => lambda { |hook, rule| hook.location.include?(rule.conditions[:file]) != true}
},
}
# Master hash used for rule data
# * +:enabled+
# * +:phrase+
# * +:score+
# * +:targets+
# * +:reason+
# Optional:
# * +:words+
# * +:max+
# * +:min+
# * +:file+
RULES = {}.merge fatal_rules.merge error_rules.merge warning_rules.merge info_rules
end
end