cuke_sniffer 0.0.5 → 0.0.6

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 (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
+