seeing_is_believing 3.0.0.beta.4 → 3.0.0.beta.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +0 -8
- data/Rakefile +1 -1
- data/Readme.md +65 -25
- data/bin/seeing_is_believing +1 -0
- data/docs/sib-streaming.gif +0 -0
- data/features/deprecated-flags.feature +62 -2
- data/features/errors.feature +12 -7
- data/features/examples.feature +143 -4
- data/features/flags.feature +89 -29
- data/features/regression.feature +58 -14
- data/features/support/env.rb +4 -0
- data/features/xmpfilter-style.feature +181 -36
- data/lib/seeing_is_believing.rb +44 -33
- data/lib/seeing_is_believing/binary.rb +31 -88
- data/lib/seeing_is_believing/binary/align_chunk.rb +30 -11
- data/lib/seeing_is_believing/binary/annotate_end_of_file.rb +10 -16
- data/lib/seeing_is_believing/binary/annotate_every_line.rb +5 -25
- data/lib/seeing_is_believing/binary/annotate_marked_lines.rb +136 -0
- data/lib/seeing_is_believing/binary/comment_lines.rb +8 -10
- data/lib/seeing_is_believing/binary/commentable_lines.rb +20 -26
- data/lib/seeing_is_believing/binary/config.rb +392 -0
- data/lib/seeing_is_believing/binary/data_structures.rb +57 -0
- data/lib/seeing_is_believing/binary/engine.rb +104 -0
- data/lib/seeing_is_believing/binary/{comment_formatter.rb → format_comment.rb} +6 -6
- data/lib/seeing_is_believing/binary/remove_annotations.rb +29 -28
- data/lib/seeing_is_believing/binary/rewrite_comments.rb +42 -43
- data/lib/seeing_is_believing/code.rb +105 -49
- data/lib/seeing_is_believing/debugger.rb +6 -5
- data/lib/seeing_is_believing/error.rb +6 -17
- data/lib/seeing_is_believing/evaluate_by_moving_files.rb +78 -129
- data/lib/seeing_is_believing/event_stream/consumer.rb +114 -64
- data/lib/seeing_is_believing/event_stream/events.rb +169 -11
- data/lib/seeing_is_believing/event_stream/handlers/debug.rb +57 -0
- data/lib/seeing_is_believing/event_stream/handlers/record_exitstatus.rb +18 -0
- data/lib/seeing_is_believing/event_stream/handlers/stream_json_events.rb +45 -0
- data/lib/seeing_is_believing/event_stream/handlers/update_result.rb +39 -0
- data/lib/seeing_is_believing/event_stream/producer.rb +25 -24
- data/lib/seeing_is_believing/hash_struct.rb +206 -0
- data/lib/seeing_is_believing/result.rb +20 -3
- data/lib/seeing_is_believing/the_matrix.rb +20 -12
- data/lib/seeing_is_believing/version.rb +1 -1
- data/lib/seeing_is_believing/wrap_expressions.rb +55 -115
- data/lib/seeing_is_believing/wrap_expressions_with_inspect.rb +14 -0
- data/seeing_is_believing.gemspec +1 -1
- data/spec/binary/alignment_specs.rb +27 -0
- data/spec/binary/comment_lines_spec.rb +3 -2
- data/spec/binary/config_spec.rb +657 -0
- data/spec/binary/engine_spec.rb +97 -0
- data/spec/binary/{comment_formatter_spec.rb → format_comment_spec.rb} +2 -2
- data/spec/binary/marker_spec.rb +71 -0
- data/spec/binary/options_spec.rb +0 -0
- data/spec/binary/remove_annotations_spec.rb +31 -18
- data/spec/binary/rewrite_comments_spec.rb +26 -11
- data/spec/code_spec.rb +190 -6
- data/spec/debugger_spec.rb +4 -0
- data/spec/evaluate_by_moving_files_spec.rb +38 -20
- data/spec/event_stream_spec.rb +265 -116
- data/spec/hash_struct_spec.rb +514 -0
- data/spec/seeing_is_believing_spec.rb +108 -46
- data/spec/spec_helper.rb +9 -0
- data/spec/wrap_expressions_spec.rb +207 -172
- metadata +30 -18
- data/docs/for-presentations +0 -33
- data/lib/seeing_is_believing/binary/annotate_xmpfilter_style.rb +0 -128
- data/lib/seeing_is_believing/binary/interpret_flags.rb +0 -156
- data/lib/seeing_is_believing/binary/parse_args.rb +0 -263
- data/lib/seeing_is_believing/event_stream/update_result.rb +0 -24
- data/lib/seeing_is_believing/inspect_expressions.rb +0 -21
- data/lib/seeing_is_believing/parser_helpers.rb +0 -82
- data/spec/binary/interpret_flags_spec.rb +0 -332
- data/spec/binary/parse_args_spec.rb +0 -415
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'seeing_is_believing/hash_struct'
|
2
|
+
|
3
|
+
class SeeingIsBelieving
|
4
|
+
module Binary
|
5
|
+
class Markers < HashStruct
|
6
|
+
attribute(:value) { Marker.new prefix: '# => ', regex: '^#\s*=>\s*' }
|
7
|
+
attribute(:exception) { Marker.new prefix: '# ~> ', regex: '^#\s*~>\s*' }
|
8
|
+
attribute(:stdout) { Marker.new prefix: '# >> ', regex: '^#\s*>>\s*' }
|
9
|
+
attribute(:stderr) { Marker.new prefix: '# !> ', regex: '^#\s*!>\s*' }
|
10
|
+
end
|
11
|
+
|
12
|
+
|
13
|
+
class Marker < HashStruct
|
14
|
+
def self.to_regex(string)
|
15
|
+
return string if string.kind_of? Regexp
|
16
|
+
flag_to_bit = {
|
17
|
+
'i' => 0b001,
|
18
|
+
'x' => 0b010,
|
19
|
+
'm' => 0b100
|
20
|
+
}
|
21
|
+
string =~ %r{\A/(.*)/([mxi]*)\Z}
|
22
|
+
body = $1 || string
|
23
|
+
flags = ($2 || "").each_char.inject(0) { |bits, flag| bits | flag_to_bit[flag] }
|
24
|
+
Regexp.new body, flags
|
25
|
+
end
|
26
|
+
|
27
|
+
attribute :prefix # e.g. "# => "
|
28
|
+
attribute :regex # identify prefix in a comment, e.g. /^# => /
|
29
|
+
|
30
|
+
def []=(key, value)
|
31
|
+
value = Marker.to_regex(value) if key == :regex
|
32
|
+
super key, value
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
class AnnotatorOptions < HashStruct
|
38
|
+
attribute(:alignment_strategy) { AlignChunk }
|
39
|
+
attribute(:markers) { Markers.new }
|
40
|
+
attribute(:max_line_length) { Float::INFINITY }
|
41
|
+
attribute(:max_result_length) { Float::INFINITY }
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
class ErrorMessage < HashStruct.for(:explanation)
|
46
|
+
def to_s
|
47
|
+
"Error: #{explanation}"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
class SyntaxErrorMessage < ErrorMessage.for(:line_number, :filename)
|
52
|
+
def to_s
|
53
|
+
"Syntax Error: #{filename}:#{line_number}\n#{explanation}\n"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
require 'seeing_is_believing/code'
|
2
|
+
require 'seeing_is_believing/binary/data_structures'
|
3
|
+
require 'seeing_is_believing/binary/remove_annotations'
|
4
|
+
require 'seeing_is_believing/event_stream/handlers/record_exitstatus'
|
5
|
+
|
6
|
+
class SeeingIsBelieving
|
7
|
+
module Binary
|
8
|
+
class MustEvaluateFirst < SeeingIsBelievingError
|
9
|
+
def initialize(method)
|
10
|
+
super "Cannot call #{method} on engine until it has evaluated the program."
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class Engine
|
15
|
+
def initialize(config)
|
16
|
+
self.config = config
|
17
|
+
end
|
18
|
+
|
19
|
+
def cleaned_body
|
20
|
+
@cleaned_body ||= if missing_newline?
|
21
|
+
normalized_cleaned_body.chomp!
|
22
|
+
else
|
23
|
+
normalized_cleaned_body
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def syntax_error?
|
28
|
+
code.syntax.invalid?
|
29
|
+
end
|
30
|
+
|
31
|
+
def syntax_error
|
32
|
+
return unless syntax_error?
|
33
|
+
SyntaxErrorMessage.new line_number: code.syntax.line_number,
|
34
|
+
explanation: code.syntax.error_message,
|
35
|
+
filename: config.lib_options.filename
|
36
|
+
end
|
37
|
+
|
38
|
+
def evaluate!
|
39
|
+
@evaluated || begin
|
40
|
+
SeeingIsBelieving.call normalized_cleaned_body,
|
41
|
+
config.lib_options.merge(event_handler: record_exitstatus)
|
42
|
+
@timed_out = false
|
43
|
+
@evaluated = true
|
44
|
+
end
|
45
|
+
rescue Timeout::Error
|
46
|
+
@timed_out = true
|
47
|
+
@evaluated = true
|
48
|
+
ensure return self unless $! # idk, maybe too tricky, but was really annoying putting it in three places
|
49
|
+
end
|
50
|
+
|
51
|
+
def timed_out?
|
52
|
+
@evaluated || raise(MustEvaluateFirst.new __method__)
|
53
|
+
@timed_out
|
54
|
+
end
|
55
|
+
|
56
|
+
def result
|
57
|
+
@evaluated || raise(MustEvaluateFirst.new __method__)
|
58
|
+
config.lib_options.event_handler.result # The stream handler will not have a result (implies this was used wrong)
|
59
|
+
end
|
60
|
+
|
61
|
+
def annotated_body
|
62
|
+
@annotated_body ||= begin
|
63
|
+
@evaluated || raise(MustEvaluateFirst.new __method__)
|
64
|
+
annotated = config.annotator.call normalized_cleaned_body,
|
65
|
+
result,
|
66
|
+
config.annotator_options.to_h
|
67
|
+
annotated.chomp! if missing_newline?
|
68
|
+
annotated
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def exitstatus
|
73
|
+
@evaluated || raise(MustEvaluateFirst.new __method__)
|
74
|
+
record_exitstatus.exitstatus
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
attr_accessor :config
|
80
|
+
|
81
|
+
def missing_newline?
|
82
|
+
@missing_newline ||= !config.body.end_with?("\n")
|
83
|
+
end
|
84
|
+
|
85
|
+
def code
|
86
|
+
@code ||= Code.new(normalized_cleaned_body, config.filename)
|
87
|
+
end
|
88
|
+
|
89
|
+
def record_exitstatus
|
90
|
+
@record_exitstatus ||= SeeingIsBelieving::EventStream::Handlers::RecordExitStatus.new config.lib_options.event_handler
|
91
|
+
end
|
92
|
+
|
93
|
+
def normalized_cleaned_body
|
94
|
+
@normalized_cleaned_body ||= begin
|
95
|
+
body_with_nl = config.body
|
96
|
+
body_with_nl += "\n" if missing_newline?
|
97
|
+
RemoveAnnotations.call body_with_nl,
|
98
|
+
config.remove_value_prefixes,
|
99
|
+
config.markers
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -6,17 +6,17 @@ class SeeingIsBelieving
|
|
6
6
|
# line_length is the length of the line this comment is being appended to
|
7
7
|
#
|
8
8
|
# For examples of what the options are, and how they all fit together, see
|
9
|
-
# spec/binary/
|
10
|
-
class
|
9
|
+
# spec/binary/format_comment.rb
|
10
|
+
class FormatComment
|
11
11
|
def self.call(*args)
|
12
12
|
new(*args).call
|
13
13
|
end
|
14
14
|
|
15
15
|
def initialize(line_length, separator, result, options)
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
16
|
+
self.line_length = line_length
|
17
|
+
self.separator = separator
|
18
|
+
self.result = result
|
19
|
+
self.options = options
|
20
20
|
end
|
21
21
|
|
22
22
|
def call
|
@@ -1,21 +1,18 @@
|
|
1
|
-
require 'seeing_is_believing/binary'
|
2
|
-
require 'seeing_is_believing/
|
1
|
+
require 'seeing_is_believing/binary' # Defines the regexes to locate the markers
|
2
|
+
require 'seeing_is_believing/code' # We have to parse the file to find the comments
|
3
3
|
|
4
4
|
class SeeingIsBelieving
|
5
5
|
module Binary
|
6
|
-
# TODO: might be here that we hit the issue where
|
7
|
-
# you sometimes have to run it 2x to get it to correctly reset whitespace
|
8
|
-
# should wipe out the full_range rather than just the comment_range
|
9
6
|
class RemoveAnnotations
|
10
|
-
def self.call(raw_code,
|
11
|
-
new(raw_code,
|
7
|
+
def self.call(raw_code, remove_value_prefixes, markers)
|
8
|
+
new(raw_code, remove_value_prefixes, markers).call
|
12
9
|
end
|
13
10
|
|
14
|
-
def initialize(raw_code,
|
15
|
-
self.
|
16
|
-
self.raw_code
|
17
|
-
self.markers
|
18
|
-
self.code
|
11
|
+
def initialize(raw_code, remove_value_prefixes, markers)
|
12
|
+
self.remove_value_prefixes = remove_value_prefixes
|
13
|
+
self.raw_code = raw_code
|
14
|
+
self.markers = markers
|
15
|
+
self.code = Code.new(raw_code, 'strip_comments')
|
19
16
|
end
|
20
17
|
|
21
18
|
def call
|
@@ -27,9 +24,13 @@ class SeeingIsBelieving
|
|
27
24
|
|
28
25
|
case comment.text
|
29
26
|
when value_regex
|
30
|
-
|
31
|
-
|
32
|
-
|
27
|
+
if remove_value_prefixes
|
28
|
+
code.rewriter.remove comment.comment_range
|
29
|
+
remove_whitespace_before comment.comment_range.begin_pos, code.buffer, code.rewriter, false
|
30
|
+
else
|
31
|
+
prefix = comment.text[value_regex].rstrip
|
32
|
+
code.rewriter.replace comment.comment_range, prefix
|
33
|
+
end
|
33
34
|
when exception_regex
|
34
35
|
code.rewriter.remove comment.comment_range
|
35
36
|
remove_whitespace_before comment.comment_range.begin_pos, code.buffer, code.rewriter, true
|
@@ -49,7 +50,7 @@ class SeeingIsBelieving
|
|
49
50
|
|
50
51
|
private
|
51
52
|
|
52
|
-
attr_accessor :raw_code, :
|
53
|
+
attr_accessor :raw_code, :remove_value_prefixes, :markers, :code
|
53
54
|
|
54
55
|
# any whitespace before the index (on the same line) will be removed
|
55
56
|
# if the preceding whitespace is at the beginning of the line, the newline will be removed
|
@@ -57,11 +58,11 @@ class SeeingIsBelieving
|
|
57
58
|
def remove_whitespace_before(index, buffer, rewriter, remove_preceding_newline)
|
58
59
|
end_pos = index
|
59
60
|
begin_pos = end_pos - 1
|
60
|
-
begin_pos -= 1 while raw_code[begin_pos] =~ /\s/ && raw_code[begin_pos] != "\n"
|
61
|
-
begin_pos -= 1 if raw_code[begin_pos] == "\n"
|
62
|
-
begin_pos -= 1 if raw_code[begin_pos] == "\n"
|
61
|
+
begin_pos -= 1 while 0 <= begin_pos && raw_code[begin_pos] =~ /\s/ && raw_code[begin_pos] != "\n"
|
62
|
+
begin_pos -= 1 if 0 <= begin_pos && raw_code[begin_pos] == "\n"
|
63
|
+
begin_pos -= 1 if remove_preceding_newline && 0 <= begin_pos && raw_code[begin_pos] == "\n"
|
63
64
|
return if begin_pos.next == end_pos
|
64
|
-
rewriter.remove
|
65
|
+
rewriter.remove code.range_for(begin_pos.next, end_pos)
|
65
66
|
end
|
66
67
|
|
67
68
|
def annotation_chunks_in(code)
|
@@ -90,24 +91,24 @@ class SeeingIsBelieving
|
|
90
91
|
}
|
91
92
|
end
|
92
93
|
|
94
|
+
def value_prefix
|
95
|
+
@value_prefix ||= markers.fetch(:value).fetch(:prefix)
|
96
|
+
end
|
97
|
+
|
93
98
|
def value_regex
|
94
|
-
markers.fetch(:value)
|
99
|
+
@value_regex ||= markers.fetch(:value).fetch(:regex)
|
95
100
|
end
|
96
101
|
|
97
102
|
def exception_regex
|
98
|
-
markers.fetch(:exception)
|
103
|
+
@exception_regex ||= markers.fetch(:exception).fetch(:regex)
|
99
104
|
end
|
100
105
|
|
101
106
|
def stdout_regex
|
102
|
-
markers.fetch(:stdout)
|
107
|
+
@stdout_regex ||= markers.fetch(:stdout).fetch(:regex)
|
103
108
|
end
|
104
109
|
|
105
110
|
def stderr_regex
|
106
|
-
markers.fetch(:stderr)
|
107
|
-
end
|
108
|
-
|
109
|
-
def nextline_regex
|
110
|
-
markers.fetch(:nextline)
|
111
|
+
@stderr_regex ||= markers.fetch(:stderr).fetch(:regex)
|
111
112
|
end
|
112
113
|
end
|
113
114
|
end
|
@@ -1,63 +1,62 @@
|
|
1
1
|
require 'seeing_is_believing/code'
|
2
|
+
require 'seeing_is_believing/binary/commentable_lines'
|
2
3
|
|
3
4
|
class SeeingIsBelieving
|
4
5
|
module Binary
|
6
|
+
# can this be joined into CommentLines?
|
7
|
+
# that one yields every commentable line, this one just lines which have comments
|
8
|
+
# what they yield is a little different, too, but their algorithms and domain are very similar
|
5
9
|
module RewriteComments
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
+
Options = HashStruct.anon do
|
11
|
+
attribute(:include_lines) { [] }
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.call(raw_code, options={}, &mapping)
|
15
|
+
code = Code.new(raw_code)
|
16
|
+
comments = code.inline_comments
|
17
|
+
extra_lines = Options.new(options).include_lines
|
10
18
|
|
19
|
+
# update existing comments
|
11
20
|
comments.each do |comment|
|
12
21
|
new_whitespace, new_comment = mapping.call comment
|
13
22
|
code.rewriter.replace comment.whitespace_range, new_whitespace
|
14
23
|
code.rewriter.replace comment.comment_range, new_comment
|
15
24
|
end
|
16
25
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
26
|
+
# remove extra lines that are handled / uncommentable
|
27
|
+
comments.each { |c| extra_lines.delete c.line_number }
|
28
|
+
commentable_linenums = CommentableLines.call(code.raw).map { |linenum, *| linenum }
|
29
|
+
extra_lines.select! { |linenum| commentable_linenums.include? linenum }
|
30
|
+
|
31
|
+
# add additional comments
|
32
|
+
extra_lines.each do |line_number|
|
33
|
+
line_begin_col = code.linenum_to_index(line_number)
|
34
|
+
nextline_begin_col = code.linenum_to_index(line_number.next)
|
35
|
+
nextline_begin_col -= 1 if code.raw[nextline_begin_col-1] == "\n"
|
36
|
+
whitespace_col = nextline_begin_col-1
|
37
|
+
whitespace_col -= 1 while line_begin_col < whitespace_col &&
|
38
|
+
code.raw[whitespace_col] =~ /\s/
|
39
|
+
whitespace_col += 1
|
40
|
+
whitespace_range = code.range_for(whitespace_col, nextline_begin_col)
|
41
|
+
comment_range = code.range_for(nextline_begin_col, nextline_begin_col)
|
42
|
+
|
43
|
+
comment = Code::InlineComment.new \
|
44
|
+
line_number: line_number,
|
45
|
+
whitespace_col: whitespace_col-line_begin_col,
|
46
|
+
whitespace: code.raw[whitespace_col...nextline_begin_col]||"",
|
47
|
+
text_col: nextline_begin_col-line_begin_col,
|
48
|
+
text: "",
|
49
|
+
full_range: whitespace_range,
|
50
|
+
whitespace_range: whitespace_range,
|
51
|
+
comment_range: comment_range
|
37
52
|
|
38
53
|
whitespace, body = mapping.call comment
|
39
|
-
code.rewriter.
|
40
|
-
|
54
|
+
code.rewriter.replace whitespace_range, "#{whitespace}#{body}"
|
55
|
+
end
|
41
56
|
|
57
|
+
# perform the rewrite
|
42
58
|
code.rewriter.process
|
43
59
|
end
|
44
|
-
|
45
|
-
# TODO: Move down into the Code obj?
|
46
|
-
# returns: [[lineno, index], ...]
|
47
|
-
def self.line_begins_for(raw_code)
|
48
|
-
# Copied from here https://github.com/whitequark/parser/blob/34c40479293bb9b5ba217039cf349111466d1f9a/lib/parser/source/buffer.rb#L213-227
|
49
|
-
# I figured it's better to copy it than to violate encapsulation since this is private
|
50
|
-
line_begins, index = [ [ 0, 0 ] ], 1
|
51
|
-
|
52
|
-
raw_code.each_char do |char|
|
53
|
-
if char == "\n"
|
54
|
-
line_begins.unshift [ line_begins.length, index ]
|
55
|
-
end
|
56
|
-
|
57
|
-
index += 1
|
58
|
-
end
|
59
|
-
line_begins
|
60
|
-
end
|
61
60
|
end
|
62
61
|
end
|
63
62
|
end
|
@@ -1,99 +1,155 @@
|
|
1
|
-
|
1
|
+
# With new versioning, there's lots of small versions
|
2
|
+
# we don't need Parser to complain that we're on 2.1.1 and its parsing 2.1.5
|
3
|
+
# https://github.com/whitequark/parser/blob/e2249d7051b1adb6979139928e14a81bc62f566e/lib/parser/current.rb#L3
|
4
|
+
class << (Parser ||= Module.new)
|
5
|
+
def warn(*) end
|
6
|
+
require 'parser/current'
|
7
|
+
remove_method :warn
|
8
|
+
end
|
9
|
+
|
10
|
+
require 'seeing_is_believing/hash_struct'
|
11
|
+
|
2
12
|
class SeeingIsBelieving
|
3
13
|
class Code
|
4
|
-
InlineComment =
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
:text,
|
9
|
-
:full_range,
|
10
|
-
:whitespace_range,
|
11
|
-
:comment_range
|
12
|
-
|
13
|
-
# At prsent, it is expected that the syntax is validated before code arrives here
|
14
|
-
# or that its validity doesn't matter (e.g. extracting comments)
|
15
|
-
def initialize(raw_ruby_code, name="SeeingIsBelieving")
|
16
|
-
self.code = raw_ruby_code
|
17
|
-
self.name = name
|
14
|
+
InlineComment = HashStruct.for :line_number, :whitespace_col, :whitespace, :text_col, :text, :full_range, :whitespace_range, :comment_range
|
15
|
+
Syntax = HashStruct.for error_message: nil, line_number: nil do
|
16
|
+
def valid?() !error_message end
|
17
|
+
def invalid?() !valid? end
|
18
18
|
end
|
19
19
|
|
20
|
-
|
21
|
-
|
22
|
-
def
|
23
|
-
|
24
|
-
|
20
|
+
attr_reader :raw, :buffer, :parser, :rewriter, :inline_comments, :root, :raw_comments, :syntax, :body_range
|
21
|
+
|
22
|
+
def initialize(raw_code, name=nil)
|
23
|
+
raw_code[-1] == "\n" || raise(SyntaxError, "Code must end in a newline for the sake of consistency (sanity)")
|
24
|
+
@raw = raw_code
|
25
|
+
@buffer = Parser::Source::Buffer.new(name||"SeeingIsBelieving")
|
26
|
+
@buffer.source = raw
|
27
|
+
builder = Parser::Builders::Default.new.tap { |b| b.emit_file_line_as_literals = false }
|
28
|
+
@rewriter = Parser::Source::Rewriter.new buffer
|
29
|
+
@raw_comments, tokens = comments_and_tokens(builder, buffer)
|
30
|
+
@body_range = body_range_from_tokens(tokens)
|
31
|
+
@parser = Parser::CurrentRuby.new builder
|
32
|
+
@inline_comments = raw_comments.select(&:inline?).map { |c| wrap_comment c }
|
33
|
+
begin
|
34
|
+
@root = @parser.parse(@buffer)
|
35
|
+
@syntax = Syntax.new
|
36
|
+
rescue Parser::SyntaxError
|
37
|
+
@syntax = Syntax.new error_message: $!.message, line_number: index_to_linenum($!.diagnostic.location.begin_pos)
|
38
|
+
ensure
|
39
|
+
@root ||= null_node
|
40
|
+
end
|
41
|
+
end
|
25
42
|
|
26
43
|
def range_for(start_index, end_index)
|
27
44
|
Parser::Source::Range.new buffer, start_index, end_index
|
28
45
|
end
|
29
46
|
|
30
|
-
|
31
|
-
|
32
|
-
|
47
|
+
def index_to_linenum(char_index)
|
48
|
+
line_indexes.index { |line_index| char_index < line_index } || line_indexes.size
|
49
|
+
end
|
33
50
|
|
34
|
-
def
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
builder.emit_file_line_as_literals = false # should be injectible?
|
39
|
-
@parser = Parser::CurrentRuby.new builder
|
40
|
-
@rewriter = Parser::Source::Rewriter.new @buffer
|
51
|
+
def linenum_to_index(line_num)
|
52
|
+
return raw.size if line_indexes.size < line_num
|
53
|
+
line_indexes[line_num - 1]
|
54
|
+
end
|
41
55
|
|
42
|
-
|
56
|
+
def heredoc?(ast)
|
57
|
+
# some strings are fucking weird.
|
58
|
+
# e.g. the "1" in `%w[1]` returns nil for ast.location.begin
|
59
|
+
# and `__FILE__` is a string whose location is a Parser::Source::Map instead of a Parser::Source::Map::Collection,
|
60
|
+
# so it has no #begin
|
61
|
+
ast.kind_of?(Parser::AST::Node) &&
|
62
|
+
(ast.type == :dstr || ast.type == :str) &&
|
63
|
+
(location = ast.location) &&
|
64
|
+
(location.kind_of? Parser::Source::Map::Heredoc)
|
65
|
+
end
|
43
66
|
|
44
|
-
|
67
|
+
def void_value?(ast)
|
68
|
+
case ast && ast.type
|
69
|
+
when :begin, :kwbegin, :resbody
|
70
|
+
void_value?(ast.children.last)
|
71
|
+
when :rescue, :ensure
|
72
|
+
ast.children.any? { |child| void_value? child }
|
73
|
+
when :if
|
74
|
+
void_value?(ast.children[1]) || void_value?(ast.children[2])
|
75
|
+
when :return, :next, :redo, :retry, :break
|
76
|
+
true
|
77
|
+
else
|
78
|
+
false
|
79
|
+
end
|
80
|
+
end
|
45
81
|
|
46
|
-
|
82
|
+
def line_indexes
|
83
|
+
@line_indexes ||= [ 0,
|
84
|
+
*raw.each_char
|
85
|
+
.with_index(1)
|
86
|
+
.select { |char, index| char == "\n" }
|
87
|
+
.map { |char, index| index },
|
88
|
+
].freeze
|
47
89
|
end
|
48
90
|
|
49
|
-
|
50
|
-
# THIS IS SO WE CAN EXTRACT COMMENTS FROM INVALID FILES.
|
91
|
+
private
|
51
92
|
|
93
|
+
def comments_and_tokens(builder, buffer)
|
94
|
+
# THIS IS SO WE CAN EXTRACT COMMENTS FROM INVALID FILES.
|
52
95
|
# We do it by telling Parser's diagnostic to not blow up.
|
53
96
|
# https://github.com/whitequark/parser/blob/2d69a1b5f34ef15b3a8330beb036ac4bf4775e29/lib/parser/diagnostic/engine.rb
|
54
|
-
|
55
97
|
# However, this probably implies SiB won't work on Rbx/JRuby
|
56
98
|
# https://github.com/whitequark/parser/blob/2d69a1b5f34ef15b3a8330beb036ac4bf4775e29/lib/parser/base.rb#L129-134
|
57
|
-
|
58
99
|
# Ideally we could just do this
|
59
100
|
# parser.diagnostics.all_errors_are_fatal = false
|
60
101
|
# parser.diagnostics.ignore_warnings = false
|
61
|
-
|
62
102
|
# But, the parser will still blow up on "fatal" errors (e.g. unterminated string) So we need to actually change it.
|
63
103
|
# https://github.com/whitequark/parser/blob/2d69a1b5f34ef15b3a8330beb036ac4bf4775e29/lib/parser/diagnostic/engine.rb#L99
|
64
|
-
|
65
104
|
# We could make a NullDiagnostics like this:
|
66
105
|
# class NullDiagnostics < Parser::Diagnostic::Engine
|
67
106
|
# def process(*)
|
68
107
|
# # no op
|
69
108
|
# end
|
70
109
|
# end
|
71
|
-
|
72
110
|
# But we don't control initialization of the variable, and the value gets passed around, at least into the lexer.
|
73
111
|
# https://github.com/whitequark/parser/blob/2d69a1b5f34ef15b3a8330beb036ac4bf4775e29/lib/parser/base.rb#L139
|
74
112
|
# and since it's all private, it could change at any time (Parser is very state based),
|
75
113
|
# so I think it's just generally safer to mutate that one object, as we do now.
|
114
|
+
parser = Parser::CurrentRuby.new builder
|
76
115
|
diagnostics = parser.diagnostics
|
77
116
|
def diagnostics.process(*)
|
78
117
|
self
|
79
118
|
end
|
119
|
+
_, all_comments, tokens = parser.tokenize(@buffer)
|
120
|
+
[all_comments, tokens]
|
121
|
+
end
|
122
|
+
|
123
|
+
def body_range_from_tokens(tokens)
|
124
|
+
return range_for(0, 0) if raw.start_with? "__END__\n"
|
125
|
+
(name, (_, range)) = tokens.max_by { |name, (data, range)| range.end_pos } ||
|
126
|
+
[nil, [nil, range_for(0, 1)]]
|
127
|
+
end_pos = range.end_pos
|
128
|
+
end_pos += 1 if name == :tCOMMENT || name == :tSTRING_END #
|
129
|
+
range_for 0, end_pos
|
80
130
|
end
|
81
131
|
|
82
132
|
def wrap_comment(comment)
|
83
133
|
last_char = comment.location.expression.begin_pos
|
84
134
|
first_char = last_char
|
85
|
-
first_char -= 1 while first_char > 0 &&
|
135
|
+
first_char -= 1 while first_char > 0 && raw[first_char-1] =~ /[ \t]/
|
86
136
|
preceding_whitespace = buffer.source[first_char...last_char]
|
87
137
|
preceding_whitespace_range = range_for first_char, last_char
|
88
138
|
|
89
|
-
InlineComment.new comment.location.line,
|
90
|
-
preceding_whitespace_range.column,
|
91
|
-
preceding_whitespace,
|
92
|
-
comment.location.column,
|
93
|
-
comment.text,
|
94
|
-
range_for(first_char, comment.location.expression.end_pos),
|
95
|
-
preceding_whitespace_range,
|
96
|
-
comment.location.expression
|
139
|
+
InlineComment.new line_number: comment.location.line,
|
140
|
+
whitespace_col: preceding_whitespace_range.column,
|
141
|
+
whitespace: preceding_whitespace,
|
142
|
+
text_col: comment.location.column,
|
143
|
+
text: comment.text,
|
144
|
+
full_range: range_for(first_char, comment.location.expression.end_pos),
|
145
|
+
whitespace_range: preceding_whitespace_range,
|
146
|
+
comment_range: comment.location.expression
|
97
147
|
end
|
148
|
+
|
149
|
+
def null_node
|
150
|
+
location = Parser::Source::Map::Collection.new nil, nil, range_for(0, 0)
|
151
|
+
Parser::AST::Node.new :null_node, [], location: location
|
152
|
+
end
|
153
|
+
|
98
154
|
end
|
99
155
|
end
|