seeing_is_believing 3.0.0.beta.4 → 3.0.0.beta.5
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/.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
|