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.
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