parser 0.9.alpha1 → 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|