cuke_sniffer 0.0.5 → 0.0.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (31) hide show
  1. data/bin/cuke_sniffer +85 -63
  2. data/lib/cuke_sniffer.rb +23 -16
  3. data/lib/cuke_sniffer/cli.rb +269 -523
  4. data/lib/cuke_sniffer/constants.rb +5 -2
  5. data/lib/cuke_sniffer/cuke_sniffer_helper.rb +165 -0
  6. data/lib/cuke_sniffer/dead_steps_helper.rb +54 -0
  7. data/lib/cuke_sniffer/feature.rb +13 -81
  8. data/lib/cuke_sniffer/feature_rules_evaluator.rb +56 -115
  9. data/lib/cuke_sniffer/formatter.rb +142 -0
  10. data/lib/cuke_sniffer/hook.rb +77 -160
  11. data/lib/cuke_sniffer/report/dead_steps.html.erb +36 -0
  12. data/lib/cuke_sniffer/report/features.html.erb +60 -0
  13. data/lib/cuke_sniffer/report/hooks.html.erb +49 -0
  14. data/lib/cuke_sniffer/report/improvement_list.html.erb +18 -0
  15. data/lib/cuke_sniffer/report/legend.html.erb +167 -0
  16. data/lib/cuke_sniffer/report/min_template.html.erb +125 -0
  17. data/lib/cuke_sniffer/report/rules.html.erb +9 -0
  18. data/lib/cuke_sniffer/report/standard_template.html.erb +137 -0
  19. data/lib/cuke_sniffer/report/step_definitions.html.erb +49 -0
  20. data/lib/cuke_sniffer/report/sub_rules.html.erb +24 -0
  21. data/lib/cuke_sniffer/report/summary.html.erb +71 -0
  22. data/lib/cuke_sniffer/rule.rb +28 -0
  23. data/lib/cuke_sniffer/rule_config.rb +241 -48
  24. data/lib/cuke_sniffer/rule_target.rb +62 -0
  25. data/lib/cuke_sniffer/rules_evaluator.rb +65 -70
  26. data/lib/cuke_sniffer/scenario.rb +102 -238
  27. data/lib/cuke_sniffer/step_definition.rb +163 -239
  28. data/lib/cuke_sniffer/summary_helper.rb +91 -0
  29. data/lib/cuke_sniffer/summary_node.rb +19 -0
  30. metadata +23 -5
  31. data/lib/cuke_sniffer/report/markup.rhtml +0 -353
@@ -10,12 +10,15 @@ module CukeSniffer
10
10
  COMMENT_REGEX = /#?\s*/ # :nodoc:
11
11
  TAG_REGEX = /(^|\s+)(?<tag>@\S*)/ # :nodoc:
12
12
  SCENARIO_TITLE_STYLES = /^\s*\#*\s*(?<type>Background|Scenario|Scenario Outline|Scenario Template):\s*/ # :nodoc:
13
- STEP_STYLES = /(?<style>Given|When|Then|And|Or|But|Transform|\*)\s+/ # :nodoc:
13
+ STEP_STYLES = /(?<style>Given|When|Then|And|Or|But|Transform|\*)\s*/ # :nodoc:
14
14
  STEP_REGEX = /^#{COMMENT_REGEX}#{STEP_STYLES}(?<step_string>.*)/ # :nodoc:
15
- STEP_DEFINITION_REGEX = /^#{STEP_STYLES}\/(?<step>.+)\/\sdo\s?(\|(?<parameters>.*)\|)?$/ # :nodoc:
15
+ STEP_DEFINITION_REGEX = /^#{STEP_STYLES}[(]?\/(?<step>.+)\/[)]?\sdo\s?(\|(?<parameters>.*)\|)?$/ # :nodoc:
16
16
  HOOK_STYLES = /(?<type>Before|After|AfterConfiguration|at_exit|Around|AfterStep)/ # :nodoc:
17
17
  HOOK_REGEX = /^#{HOOK_STYLES}(\((?<tags>.*)\)\sdo|\s+do)(\s\|(?<parameters>.*)\|)?/
18
18
 
19
+ MARKUP_SOURCE = File.join(File.dirname(__FILE__), 'report')
20
+ DEFAULT_OUTPUT_FILE_NAME = "cuke_sniffer_result"
21
+
19
22
  # hash: Stores scores to compare against for determining if an object is good
20
23
  # * Key: String of the object name
21
24
  # Project, Feature, Scenario, StepDefinition
@@ -0,0 +1,165 @@
1
+ module CukeSniffer
2
+ # Author:: Robert Cochran (mailto:cochrarj@miamioh.edu)
3
+ # Copyright:: Copyright (C) 2013 Robert Cochran
4
+ # License:: Distributes under the MIT License
5
+ # Static class used for aiding cuke_sniffer in various tasks
6
+ class CukeSnifferHelper
7
+
8
+ # Iterates over the passed features list and returns all steps found in scenarios and backgrounds.
9
+ def self.extract_steps_from_features(features)
10
+ steps = {}
11
+ features.each do |feature|
12
+ steps.merge! extract_scenario_steps(feature.background) unless feature.background.nil?
13
+ feature.scenarios.each do |scenario|
14
+ if scenario.type == "Scenario Outline"
15
+ steps.merge! extract_scenario_outline_steps(scenario)
16
+ else
17
+ steps.merge! extract_scenario_steps(scenario)
18
+ end
19
+ end
20
+ end
21
+ steps
22
+ end
23
+
24
+ # Iterates over the passed features list and returns all scenarios and backgrounds found.
25
+ def self.get_all_scenarios(features)
26
+ scenarios = []
27
+ features.each do |feature|
28
+ scenarios << feature.background unless feature.background.nil?
29
+ scenarios << feature.scenarios
30
+ end
31
+ scenarios.flatten
32
+ end
33
+
34
+ # Grabs the values from an example without the bars
35
+ def self.extract_variables_from_example(example)
36
+ example = example[example.index('|')..example.length]
37
+ example.split(/\s*\|\s*/) - [""]
38
+ end
39
+
40
+ # Creates a step call from the details of an example table
41
+ def self.build_updated_step_from_example(step, variable_list, row_variables)
42
+ new_step = step.dup
43
+ variable_list.each do |variable|
44
+ if step.include? variable
45
+ table_variable_to_insert = row_variables[variable_list.index(variable)]
46
+ table_variable_to_insert ||= ""
47
+ new_step.gsub!("<#{variable}>", table_variable_to_insert)
48
+ end
49
+ end
50
+ new_step
51
+ end
52
+
53
+ # Creates a hash of steps with the build up example step calls.
54
+ def self.extract_scenario_outline_steps(scenario)
55
+ steps = {}
56
+ examples = scenario.examples_table
57
+ return {} if examples.empty?
58
+ variable_list = extract_variables_from_example(examples.first)
59
+ (1...examples.size).each do |example_counter|
60
+ #TODO Abstraction needed for this regex matcher (constants?)
61
+ next if examples[example_counter] =~ /^\#.*$/
62
+ row_variables = extract_variables_from_example(examples[example_counter])
63
+ step_counter = 1
64
+ scenario.steps.each do |step|
65
+ step_line = scenario.start_line + step_counter
66
+ location = "#{scenario.location.gsub(/\d+$/, step_line.to_s)}(Example #{example_counter})"
67
+ steps[location] = build_updated_step_from_example(step, variable_list, row_variables)
68
+ step_counter += 1
69
+ end
70
+ end
71
+ steps
72
+ end
73
+
74
+ # Returns all steps found in a scenario
75
+ def self.extract_scenario_steps(scenario)
76
+ steps_hash = {}
77
+ counter = 1
78
+ scenario.steps.each do |step|
79
+ location = scenario.location.gsub(/:\d*$/, ":#{scenario.start_line + counter}")
80
+ steps_hash[location] = step
81
+ counter += 1
82
+ end
83
+ steps_hash
84
+ end
85
+
86
+ # Builds a list of rule objects out of a hash. See CukeSniffer::RulesConfig for hash example.
87
+ def self.build_rules(rules)
88
+ return [] if rules.nil?
89
+ rules.collect do |key, value|
90
+ CukeSniffer::CukeSnifferHelper.build_rule(value)
91
+ end
92
+ end
93
+
94
+ # Builds rule object out of a hash. See CukeSniffer::RulesConfig for hash example.
95
+ def self.build_rule(rule_hash)
96
+ rule = CukeSniffer::Rule.new
97
+ rule.phrase = rule_hash[:phrase]
98
+ rule.score = rule_hash[:score]
99
+ rule.enabled = rule_hash[:enabled]
100
+ conditional_keys = rule_hash.keys - [:phrase, :score, :enabled, :targets, :reason]
101
+ conditions = {}
102
+ conditional_keys.each do |key|
103
+ conditions[key] = (rule_hash[key].kind_of? Array) ? Array.new(rule_hash[key]) : rule_hash[key]
104
+ end
105
+ rule.conditions = conditions
106
+ rule.reason = rule_hash[:reason]
107
+ rule.targets = rule_hash[:targets]
108
+ rule
109
+ end
110
+
111
+ # Returns a list of all nested step calls found in a step definition.
112
+ def self.extract_steps_from_step_definitions(step_definitions)
113
+ steps = {}
114
+ step_definitions.each do |definition|
115
+ definition.nested_steps.each_key do |key|
116
+ steps[key] = definition.nested_steps[key]
117
+ end
118
+ end
119
+ steps
120
+ end
121
+
122
+ # Returns a fuzzy match for a step definition for cataloging steps.
123
+ def self.convert_steps_with_expressions(steps_with_expressions)
124
+ step_regex_hash = {}
125
+ steps_with_expressions.each do |step_location, step_value|
126
+ modified_step = step_value.gsub(/\#{[^}]*}/, '.*')
127
+ step_regex_hash[step_location] = Regexp.new('^' + modified_step + '$')
128
+ end
129
+ step_regex_hash
130
+ end
131
+
132
+ # Extracts all possible step calls from the passed features and step definitions.
133
+ def self.get_all_steps(features, step_definitions)
134
+ feature_steps = CukeSniffer::CukeSnifferHelper.extract_steps_from_features(features)
135
+ step_definition_steps = CukeSniffer::CukeSnifferHelper.extract_steps_from_step_definitions(step_definitions)
136
+ feature_steps.merge step_definition_steps
137
+ end
138
+
139
+ # Applies all possible fuzzy calls to a step definition.
140
+ def self.catalog_possible_dead_steps(step_definitions, steps_with_expressions)
141
+ step_definitions.each do |step_definition|
142
+ next unless step_definition.calls.empty?
143
+ regex_as_string = step_definition.regex.to_s.gsub(/\(\?-mix:\^?/, "").gsub(/\$\)$/, "")
144
+ steps_with_expressions.each do |step_location, step_value|
145
+ if regex_as_string =~ step_value
146
+ step_definition.add_call(step_location, step_value)
147
+ end
148
+ end
149
+ end
150
+ step_definitions
151
+ end
152
+
153
+ # Returns a list of all step definitions with a capture group
154
+ def self.get_steps_with_expressions(steps)
155
+ steps_with_expressions = {}
156
+ steps.each do |step_location, step_value|
157
+ if step_value =~ /\#{.*}/
158
+ steps_with_expressions[step_location] = step_value
159
+ end
160
+ end
161
+ steps_with_expressions
162
+ end
163
+
164
+ end
165
+ end
@@ -0,0 +1,54 @@
1
+ module CukeSniffer
2
+ # Author:: Robert Cochran (mailto:cochrarj@miamioh.edu)
3
+ # Copyright:: Copyright (C) 2013 Robert Cochran
4
+ # License:: Distributes under the MIT License
5
+ # Mixins: CukeSniffer::Constants
6
+ # A static class to aid in the identification of dead steps.
7
+ class DeadStepsHelper
8
+ include CukeSniffer::Constants
9
+
10
+ # Returns a hash of dead steps for displaying in the html.
11
+ def self.build_dead_steps_hash(step_definitions)
12
+ dead_steps_hash = gather_all_dead_steps_by_file(step_definitions)
13
+ sort_dead_steps_in_file!(dead_steps_hash)
14
+ dead_steps_hash[:total] = count_dead_steps(dead_steps_hash)
15
+ dead_steps_hash
16
+ end
17
+
18
+ # Returns all dead step definitions in a file
19
+ def self.gather_all_dead_steps_by_file(step_definitions)
20
+ dead_steps_hash = {}
21
+ step_definitions.each do |step_definition|
22
+ location_match = step_definition.location.match(/(?<file>.*).rb:(?<line>\d+)/)
23
+ file_name = location_match[:file]
24
+ regex = format_step_definition_regex(step_definition.regex)
25
+ if step_definition.calls.empty?
26
+ dead_steps_hash[file_name] ||= []
27
+ dead_steps_hash[file_name] << "#{location_match[:line]}: /#{regex}/"
28
+ end
29
+ end
30
+ dead_steps_hash
31
+ end
32
+
33
+ # Formats the regex of a step definition to remove the (?-mix) part of the to_s
34
+ def self.format_step_definition_regex(regex)
35
+ regex.to_s.match(/\(\?\-mix\:(?<regex>.*)\)/)[:regex]
36
+ end
37
+
38
+ # Sorts the dead steps found in a hash by the line number in the file
39
+ def self.sort_dead_steps_in_file!(dead_steps_hash)
40
+ dead_steps_hash.each_key do |file|
41
+ dead_steps_hash[file].sort_by! { |row| row[/^\d+/].to_i }
42
+ end
43
+ end
44
+
45
+ # Returns the count of all possible dead steps in each file
46
+ def self.count_dead_steps(dead_steps_hash)
47
+ count = 0
48
+ dead_steps_hash.each_value do |dead_steps_in_file_list|
49
+ count += dead_steps_in_file_list.size
50
+ end
51
+ count
52
+ end
53
+ end
54
+ end
@@ -6,9 +6,9 @@ module CukeSniffer
6
6
  # Handles feature files and disassembles and evaluates
7
7
  # its components.
8
8
  # Extends CukeSniffer::FeatureRulesEvaluator
9
- class Feature < FeatureRulesEvaluator
9
+ class Feature < FeatureRuleTarget
10
10
 
11
- xml_accessor :scenarios, :as => [CukeSniffer::FeatureRulesEvaluator], :in => "scenarios"
11
+ xml_accessor :scenarios, :as => [CukeSniffer::FeatureRuleTarget], :in => "scenarios"
12
12
 
13
13
  SCENARIO_TITLE_REGEX = /#{COMMENT_REGEX}#{SCENARIO_TITLE_STYLES}(?<name>.*)/ # :nodoc:
14
14
 
@@ -24,20 +24,17 @@ module CukeSniffer
24
24
  # int: Total score of the feature and its scenarios
25
25
  attr_accessor :total_score
26
26
 
27
+ # String array: A list of all the lines in a feature file
28
+ attr_accessor :feature_lines
29
+
27
30
  # file_name must be in the format of "file_path\file_name.feature"
28
31
  def initialize(file_name)
29
32
  super(file_name)
30
33
  @scenarios = []
31
34
  @scenarios_score = 0
32
35
  @total_score = 0
33
- feature_lines = extract_feature_from_file(file_name)
34
- if feature_lines == []
35
- rule = RULES[:empty_feature]
36
- store_rule(rule)
37
- else
38
- split_feature(file_name, feature_lines)
39
- evaluate_score
40
- end
36
+ @feature_lines = IO.readlines(file_name)
37
+ split_feature(file_name, feature_lines) unless @feature_lines == []
41
38
  end
42
39
 
43
40
  def ==(comparison_object) # :nodoc:
@@ -45,16 +42,14 @@ module CukeSniffer
45
42
  comparison_object.scenarios == scenarios
46
43
  end
47
44
 
48
- private
49
-
50
- def extract_feature_from_file(file_name)
51
- feature_lines = []
52
- feature_file = File.open(file_name)
53
- feature_file.each_line { |line| feature_lines << line }
54
- feature_file.close
55
- feature_lines
45
+ def update_score
46
+ @scenarios_score += @background.score unless @background.nil?
47
+ @scenarios.each { |scenario| @scenarios_score += scenario.score }
48
+ @total_score = @scenarios_score + @score
56
49
  end
57
50
 
51
+ private
52
+
58
53
  def split_feature(file_name, feature_lines)
59
54
  index = 0
60
55
  until feature_lines[index].match /Feature:\s*(?<name>.*)/
@@ -99,74 +94,11 @@ module CukeSniffer
99
94
 
100
95
  def add_scenario_to_feature(code_block, index_of_title)
101
96
  scenario = CukeSniffer::Scenario.new(index_of_title, code_block)
102
- feature_applied_scenario_rules(scenario)
103
97
  if scenario.type == "Background"
104
98
  @background = scenario
105
99
  else
106
100
  @scenarios << scenario
107
101
  end
108
102
  end
109
-
110
- def evaluate_score
111
- super
112
- rule_no_scenarios
113
- rule_too_many_scenarios
114
- rule_background_with_no_scenarios
115
- rule_background_with_one_scenario
116
- rule_scenario_same_tag
117
- get_scenarios_score
118
- @total_score = score + @scenarios_score
119
- end
120
-
121
- def feature_applied_scenario_rules(scenario)
122
- rule_feature_same_tags(scenario)
123
- end
124
-
125
- def rule_feature_same_tags(scenario)
126
- rule = RULES[:feature_same_tag]
127
- tags.each do |tag|
128
- scenario.store_rule(rule, rule[:phrase]) if scenario.tags.include?(tag)
129
- end
130
- end
131
-
132
- def rule_scenario_same_tag
133
- rule = RULES[:scenario_same_tag]
134
- unless scenarios.empty?
135
- base_tag_list = scenarios.first.tags.clone
136
- scenarios.each do |scenario|
137
- base_tag_list.each do |tag|
138
- base_tag_list.delete(tag) unless scenario.tags.include?(tag)
139
- end
140
- end
141
- base_tag_list.count.times do
142
- store_rule(rule, rule[:phrase])
143
- end
144
- end
145
- end
146
-
147
- def get_scenarios_score
148
- @scenarios_score += @background.score unless @background.nil?
149
- @scenarios.each { |scenario| @scenarios_score += scenario.score }
150
- end
151
-
152
- def rule_no_scenarios
153
- rule = RULES[:no_scenarios]
154
- store_rule(rule) if @scenarios.empty?
155
- end
156
-
157
- def rule_too_many_scenarios
158
- rule = RULES[:too_many_scenarios]
159
- store_rule(rule) if @scenarios.size >= rule[:max]
160
- end
161
-
162
- def rule_background_with_no_scenarios
163
- rule = RULES[:background_with_no_scenarios]
164
- store_rule(rule) if @scenarios.empty? and !@background.nil?
165
- end
166
-
167
- def rule_background_with_one_scenario
168
- rule = RULES[:background_with_one_scenario]
169
- store_rule(rule) if @scenarios.size == 1 and !@background.nil?
170
- end
171
103
  end
172
104
  end
@@ -1,115 +1,56 @@
1
- require 'cuke_sniffer/constants'
2
- require 'cuke_sniffer/rule_config'
3
- require 'cuke_sniffer/rules_evaluator'
4
-
5
- module CukeSniffer
6
-
7
- # Author:: Robert Cochran (mailto:cochrarj@miamioh.edu)
8
- # Copyright:: Copyright (C) 2013 Robert Cochran
9
- # License:: Distributes under the MIT License
10
- # Parent class for Feature and Scenario objects
11
- # holds shared attributes and rules.
12
- # Extends CukeSniffer::RulesEvaluator
13
- class FeatureRulesEvaluator < RulesEvaluator
14
-
15
- # string array: Contains all tags attached to a Feature or Scenario
16
- attr_accessor :tags
17
-
18
- # string: Name of the Feature or Scenario
19
- attr_accessor :name
20
-
21
- # Location must be in the format of "file_path\file_name.rb:line_number"
22
- def initialize(location)
23
- @name = ""
24
- @tags = []
25
- super(location)
26
- end
27
-
28
- def == (comparison_object) # :nodoc:
29
- super(comparison_object) &&
30
- comparison_object.name == name &&
31
- comparison_object.tags == tags
32
- end
33
-
34
- private
35
-
36
- def create_name(line, filter)
37
- line.gsub!(/#{COMMENT_REGEX}#{filter}/, "")
38
- line.strip!
39
- @name += " " unless @name.empty? or line.empty?
40
- @name += line
41
- end
42
-
43
- def update_tag_list(line)
44
- comment_start = (line =~ /([^@\w]#)|(^#)/)
45
-
46
- if comment_start
47
- line[0...comment_start].split.each { |single_tag| @tags << single_tag }
48
- @tags << line[comment_start..line.length].strip
49
- else
50
- line.split.each { |single_tag| @tags << single_tag }
51
- end
52
- end
53
-
54
- def evaluate_score
55
- cls_name = self.class.to_s.gsub('CukeSniffer::', '')
56
- rule_too_many_tags(cls_name)
57
- rule_no_description(cls_name)
58
- rule_numbers_in_name(cls_name)
59
- rule_long_name(cls_name)
60
- rule_commas_in_description(cls_name)
61
- rule_comment_after_tag(cls_name)
62
- rule_commented_tag(cls_name)
63
- end
64
-
65
- def rule_too_many_tags(type)
66
- rule = RULES[:too_many_tags]
67
- rule_phrase = rule[:phrase].gsub(/{.*}/, type)
68
- store_rule(rule, rule_phrase) if tags.size >= rule[:max]
69
- end
70
-
71
- def rule_no_description(type)
72
- rule = RULES[:no_description]
73
- rule_phrase = rule[:phrase].gsub(/{.*}/, type)
74
- store_rule(rule, rule_phrase) if name.empty?
75
- end
76
-
77
- def rule_numbers_in_name(type)
78
- rule = RULES[:numbers_in_description]
79
- rule_phrase = rule[:phrase].gsub(/{.*}/, type)
80
- store_rule(rule, rule_phrase) if name =~ /\d/
81
- end
82
-
83
- def rule_long_name(type)
84
- rule = RULES[:long_name]
85
- rule_phrase = rule[:phrase].gsub(/{.*}/, type)
86
- store_rule(rule, rule_phrase) if name.size >= rule[:max]
87
- end
88
-
89
- def rule_commas_in_description(type)
90
- rule = RULES[:commas_in_description]
91
- rule_phrase = rule[:phrase].gsub(/{.*}/, type)
92
- store_rule(rule, rule_phrase) if name.include?(',')
93
- end
94
-
95
- def rule_comment_after_tag(type)
96
- rule = RULES[:comment_after_tag]
97
-
98
- last_comment_index = tags.rindex { |single_tag| is_comment?(single_tag) }
99
- if last_comment_index
100
- comment_after_tag = tags[0...last_comment_index].any? { |single_tag| !is_comment?(single_tag) }
101
- rule_phrase = rule[:phrase].gsub(/{.*}/, type)
102
- store_rule(rule, rule_phrase) if comment_after_tag
103
- end
104
- end
105
-
106
- def rule_commented_tag(type)
107
- rule = RULES[:commented_tag]
108
- tags.each do |tag|
109
- store_rule(rule, rule[:phrase].gsub(/{.*}/, type)) if is_comment?(tag) && tag.match(/@\S*/)
110
- end
111
- end
112
-
113
- end
114
- end
115
-
1
+ require 'cuke_sniffer/constants'
2
+ require 'cuke_sniffer/rule_config'
3
+ require 'cuke_sniffer/rule_target'
4
+
5
+ module CukeSniffer
6
+
7
+ # Author:: Robert Cochran (mailto:cochrarj@miamioh.edu)
8
+ # Copyright:: Copyright (C) 2013 Robert Cochran
9
+ # License:: Distributes under the MIT License
10
+ # Parent class for Feature and Scenario objects
11
+ # holds shared attributes and rules.
12
+ # Extends CukeSniffer::RuleTarget
13
+ class FeatureRuleTarget < RuleTarget
14
+
15
+ # string array: Contains all tags attached to a Feature or Scenario
16
+ attr_accessor :tags
17
+
18
+ # string: Name of the Feature or Scenario
19
+ attr_accessor :name
20
+
21
+ # Location must be in the format of "file_path\file_name.rb:line_number"
22
+ def initialize(location)
23
+ @name = ""
24
+ @tags = []
25
+ super(location)
26
+ end
27
+
28
+ def == (comparison_object) # :nodoc:
29
+ super(comparison_object) &&
30
+ comparison_object.name == name &&
31
+ comparison_object.tags == tags
32
+ end
33
+
34
+ private
35
+
36
+ def create_name(line, filter)
37
+ line.gsub!(/#{COMMENT_REGEX}#{filter}/, "")
38
+ line.strip!
39
+ @name += " " unless @name.empty? or line.empty?
40
+ @name += line
41
+ end
42
+
43
+ def update_tag_list(line)
44
+ comment_start = (line =~ /([^@\w]#)|(^#)/)
45
+
46
+ if comment_start
47
+ line[0...comment_start].split.each { |single_tag| @tags << single_tag }
48
+ @tags << line[comment_start..line.length].strip
49
+ else
50
+ line.split.each { |single_tag| @tags << single_tag }
51
+ end
52
+ end
53
+
54
+ end
55
+ end
56
+