andyw8-seeing_is_believing 4.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.github/workflows/test.yml +60 -0
- data/.gitignore +19 -0
- data/.rspec +2 -0
- data/Gemfile +2 -0
- data/README.md +70 -0
- data/Rakefile +88 -0
- data/appveyor.yml +32 -0
- data/bin/seeing_is_believing +7 -0
- data/docs/example.gif +0 -0
- data/docs/frog-brown.png +0 -0
- data/docs/sib-streaming.gif +0 -0
- data/features/deprecated-flags.feature +91 -0
- data/features/errors.feature +155 -0
- data/features/examples.feature +423 -0
- data/features/flags.feature +852 -0
- data/features/regression.feature +898 -0
- data/features/support/env.rb +102 -0
- data/features/xmpfilter-style.feature +471 -0
- data/lib/seeing_is_believing/binary/align_chunk.rb +47 -0
- data/lib/seeing_is_believing/binary/align_file.rb +24 -0
- data/lib/seeing_is_believing/binary/align_line.rb +25 -0
- data/lib/seeing_is_believing/binary/annotate_end_of_file.rb +56 -0
- data/lib/seeing_is_believing/binary/annotate_every_line.rb +52 -0
- data/lib/seeing_is_believing/binary/annotate_marked_lines.rb +179 -0
- data/lib/seeing_is_believing/binary/comment_lines.rb +36 -0
- data/lib/seeing_is_believing/binary/commentable_lines.rb +126 -0
- data/lib/seeing_is_believing/binary/config.rb +455 -0
- data/lib/seeing_is_believing/binary/data_structures.rb +58 -0
- data/lib/seeing_is_believing/binary/engine.rb +161 -0
- data/lib/seeing_is_believing/binary/format_comment.rb +79 -0
- data/lib/seeing_is_believing/binary/interline_align.rb +57 -0
- data/lib/seeing_is_believing/binary/remove_annotations.rb +113 -0
- data/lib/seeing_is_believing/binary/rewrite_comments.rb +62 -0
- data/lib/seeing_is_believing/binary.rb +73 -0
- data/lib/seeing_is_believing/code.rb +139 -0
- data/lib/seeing_is_believing/compatibility.rb +28 -0
- data/lib/seeing_is_believing/debugger.rb +32 -0
- data/lib/seeing_is_believing/error.rb +17 -0
- data/lib/seeing_is_believing/evaluate_by_moving_files.rb +195 -0
- data/lib/seeing_is_believing/event_stream/consumer.rb +221 -0
- data/lib/seeing_is_believing/event_stream/events.rb +193 -0
- data/lib/seeing_is_believing/event_stream/handlers/debug.rb +61 -0
- data/lib/seeing_is_believing/event_stream/handlers/record_exit_events.rb +26 -0
- data/lib/seeing_is_believing/event_stream/handlers/stream_json_events.rb +23 -0
- data/lib/seeing_is_believing/event_stream/handlers/update_result.rb +41 -0
- data/lib/seeing_is_believing/event_stream/producer.rb +178 -0
- data/lib/seeing_is_believing/hard_core_ensure.rb +58 -0
- data/lib/seeing_is_believing/hash_struct.rb +206 -0
- data/lib/seeing_is_believing/result.rb +89 -0
- data/lib/seeing_is_believing/safe.rb +112 -0
- data/lib/seeing_is_believing/swap_files.rb +90 -0
- data/lib/seeing_is_believing/the_matrix.rb +97 -0
- data/lib/seeing_is_believing/version.rb +3 -0
- data/lib/seeing_is_believing/wrap_expressions.rb +265 -0
- data/lib/seeing_is_believing/wrap_expressions_with_inspect.rb +19 -0
- data/lib/seeing_is_believing.rb +69 -0
- data/seeing_is_believing.gemspec +84 -0
- data/spec/binary/alignment_specs.rb +27 -0
- data/spec/binary/comment_lines_spec.rb +852 -0
- data/spec/binary/config_spec.rb +831 -0
- data/spec/binary/engine_spec.rb +114 -0
- data/spec/binary/format_comment_spec.rb +210 -0
- data/spec/binary/marker_spec.rb +71 -0
- data/spec/binary/remove_annotations_spec.rb +342 -0
- data/spec/binary/rewrite_comments_spec.rb +106 -0
- data/spec/code_spec.rb +233 -0
- data/spec/debugger_spec.rb +45 -0
- data/spec/evaluate_by_moving_files_spec.rb +204 -0
- data/spec/event_stream_spec.rb +762 -0
- data/spec/hard_core_ensure_spec.rb +120 -0
- data/spec/hash_struct_spec.rb +514 -0
- data/spec/seeing_is_believing_spec.rb +1094 -0
- data/spec/sib_spec_helpers/version.rb +17 -0
- data/spec/spec_helper.rb +26 -0
- data/spec/spec_helper_spec.rb +16 -0
- data/spec/wrap_expressions_spec.rb +1013 -0
- metadata +340 -0
@@ -0,0 +1,79 @@
|
|
1
|
+
# Polyfill String#scrub on Ruby 2.0.0
|
2
|
+
require 'seeing_is_believing/compatibility'
|
3
|
+
using SeeingIsBelieving::Compatibility
|
4
|
+
|
5
|
+
class SeeingIsBelieving
|
6
|
+
module Binary
|
7
|
+
# not sure I like this name, it formats comments that show results
|
8
|
+
# e.g. "# => [1, 2, 3]"
|
9
|
+
#
|
10
|
+
# line_length is the length of the line this comment is being appended to
|
11
|
+
#
|
12
|
+
# For examples of what the options are, and how they all fit together, see
|
13
|
+
# spec/binary/format_comment.rb
|
14
|
+
class FormatComment
|
15
|
+
def self.call(*args)
|
16
|
+
new(*args).call
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize(line_length, separator, result, options)
|
20
|
+
self.line_length = line_length
|
21
|
+
self.separator = separator
|
22
|
+
self.result = result
|
23
|
+
self.options = options
|
24
|
+
end
|
25
|
+
|
26
|
+
def call
|
27
|
+
@formatted ||= begin
|
28
|
+
formatted = escape_non_printable result, chars_not_to_escape
|
29
|
+
formatted = truncate "#{separator}#{formatted}", max_result_length
|
30
|
+
formatted = "#{' '*padding_length}#{formatted}"
|
31
|
+
formatted = truncate formatted, max_line_length
|
32
|
+
formatted = '' unless formatted.sub(/^ */, '').start_with? separator
|
33
|
+
formatted
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
attr_accessor :line_length, :separator, :result, :options
|
40
|
+
|
41
|
+
def max_line_length
|
42
|
+
length = options.fetch(:max_line_length, Float::INFINITY) - line_length
|
43
|
+
length = 0 if length < 0
|
44
|
+
length
|
45
|
+
end
|
46
|
+
|
47
|
+
def max_result_length
|
48
|
+
options.fetch :max_result_length, Float::INFINITY
|
49
|
+
end
|
50
|
+
|
51
|
+
def padding_length
|
52
|
+
padding_length = options.fetch(:pad_to, 0) - line_length
|
53
|
+
padding_length = 0 if padding_length < 0
|
54
|
+
padding_length
|
55
|
+
end
|
56
|
+
|
57
|
+
def truncate(string, length)
|
58
|
+
return string if string.size <= length
|
59
|
+
ellipsify string.slice(0, length)
|
60
|
+
end
|
61
|
+
|
62
|
+
def ellipsify(string)
|
63
|
+
string.sub(/.{0,3}$/) { |last_chars| '.' * last_chars.size }
|
64
|
+
end
|
65
|
+
|
66
|
+
def chars_not_to_escape
|
67
|
+
options.fetch :dont_escape, []
|
68
|
+
end
|
69
|
+
|
70
|
+
def escape_non_printable(str, omissions)
|
71
|
+
str.scrub { |c| c.inspect[1...-1] }
|
72
|
+
.gsub(/[\u0000-\u0020]/) { |char|
|
73
|
+
next char if omissions.include? char
|
74
|
+
char.inspect[1...-1]
|
75
|
+
}
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
class SeeingIsBelieving
|
2
|
+
module Binary
|
3
|
+
class InterlineAlign
|
4
|
+
def initialize(results)
|
5
|
+
@results = results
|
6
|
+
@format_strings = {}
|
7
|
+
end
|
8
|
+
|
9
|
+
def call(lineno, results)
|
10
|
+
format_string_for_line(lineno) % results
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
attr_accessor :results
|
16
|
+
|
17
|
+
def format_string_for_line(lineno)
|
18
|
+
group = groups_with_same_number_of_results(@results)[lineno]
|
19
|
+
format_string_for(results, group, lineno)
|
20
|
+
end
|
21
|
+
|
22
|
+
def groups_with_same_number_of_results(results)
|
23
|
+
@grouped_by_no_results ||= begin
|
24
|
+
length = 0
|
25
|
+
groups = 1.upto(results.num_lines)
|
26
|
+
.slice_before { |num|
|
27
|
+
new_length = results[num].length
|
28
|
+
slice = length != new_length
|
29
|
+
length = new_length
|
30
|
+
slice
|
31
|
+
}.to_a
|
32
|
+
|
33
|
+
groups.each_with_object Hash.new do |group, lineno_to_group|
|
34
|
+
group.each { |lineno| lineno_to_group[lineno] = group }
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def format_string_for(results, group, lineno)
|
40
|
+
@format_strings[lineno] ||= begin
|
41
|
+
index = group.index lineno
|
42
|
+
group
|
43
|
+
.map { |lineno| results[lineno] }
|
44
|
+
.transpose
|
45
|
+
.map { |col|
|
46
|
+
lengths = col.map(&:length)
|
47
|
+
max = lengths.max
|
48
|
+
crnt = lengths[index]
|
49
|
+
"%-#{crnt}s,#{" "*(max-crnt)} "
|
50
|
+
}
|
51
|
+
.join
|
52
|
+
.sub(/, *$/, "")
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,113 @@
|
|
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
|
+
|
4
|
+
class SeeingIsBelieving
|
5
|
+
module Binary
|
6
|
+
class RemoveAnnotations
|
7
|
+
def self.call(raw_code, remove_value_prefixes, markers)
|
8
|
+
new(raw_code, remove_value_prefixes, markers).call
|
9
|
+
end
|
10
|
+
|
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')
|
16
|
+
end
|
17
|
+
|
18
|
+
def call
|
19
|
+
annotation_chunks_in(code).each do |comment, rest|
|
20
|
+
rest.each { |comment|
|
21
|
+
code.rewriter.remove comment.comment_range
|
22
|
+
remove_whitespace_before comment.comment_range.begin_pos, code.buffer, code.rewriter, false
|
23
|
+
}
|
24
|
+
|
25
|
+
case comment.text
|
26
|
+
when value_regex
|
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
|
34
|
+
when exception_regex
|
35
|
+
code.rewriter.remove comment.comment_range
|
36
|
+
remove_whitespace_before comment.comment_range.begin_pos, code.buffer, code.rewriter, true
|
37
|
+
when stdout_regex
|
38
|
+
code.rewriter.remove comment.comment_range
|
39
|
+
remove_whitespace_before comment.comment_range.begin_pos, code.buffer, code.rewriter, true
|
40
|
+
when stderr_regex
|
41
|
+
code.rewriter.remove comment.comment_range
|
42
|
+
remove_whitespace_before comment.comment_range.begin_pos, code.buffer, code.rewriter, true
|
43
|
+
else
|
44
|
+
raise "This should be impossible! Something must be broken in the comment section above"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
code.rewriter.process
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
attr_accessor :raw_code, :remove_value_prefixes, :markers, :code
|
54
|
+
|
55
|
+
# any whitespace before the index (on the same line) will be removed
|
56
|
+
# if the preceding whitespace is at the beginning of the line, the newline will be removed
|
57
|
+
# if there is a newline before all of that, and remove_preceding_newline is true, it will be removed as well
|
58
|
+
def remove_whitespace_before(index, buffer, rewriter, remove_preceding_newline)
|
59
|
+
end_pos = index
|
60
|
+
begin_pos = end_pos - 1
|
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"
|
64
|
+
return if begin_pos.next == end_pos
|
65
|
+
rewriter.remove code.range_for(begin_pos.next, end_pos)
|
66
|
+
end
|
67
|
+
|
68
|
+
def annotation_chunks_in(code)
|
69
|
+
code
|
70
|
+
.inline_comments
|
71
|
+
.map { |comment| # associate each annotation to its comment
|
72
|
+
annotation = comment.text[value_regex] ||
|
73
|
+
comment.text[exception_regex] ||
|
74
|
+
comment.text[stdout_regex] ||
|
75
|
+
comment.text[stderr_regex]
|
76
|
+
[annotation, comment]
|
77
|
+
}
|
78
|
+
.slice_before { |annotation, comment| annotation } # annotations begin chunks
|
79
|
+
.select { |(annotation, _start), *| annotation } # discard chunks not beginning with an annotation (probably can only happens on first comment)
|
80
|
+
.map { |(annotation, start), *rest| # end the chunk if the comment doesn't meet nextline criteria
|
81
|
+
nextline_comments = []
|
82
|
+
prev = start
|
83
|
+
rest.each { |_, potential_nextline|
|
84
|
+
sequential = (prev.line_number.next == potential_nextline.line_number)
|
85
|
+
vertically_aligned = start.text_col == potential_nextline.text_col
|
86
|
+
only_preceded_by_whitespace = potential_nextline.whitespace_col.zero?
|
87
|
+
indention_matches_annotation = annotation.length <= potential_nextline.text[/#\s*/].length
|
88
|
+
break unless sequential && vertically_aligned && only_preceded_by_whitespace && indention_matches_annotation
|
89
|
+
nextline_comments << potential_nextline
|
90
|
+
prev = potential_nextline
|
91
|
+
}
|
92
|
+
[start, nextline_comments]
|
93
|
+
}
|
94
|
+
end
|
95
|
+
|
96
|
+
def value_regex
|
97
|
+
@value_regex ||= markers.fetch(:value).fetch(:regex)
|
98
|
+
end
|
99
|
+
|
100
|
+
def exception_regex
|
101
|
+
@exception_regex ||= markers.fetch(:exception).fetch(:regex)
|
102
|
+
end
|
103
|
+
|
104
|
+
def stdout_regex
|
105
|
+
@stdout_regex ||= markers.fetch(:stdout).fetch(:regex)
|
106
|
+
end
|
107
|
+
|
108
|
+
def stderr_regex
|
109
|
+
@stderr_regex ||= markers.fetch(:stderr).fetch(:regex)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'seeing_is_believing/code'
|
2
|
+
require 'seeing_is_believing/binary/commentable_lines'
|
3
|
+
|
4
|
+
class SeeingIsBelieving
|
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
|
9
|
+
module RewriteComments
|
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
|
18
|
+
|
19
|
+
# update existing comments
|
20
|
+
comments.each do |comment|
|
21
|
+
new_whitespace, new_comment = mapping.call comment
|
22
|
+
code.rewriter.replace comment.whitespace_range, new_whitespace
|
23
|
+
code.rewriter.replace comment.comment_range, new_comment
|
24
|
+
end
|
25
|
+
|
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
|
52
|
+
|
53
|
+
whitespace, body = mapping.call comment
|
54
|
+
code.rewriter.replace whitespace_range, "#{whitespace}#{body}"
|
55
|
+
end
|
56
|
+
|
57
|
+
# perform the rewrite
|
58
|
+
code.rewriter.process
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'seeing_is_believing'
|
2
|
+
require 'seeing_is_believing/binary/config'
|
3
|
+
require 'seeing_is_believing/binary/engine'
|
4
|
+
|
5
|
+
class SeeingIsBelieving
|
6
|
+
module Binary
|
7
|
+
SUCCESS_STATUS = 0
|
8
|
+
DISPLAYABLE_ERROR_STATUS = 1 # e.g. user code raises an exception (we can display this in the output)
|
9
|
+
NONDISPLAYABLE_ERROR_STATUS = 2 # e.g. SiB was invoked incorrectly
|
10
|
+
|
11
|
+
def self.call(argv, stdin, stdout, stderr)
|
12
|
+
config = Config.new.parse_args(argv).finalize(stdin, stdout, stderr, File)
|
13
|
+
engine = Engine.new config
|
14
|
+
|
15
|
+
if config.print_help?
|
16
|
+
stdout.puts config.help_screen
|
17
|
+
return SUCCESS_STATUS
|
18
|
+
end
|
19
|
+
|
20
|
+
if config.print_version?
|
21
|
+
stdout.puts SeeingIsBelieving::VERSION
|
22
|
+
return SUCCESS_STATUS
|
23
|
+
end
|
24
|
+
|
25
|
+
if config.errors.any?
|
26
|
+
stderr.puts(*config.errors, *config.deprecations)
|
27
|
+
return NONDISPLAYABLE_ERROR_STATUS
|
28
|
+
end
|
29
|
+
|
30
|
+
if config.print_cleaned?
|
31
|
+
stdout.print engine.cleaned_body
|
32
|
+
return SUCCESS_STATUS
|
33
|
+
end
|
34
|
+
|
35
|
+
if config.toggle_mark?
|
36
|
+
stdout.print engine.toggled_mark
|
37
|
+
return SUCCESS_STATUS
|
38
|
+
end
|
39
|
+
|
40
|
+
if engine.syntax_error?
|
41
|
+
stderr.puts engine.syntax_error
|
42
|
+
return NONDISPLAYABLE_ERROR_STATUS
|
43
|
+
end
|
44
|
+
|
45
|
+
engine.evaluate!
|
46
|
+
|
47
|
+
if engine.timed_out?
|
48
|
+
stderr.puts "Timeout Error after #{engine.timeout_seconds} seconds!"
|
49
|
+
return NONDISPLAYABLE_ERROR_STATUS
|
50
|
+
end
|
51
|
+
|
52
|
+
if config.result_as_json?
|
53
|
+
require 'json'
|
54
|
+
stdout.puts JSON.dump(engine.result.as_json)
|
55
|
+
return SUCCESS_STATUS
|
56
|
+
elsif config.print_event_stream?
|
57
|
+
# no op, the event stream handler has been printing it all along
|
58
|
+
elsif config.debug?
|
59
|
+
config.debugger.context("OUTPUT") { engine.annotated_body }
|
60
|
+
else
|
61
|
+
stdout.print engine.annotated_body
|
62
|
+
end
|
63
|
+
|
64
|
+
if config.inherit_exitstatus?
|
65
|
+
engine.exitstatus
|
66
|
+
elsif engine.exitstatus.zero?
|
67
|
+
SUCCESS_STATUS
|
68
|
+
else
|
69
|
+
DISPLAYABLE_ERROR_STATUS
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,139 @@
|
|
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 'set'
|
11
|
+
require 'seeing_is_believing/hash_struct'
|
12
|
+
|
13
|
+
class SeeingIsBelieving
|
14
|
+
class Code
|
15
|
+
InlineComment = HashStruct.for :line_number, :whitespace_col, :whitespace, :text_col, :text, :full_range, :whitespace_range, :comment_range
|
16
|
+
Syntax = HashStruct.for error_message: nil, line_number: nil do
|
17
|
+
def valid?() !error_message end
|
18
|
+
def invalid?() !valid? end
|
19
|
+
end
|
20
|
+
|
21
|
+
attr_reader :raw, :buffer, :parser, :rewriter, :inline_comments, :root, :raw_comments, :syntax, :body_range
|
22
|
+
|
23
|
+
def initialize(raw_code, name=nil)
|
24
|
+
raw_code[-1] == "\n" || raise(SyntaxError, "Code must end in a newline for the sake of consistency (sanity)")
|
25
|
+
@raw = raw_code
|
26
|
+
@buffer = Parser::Source::Buffer.new(name||"SeeingIsBelieving")
|
27
|
+
@buffer.source = raw
|
28
|
+
@rewriter = Parser::Source::TreeRewriter.new buffer
|
29
|
+
builder = Parser::Builders::Default.new.tap { |b| b.emit_file_line_as_literals = false }
|
30
|
+
@parser = Parser::CurrentRuby.new builder
|
31
|
+
@syntax = Syntax.new
|
32
|
+
parser.diagnostics.consumer = lambda do |diagnostic|
|
33
|
+
if :fatal == diagnostic.level || :error == diagnostic.level
|
34
|
+
@syntax = Syntax.new error_message: diagnostic.message, line_number: index_to_linenum(diagnostic.location.begin_pos)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
@root, @raw_comments, @tokens = parser.tokenize(@buffer, true)
|
38
|
+
@body_range = body_range_from_tokens(@tokens)
|
39
|
+
@inline_comments = raw_comments.select(&:inline?).map { |c| wrap_comment c }
|
40
|
+
@root ||= null_node
|
41
|
+
end
|
42
|
+
|
43
|
+
def range_for(start_index, end_index)
|
44
|
+
Parser::Source::Range.new buffer, start_index, end_index
|
45
|
+
end
|
46
|
+
|
47
|
+
def index_to_linenum(char_index)
|
48
|
+
line_indexes.index { |line_index| char_index < line_index } || line_indexes.size
|
49
|
+
end
|
50
|
+
|
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
|
55
|
+
|
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
|
66
|
+
|
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
|
81
|
+
|
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
|
89
|
+
end
|
90
|
+
|
91
|
+
def indexes_of_ors_at_eol
|
92
|
+
Set.new(
|
93
|
+
@tokens.select { |type, *| type == :tGVAR }
|
94
|
+
.select { |_, (var, _range)| var == '$\\'.freeze }
|
95
|
+
.map { |_, (_var, range)| range.end_pos }
|
96
|
+
)
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
|
101
|
+
def comments_and_tokens(builder, buffer)
|
102
|
+
parser = Parser::CurrentRuby.new builder
|
103
|
+
_, all_comments, tokens = parser.tokenize(@buffer, true)
|
104
|
+
[all_comments, tokens]
|
105
|
+
end
|
106
|
+
|
107
|
+
def body_range_from_tokens(tokens)
|
108
|
+
return range_for(0, 0) if raw.start_with? "__END__\n"
|
109
|
+
(name, (_, range)) = tokens.max_by { |name, (_data, range)| range.end_pos } ||
|
110
|
+
[nil, [nil, range_for(0, 1)]]
|
111
|
+
end_pos = range.end_pos
|
112
|
+
end_pos += 1 if name == :tCOMMENT || name == :tSTRING_END || name == :tSEMI
|
113
|
+
range_for 0, end_pos
|
114
|
+
end
|
115
|
+
|
116
|
+
def wrap_comment(comment)
|
117
|
+
last_char = comment.location.expression.begin_pos
|
118
|
+
first_char = last_char
|
119
|
+
first_char -= 1 while first_char > 0 && raw[first_char-1] =~ /[ \t]/
|
120
|
+
preceding_whitespace = buffer.source[first_char...last_char]
|
121
|
+
preceding_whitespace_range = range_for first_char, last_char
|
122
|
+
|
123
|
+
InlineComment.new line_number: comment.location.line,
|
124
|
+
whitespace_col: preceding_whitespace_range.column,
|
125
|
+
whitespace: preceding_whitespace,
|
126
|
+
text_col: comment.location.column,
|
127
|
+
text: comment.text,
|
128
|
+
full_range: range_for(first_char, comment.location.expression.end_pos),
|
129
|
+
whitespace_range: preceding_whitespace_range,
|
130
|
+
comment_range: comment.location.expression
|
131
|
+
end
|
132
|
+
|
133
|
+
def null_node
|
134
|
+
location = Parser::Source::Map::Collection.new nil, nil, range_for(0, 0)
|
135
|
+
Parser::AST::Node.new :null_node, [], location: location
|
136
|
+
end
|
137
|
+
|
138
|
+
end
|
139
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
class SeeingIsBelieving
|
2
|
+
module Compatibility
|
3
|
+
end
|
4
|
+
end
|
5
|
+
|
6
|
+
# Ruby 2.0.0 is soooooo painful >.<
|
7
|
+
# want to stop supporting this so bad!!
|
8
|
+
is_v2_0 = !String.instance_methods.include?(:scrub)
|
9
|
+
|
10
|
+
is_v2_0 && begin
|
11
|
+
old_verbose, $VERBOSE = $VERBOSE, nil
|
12
|
+
module SeeingIsBelieving::Compatibility
|
13
|
+
refine String do
|
14
|
+
def scrub(char=nil, &block)
|
15
|
+
char && block = lambda { |c| char }
|
16
|
+
each_char.inject("") do |new_str, char|
|
17
|
+
if char.valid_encoding?
|
18
|
+
new_str << char
|
19
|
+
else
|
20
|
+
new_str << block.call(char)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
ensure
|
27
|
+
$VERBOSE = old_verbose
|
28
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
class SeeingIsBelieving
|
2
|
+
class Debugger
|
3
|
+
CONTEXT_COLOUR = "\e[37;44m" # background blue
|
4
|
+
RESET_COLOUR = "\e[0m"
|
5
|
+
|
6
|
+
def initialize(options={})
|
7
|
+
@coloured = options[:colour]
|
8
|
+
@stream = options[:stream]
|
9
|
+
end
|
10
|
+
|
11
|
+
Null = new stream: nil
|
12
|
+
|
13
|
+
def coloured?
|
14
|
+
@coloured
|
15
|
+
end
|
16
|
+
|
17
|
+
attr_reader :stream
|
18
|
+
alias enabled? stream
|
19
|
+
|
20
|
+
def context(name, &block)
|
21
|
+
if enabled?
|
22
|
+
stream << CONTEXT_COLOUR if coloured?
|
23
|
+
stream << "#{name}:"
|
24
|
+
stream << RESET_COLOUR if coloured?
|
25
|
+
stream << "\n"
|
26
|
+
stream << block.call.to_s << "\n" if block
|
27
|
+
end
|
28
|
+
self
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
class SeeingIsBelieving
|
2
|
+
# All our errors will inherit from this so that a user can catch any error generated by this lib
|
3
|
+
SeeingIsBelievingError = Class.new StandardError
|
4
|
+
|
5
|
+
class TempFileAlreadyExists < SeeingIsBelievingError
|
6
|
+
def initialize(from_filename, backup_filename)
|
7
|
+
super "Trying to back up #{from_filename.inspect} (FILE) to"\
|
8
|
+
" #{backup_filename.inspect} (TEMPFILE) but TEMPFILE already exists."\
|
9
|
+
" You should check the contents of these files. If FILE is correct,"\
|
10
|
+
" then delete TEMPFILE. Otherwise rename TEMPFILE to FILE."
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# EventStream
|
15
|
+
NoMoreEvents = Class.new SeeingIsBelievingError
|
16
|
+
UnknownEvent = Class.new SeeingIsBelievingError
|
17
|
+
end
|