reek 1.4.0 → 1.5.0
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.
- 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
|