parser 0.9.alpha1 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +4 -3
  3. data/AST_FORMAT.md +1338 -0
  4. data/README.md +58 -3
  5. data/Rakefile +32 -12
  6. data/bin/benchmark +47 -0
  7. data/bin/explain-parse +14 -0
  8. data/bin/parse +6 -0
  9. data/lib/parser.rb +84 -0
  10. data/lib/parser/all.rb +2 -0
  11. data/lib/parser/ast/node.rb +11 -0
  12. data/lib/parser/ast/processor.rb +8 -0
  13. data/lib/parser/base.rb +116 -0
  14. data/lib/parser/builders/default.rb +654 -0
  15. data/lib/parser/compatibility/ruby1_8.rb +13 -0
  16. data/lib/parser/diagnostic.rb +44 -0
  17. data/lib/parser/diagnostic/engine.rb +44 -0
  18. data/lib/parser/lexer.rl +335 -245
  19. data/lib/parser/lexer/explanation.rb +37 -0
  20. data/lib/parser/{lexer_literal.rb → lexer/literal.rb} +22 -12
  21. data/lib/parser/lexer/stack_state.rb +38 -0
  22. data/lib/parser/ruby18.y +1957 -0
  23. data/lib/parser/ruby19.y +2154 -0
  24. data/lib/parser/source/buffer.rb +78 -0
  25. data/lib/parser/source/map.rb +20 -0
  26. data/lib/parser/source/map/operator.rb +15 -0
  27. data/lib/parser/source/map/variable_assignment.rb +15 -0
  28. data/lib/parser/source/range.rb +66 -0
  29. data/lib/parser/static_environment.rb +12 -6
  30. data/parser.gemspec +23 -13
  31. data/test/helper.rb +45 -0
  32. data/test/parse_helper.rb +204 -0
  33. data/test/racc_coverage_helper.rb +130 -0
  34. data/test/test_diagnostic.rb +47 -0
  35. data/test/test_diagnostic_engine.rb +58 -0
  36. data/test/test_lexer.rb +601 -357
  37. data/test/test_lexer_stack_state.rb +69 -0
  38. data/test/test_parse_helper.rb +74 -0
  39. data/test/test_parser.rb +3654 -0
  40. data/test/test_source_buffer.rb +80 -0
  41. data/test/test_source_range.rb +51 -0
  42. data/test/test_static_environment.rb +1 -4
  43. 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,15 @@
1
+ module Parser
2
+ module Source
3
+
4
+ class Map::Operator < Map
5
+ attr_reader :operator
6
+
7
+ def initialize(operator, expression)
8
+ @operator = operator
9
+
10
+ super(expression)
11
+ end
12
+ end
13
+
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ module Parser
2
+ module Source
3
+
4
+ class Map::VariableAssignment < Map::Operator
5
+ attr_reader :name
6
+
7
+ def initialize(name, operator, expression)
8
+ @name = name
9
+
10
+ super(operator, expression)
11
+ end
12
+ end
13
+
14
+ end
15
+ 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 @variables
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 @variables
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 name
34
+ @variables.add(name.to_sym)
35
+
36
+ self
31
37
  end
32
38
 
33
39
  def declared?(name)
34
- @variables.include? name
40
+ @variables.include?(name.to_sym)
35
41
  end
36
42
  end
37
43
 
@@ -1,25 +1,35 @@
1
- # coding: utf-8
1
+ # encoding: utf-8
2
2
 
3
3
  Gem::Specification.new do |spec|
4
- spec.name = "parser"
5
- spec.version = "0.9.alpha1"
6
- spec.authors = ["Peter Zotov"]
7
- spec.email = ["whitequark@whitequark.org"]
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 = "http://github.com/whitequark/parser"
11
- spec.license = "MIT"
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 = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
+ spec.executables = %w()
17
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
- spec.require_paths = ["lib"]
19
+ spec.require_paths = ['lib']
19
20
 
20
21
  spec.required_ruby_version = '>= 1.9'
21
22
 
22
- spec.add_development_dependency "bundler", "~> 1.3"
23
- spec.add_development_dependency "rake", "~> 10.0"
24
- spec.add_development_dependency "racc"
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
@@ -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