seeing_is_believing 0.0.26 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Readme.md +0 -2
- data/features/errors.feature +13 -3
- data/features/examples.feature +5 -0
- data/features/flags.feature +64 -1
- data/features/regression.feature +63 -0
- data/lib/seeing_is_believing.rb +12 -7
- data/lib/seeing_is_believing/binary.rb +5 -4
- data/lib/seeing_is_believing/binary/{print_results_next_to_lines.rb → add_annotations.rb} +35 -19
- data/lib/seeing_is_believing/binary/arg_parser.rb +6 -1
- data/lib/seeing_is_believing/binary/remove_previous_annotations.rb +75 -0
- data/lib/seeing_is_believing/debugger.rb +36 -0
- data/lib/seeing_is_believing/expression_list.rb +15 -17
- data/lib/seeing_is_believing/remove_inline_comments.rb +33 -9
- data/lib/seeing_is_believing/syntax_analyzer.rb +2 -1
- data/lib/seeing_is_believing/version.rb +1 -1
- data/seeing_is_believing.gemspec +1 -1
- data/spec/{arg_parser_spec.rb → binary/arg_parser_spec.rb} +11 -0
- data/spec/{line_formatter_spec.rb → binary/line_formatter_spec.rb} +0 -0
- data/spec/binary/remove_previous_annotations_spec.rb +198 -0
- data/spec/debugger_spec.rb +27 -0
- data/spec/expression_list_spec.rb +4 -4
- data/spec/seeing_is_believing_spec.rb +42 -7
- data/spec/syntax_analyzer_spec.rb +1 -0
- metadata +16 -10
@@ -1,6 +1,7 @@
|
|
1
1
|
require 'seeing_is_believing'
|
2
2
|
require 'seeing_is_believing/binary/arg_parser'
|
3
|
-
require 'seeing_is_believing/binary/
|
3
|
+
require 'seeing_is_believing/binary/add_annotations'
|
4
|
+
require 'seeing_is_believing/binary/remove_previous_annotations'
|
4
5
|
require 'timeout'
|
5
6
|
|
6
7
|
|
@@ -85,11 +86,11 @@ class SeeingIsBelieving
|
|
85
86
|
end
|
86
87
|
|
87
88
|
def print_unexpected_error
|
88
|
-
stderr.puts unexpected_exception.class, unexpected_exception.message
|
89
|
+
stderr.puts unexpected_exception.class, unexpected_exception.message, "", unexpected_exception.backtrace
|
89
90
|
end
|
90
91
|
|
91
92
|
def printer
|
92
|
-
@printer ||=
|
93
|
+
@printer ||= AddAnnotations.new body, flags.merge(stdin: (file_is_on_stdin? ? '' : stdin))
|
93
94
|
end
|
94
95
|
|
95
96
|
def results
|
@@ -154,7 +155,7 @@ class SeeingIsBelieving
|
|
154
155
|
end
|
155
156
|
|
156
157
|
def print_cleaned_program
|
157
|
-
stdout.print
|
158
|
+
stdout.print RemovePreviousAnnotations.call body
|
158
159
|
end
|
159
160
|
end
|
160
161
|
end
|
@@ -1,6 +1,8 @@
|
|
1
|
+
require 'stringio'
|
1
2
|
require 'seeing_is_believing/queue'
|
2
3
|
require 'seeing_is_believing/has_exception'
|
3
4
|
require 'seeing_is_believing/binary/line_formatter'
|
5
|
+
require 'seeing_is_believing/binary/remove_previous_annotations'
|
4
6
|
|
5
7
|
# I think there is a bug where with xmpfilter_style set,
|
6
8
|
# the exceptions won't be shown. But it's not totally clear
|
@@ -16,20 +18,13 @@ require 'seeing_is_believing/binary/line_formatter'
|
|
16
18
|
|
17
19
|
class SeeingIsBelieving
|
18
20
|
class Binary
|
19
|
-
class
|
21
|
+
class AddAnnotations
|
20
22
|
include HasException
|
21
23
|
|
24
|
+
RESULT_PREFIX = '# =>'
|
25
|
+
EXCEPTION_PREFIX = '# ~>'
|
22
26
|
STDOUT_PREFIX = '# >>'
|
23
27
|
STDERR_PREFIX = '# !>'
|
24
|
-
EXCEPTION_PREFIX = '# ~>'
|
25
|
-
RESULT_PREFIX = '# =>'
|
26
|
-
|
27
|
-
def self.remove_previous_output_from(string)
|
28
|
-
string.gsub(/\s+(#{EXCEPTION_PREFIX}|#{RESULT_PREFIX}).*?$/, '')
|
29
|
-
.gsub(/(^\n)?(^#{STDOUT_PREFIX}[^\n]*\r?\n?)+/m, '')
|
30
|
-
.gsub(/(^\n)?(^#{STDERR_PREFIX}[^\n]*\r?\n?)+/m, '')
|
31
|
-
end
|
32
|
-
|
33
28
|
|
34
29
|
def self.method_from_options(*args)
|
35
30
|
define_method(args.first) { options.fetch *args }
|
@@ -41,19 +36,21 @@ class SeeingIsBelieving
|
|
41
36
|
method_from_options :line_length, Float::INFINITY
|
42
37
|
method_from_options :result_length, Float::INFINITY
|
43
38
|
method_from_options :xmpfilter_style
|
39
|
+
method_from_options :debugger
|
44
40
|
|
45
41
|
attr_accessor :file_result
|
46
42
|
def initialize(body, options={})
|
47
|
-
cleaned_body =
|
43
|
+
cleaned_body = RemovePreviousAnnotations.call body
|
48
44
|
self.options = options
|
49
45
|
self.body = (xmpfilter_style ? body : cleaned_body)
|
50
46
|
self.file_result = SeeingIsBelieving.call body(),
|
51
|
-
filename:
|
52
|
-
require:
|
53
|
-
load_path:
|
54
|
-
encoding:
|
55
|
-
stdin:
|
56
|
-
timeout:
|
47
|
+
filename: (options[:as] || options[:filename]),
|
48
|
+
require: options[:require],
|
49
|
+
load_path: options[:load_path],
|
50
|
+
encoding: options[:encoding],
|
51
|
+
stdin: options[:stdin],
|
52
|
+
timeout: options[:timeout],
|
53
|
+
debugger: debugger
|
57
54
|
self.alignment_strategy = options[:alignment_strategy].new cleaned_body, start_line, end_line
|
58
55
|
end
|
59
56
|
|
@@ -68,8 +65,13 @@ class SeeingIsBelieving
|
|
68
65
|
.each { |line, line_number| add_line line, line_number }
|
69
66
|
add_stdout
|
70
67
|
add_stderr
|
68
|
+
add_exception
|
71
69
|
add_remaining_lines
|
72
|
-
|
70
|
+
if debugger.enabled?
|
71
|
+
debugger.context("RESULT") { new_body }.to_s
|
72
|
+
else
|
73
|
+
new_body
|
74
|
+
end
|
73
75
|
end
|
74
76
|
end
|
75
77
|
|
@@ -125,6 +127,20 @@ class SeeingIsBelieving
|
|
125
127
|
end
|
126
128
|
end
|
127
129
|
|
130
|
+
def add_exception
|
131
|
+
return unless file_result.has_exception?
|
132
|
+
exception = file_result.exception
|
133
|
+
new_body << "\n"
|
134
|
+
new_body << LineFormatter.new('', "#{EXCEPTION_PREFIX} ", exception.class_name, options).call << "\n"
|
135
|
+
exception.message.each_line do |line|
|
136
|
+
new_body << LineFormatter.new('', "#{EXCEPTION_PREFIX} ", line.chomp, options).call << "\n"
|
137
|
+
end
|
138
|
+
new_body << "#{EXCEPTION_PREFIX}\n"
|
139
|
+
exception.backtrace.each do |line|
|
140
|
+
new_body << LineFormatter.new('', "#{EXCEPTION_PREFIX} ", line.chomp, options).call << "\n"
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
128
144
|
def add_remaining_lines
|
129
145
|
line_queue.each { |line, line_number| new_body << line }
|
130
146
|
end
|
@@ -132,7 +148,7 @@ class SeeingIsBelieving
|
|
132
148
|
def format_line(line, line_number, line_results)
|
133
149
|
options = options().merge pad_to: alignment_strategy.line_length_for(line_number)
|
134
150
|
formatted_line = if line_results.has_exception?
|
135
|
-
result = sprintf "%s: %s", line_results.exception.class_name, line_results.exception.message
|
151
|
+
result = sprintf "%s: %s", line_results.exception.class_name, line_results.exception.message.gsub("\n", '\n')
|
136
152
|
LineFormatter.new(line, "#{EXCEPTION_PREFIX} ", result, options).call
|
137
153
|
elsif line_results.any?
|
138
154
|
LineFormatter.new(line, "#{RESULT_PREFIX} ", line_results.join(', '), options).call
|
@@ -1,4 +1,6 @@
|
|
1
|
+
require 'stringio'
|
1
2
|
require 'seeing_is_believing/version'
|
3
|
+
require 'seeing_is_believing/debugger'
|
2
4
|
require 'seeing_is_believing/binary/align_file'
|
3
5
|
require 'seeing_is_believing/binary/align_line'
|
4
6
|
require 'seeing_is_believing/binary/align_chunk'
|
@@ -22,10 +24,11 @@ class SeeingIsBelieving
|
|
22
24
|
until args.empty?
|
23
25
|
case (arg = args.shift)
|
24
26
|
when '-h', '--help' then options[:help] = self.class.help_screen
|
25
|
-
when '-v', '--version' then options[:version] = true
|
26
27
|
when '-c', '--clean' then options[:clean] = true
|
28
|
+
when '-v', '--version' then options[:version] = true
|
27
29
|
when '-x', '--xmpfilter-style' then options[:xmpfilter_style] = true
|
28
30
|
when '-i', '--inherit-exit-status' then options[:inherit_exit_status] = true
|
31
|
+
when '-g', '--debug' then options[:debugger] = Debugger.new(enabled: true, colour: true)
|
29
32
|
when '-l', '--start-line' then extract_positive_int_for :start_line, arg
|
30
33
|
when '-L', '--end-line' then extract_positive_int_for :end_line, arg
|
31
34
|
when '-d', '--line-length' then extract_positive_int_for :line_length, arg
|
@@ -67,6 +70,7 @@ class SeeingIsBelieving
|
|
67
70
|
|
68
71
|
def options
|
69
72
|
@options ||= {
|
73
|
+
debugger: Debugger.new(enabled: false, colour: true),
|
70
74
|
version: false,
|
71
75
|
clean: false,
|
72
76
|
xmpfilter_style: false,
|
@@ -149,6 +153,7 @@ Usage: seeing_is_believing [options] [filename]
|
|
149
153
|
-K, --encoding encoding # sets file encoding, equivalent to Ruby's -Kx (see `man ruby` for valid values)
|
150
154
|
-a, --as filename # run the program as if it was the specified filename
|
151
155
|
-c, --clean # remove annotations from previous runs of seeing_is_believing
|
156
|
+
-g, --debug # print debugging information (useful if program is fucking up, or if you want to brag)
|
152
157
|
-x, --xmpfilter-style # annotate marked lines instead of every line
|
153
158
|
-i, --inherit-exit-status # exit with the exit status of the program being eval
|
154
159
|
-v, --version # print the version (#{VERSION})
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'seeing_is_believing/remove_inline_comments'
|
2
|
+
|
3
|
+
class SeeingIsBelieving
|
4
|
+
class Binary
|
5
|
+
class RemovePreviousAnnotations
|
6
|
+
def self.call(code)
|
7
|
+
new(code).call
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(code)
|
11
|
+
self.code = code
|
12
|
+
self.comments = { result: [],
|
13
|
+
exception: [],
|
14
|
+
stdout: [],
|
15
|
+
stderr: [],
|
16
|
+
}
|
17
|
+
end
|
18
|
+
|
19
|
+
def call
|
20
|
+
RemoveInlineComments.call code, additional_rewrites: remove_whitespace_preceeding_comments do |comment|
|
21
|
+
if comment.text[/\A#\s*=>/] then comments[:result] << comment; true
|
22
|
+
elsif comment.text[/\A#\s*~>/] then comments[:exception] << comment; true
|
23
|
+
elsif comment.text[/\A#\s*>>/] then comments[:stdout] << comment; true
|
24
|
+
elsif comment.text[/\A#\s*!>/] then comments[:stderr] << comment; true
|
25
|
+
else false
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
attr_accessor :code, :comments
|
33
|
+
|
34
|
+
def remove_whitespace_preceeding_comments
|
35
|
+
lambda do |buffer, rewriter|
|
36
|
+
comments[:result].each { |comment| remove_whitespace_before comment.location.begin_pos, buffer, rewriter, false }
|
37
|
+
comments[:exception].each { |comment| remove_whitespace_before comment.location.begin_pos, buffer, rewriter, true }
|
38
|
+
comments[:stdout].each { |comment| remove_whitespace_before comment.location.begin_pos, buffer, rewriter, true }
|
39
|
+
comments[:stderr].each { |comment| remove_whitespace_before comment.location.begin_pos, buffer, rewriter, true }
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# any whitespace before the index (on the same line) will be removed
|
44
|
+
# if the preceeding whitespace is at the beginning of the line, the newline will be removed
|
45
|
+
# if there is a newline before all of that, and remove_preceeding_newline is true, it will be removed as well
|
46
|
+
def remove_whitespace_before(index, buffer, rewriter, remove_preceeding_newline)
|
47
|
+
end_pos = index
|
48
|
+
begin_pos = end_pos - 1
|
49
|
+
begin_pos -= 1 while code[begin_pos] =~ /\s/ && code[begin_pos] != "\n"
|
50
|
+
begin_pos -= 1 if code[begin_pos] == "\n"
|
51
|
+
begin_pos -= 1 if code[begin_pos] == "\n" && remove_preceeding_newline
|
52
|
+
return if begin_pos.next == end_pos
|
53
|
+
rewriter.remove Parser::Source::Range.new(buffer, begin_pos.next, end_pos)
|
54
|
+
end
|
55
|
+
|
56
|
+
# returns comments in groups that are on consecutive lines
|
57
|
+
def adjacent_comments(comments, buffer)
|
58
|
+
comments = comments.sort_by { |comment| comment.location.begin_pos }
|
59
|
+
current_chunk = 0
|
60
|
+
last_line_seen = -100
|
61
|
+
chunks_to_comment = comments.chunk do |comment|
|
62
|
+
line, col = buffer.decompose_position comment.location.begin_pos
|
63
|
+
if last_line_seen.next == line
|
64
|
+
last_line_seen = line
|
65
|
+
current_chunk
|
66
|
+
else
|
67
|
+
last_line_seen = line
|
68
|
+
current_chunk += 1
|
69
|
+
end
|
70
|
+
end
|
71
|
+
chunks_to_comment.map &:last
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
class SeeingIsBelieving
|
2
|
+
class Debugger
|
3
|
+
|
4
|
+
CONTEXT_COLOUR = "\e[37;44m"
|
5
|
+
RESET_COLOUR = "\e[0m"
|
6
|
+
|
7
|
+
def initialize(options={})
|
8
|
+
@contexts = Hash.new { |h, k| h[k] = [] }
|
9
|
+
@enabled = options.fetch :enabled, true
|
10
|
+
@coloured = options.fetch :colour, false
|
11
|
+
end
|
12
|
+
|
13
|
+
def enabled?
|
14
|
+
@enabled
|
15
|
+
end
|
16
|
+
|
17
|
+
def coloured?
|
18
|
+
@coloured
|
19
|
+
end
|
20
|
+
|
21
|
+
def context(name, &block)
|
22
|
+
@contexts[name] << block.call if enabled?
|
23
|
+
self
|
24
|
+
end
|
25
|
+
|
26
|
+
def to_s
|
27
|
+
@contexts.map { |name, values|
|
28
|
+
string = ""
|
29
|
+
string << CONTEXT_COLOUR if coloured?
|
30
|
+
string << "#{name}:"
|
31
|
+
string << RESET_COLOUR if coloured?
|
32
|
+
string << "\n#{values.join "\n"}\n"
|
33
|
+
}.join("\n")
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -1,8 +1,8 @@
|
|
1
1
|
require 'open3'
|
2
|
+
require 'seeing_is_believing/debugger'
|
2
3
|
require 'seeing_is_believing/syntax_analyzer'
|
3
4
|
|
4
|
-
#
|
5
|
-
|
5
|
+
# can we get better debugging support so that we don't need to drop ANSI escape sequences in the middle of strings?
|
6
6
|
class SeeingIsBelieving
|
7
7
|
class ExpressionList
|
8
8
|
PendingExpression = Struct.new :expression, :children do
|
@@ -16,8 +16,7 @@ class SeeingIsBelieving
|
|
16
16
|
end
|
17
17
|
|
18
18
|
def initialize(options)
|
19
|
-
self.
|
20
|
-
self.should_debug = options.fetch :debug, false
|
19
|
+
self.debugger = options.fetch :debugger, Debugger.new(enabled: false)
|
21
20
|
self.get_next_line = options.fetch :get_next_line
|
22
21
|
self.peek_next_line = options.fetch :peek_next_line
|
23
22
|
self.on_complete = options.fetch :on_complete
|
@@ -27,7 +26,10 @@ class SeeingIsBelieving
|
|
27
26
|
offset, expressions, expression = 0, [], nil
|
28
27
|
begin
|
29
28
|
pending_expression = generate(expressions)
|
30
|
-
|
29
|
+
|
30
|
+
debugger.context debugger_context do
|
31
|
+
"GENERATED: #{pending_expression.expression.inspect}, ADDING IT TO #{inspected_expressions expressions}"
|
32
|
+
end
|
31
33
|
|
32
34
|
expression = reduce expressions, offset unless next_line_modifies_current?
|
33
35
|
|
@@ -38,7 +40,11 @@ class SeeingIsBelieving
|
|
38
40
|
|
39
41
|
private
|
40
42
|
|
41
|
-
attr_accessor :
|
43
|
+
attr_accessor :debugger, :get_next_line, :peek_next_line, :on_complete, :expressions
|
44
|
+
|
45
|
+
def debugger_context
|
46
|
+
"EXPRESSION EVALUATION"
|
47
|
+
end
|
42
48
|
|
43
49
|
def generate(expressions)
|
44
50
|
expression = get_next_line.call
|
@@ -59,15 +65,7 @@ class SeeingIsBelieving
|
|
59
65
|
end
|
60
66
|
|
61
67
|
def inspected_expressions(expressions)
|
62
|
-
"[#{expressions.map { |pe| pe.inspect
|
63
|
-
end
|
64
|
-
|
65
|
-
def debug?
|
66
|
-
@should_debug
|
67
|
-
end
|
68
|
-
|
69
|
-
def debug
|
70
|
-
@debug_stream.puts yield if debug?
|
68
|
+
"[#{expressions.map { |pe| pe.inspect debugger.enabled? }.join(', ')}]"
|
71
69
|
end
|
72
70
|
|
73
71
|
def reduce(expressions, offset)
|
@@ -83,14 +81,14 @@ class SeeingIsBelieving
|
|
83
81
|
offset)
|
84
82
|
expressions.replace expressions[0, i]
|
85
83
|
expressions[i-1].children << result unless expressions.empty?
|
86
|
-
|
84
|
+
debugger.context(debugger_context) { "REDUCED: #{result.inspect}, LIST: #{inspected_expressions expressions}" }
|
87
85
|
return result
|
88
86
|
end
|
89
87
|
end
|
90
88
|
|
91
89
|
def valid_ruby?(expression)
|
92
90
|
valid = SyntaxAnalyzer.valid_ruby? expression
|
93
|
-
|
91
|
+
debugger.context(debugger_context) { "#{valid ? "\e[32mIS VALID:" : "\e[31mIS NOT VALID:"}: #{expression.inspect}\e[0m" }
|
94
92
|
valid
|
95
93
|
end
|
96
94
|
|
@@ -2,21 +2,45 @@ require 'parser/current'
|
|
2
2
|
|
3
3
|
class SeeingIsBelieving
|
4
4
|
module RemoveInlineComments
|
5
|
-
|
5
|
+
module NonLeading
|
6
|
+
def self.call(code)
|
7
|
+
ranges = []
|
8
|
+
|
9
|
+
nonleading_comments = lambda do |buffer, rewriter|
|
10
|
+
ranges.sort_by(&:begin_pos)
|
11
|
+
.drop_while.with_index(1) { |range, index|
|
12
|
+
line, col = buffer.decompose_position range.begin_pos
|
13
|
+
index == line && col.zero?
|
14
|
+
}
|
15
|
+
.each { |range| rewriter.remove range }
|
16
|
+
end
|
6
17
|
|
7
|
-
|
8
|
-
|
18
|
+
RemoveInlineComments.call code, additional_rewrites: nonleading_comments do |comment|
|
19
|
+
ranges << comment.location
|
20
|
+
false
|
21
|
+
end
|
22
|
+
end
|
9
23
|
end
|
10
24
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
25
|
+
extend self
|
26
|
+
|
27
|
+
# selector is a block that will receive the comment object
|
28
|
+
# if it returns true, the comment will be removed
|
29
|
+
def self.call(code, options={}, &selector)
|
30
|
+
selector ||= Proc.new { true }
|
31
|
+
additional_rewrites = options.fetch :additional_rewrites, Proc.new {}
|
32
|
+
buffer = Parser::Source::Buffer.new "strip_comments"
|
33
|
+
buffer.source = code
|
34
|
+
parser = Parser::CurrentRuby.new
|
35
|
+
rewriter = Parser::Source::Rewriter.new(buffer)
|
36
|
+
ast, comments = parser.parse_with_comments(buffer)
|
17
37
|
comments.select { |comment| comment.type == :inline }
|
38
|
+
.select { |comment| selector.call comment }
|
18
39
|
.each { |comment| rewriter.remove comment.location }
|
40
|
+
additional_rewrites.call buffer, rewriter
|
19
41
|
rewriter.process
|
42
|
+
rescue Parser::SyntaxError => e
|
43
|
+
raise SyntaxError, e.message
|
20
44
|
end
|
21
45
|
end
|
22
46
|
end
|
@@ -139,7 +139,8 @@ class SeeingIsBelieving
|
|
139
139
|
end
|
140
140
|
|
141
141
|
def self.ends_in_comment?(code)
|
142
|
-
|
142
|
+
# must do the newline hack or it totally fucks up on comments like "# Transfer-Encoding: chunked"
|
143
|
+
code =~ /^=end\Z/ || parsed("\n#{code.lines.to_a.last.to_s}").has_comment?
|
143
144
|
end
|
144
145
|
|
145
146
|
def self.unclosed_comment?(code)
|
data/seeing_is_believing.gemspec
CHANGED
@@ -19,7 +19,7 @@ Gem::Specification.new do |s|
|
|
19
19
|
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
20
20
|
s.require_paths = ["lib"]
|
21
21
|
|
22
|
-
s.add_dependency "parser", "~> 2.0.0.
|
22
|
+
s.add_dependency "parser", "~> 2.0.0.pre1"
|
23
23
|
|
24
24
|
s.add_development_dependency "haiti", "~> 0.0.3"
|
25
25
|
s.add_development_dependency "rake", "~> 10.0.3"
|
@@ -333,5 +333,16 @@ describe SeeingIsBelieving::Binary::ArgParser do
|
|
333
333
|
parse(['-x'])[:xmpfilter_style].should be_true
|
334
334
|
end
|
335
335
|
end
|
336
|
+
|
337
|
+
describe ':debugger' do
|
338
|
+
it 'defaults to a debugger that is disabled' do
|
339
|
+
parse([])[:debugger].should_not be_enabled
|
340
|
+
end
|
341
|
+
|
342
|
+
it 'can be enabled with --debug or -g' do
|
343
|
+
parse(['--debug'])[:debugger].should be_enabled
|
344
|
+
parse(['-g'])[:debugger].should be_enabled
|
345
|
+
end
|
346
|
+
end
|
336
347
|
end
|
337
348
|
|