chutney 1.6.3 → 2.0.0.rc1

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 (62) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +1 -0
  3. data/.rubocop.yml +3 -3
  4. data/README.md +44 -30
  5. data/Rakefile +10 -24
  6. data/chutney.gemspec +13 -10
  7. data/config/{default.yml → chutney.yml} +6 -3
  8. data/docs/.keep +0 -0
  9. data/exe/chutney +28 -22
  10. data/img/chutney.svg +852 -0
  11. data/img/formatters.png +0 -0
  12. data/lib/chutney.rb +61 -85
  13. data/lib/chutney/configuration.rb +6 -7
  14. data/lib/chutney/formatter.rb +21 -0
  15. data/lib/chutney/formatter/json_formatter.rb +8 -0
  16. data/lib/chutney/formatter/pie_formatter.rb +78 -0
  17. data/lib/chutney/formatter/rainbow_formatter.rb +47 -0
  18. data/lib/chutney/linter.rb +145 -113
  19. data/lib/chutney/linter/avoid_full_stop.rb +12 -0
  20. data/lib/chutney/linter/avoid_outline_for_single_example.rb +3 -6
  21. data/lib/chutney/linter/avoid_scripting.rb +10 -15
  22. data/lib/chutney/linter/background_does_more_than_setup.rb +7 -10
  23. data/lib/chutney/linter/background_requires_multiple_scenarios.rb +2 -3
  24. data/lib/chutney/linter/bad_scenario_name.rb +4 -11
  25. data/lib/chutney/linter/file_name_differs_feature_name.rb +5 -10
  26. data/lib/chutney/linter/givens_after_background.rb +17 -0
  27. data/lib/chutney/linter/invalid_file_name.rb +14 -6
  28. data/lib/chutney/linter/invalid_step_flow.rb +18 -18
  29. data/lib/chutney/linter/missing_example_name.rb +13 -11
  30. data/lib/chutney/linter/missing_feature_description.rb +2 -9
  31. data/lib/chutney/linter/missing_feature_name.rb +3 -12
  32. data/lib/chutney/linter/missing_scenario_name.rb +3 -7
  33. data/lib/chutney/linter/missing_test_action.rb +3 -6
  34. data/lib/chutney/linter/missing_verification.rb +3 -4
  35. data/lib/chutney/linter/required_tags_starts_with.rb +20 -5
  36. data/lib/chutney/linter/same_tag_for_all_scenarios.rb +24 -28
  37. data/lib/chutney/linter/scenario_names_match.rb +6 -8
  38. data/lib/chutney/linter/tag_used_multiple_times.rb +6 -13
  39. data/lib/chutney/linter/too_clumsy.rb +4 -4
  40. data/lib/chutney/linter/too_long_step.rb +8 -18
  41. data/lib/chutney/linter/too_many_different_tags.rb +13 -34
  42. data/lib/chutney/linter/too_many_steps.rb +10 -6
  43. data/lib/chutney/linter/too_many_tags.rb +11 -10
  44. data/lib/chutney/linter/unique_scenario_names.rb +19 -13
  45. data/lib/chutney/linter/unknown_variable.rb +5 -6
  46. data/lib/chutney/linter/unused_variable.rb +6 -7
  47. data/lib/chutney/linter/use_background.rb +11 -29
  48. data/lib/chutney/linter/use_outline.rb +21 -15
  49. data/lib/chutney/version.rb +1 -1
  50. data/lib/config/locales/en.yml +93 -0
  51. data/spec/chutney_spec.rb +54 -62
  52. data/spec/spec_helper.rb +103 -0
  53. metadata +75 -44
  54. data/Guardfile +0 -3
  55. data/lib/chutney/linter/avoid_period.rb +0 -19
  56. data/lib/chutney/linter/be_declarative.rb +0 -49
  57. data/lib/chutney/linter/tag_collector.rb +0 -10
  58. data/lib/chutney/linter/tag_constraint.rb +0 -35
  59. data/spec/configuration_spec.rb +0 -58
  60. data/spec/required_tags_starts_with_spec.rb +0 -74
  61. data/spec/shared_contexts/file_exists.rb +0 -12
  62. data/spec/shared_contexts/gherkin_linter.rb +0 -14
Binary file
@@ -1,13 +1,14 @@
1
- require 'gherkin/parser'
1
+ require 'amatch'
2
+ require 'chutney/configuration'
2
3
  require 'chutney/linter'
4
+ require 'chutney/linter/avoid_full_stop'
3
5
  require 'chutney/linter/avoid_outline_for_single_example'
4
- require 'chutney/linter/avoid_period'
5
6
  require 'chutney/linter/avoid_scripting'
6
7
  require 'chutney/linter/background_does_more_than_setup'
7
8
  require 'chutney/linter/background_requires_multiple_scenarios'
8
9
  require 'chutney/linter/bad_scenario_name'
9
- require 'chutney/linter/be_declarative'
10
10
  require 'chutney/linter/file_name_differs_feature_name'
11
+ require 'chutney/linter/givens_after_background'
11
12
  require 'chutney/linter/invalid_file_name'
12
13
  require 'chutney/linter/invalid_step_flow'
13
14
  require 'chutney/linter/missing_example_name'
@@ -30,103 +31,78 @@ require 'chutney/linter/unknown_variable'
30
31
  require 'chutney/linter/unused_variable'
31
32
  require 'chutney/linter/use_background'
32
33
  require 'chutney/linter/use_outline'
33
- require 'multi_json'
34
+ require 'forwardable'
35
+ require 'gherkin/dialect'
36
+ require 'gherkin/parser'
37
+ require 'i18n'
34
38
  require 'set'
35
- require 'chutney/configuration'
39
+ require 'yaml'
36
40
 
37
41
  module Chutney
38
42
  # gherkin linter
39
43
  class ChutneyLint
44
+ extend Forwardable
40
45
  attr_accessor :verbose
41
- default_file = File.expand_path('..', __dir__), '**/config', 'default.yml'
42
- DEFAULT_CONFIG = Dir.glob(File.join(default_file)).first.freeze
43
- LINTER = Linter.descendants
44
-
45
- def initialize(path = nil)
46
- @files = {}
47
- @linter = []
48
- @config = Configuration.new path || DEFAULT_CONFIG
49
- @verbose = false
50
- end
51
-
52
- def enabled(linter_name, value)
53
- @config.config[linter_name]['Enabled'] = value if @config.config.key? linter_name
46
+ attr_reader :files
47
+ attr_reader :results
48
+
49
+ def_delegators :@files, :<<, :clear, :delete, :include?
50
+
51
+ def initialize(*files)
52
+ @files = files
53
+ @results = Hash.new { |h, k| h[k] = [] }
54
+ i18n_paths = Dir[File.expand_path(File.join(__dir__, 'config/locales')) + '/*.yml']
55
+ return if I18n.load_path.include?(i18n_paths)
56
+
57
+ I18n.load_path << i18n_paths
54
58
  end
55
-
56
- def enable(enabled_linters)
57
- enabled_linters.each do |linter|
58
- enabled linter, true
59
- end
60
- end
61
-
62
- def disable(disabled_linters)
63
- disabled_linters.each do |linter|
64
- enabled linter, false
65
- end
66
- end
67
-
68
- # Testing feature
69
- def disable_all
70
- @config.config.each do |member|
71
- @config.config[member[0]]['Enabled'] = false
59
+
60
+ def configuration
61
+ unless @config
62
+ default_file = [File.expand_path('..', __dir__), '**/config', 'chutney.yml']
63
+ config_file = Dir.glob(File.join(default_file)).first.freeze
64
+ @config = Configuration.new(config_file)
72
65
  end
66
+ @config
73
67
  end
74
-
75
- def set_linter
76
- @linter = []
77
- LINTER.each do |linter|
78
- new_linter = linter.new
79
- linter_enabled = @config.config[new_linter.class.name.split('::').last]['Enabled']
80
- evaluate_members(new_linter) if linter_enabled
81
- @linter.push new_linter if linter_enabled
82
- end
68
+
69
+ def configuration=(config)
70
+ @config = config
83
71
  end
84
-
85
- def evaluate_members(linter)
86
- @config.config[linter.class.name.split('::').last].each do |member, value|
87
- next if member.downcase.casecmp('enabled').zero?
88
-
89
- member = member.downcase.to_sym
90
- raise 'Member not found! Check the YAML' unless linter.respond_to? member
91
-
92
- linter.public_send(member, value)
72
+
73
+ def analyse
74
+ files.each do |f|
75
+ lint(f)
93
76
  end
77
+ @results
94
78
  end
95
-
96
- def analyze(file)
97
- @files[file] = parse file
98
- end
99
-
100
- def parse(file)
101
- to_json File.read(file)
102
- end
103
-
104
- def report
105
- issues = @linter.map do |linter|
106
- linter.lint_files(@files, disable_tags)
107
- linter.issues
108
- end.flatten
109
-
110
- print issues
111
- return 0 if issues.select { |issue| issue.class == Error }.empty?
112
-
113
- -1
79
+ # alias for non-british English
80
+ # https://dictionary.cambridge.org/dictionary/english/analyse
81
+ alias analyze analyse
82
+
83
+ def linters
84
+ @linters ||= Linter.descendants.filter { |l| configuration.dig(l.linter_name, 'Enabled') }
114
85
  end
115
-
116
- def disable_tags
117
- LINTER.map { |lint| "disable#{lint.new.class.name.split('::').last}" }
86
+
87
+ def linters=(*linters)
88
+ @linters = linters
118
89
  end
119
-
120
- def to_json(input)
121
- parser = Gherkin::Parser.new
122
- scanner = Gherkin::TokenScanner.new input
123
-
124
- parser.parse(scanner)
90
+
91
+ private
92
+
93
+ def parse(text)
94
+ @parser ||= Gherkin::Parser.new
95
+ scanner = Gherkin::TokenScanner.new(text)
96
+ @parser.parse(scanner)
125
97
  end
126
-
127
- def print(issues)
128
- puts 'There are no issues' if issues.empty? && @verbose
129
- issues.each { |issue| puts issue.render }
98
+
99
+ def lint(file)
100
+ parsed = parse(File.read(file))
101
+ linters.each do |linter_class|
102
+ linter = linter_class.new(file, parsed, configuration[linter_class.linter_name])
103
+ linter.lint
104
+ @results[file] << { linter: linter.linter_name, issues: linter.issues }
105
+ end
130
106
  end
131
107
  end
132
108
  end
@@ -1,13 +1,12 @@
1
- require 'yaml'
2
- module Chutney
1
+ require 'delegate'
2
+ module Chutney
3
3
  # gherkin_lint configuration object
4
- class Configuration
5
- attr_reader :config
6
-
4
+ class Configuration < SimpleDelegator
7
5
  def initialize(path)
8
6
  @path = path
9
- @config = load_configuration || ''
7
+ @config = load_configuration || {}
10
8
  load_user_configuration
9
+ super(@config)
11
10
  end
12
11
 
13
12
  def configuration_path
@@ -15,7 +14,7 @@ module Chutney
15
14
  end
16
15
 
17
16
  def load_configuration
18
- YAML.load_file configuration_path || '' if File.exist? configuration_path
17
+ YAML.load_file configuration_path || '' if configuration_path
19
18
  end
20
19
 
21
20
  def load_user_configuration
@@ -0,0 +1,21 @@
1
+ module Chutney
2
+ # base class for all formatters
3
+ class Formatter
4
+ attr_accessor :results
5
+
6
+ def initialize
7
+ @results = {}
8
+ end
9
+
10
+ def format(results); end
11
+
12
+ def files
13
+ results.map { |k, _v| k }
14
+ end
15
+
16
+ def files_with_issues
17
+ results.filter { |_k, v| v.any? { |r| r[:issues].count.positive? } }
18
+ end
19
+
20
+ end
21
+ end
@@ -0,0 +1,8 @@
1
+ module Chutney
2
+ # Plain old JSON formatter
3
+ class JSONFormatter < Formatter
4
+ def format
5
+ puts @results.to_json
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,78 @@
1
+ require 'pastel'
2
+ require 'tty/pie'
3
+
4
+ module Chutney
5
+ # format results as pie charts
6
+ class PieFormatter < Formatter
7
+ def initialize
8
+ super
9
+ end
10
+
11
+ def format
12
+ data = top_offences.map do |offence|
13
+ {
14
+ name: offence.first,
15
+ value: offence.last,
16
+ color: colour_loop,
17
+ fill: char_loop
18
+ }
19
+ end
20
+ print_report(data)
21
+ end
22
+
23
+ def print_report(data)
24
+ unless data.empty?
25
+ print TTY::Pie.new(data: data, radius: 8, legend: { format: '%<label>s %<name>s %<value>i' })
26
+ puts
27
+ end
28
+ # put_summary
29
+ end
30
+
31
+ def top_offences
32
+ offence = Hash.new(0)
33
+ files_with_issues.each do |_file, linter|
34
+ linter.each do |lint|
35
+ offence[lint[:linter]] += lint[:issues].count
36
+ end
37
+ end
38
+ offence.reject { |_k, v| v.zero? }.sort_by { |_linter, count| -count }
39
+ end
40
+
41
+ def char_loop
42
+ @char_looper ||= Fiber.new do
43
+ chars = %w[• x + @ * / -]
44
+ current = 0
45
+ loop do
46
+ current = 0 if current >= chars.count
47
+ Fiber.yield chars[current]
48
+ current += 1
49
+ end
50
+ end
51
+ @char_looper.resume
52
+ end
53
+
54
+ def colour_loop
55
+ @colour_looper ||= Fiber.new do
56
+ colours = %i[bright_cyan bright_magenta bright_yellow bright_green]
57
+ current = 0
58
+ loop do
59
+ current = 0 if current >= colours.count
60
+ Fiber.yield colours[current]
61
+ current += 1
62
+ end
63
+ end
64
+ @colour_looper.resume
65
+ end
66
+
67
+ def put_summary
68
+ pastel = Pastel.new
69
+ print "#{files.count} features inspected, "
70
+ if files_with_issues.count.zero?
71
+ puts pastel.green('all taste delicious')
72
+ else
73
+ puts pastel.red("#{files_with_issues.count} taste nasty")
74
+ end
75
+ end
76
+
77
+ end
78
+ end
@@ -0,0 +1,47 @@
1
+ require 'pastel'
2
+
3
+ module Chutney
4
+ # pretty formatter
5
+ class RainbowFormatter < Formatter
6
+
7
+ def initialize
8
+ super
9
+
10
+ @pastel = Pastel.new
11
+ end
12
+
13
+ def format
14
+ files_with_issues.each do |file, linter|
15
+ put_file(file)
16
+ linter.filter { |l| !l[:issues].empty? }.each do |linter_with_issues|
17
+
18
+ put_linter(linter_with_issues)
19
+ linter_with_issues[:issues].each { |i| put_issue(i) }
20
+ end
21
+ end
22
+ put_summary
23
+ end
24
+
25
+ def put_file(file)
26
+ puts @pastel.cyan(file.to_s)
27
+ end
28
+
29
+ def put_linter(linter)
30
+ puts @pastel.red(" #{linter[:linter]}")
31
+ end
32
+
33
+ def put_issue(issue)
34
+ puts " #{@pastel.dim(issue.dig(:location, :line))} #{issue[:message]}"
35
+ end
36
+
37
+ def put_summary
38
+ print "#{files.count} features inspected, "
39
+ if files_with_issues.count.zero?
40
+ puts @pastel.green('all taste delicious')
41
+ else
42
+ puts @pastel.red("#{files_with_issues.count} taste nasty")
43
+ end
44
+ end
45
+
46
+ end
47
+ end
@@ -1,149 +1,181 @@
1
- require 'chutney/issue'
2
-
3
1
  # gherkin utilities
4
2
  module Chutney
5
3
  # base class for all linters
6
4
  class Linter
7
- attr_reader :issues
5
+ attr_accessor :issues
6
+ attr_reader :filename
7
+ attr_reader :configuration
8
+
9
+ Lint = Struct.new(:message, :gherkin_type, :location, :feature, :scenario, :step, keyword_init: true)
8
10
 
9
11
  def self.descendants
10
12
  ObjectSpace.each_object(::Class).select { |klass| klass < self }
11
13
  end
12
14
 
13
- def initialize
15
+ def initialize(filename, content, configuration)
16
+ @content = content
17
+ @filename = filename
14
18
  @issues = []
15
- @files = {}
19
+ @configuration = configuration
20
+ language = @content.dig(:feature, :language) || 'en'
21
+ @dialect = Gherkin::Dialect.for(language)
16
22
  end
17
23
 
18
- def features
19
- @files.each do |file, content|
20
- feature = content[:feature]
21
- next if feature.nil?
22
-
23
- yield(file, feature)
24
+ def lint
25
+ raise 'not implemented'
26
+ end
27
+
28
+ def and_word?(word)
29
+ @dialect.and_keywords.include?(word)
30
+ end
31
+
32
+ def background_word?(word)
33
+ @dialect.background_keywords.include?(word)
34
+ end
35
+
36
+ def but_word?(word)
37
+ @dialect.but_keywords.include?(word)
38
+ end
39
+
40
+ def examples_word?(word)
41
+ @dialect.example_keywords.include?(word)
42
+ end
43
+
44
+ def feature_word?(word)
45
+ @dialect.feature_keywords.include?(word)
46
+ end
47
+
48
+ def given_word?(word)
49
+ @dialect.given_keywords.include?(word)
50
+ end
51
+
52
+ def scenario_outline_word?(word)
53
+ @dialect.scenario_outline_keywords.include?(word)
54
+ end
55
+
56
+ def then_word?(word)
57
+ @dialect.then_keywords.include?(word)
58
+ end
59
+
60
+ def when_word?(word)
61
+ @dialect.when_keywords.include?(word)
62
+ end
63
+
64
+ def tags_for(element)
65
+ return [] unless element.include? :tags
66
+
67
+ element[:tags].map { |tag| tag[:name][1..-1] }
68
+ end
69
+
70
+ def add_issue(message, feature, scenario = nil, step = nil)
71
+ issues << Lint.new(
72
+ message: message,
73
+ gherkin_type: type(feature, scenario, step),
74
+ location: location(feature, scenario, step),
75
+ feature: feature[:name],
76
+ scenario: scenario ? scenario[:name] : nil,
77
+ step: step ? step[:text] : nil
78
+ ).to_h
79
+ end
80
+
81
+ def location(feature, scenario, step)
82
+ if step
83
+ step[:location]
84
+ else
85
+ scenario ? scenario[:location] : feature[:location]
24
86
  end
25
87
  end
26
-
27
- def files
28
- @files.each_key { |file| yield file }
88
+
89
+ def type(_feature, scenario, step)
90
+ if step
91
+ :step
92
+ else
93
+ scenario ? :scenario : :feature
94
+ end
29
95
  end
30
96
 
31
- def scenarios
32
- elements do |file, feature, scenario|
33
- next if scenario[:type] == :Background
34
-
35
- yield(file, feature, scenario)
97
+ def feature
98
+ if block_given?
99
+ yield(@content[:feature]) if @content[:feature]
100
+ else
101
+ @content[:feature]
36
102
  end
37
103
  end
38
-
39
- def filled_scenarios
40
- scenarios do |file, feature, scenario|
41
- next unless scenario.include? :steps
42
- next if scenario[:steps].empty?
104
+
105
+ def elements
106
+ if block_given?
107
+ feature[:children].each do |child|
108
+ next if off_switch?(child)
43
109
 
44
- yield(file, feature, scenario)
110
+ yield(feature, child)
111
+ end
112
+ else
113
+ feature[:children]
45
114
  end
46
115
  end
47
-
48
- def steps
49
- elements do |file, feature, scenario|
50
- next unless scenario.include? :steps
51
-
52
- scenario[:steps].each { |step| yield(file, feature, scenario, step) }
116
+
117
+ def off_switch?(element = feature)
118
+ off_switch = element[:tags]
119
+ .then { |tags| tags || [] }
120
+ .filter { |tag| tag[:type] == :Tag }
121
+ .filter { |tag| tag[:name] == "@disable#{linter_name}" }
122
+ .count
123
+ .positive?
124
+ off_switch ||= off_switch?(feature) unless element == feature
125
+ off_switch
126
+ end
127
+
128
+ def background
129
+ if block_given?
130
+ elements do |feature, child|
131
+ next unless child[:type] == :Background
132
+
133
+ yield(feature, child)
134
+ end
135
+ else
136
+ elements.filter { |child| child[:type] == :Background }
53
137
  end
54
138
  end
55
-
56
- def backgrounds
57
- elements do |file, feature, scenario|
58
- next unless scenario[:type] == :Background
139
+
140
+ def scenarios
141
+ if block_given?
142
+ elements do |feature, child|
143
+ next unless %i[ScenarioOutline Scenario].include? child[:type]
59
144
 
60
- yield(file, feature, scenario)
145
+ yield(feature, child)
146
+ end
147
+ else
148
+ elements.filter { |child| %i[ScenarioOutline Scenario].include? child[:type] }
61
149
  end
62
150
  end
63
-
64
- def elements
65
- @files.each do |file, content|
66
- feature = content[:feature]
67
- next if feature.nil?
68
- next unless feature.key? :children
151
+
152
+ def filled_scenarios
153
+ if block_given?
154
+ scenarios do |feature, scenario|
155
+ next unless scenario.include? :steps
156
+ next if scenario[:steps].empty?
69
157
 
70
- feature[:children].each do |scenario|
71
- yield(file, feature, scenario)
158
+ yield(feature, scenario)
72
159
  end
160
+ else
161
+ scenarios.filter { |s| !s[:steps].empty? }
73
162
  end
74
163
  end
75
-
76
- def name
77
- self.class.name.split('::').last
78
- end
79
-
80
- def lint_files(files, tags_to_suppress)
81
- @files = files
82
- @files = filter_tag(@files, "disable#{name}")
83
- @files = suppress_tags(@files, tags_to_suppress)
84
- lint
85
- end
86
-
87
- def filter_tag(data, tag)
88
- return data.reject { |item| tag?(item, tag) }.map { |item| filter_tag(item, tag) } if data.class == Array
89
- return {} if (data.class == Hash) && (data.include? :feature) && tag?(data[:feature], tag)
90
- return data unless data.respond_to? :each_pair
91
-
92
- result = {}
93
- data.each_pair { |key, value| result[key] = filter_tag(value, tag) }
94
- result
95
- end
96
-
97
- def tag?(data, tag)
98
- return false if data.class != Hash
99
- return false unless data.include? :tags
100
-
101
- data[:tags].map { |item| item[:name] }.include? "@#{tag}"
102
- end
103
-
104
- def suppress_tags(data, tags)
105
- return data.map { |item| suppress_tags(item, tags) } if data.class == Array
106
- return data unless data.class == Hash
107
-
108
- result = {}
109
-
110
- data.each_pair do |key, value|
111
- value = suppress(value, tags) if key == :tags
112
- result[key] = suppress_tags(value, tags)
164
+
165
+ def steps
166
+ elements do |feature, child|
167
+ next unless child.include? :steps
168
+
169
+ child[:steps].each { |step| yield(feature, child, step) }
113
170
  end
114
- result
115
171
  end
116
172
 
117
- def suppress(data, tags)
118
- data.reject { |item| tags.map { |tag| "@#{tag}" }.include? item[:name] }
173
+ def self.linter_name
174
+ name.split('::').last
119
175
  end
120
-
121
- def lint
122
- raise 'not implemented'
123
- end
124
-
125
- def reference(file, feature = nil, scenario = nil, step = nil)
126
- return file if feature.nil? || feature[:name].empty?
127
-
128
- result = "#{file} (#{line(feature, scenario, step)}): #{feature[:name]}"
129
- result += ".#{scenario[:name]}" unless scenario.nil? || scenario[:name].empty?
130
- result += " step: #{step[:text]}" unless step.nil?
131
- result
132
- end
133
-
134
- def line(feature, scenario, step)
135
- line = feature.nil? ? nil : feature[:location][:line]
136
- line = scenario[:location][:line] unless scenario.nil?
137
- line = step[:location][:line] unless step.nil?
138
- line
139
- end
140
-
141
- def add_error(references, description = nil)
142
- @issues.push Error.new(name, references, description)
143
- end
144
-
145
- def add_warning(references, description = nil)
146
- @issues.push Warning.new(name, references, description)
176
+
177
+ def linter_name
178
+ self.class.linter_name
147
179
  end
148
180
 
149
181
  def render_step(step)
@@ -151,7 +183,7 @@ module Chutney
151
183
  value += render_step_argument step[:argument] if step.include? :argument
152
184
  value
153
185
  end
154
-
186
+
155
187
  def render_step_argument(argument)
156
188
  return "\n#{argument[:content]}" if argument[:type] == :DocString
157
189