reek 1.4.0 → 1.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG +5 -0
- data/README.md +70 -92
- data/config/defaults.reek +3 -0
- data/features/samples.feature +24 -20
- data/features/step_definitions/reek_steps.rb +1 -1
- data/features/support/env.rb +7 -7
- data/lib/reek/core/code_context.rb +1 -1
- data/lib/reek/core/code_parser.rb +19 -18
- data/lib/reek/core/method_context.rb +8 -7
- data/lib/reek/core/module_context.rb +1 -1
- data/lib/reek/core/smell_repository.rb +1 -0
- data/lib/reek/core/sniffer.rb +3 -1
- data/lib/reek/rake/task.rb +1 -5
- data/lib/reek/smell_description.rb +26 -0
- data/lib/reek/smell_warning.rb +35 -49
- data/lib/reek/smells.rb +1 -0
- data/lib/reek/smells/attribute.rb +1 -1
- data/lib/reek/smells/control_parameter.rb +14 -7
- data/lib/reek/smells/data_clump.rb +1 -1
- data/lib/reek/smells/duplicate_method_call.rb +2 -9
- data/lib/reek/smells/module_initialize.rb +38 -0
- data/lib/reek/smells/nested_iterators.rb +1 -1
- data/lib/reek/smells/nil_check.rb +3 -3
- data/lib/reek/smells/repeated_conditional.rb +3 -2
- data/lib/reek/smells/smell_detector.rb +1 -1
- data/lib/reek/smells/too_many_instance_variables.rb +1 -1
- data/lib/reek/smells/too_many_methods.rb +1 -1
- data/lib/reek/smells/uncommunicative_method_name.rb +0 -4
- data/lib/reek/smells/uncommunicative_parameter_name.rb +0 -4
- data/lib/reek/smells/uncommunicative_variable_name.rb +11 -9
- data/lib/reek/smells/utility_function.rb +2 -2
- data/lib/reek/source/ast_node.rb +40 -0
- data/lib/reek/source/ast_node_class_map.rb +37 -0
- data/lib/reek/source/reference_collector.rb +3 -3
- data/lib/reek/source/sexp_extensions.rb +133 -59
- data/lib/reek/source/sexp_formatter.rb +10 -4
- data/lib/reek/source/sexp_node.rb +25 -17
- data/lib/reek/source/source_code.rb +21 -9
- data/lib/reek/source/tree_dresser.rb +10 -33
- data/lib/reek/version.rb +1 -1
- data/reek.gemspec +2 -4
- data/spec/matchers/smell_of_matcher.rb +9 -1
- data/spec/quality/reek_source_spec.rb +0 -35
- data/spec/reek/core/code_context_spec.rb +22 -8
- data/spec/reek/core/method_context_spec.rb +10 -10
- data/spec/reek/smell_description_spec.rb +43 -0
- data/spec/reek/smell_warning_spec.rb +0 -3
- data/spec/reek/smells/control_parameter_spec.rb +24 -0
- data/spec/reek/smells/feature_envy_spec.rb +50 -17
- data/spec/reek/smells/irresponsible_module_spec.rb +25 -17
- data/spec/reek/smells/module_initialize_spec.rb +20 -0
- data/spec/reek/smells/prima_donna_method_spec.rb +2 -2
- data/spec/reek/smells/repeated_conditional_spec.rb +10 -4
- data/spec/reek/smells/too_many_instance_variables_spec.rb +47 -21
- data/spec/reek/smells/too_many_statements_spec.rb +11 -1
- data/spec/reek/smells/uncommunicative_variable_name_spec.rb +1 -1
- data/spec/reek/smells/utility_function_spec.rb +26 -25
- data/spec/reek/source/sexp_extensions_spec.rb +164 -91
- data/spec/reek/source/sexp_formatter_spec.rb +13 -1
- data/spec/reek/source/sexp_node_spec.rb +5 -5
- data/spec/reek/source/source_code_spec.rb +18 -6
- data/spec/reek/source/tree_dresser_spec.rb +5 -5
- data/spec/spec_helper.rb +8 -4
- metadata +16 -50
@@ -1,4 +1,6 @@
|
|
1
|
-
|
1
|
+
old_verbose, $VERBOSE = $VERBOSE, nil
|
2
|
+
require 'unparser'
|
3
|
+
$VERBOSE = old_verbose
|
2
4
|
|
3
5
|
module Reek
|
4
6
|
module Source
|
@@ -7,9 +9,13 @@ module Reek
|
|
7
9
|
#
|
8
10
|
class SexpFormatter
|
9
11
|
def self.format(sexp)
|
10
|
-
return sexp.to_s unless sexp.is_a?
|
11
|
-
|
12
|
-
|
12
|
+
return sexp.to_s unless sexp.is_a? AST::Node
|
13
|
+
lines = Unparser.unparse(sexp).split "\n"
|
14
|
+
if lines.length > 1
|
15
|
+
"#{lines.first} ... #{lines.last}"
|
16
|
+
else
|
17
|
+
lines.first
|
18
|
+
end
|
13
19
|
end
|
14
20
|
end
|
15
21
|
end
|
@@ -7,7 +7,7 @@ module Reek
|
|
7
7
|
module SexpNode
|
8
8
|
def self.format(expr)
|
9
9
|
case expr
|
10
|
-
when
|
10
|
+
when AST::Node then expr.format_ruby
|
11
11
|
else expr.to_s
|
12
12
|
end
|
13
13
|
end
|
@@ -26,20 +26,14 @@ module Reek
|
|
26
26
|
end
|
27
27
|
end
|
28
28
|
|
29
|
-
def
|
29
|
+
def find_nodes(types, ignoring = [])
|
30
30
|
result = []
|
31
|
-
|
32
|
-
result << self
|
33
|
-
else
|
34
|
-
each_sexp do |elem|
|
35
|
-
result += elem.unnested_nodes(types)
|
36
|
-
end
|
37
|
-
end
|
31
|
+
look_for_alt(types, ignoring) { |exp| result << exp }
|
38
32
|
result
|
39
33
|
end
|
40
34
|
|
41
35
|
def each_sexp
|
42
|
-
each { |elem| yield elem if elem.is_a?
|
36
|
+
children.each { |elem| yield elem if elem.is_a? AST::Node }
|
43
37
|
end
|
44
38
|
|
45
39
|
#
|
@@ -49,9 +43,27 @@ module Reek
|
|
49
43
|
#
|
50
44
|
def look_for(target_type, ignoring = [], &blk)
|
51
45
|
each_sexp do |elem|
|
52
|
-
elem.look_for(target_type, ignoring, &blk) unless ignoring.include?(elem.
|
46
|
+
elem.look_for(target_type, ignoring, &blk) unless ignoring.include?(elem.type)
|
47
|
+
end
|
48
|
+
blk.call(self) if type == target_type
|
49
|
+
end
|
50
|
+
|
51
|
+
#
|
52
|
+
# Carries out a depth-first traversal of this syntax tree, yielding
|
53
|
+
# every Sexp of type +target_type+. The traversal ignores any node
|
54
|
+
# whose type is listed in the Array +ignoring+, includeing the top node.
|
55
|
+
#
|
56
|
+
# Also, doesn't nest
|
57
|
+
#
|
58
|
+
def look_for_alt(target_types, ignoring = [], &blk)
|
59
|
+
return if ignoring.include?(type)
|
60
|
+
if target_types.include? type
|
61
|
+
blk.call(self)
|
62
|
+
else
|
63
|
+
each_sexp do |elem|
|
64
|
+
elem.look_for_alt(target_types, ignoring, &blk)
|
65
|
+
end
|
53
66
|
end
|
54
|
-
blk.call(self) if first == target_type
|
55
67
|
end
|
56
68
|
|
57
69
|
def contains_nested_node?(target_type)
|
@@ -60,11 +72,7 @@ module Reek
|
|
60
72
|
end
|
61
73
|
|
62
74
|
def format_ruby
|
63
|
-
|
64
|
-
end
|
65
|
-
|
66
|
-
def deep_copy
|
67
|
-
Sexp.new(*map { |elem| elem.is_a?(Sexp) ? elem.deep_copy : elem })
|
75
|
+
SexpFormatter.format(self)
|
68
76
|
end
|
69
77
|
end
|
70
78
|
end
|
@@ -1,6 +1,9 @@
|
|
1
|
-
|
1
|
+
old_verbose, $VERBOSE = $VERBOSE, nil
|
2
|
+
require 'parser/current'
|
3
|
+
$VERBOSE = old_verbose
|
2
4
|
require 'reek/source/config_file'
|
3
5
|
require 'reek/source/tree_dresser'
|
6
|
+
require 'reek/source/ast_node'
|
4
7
|
|
5
8
|
module Reek
|
6
9
|
module Source
|
@@ -10,7 +13,7 @@ module Reek
|
|
10
13
|
class SourceCode
|
11
14
|
attr_reader :desc
|
12
15
|
|
13
|
-
def initialize(code, desc, parser =
|
16
|
+
def initialize(code, desc, parser = Parser::Ruby21.new)
|
14
17
|
@source = code
|
15
18
|
@desc = desc
|
16
19
|
@parser = parser
|
@@ -21,13 +24,22 @@ module Reek
|
|
21
24
|
end
|
22
25
|
|
23
26
|
def syntax_tree
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
27
|
+
@syntax_tree ||=
|
28
|
+
begin
|
29
|
+
buffer = Parser::Source::Buffer.new(@desc)
|
30
|
+
buffer.source = @source
|
31
|
+
|
32
|
+
begin
|
33
|
+
ast, comments = @parser.parse_with_comments(buffer)
|
34
|
+
rescue Racc::ParseError, Parser::SyntaxError => error
|
35
|
+
$stderr.puts "#{desc}: #{error.class.name}: #{error}"
|
36
|
+
end
|
37
|
+
ast ||= AstNode.new(:empty)
|
38
|
+
comments ||= []
|
39
|
+
|
40
|
+
comment_map = Parser::Source::Comment.associate(ast, comments)
|
41
|
+
TreeDresser.new.dress(ast, comment_map)
|
42
|
+
end
|
31
43
|
end
|
32
44
|
end
|
33
45
|
end
|
@@ -1,5 +1,4 @@
|
|
1
|
-
require 'reek/source/
|
2
|
-
require 'reek/source/sexp_extensions'
|
1
|
+
require 'reek/source/ast_node_class_map'
|
3
2
|
|
4
3
|
module Reek
|
5
4
|
module Source
|
@@ -8,39 +7,17 @@ module Reek
|
|
8
7
|
# the tree more understandable and less implementation-dependent.
|
9
8
|
#
|
10
9
|
class TreeDresser
|
11
|
-
def initialize(
|
12
|
-
@
|
13
|
-
@node_module = node_module
|
10
|
+
def initialize(klass_map = AstNodeClassMap.new)
|
11
|
+
@klass_map = klass_map
|
14
12
|
end
|
15
13
|
|
16
|
-
def dress(sexp)
|
17
|
-
|
18
|
-
sexp.
|
19
|
-
sexp
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
def extend_sexp(sexp)
|
25
|
-
sexp.extend(@node_module)
|
26
|
-
extension_module = extension_for(sexp)
|
27
|
-
sexp.extend(extension_module) if extension_module
|
28
|
-
end
|
29
|
-
|
30
|
-
def extension_for(sexp)
|
31
|
-
extension_map[sexp.sexp_type]
|
32
|
-
end
|
33
|
-
|
34
|
-
def extension_map
|
35
|
-
@extension_map ||= begin
|
36
|
-
assoc = @extensions_module.constants.map do |const|
|
37
|
-
[
|
38
|
-
const.to_s.sub(/Node$/, '').downcase.to_sym,
|
39
|
-
@extensions_module.const_get(const)
|
40
|
-
]
|
41
|
-
end
|
42
|
-
Hash[assoc]
|
43
|
-
end
|
14
|
+
def dress(sexp, comment_map)
|
15
|
+
return sexp unless sexp.is_a? AST::Node
|
16
|
+
type = sexp.type
|
17
|
+
children = sexp.children.map { |child| dress(child, comment_map) }
|
18
|
+
comments = comment_map[sexp]
|
19
|
+
@klass_map.klass_for(type).new(type, children,
|
20
|
+
location: sexp.loc, comments: comments)
|
44
21
|
end
|
45
22
|
end
|
46
23
|
end
|
data/lib/reek/version.rb
CHANGED
data/reek.gemspec
CHANGED
@@ -30,15 +30,13 @@ Gem::Specification.new do |s|
|
|
30
30
|
s.required_ruby_version = '>= 1.9.2'
|
31
31
|
s.summary = 'Code smell detector for Ruby'
|
32
32
|
|
33
|
-
s.add_runtime_dependency('
|
34
|
-
s.add_runtime_dependency('
|
35
|
-
s.add_runtime_dependency('ruby2ruby', ['>= 2.0.8', '< 3.0'])
|
33
|
+
s.add_runtime_dependency('parser', ['~> 2.2.0.pre.7'])
|
34
|
+
s.add_runtime_dependency('unparser', ['~> 0.1.16'])
|
36
35
|
s.add_runtime_dependency('rainbow', ['>= 1.99', '< 3.0'])
|
37
36
|
|
38
37
|
s.add_development_dependency('bundler', ['~> 1.1'])
|
39
38
|
s.add_development_dependency('rake', ['~> 10.0'])
|
40
39
|
s.add_development_dependency('cucumber', ['~> 1.3'])
|
41
40
|
s.add_development_dependency('rspec', ['~> 3.0'])
|
42
|
-
s.add_development_dependency('flay', ['~> 2.4'])
|
43
41
|
s.add_development_dependency('yard', ['>= 0.8.7', '< 0.9'])
|
44
42
|
end
|
@@ -34,7 +34,15 @@ module SmellOfMatcher
|
|
34
34
|
private
|
35
35
|
|
36
36
|
def detect_smells
|
37
|
-
|
37
|
+
tree = @source.syntax_tree
|
38
|
+
ctx = case tree.type
|
39
|
+
when :def, :defs
|
40
|
+
MethodContext.new(nil, tree)
|
41
|
+
when :module, :class
|
42
|
+
ModuleContext.new(nil, tree)
|
43
|
+
else
|
44
|
+
CodeContext.new(nil, tree)
|
45
|
+
end
|
38
46
|
detector = @klass.new(@source.desc, @klass.default_config.merge(@config))
|
39
47
|
detector.examine(ctx)
|
40
48
|
@actual_smells = detector.smells_found.to_a
|
@@ -1,42 +1,7 @@
|
|
1
1
|
require 'spec_helper'
|
2
|
-
require 'flay'
|
3
|
-
|
4
|
-
RSpec::Matchers.define :flay do |threshold|
|
5
|
-
match do |dirs_and_files|
|
6
|
-
@threshold = threshold
|
7
|
-
@flay = Flay.new(fuzzy: false, verbose: false, mass: @threshold)
|
8
|
-
@flay.process(*Flay.expand_dirs_to_files(dirs_and_files))
|
9
|
-
@flay.total > 0
|
10
|
-
end
|
11
|
-
|
12
|
-
failure_message_for_should do
|
13
|
-
"Expected source to contain duplication, but it didn't"
|
14
|
-
end
|
15
|
-
|
16
|
-
failure_message_for_should_not do
|
17
|
-
"Expected source not to contain duplication, but got:\n#{report}"
|
18
|
-
end
|
19
|
-
|
20
|
-
def report
|
21
|
-
lines = ["Total mass = #{@flay.total} (threshold = #{@threshold})"]
|
22
|
-
@flay.masses.each do |hash, mass|
|
23
|
-
nodes = @flay.hashes[hash]
|
24
|
-
match = @flay.identical[hash] ? 'IDENTICAL' : 'Similar'
|
25
|
-
lines << format('%s code found in %p (%d)', match, nodes.first.first, mass)
|
26
|
-
nodes.each { |x| lines << " #{x.file}:#{x.line}" }
|
27
|
-
end
|
28
|
-
lines.join("\n")
|
29
|
-
end
|
30
|
-
end
|
31
2
|
|
32
3
|
describe 'Reek source code' do
|
33
4
|
it 'has no smells' do
|
34
5
|
Dir['lib/**/*.rb'].should_not reek
|
35
6
|
end
|
36
|
-
it 'has no structural duplication' do
|
37
|
-
['lib'].should_not flay(16)
|
38
|
-
end
|
39
|
-
it 'has no structural duplication in the tests' do
|
40
|
-
['spec/reek'].should_not flay(25)
|
41
|
-
end
|
42
7
|
end
|
@@ -56,8 +56,8 @@ describe CodeContext do
|
|
56
56
|
it 'should pass unknown method calls down the stack' do
|
57
57
|
stop = StopContext.new
|
58
58
|
def stop.bananas(arg1, arg2) arg1 + arg2 + 43 end
|
59
|
-
element = ModuleContext.new(stop,
|
60
|
-
element = MethodContext.new(element,
|
59
|
+
element = ModuleContext.new(stop, s(:module, :mod, nil))
|
60
|
+
element = MethodContext.new(element, s(:def, :bad, s(:args), nil))
|
61
61
|
expect(element.bananas(17, -5)).to eq(55)
|
62
62
|
end
|
63
63
|
end
|
@@ -70,16 +70,21 @@ describe CodeContext do
|
|
70
70
|
ast = src.to_reek_source.syntax_tree
|
71
71
|
@ctx = CodeContext.new(nil, ast)
|
72
72
|
end
|
73
|
+
|
73
74
|
it 'yields no calls' do
|
74
|
-
@ctx.each_node(:
|
75
|
+
@ctx.each_node(:send, []) { |exp| raise "#{exp} yielded by empty module!" }
|
75
76
|
end
|
77
|
+
|
76
78
|
it 'yields one module' do
|
77
79
|
mods = 0
|
78
80
|
@ctx.each_node(:module, []) { |_exp| mods += 1 }
|
79
81
|
expect(mods).to eq(1)
|
80
82
|
end
|
83
|
+
|
81
84
|
it "yields the module's full AST" do
|
82
|
-
@ctx.each_node(:module, [])
|
85
|
+
@ctx.each_node(:module, []) do |exp|
|
86
|
+
expect(exp).to eq(s(:module, s(:const, nil, @module_name.to_sym), nil))
|
87
|
+
end
|
83
88
|
end
|
84
89
|
|
85
90
|
context 'with no block' do
|
@@ -103,19 +108,28 @@ describe CodeContext do
|
|
103
108
|
it 'yields one module' do
|
104
109
|
expect(@ctx.each_node(:module, []).length).to eq(1)
|
105
110
|
end
|
111
|
+
|
106
112
|
it "yields the module's full AST" do
|
107
|
-
@ctx.each_node(:module, [])
|
113
|
+
@ctx.each_node(:module, []) do |exp|
|
114
|
+
expect(exp).to eq s(:module,
|
115
|
+
s(:const, nil, @module_name.to_sym),
|
116
|
+
s(:def, :calloo,
|
117
|
+
s(:args),
|
118
|
+
s(:send, nil, :puts, s(:str, 'hello'))))
|
119
|
+
end
|
108
120
|
end
|
121
|
+
|
109
122
|
it 'yields one method' do
|
110
|
-
expect(@ctx.each_node(:
|
123
|
+
expect(@ctx.each_node(:def, []).length).to eq(1)
|
111
124
|
end
|
125
|
+
|
112
126
|
it "yields the method's full AST" do
|
113
|
-
@ctx.each_node(:
|
127
|
+
@ctx.each_node(:def, []) { |exp| expect(exp[1]).to eq(@method_name.to_sym) }
|
114
128
|
end
|
115
129
|
|
116
130
|
context 'pruning the traversal' do
|
117
131
|
it 'ignores the call inside the method' do
|
118
|
-
expect(@ctx.each_node(:
|
132
|
+
expect(@ctx.each_node(:send, [:def])).to be_empty
|
119
133
|
end
|
120
134
|
end
|
121
135
|
end
|
@@ -24,23 +24,23 @@ end
|
|
24
24
|
|
25
25
|
describe MethodContext do
|
26
26
|
it 'should record ivars as refs to self' do
|
27
|
-
mctx = MethodContext.new(StopContext.new,
|
27
|
+
mctx = MethodContext.new(StopContext.new, s(:def, :feed, s(:args), nil))
|
28
28
|
expect(mctx.envious_receivers).to eq([])
|
29
|
-
mctx.record_call_to(
|
29
|
+
mctx.record_call_to(s(:send, s(:ivar, :@cow), :feed_to))
|
30
30
|
expect(mctx.envious_receivers).to eq([])
|
31
31
|
end
|
32
32
|
|
33
33
|
it 'should count calls to self' do
|
34
|
-
mctx = MethodContext.new(StopContext.new,
|
34
|
+
mctx = MethodContext.new(StopContext.new, s(:def, :equals, s(:args), nil))
|
35
35
|
mctx.refs.record_reference_to([:lvar, :other])
|
36
|
-
mctx.record_call_to(
|
36
|
+
mctx.record_call_to(s(:send, s(:self), :thing))
|
37
37
|
expect(mctx.envious_receivers).to be_empty
|
38
38
|
end
|
39
39
|
|
40
40
|
it 'should recognise a call on self' do
|
41
|
-
mc = MethodContext.new(StopContext.new, s(:
|
42
|
-
mc.record_call_to(
|
43
|
-
mc.record_call_to(
|
41
|
+
mc = MethodContext.new(StopContext.new, s(:def, :deep, s(:args), nil))
|
42
|
+
mc.record_call_to(s(:send, s(:lvar, :text), :each, s(:arglist)))
|
43
|
+
mc.record_call_to(s(:send, nil, :shelve, s(:arglist)))
|
44
44
|
expect(mc.envious_receivers).to be_empty
|
45
45
|
end
|
46
46
|
end
|
@@ -65,7 +65,7 @@ describe MethodParameters, 'default assignments' do
|
|
65
65
|
@defaults = assignments_from(src)
|
66
66
|
end
|
67
67
|
it 'returns the param-value pair' do
|
68
|
-
expect(@defaults[0]).to eq
|
68
|
+
expect(@defaults[0]).to eq [:argb, s(:int, 456)]
|
69
69
|
end
|
70
70
|
it 'returns the nothing else' do
|
71
71
|
expect(@defaults.length).to eq(1)
|
@@ -78,8 +78,8 @@ describe MethodParameters, 'default assignments' do
|
|
78
78
|
@defaults = assignments_from(src)
|
79
79
|
end
|
80
80
|
it 'returns both param-value pairs' do
|
81
|
-
expect(@defaults[0]).to eq
|
82
|
-
expect(@defaults[1]).to eq
|
81
|
+
expect(@defaults[0]).to eq [:arga, s(:int, 123)]
|
82
|
+
expect(@defaults[1]).to eq [:argb, s(:int, 456)]
|
83
83
|
end
|
84
84
|
it 'returns nothing else' do
|
85
85
|
expect(@defaults.length).to eq(2)
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'reek/smell_description'
|
3
|
+
|
4
|
+
describe Reek::SmellDescription do
|
5
|
+
let(:smell_class) { 'SmellClass' }
|
6
|
+
let(:smell_subclass) { 'SmellySubclass' }
|
7
|
+
let(:message) { 'smell message' }
|
8
|
+
let(:details) { { 'key1' => 'value1', 'key2' => 'value2' } }
|
9
|
+
|
10
|
+
let(:description) { described_class.new(smell_class, smell_subclass, message, details) }
|
11
|
+
|
12
|
+
it "knows its smell class" do
|
13
|
+
expect(description.smell_class).to eq smell_class
|
14
|
+
end
|
15
|
+
|
16
|
+
it "knows its smell subclass" do
|
17
|
+
expect(description.smell_subclass).to eq smell_subclass
|
18
|
+
end
|
19
|
+
|
20
|
+
it "knows its smell message" do
|
21
|
+
expect(description.message).to eq message
|
22
|
+
end
|
23
|
+
|
24
|
+
it "knows its details" do
|
25
|
+
expect(description.details).to eq details
|
26
|
+
end
|
27
|
+
|
28
|
+
it "accesses its details through #[]" do
|
29
|
+
expect(description['key1']).to eq 'value1'
|
30
|
+
expect(description['key2']).to eq 'value2'
|
31
|
+
end
|
32
|
+
|
33
|
+
it "outputs the correct YAML" do
|
34
|
+
expect(description.to_yaml).to eq <<-END
|
35
|
+
---
|
36
|
+
class: SmellClass
|
37
|
+
subclass: SmellySubclass
|
38
|
+
message: smell message
|
39
|
+
key1: value1
|
40
|
+
key2: value2
|
41
|
+
END
|
42
|
+
end
|
43
|
+
end
|