parser 0.9.alpha1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.travis.yml +4 -3
- data/AST_FORMAT.md +1338 -0
- data/README.md +58 -3
- data/Rakefile +32 -12
- data/bin/benchmark +47 -0
- data/bin/explain-parse +14 -0
- data/bin/parse +6 -0
- data/lib/parser.rb +84 -0
- data/lib/parser/all.rb +2 -0
- data/lib/parser/ast/node.rb +11 -0
- data/lib/parser/ast/processor.rb +8 -0
- data/lib/parser/base.rb +116 -0
- data/lib/parser/builders/default.rb +654 -0
- data/lib/parser/compatibility/ruby1_8.rb +13 -0
- data/lib/parser/diagnostic.rb +44 -0
- data/lib/parser/diagnostic/engine.rb +44 -0
- data/lib/parser/lexer.rl +335 -245
- data/lib/parser/lexer/explanation.rb +37 -0
- data/lib/parser/{lexer_literal.rb → lexer/literal.rb} +22 -12
- data/lib/parser/lexer/stack_state.rb +38 -0
- data/lib/parser/ruby18.y +1957 -0
- data/lib/parser/ruby19.y +2154 -0
- data/lib/parser/source/buffer.rb +78 -0
- data/lib/parser/source/map.rb +20 -0
- data/lib/parser/source/map/operator.rb +15 -0
- data/lib/parser/source/map/variable_assignment.rb +15 -0
- data/lib/parser/source/range.rb +66 -0
- data/lib/parser/static_environment.rb +12 -6
- data/parser.gemspec +23 -13
- data/test/helper.rb +45 -0
- data/test/parse_helper.rb +204 -0
- data/test/racc_coverage_helper.rb +130 -0
- data/test/test_diagnostic.rb +47 -0
- data/test/test_diagnostic_engine.rb +58 -0
- data/test/test_lexer.rb +601 -357
- data/test/test_lexer_stack_state.rb +69 -0
- data/test/test_parse_helper.rb +74 -0
- data/test/test_parser.rb +3654 -0
- data/test/test_source_buffer.rb +80 -0
- data/test/test_source_range.rb +51 -0
- data/test/test_static_environment.rb +1 -4
- metadata +137 -12
@@ -0,0 +1,78 @@
|
|
1
|
+
module Parser
|
2
|
+
module Source
|
3
|
+
|
4
|
+
class Buffer
|
5
|
+
attr_reader :name, :first_line
|
6
|
+
|
7
|
+
def initialize(name, first_line = 1)
|
8
|
+
@name = name
|
9
|
+
@first_line = first_line
|
10
|
+
@source = nil
|
11
|
+
end
|
12
|
+
|
13
|
+
def read
|
14
|
+
self.source = File.read(@name)
|
15
|
+
|
16
|
+
self
|
17
|
+
end
|
18
|
+
|
19
|
+
def source
|
20
|
+
if @source.nil?
|
21
|
+
raise RuntimeError, 'Cannot extract source from uninitialized Source::Buffer'
|
22
|
+
end
|
23
|
+
|
24
|
+
@source
|
25
|
+
end
|
26
|
+
|
27
|
+
def source=(source)
|
28
|
+
@source = source.freeze
|
29
|
+
|
30
|
+
freeze
|
31
|
+
end
|
32
|
+
|
33
|
+
def decompose_position(position)
|
34
|
+
line = line_for(position)
|
35
|
+
line_begin = line_begin_positions[line]
|
36
|
+
|
37
|
+
[ @first_line + line, position - line_begin ]
|
38
|
+
end
|
39
|
+
|
40
|
+
def source_line(line)
|
41
|
+
mapped_line = line - @first_line
|
42
|
+
|
43
|
+
# Consider improving this naïve implementation.
|
44
|
+
source_line = source.lines.drop(mapped_line).first
|
45
|
+
|
46
|
+
# Line endings will be commonly present for all lines
|
47
|
+
# except the last one. It does not make sense to keep them.
|
48
|
+
if source_line.end_with? "\n"
|
49
|
+
source_line.chomp
|
50
|
+
else
|
51
|
+
source_line
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def line_begin_positions
|
58
|
+
# TODO: Optimize this.
|
59
|
+
[0] + source.
|
60
|
+
each_char.
|
61
|
+
with_index.
|
62
|
+
select do |char, index|
|
63
|
+
char == "\n"
|
64
|
+
end.map do |char, index|
|
65
|
+
index + 1
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def line_for(position)
|
70
|
+
# TODO: Optimize this.
|
71
|
+
line_begin_positions.rindex do |line_beg|
|
72
|
+
line_beg <= position
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Parser
|
2
|
+
module Source
|
3
|
+
|
4
|
+
# General idea for Map subclasses: only store what's
|
5
|
+
# absolutely necessary; don't duplicate the info contained in
|
6
|
+
# ASTs; if it can be extracted from source given only the other
|
7
|
+
# stored information, don't store it.
|
8
|
+
#
|
9
|
+
class Map
|
10
|
+
attr_reader :expression
|
11
|
+
|
12
|
+
def initialize(expression)
|
13
|
+
@expression = expression
|
14
|
+
|
15
|
+
freeze
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Parser
|
2
|
+
module Source
|
3
|
+
|
4
|
+
class Range
|
5
|
+
attr_reader :source_buffer
|
6
|
+
attr_reader :begin, :end
|
7
|
+
|
8
|
+
def initialize(source_buffer, begin_, end_)
|
9
|
+
@source_buffer = source_buffer
|
10
|
+
@begin, @end = begin_, end_
|
11
|
+
|
12
|
+
freeze
|
13
|
+
end
|
14
|
+
|
15
|
+
def size
|
16
|
+
@end - @begin + 1
|
17
|
+
end
|
18
|
+
|
19
|
+
def line
|
20
|
+
line, _ = @source_buffer.decompose_position(@begin)
|
21
|
+
|
22
|
+
line
|
23
|
+
end
|
24
|
+
|
25
|
+
def begin_column
|
26
|
+
_, column = @source_buffer.decompose_position(@begin)
|
27
|
+
|
28
|
+
column
|
29
|
+
end
|
30
|
+
|
31
|
+
def end_column
|
32
|
+
_, column = @source_buffer.decompose_position(@end)
|
33
|
+
|
34
|
+
column
|
35
|
+
end
|
36
|
+
|
37
|
+
def column_range
|
38
|
+
begin_column..end_column
|
39
|
+
end
|
40
|
+
|
41
|
+
def source_line
|
42
|
+
@source_buffer.source_line(line)
|
43
|
+
end
|
44
|
+
|
45
|
+
def to_s
|
46
|
+
line, column = @source_buffer.decompose_position(@begin)
|
47
|
+
[@source_buffer.name, line, column + 1].join(':')
|
48
|
+
end
|
49
|
+
|
50
|
+
def join(other)
|
51
|
+
if other.source_buffer == @source_buffer
|
52
|
+
Range.new(@source_buffer,
|
53
|
+
[@begin, other.begin].min,
|
54
|
+
[@end, other.end].max)
|
55
|
+
else
|
56
|
+
raise ArgumentError, "Cannot join SourceRanges for different SourceFiles"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def inspect
|
61
|
+
"#<Source::Range #{@source_buffer.name} #{@begin}..#{@end}>"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
end
|
@@ -1,22 +1,24 @@
|
|
1
|
-
require 'set'
|
2
|
-
|
3
1
|
module Parser
|
4
2
|
|
5
3
|
class StaticEnvironment
|
6
4
|
def initialize
|
5
|
+
reset
|
6
|
+
end
|
7
|
+
|
8
|
+
def reset
|
7
9
|
@variables = Set[]
|
8
10
|
@stack = []
|
9
11
|
end
|
10
12
|
|
11
13
|
def extend_static
|
12
|
-
@stack.push
|
14
|
+
@stack.push(@variables)
|
13
15
|
@variables = Set[]
|
14
16
|
|
15
17
|
self
|
16
18
|
end
|
17
19
|
|
18
20
|
def extend_dynamic
|
19
|
-
@stack.push
|
21
|
+
@stack.push(@variables)
|
20
22
|
@variables = @variables.dup
|
21
23
|
|
22
24
|
self
|
@@ -24,14 +26,18 @@ module Parser
|
|
24
26
|
|
25
27
|
def unextend
|
26
28
|
@variables = @stack.pop
|
29
|
+
|
30
|
+
self
|
27
31
|
end
|
28
32
|
|
29
33
|
def declare(name)
|
30
|
-
@variables.add
|
34
|
+
@variables.add(name.to_sym)
|
35
|
+
|
36
|
+
self
|
31
37
|
end
|
32
38
|
|
33
39
|
def declared?(name)
|
34
|
-
@variables.include?
|
40
|
+
@variables.include?(name.to_sym)
|
35
41
|
end
|
36
42
|
end
|
37
43
|
|
data/parser.gemspec
CHANGED
@@ -1,25 +1,35 @@
|
|
1
|
-
#
|
1
|
+
# encoding: utf-8
|
2
2
|
|
3
3
|
Gem::Specification.new do |spec|
|
4
|
-
spec.name =
|
5
|
-
spec.version =
|
6
|
-
spec.authors = [
|
7
|
-
spec.email = [
|
8
|
-
spec.description = %q{A Ruby parser.}
|
4
|
+
spec.name = 'parser'
|
5
|
+
spec.version = '0.9.0'
|
6
|
+
spec.authors = ['Peter Zotov']
|
7
|
+
spec.email = ['whitequark@whitequark.org']
|
8
|
+
spec.description = %q{A Ruby parser written in pure Ruby.}
|
9
9
|
spec.summary = spec.description
|
10
|
-
spec.homepage =
|
11
|
-
spec.license =
|
10
|
+
spec.homepage = 'http://github.com/whitequark/parser'
|
11
|
+
spec.license = 'MIT'
|
12
12
|
|
13
13
|
spec.files = `git ls-files`.split($/) + %w(
|
14
14
|
lib/parser/lexer.rb
|
15
|
+
lib/parser/ruby18.rb
|
15
16
|
)
|
16
|
-
spec.executables =
|
17
|
+
spec.executables = %w()
|
17
18
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
18
|
-
spec.require_paths = [
|
19
|
+
spec.require_paths = ['lib']
|
19
20
|
|
20
21
|
spec.required_ruby_version = '>= 1.9'
|
21
22
|
|
22
|
-
spec.
|
23
|
-
|
24
|
-
spec.add_development_dependency
|
23
|
+
spec.add_dependency 'ast', '~> 1.0'
|
24
|
+
|
25
|
+
spec.add_development_dependency 'bundler', '~> 1.2'
|
26
|
+
spec.add_development_dependency 'rake', '~> 0.9'
|
27
|
+
spec.add_development_dependency 'racc'
|
28
|
+
|
29
|
+
spec.add_development_dependency 'minitest', '~> 4.7.0'
|
30
|
+
spec.add_development_dependency 'simplecov', '~> 0.7'
|
31
|
+
spec.add_development_dependency 'coveralls'
|
32
|
+
spec.add_development_dependency 'json_pure' # for coveralls on 1.9.2
|
33
|
+
|
34
|
+
spec.add_development_dependency 'simplecov-sublime-ruby-coverage'
|
25
35
|
end
|
data/test/helper.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'tempfile'
|
2
|
+
|
3
|
+
require 'simplecov'
|
4
|
+
require 'coveralls'
|
5
|
+
|
6
|
+
if SimpleCov.usable?
|
7
|
+
if defined?(TracePoint)
|
8
|
+
require_relative 'racc_coverage_helper'
|
9
|
+
|
10
|
+
RaccCoverage.start(%w(ruby18.y ruby19.y),
|
11
|
+
File.expand_path('../../lib/parser', __FILE__))
|
12
|
+
|
13
|
+
# Report results faster.
|
14
|
+
at_exit { RaccCoverage.stop }
|
15
|
+
end
|
16
|
+
|
17
|
+
require 'simplecov-sublime-ruby-coverage'
|
18
|
+
|
19
|
+
SimpleCov.start do
|
20
|
+
self.formatter = SimpleCov::Formatter::MultiFormatter[
|
21
|
+
SimpleCov::Formatter::HTMLFormatter,
|
22
|
+
SimpleCov::Formatter::SublimeRubyCoverageFormatter,
|
23
|
+
Coveralls::SimpleCov::Formatter
|
24
|
+
]
|
25
|
+
|
26
|
+
add_group "Grammars" do |source_file|
|
27
|
+
source_file.filename =~ %r{\.y$}
|
28
|
+
end
|
29
|
+
|
30
|
+
# Exclude the testsuite itself.
|
31
|
+
add_filter "/test/"
|
32
|
+
|
33
|
+
# Exclude generated files.
|
34
|
+
add_filter do |source_file|
|
35
|
+
source_file.filename =~ %r{/lib/parser/(lexer|ruby\d+)\.rb$}
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# minitest/autorun must go after SimpleCov to preserve
|
41
|
+
# correct order of at_exit hooks.
|
42
|
+
require 'minitest/autorun'
|
43
|
+
|
44
|
+
$LOAD_PATH.unshift(File.expand_path('../../lib', __FILE__))
|
45
|
+
require 'parser'
|
@@ -0,0 +1,204 @@
|
|
1
|
+
require 'parser/all'
|
2
|
+
|
3
|
+
module ParseHelper
|
4
|
+
include AST::Sexp
|
5
|
+
|
6
|
+
ALL_VERSIONS = %w(1.8 1.9)
|
7
|
+
|
8
|
+
def setup
|
9
|
+
@diagnostics = []
|
10
|
+
|
11
|
+
super if defined?(super)
|
12
|
+
end
|
13
|
+
|
14
|
+
def parser_for_ruby_version(version)
|
15
|
+
case version
|
16
|
+
when '1.8'; parser = Parser::Ruby18.new
|
17
|
+
when '1.9'; parser = Parser::Ruby19.new
|
18
|
+
# when '2.0'; parser = Parser::Ruby20.new # not yet
|
19
|
+
else raise "Unrecognized Ruby version #{version}"
|
20
|
+
end
|
21
|
+
|
22
|
+
parser.diagnostics.consumer = lambda do |diagnostic|
|
23
|
+
@diagnostics << diagnostic
|
24
|
+
end
|
25
|
+
|
26
|
+
parser
|
27
|
+
end
|
28
|
+
|
29
|
+
def with_versions(code, versions)
|
30
|
+
versions.each do |version|
|
31
|
+
@diagnostics.clear
|
32
|
+
|
33
|
+
parser = parser_for_ruby_version(version)
|
34
|
+
yield version, parser
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def assert_source_range(begin_pos, end_pos, range, version, what)
|
39
|
+
assert range.is_a?(Parser::Source::Range),
|
40
|
+
"(#{version}) is_a?(Source::Range) for #{what}"
|
41
|
+
|
42
|
+
assert_equal begin_pos, range.begin,
|
43
|
+
"(#{version}) begin of #{what}"
|
44
|
+
|
45
|
+
assert_equal end_pos, range.end,
|
46
|
+
"(#{version}) end of #{what}"
|
47
|
+
end
|
48
|
+
|
49
|
+
# Use like this:
|
50
|
+
# ```
|
51
|
+
# assert_parses(
|
52
|
+
# s(:send, s(:lit, 10), :+, s(:lit, 20))
|
53
|
+
# %q{10 + 20},
|
54
|
+
# %q{~~~~~~~ expression
|
55
|
+
# | ^ operator
|
56
|
+
# | ~~ expression (lit)
|
57
|
+
# },
|
58
|
+
# %w(1.8 1.9) # optional
|
59
|
+
# )
|
60
|
+
# ```
|
61
|
+
def assert_parses(ast, code, source_maps='', versions=ALL_VERSIONS)
|
62
|
+
with_versions(code, versions) do |version, parser|
|
63
|
+
source_file = Parser::Source::Buffer.new('(assert_parses)')
|
64
|
+
source_file.source = code
|
65
|
+
|
66
|
+
parsed_ast = parser.parse(source_file)
|
67
|
+
|
68
|
+
assert_equal ast, parsed_ast,
|
69
|
+
"(#{version}) AST equality"
|
70
|
+
|
71
|
+
parse_source_map_descriptions(source_maps) \
|
72
|
+
do |begin_pos, end_pos, map_field, ast_path, line|
|
73
|
+
|
74
|
+
astlet = traverse_ast(parsed_ast, ast_path)
|
75
|
+
|
76
|
+
if astlet.nil?
|
77
|
+
# This is a testsuite bug.
|
78
|
+
raise "No entity with AST path #{ast_path} in #{parsed_ast.inspect}"
|
79
|
+
end
|
80
|
+
|
81
|
+
next # TODO skip location checking
|
82
|
+
|
83
|
+
assert astlet.source_map.respond_to?(map_field),
|
84
|
+
"(#{version}) source_map.respond_to?(#{map_field.inspect}) for:\n#{parsed_ast.inspect}"
|
85
|
+
|
86
|
+
range = astlet.source_map.send(map_field)
|
87
|
+
|
88
|
+
assert_source_range(begin_pos, end_pos, range, version, line.inspect)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# Use like this:
|
94
|
+
# ```
|
95
|
+
# assert_diagnoses(
|
96
|
+
# [:warning, :ambiguous_prefix, { prefix: '*' }],
|
97
|
+
# %q{foo *bar},
|
98
|
+
# %q{ ^ location
|
99
|
+
# | ~~~ highlights (0)})
|
100
|
+
# ```
|
101
|
+
def assert_diagnoses(diagnostic, code, source_maps='', versions=ALL_VERSIONS)
|
102
|
+
with_versions(code, versions) do |version, parser|
|
103
|
+
source_file = Parser::Source::Buffer.new('(assert_diagnoses)')
|
104
|
+
source_file.source = code
|
105
|
+
|
106
|
+
begin
|
107
|
+
parser = parser.parse(source_file)
|
108
|
+
rescue Parser::SyntaxError
|
109
|
+
# do nothing; the diagnostic was reported
|
110
|
+
end
|
111
|
+
|
112
|
+
# Remove this `if' when no diagnostics fail to render.
|
113
|
+
if @diagnostics.count != 1
|
114
|
+
assert_equal 1, @diagnostics.count,
|
115
|
+
"(#{version}) emits a single diagnostic, not\n" \
|
116
|
+
"#{@diagnostics.map(&:render).join("\n")}"
|
117
|
+
end
|
118
|
+
|
119
|
+
emitted_diagnostic = @diagnostics.first
|
120
|
+
|
121
|
+
level, kind, substitutions = diagnostic
|
122
|
+
message = Parser::ERRORS[kind] % substitutions
|
123
|
+
|
124
|
+
assert_equal level, emitted_diagnostic.level
|
125
|
+
assert_equal message, emitted_diagnostic.message
|
126
|
+
|
127
|
+
parse_source_map_descriptions(source_maps) \
|
128
|
+
do |begin_pos, end_pos, map_field, ast_path, line|
|
129
|
+
|
130
|
+
next # TODO skip location checking
|
131
|
+
|
132
|
+
case map_field
|
133
|
+
when 'location'
|
134
|
+
assert_source_range begin_pos, end_pos,
|
135
|
+
emitted_diagnostic.location,
|
136
|
+
version, "location"
|
137
|
+
|
138
|
+
when 'highlights'
|
139
|
+
index = ast_path.first.to_i
|
140
|
+
|
141
|
+
assert_source_range begin_pos, end_pos,
|
142
|
+
emitted_diagnostic.highlights[index],
|
143
|
+
version, "#{index}th highlight"
|
144
|
+
|
145
|
+
else
|
146
|
+
raise "Unknown diagnostic range #{map_field}"
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
SOURCE_MAP_DESCRIPTION_RE =
|
153
|
+
/(?x)
|
154
|
+
^(?# $1 skip) ^(\s*)
|
155
|
+
(?# $2 highlight) ([~\^]+)
|
156
|
+
\s+
|
157
|
+
(?# $3 source_map_field) ([a-z]+)
|
158
|
+
(?# $5 ast_path) (\s+\(([a-z_.\/0-9]+)\))?
|
159
|
+
$/
|
160
|
+
|
161
|
+
def parse_source_map_descriptions(descriptions)
|
162
|
+
unless block_given?
|
163
|
+
return to_enum(:parse_source_map_descriptions, descriptions)
|
164
|
+
end
|
165
|
+
|
166
|
+
descriptions.each_line do |line|
|
167
|
+
# Remove leading " |", if it exists.
|
168
|
+
line = line.sub(/^\s*\|/, '').rstrip
|
169
|
+
|
170
|
+
next if line.empty?
|
171
|
+
|
172
|
+
if (match = SOURCE_MAP_DESCRIPTION_RE.match(line))
|
173
|
+
begin_pos = match[1].length
|
174
|
+
end_pos = begin_pos + match[2].length - 1
|
175
|
+
source_map_field = match[3]
|
176
|
+
|
177
|
+
if match[5]
|
178
|
+
ast_path = match[5].split('.')
|
179
|
+
else
|
180
|
+
ast_path = []
|
181
|
+
end
|
182
|
+
|
183
|
+
yield begin_pos, end_pos, source_map_field, ast_path, line
|
184
|
+
else
|
185
|
+
raise "Cannot parse source map description line: #{line.inspect}."
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
def traverse_ast(ast, path)
|
191
|
+
path.inject(ast) do |astlet, path_component|
|
192
|
+
# Split "dstr/2" to :dstr and 1
|
193
|
+
type_str, index_str = path_component.split('/')
|
194
|
+
type, index = type_str.to_sym, index_str.to_i - 1
|
195
|
+
|
196
|
+
matching_children = \
|
197
|
+
astlet.children.select do |child|
|
198
|
+
AST::Node === child && child.type == type
|
199
|
+
end
|
200
|
+
|
201
|
+
matching_children[index]
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|