parslet 1.7.0 → 1.7.1

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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/README +1 -1
  3. data/lib/parslet/context.rb +1 -1
  4. data/lib/parslet/parser.rb +1 -1
  5. data/parslet.gemspec +18 -0
  6. data/spec/acceptance/examples_spec.rb +37 -0
  7. data/spec/acceptance/infix_parser_spec.rb +112 -0
  8. data/spec/acceptance/regression_spec.rb +314 -0
  9. data/spec/acceptance/repetition_and_maybe_spec.rb +42 -0
  10. data/spec/acceptance/unconsumed_input_spec.rb +21 -0
  11. data/spec/parslet/atom_results_spec.rb +39 -0
  12. data/spec/parslet/atoms/alternative_spec.rb +26 -0
  13. data/spec/parslet/atoms/base_spec.rb +126 -0
  14. data/spec/parslet/atoms/capture_spec.rb +21 -0
  15. data/spec/parslet/atoms/combinations_spec.rb +5 -0
  16. data/spec/parslet/atoms/dsl_spec.rb +25 -0
  17. data/spec/parslet/atoms/entity_spec.rb +77 -0
  18. data/spec/parslet/atoms/infix_spec.rb +5 -0
  19. data/spec/parslet/atoms/lookahead_spec.rb +22 -0
  20. data/spec/parslet/atoms/named_spec.rb +4 -0
  21. data/spec/parslet/atoms/re_spec.rb +14 -0
  22. data/spec/parslet/atoms/repetition_spec.rb +24 -0
  23. data/spec/parslet/atoms/scope_spec.rb +26 -0
  24. data/spec/parslet/atoms/sequence_spec.rb +28 -0
  25. data/spec/parslet/atoms/str_spec.rb +15 -0
  26. data/spec/parslet/atoms/visitor_spec.rb +80 -0
  27. data/spec/parslet/atoms_spec.rb +429 -0
  28. data/spec/parslet/convenience_spec.rb +48 -0
  29. data/spec/parslet/error_reporter/contextual_spec.rb +115 -0
  30. data/spec/parslet/error_reporter/deepest_spec.rb +73 -0
  31. data/spec/parslet/error_reporter/tree_spec.rb +7 -0
  32. data/spec/parslet/export_spec.rb +67 -0
  33. data/spec/parslet/expression/treetop_spec.rb +74 -0
  34. data/spec/parslet/minilisp.citrus +29 -0
  35. data/spec/parslet/minilisp.tt +29 -0
  36. data/spec/parslet/parser_spec.rb +31 -0
  37. data/spec/parslet/parslet_spec.rb +38 -0
  38. data/spec/parslet/pattern_spec.rb +272 -0
  39. data/spec/parslet/position_spec.rb +14 -0
  40. data/spec/parslet/rig/rspec_spec.rb +54 -0
  41. data/spec/parslet/scope_spec.rb +45 -0
  42. data/spec/parslet/slice_spec.rb +144 -0
  43. data/spec/parslet/source/line_cache_spec.rb +74 -0
  44. data/spec/parslet/source_spec.rb +168 -0
  45. data/spec/parslet/transform/context_spec.rb +35 -0
  46. data/spec/parslet/transform_spec.rb +165 -0
  47. data/spec/spec_helper.rb +38 -0
  48. metadata +46 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d212114155ea2fce120ce17157dcd10a9bb81ebf
4
- data.tar.gz: 07ad0296a6bea037a87594ffc565429b39e6258b
3
+ metadata.gz: 546221c33c4829eb1bede8a241b40418197462de
4
+ data.tar.gz: d440cbbac367cf7909bdeeb764b431bc3a32bdf7
5
5
  SHA512:
6
- metadata.gz: 704d66fde31caf3741fa202d2ac56eaf410ec3ed0cf9050bc834af92a115d847a14e72d61cd97b44c60ba8bc1e50bf792cfadff3257e2b789a7bc9550d0b944a
7
- data.tar.gz: 6ae7e679bfe78d0de4f6c66f4d096f2985b23ece311d5705deb5fe9098ce516ba4d6a7ecb2df7f34d942c7c2370dac3b075d0a6c168e887ac1bdbbdc020a21b2
6
+ metadata.gz: adae2aa91f57458cf679d60c3aefd5545a02f99895745cfc61e05290e86cae69adc1b38c121756e96beb4c6ffb4bce375af04e39050ecfe59154ecd3e187a76a
7
+ data.tar.gz: f8eeac537a6b4dcc958efeb1355d4ad6040e88e91efa2e0397706803ba58c42f69b970720b62cae7f29ff550f50e652c984853af41cab31da7a6ebb4cccbc4ae
data/README CHANGED
@@ -70,4 +70,4 @@ STATUS
70
70
 
71
71
  Production worthy.
72
72
 
73
- (c) 2010-2014 Kaspar Schiess
73
+ (c) 2010-2016 Kaspar Schiess
@@ -21,7 +21,7 @@ class Parslet::Context < BlankSlate
21
21
  include Parslet
22
22
 
23
23
  def meta_def(name, &body)
24
- metaclass = class <<self; self; end
24
+ metaclass = class << self; self; end
25
25
 
26
26
  metaclass.send(:define_method, name, &body)
27
27
  end
@@ -30,7 +30,7 @@
30
30
  class Parslet::Parser < Parslet::Atoms::Base
31
31
  include Parslet
32
32
 
33
- class <<self # class methods
33
+ class << self # class methods
34
34
  # Define the parsers #root function. This is the place where you start
35
35
  # parsing; if you have a rule for 'file' that describes what should be
36
36
  # in a file, this would be your root declaration:
@@ -0,0 +1,18 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = 'parslet'
5
+ s.version = '1.7.1'
6
+
7
+ s.authors = ['Kaspar Schiess']
8
+ s.email = 'kaspar.schiess@absurd.li'
9
+ s.extra_rdoc_files = ['README']
10
+ s.files = %w(HISTORY.txt LICENSE Rakefile README parslet.gemspec) + Dir.glob("{lib,spec,example}/**/*")
11
+ s.homepage = 'http://kschiess.github.io/parslet'
12
+ s.license = 'MIT'
13
+ s.rdoc_options = ['--main', 'README']
14
+ s.require_paths = ['lib']
15
+ s.summary = 'Parser construction library with great error reporting in Ruby.'
16
+
17
+ s.add_dependency 'blankslate', '>= 2.0', '<= 4.0'
18
+ end
@@ -0,0 +1,37 @@
1
+ require 'spec_helper'
2
+ require 'open3'
3
+
4
+ describe "Regression on" do
5
+ Dir["example/*.rb"].each do |example|
6
+ context example do
7
+ # Generates a product path for a given example file.
8
+ def product_path(str, ext)
9
+ str.
10
+ gsub('.rb', ".#{ext}").
11
+ gsub('example/','example/output/')
12
+ end
13
+
14
+ it "runs successfully" do
15
+ stdin, stdout, stderr = Open3.popen3("ruby #{example}")
16
+
17
+ handle_map = {
18
+ stdout => :out,
19
+ stderr => :err
20
+ }
21
+ expectation_found = handle_map.any? do |io, ext|
22
+ name = product_path(example, ext)
23
+
24
+ if File.exists?(name)
25
+ io.read.strip.should == File.read(name).strip
26
+ true
27
+ end
28
+ end
29
+
30
+ unless expectation_found
31
+ fail "Example doesn't have either an .err or an .out file. "+
32
+ "Please create in examples/output!"
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,112 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'Infix expression parsing' do
4
+ class InfixExpressionParser < Parslet::Parser
5
+ rule(:space) { match['\s'] }
6
+
7
+ def cts atom
8
+ atom >> space.repeat
9
+ end
10
+ def infix *args
11
+ Infix.new(*args)
12
+ end
13
+
14
+ rule(:mul_op) { match['*/'] >> str(' ').maybe }
15
+ rule(:add_op) { match['+-'] >> str(' ').maybe }
16
+ rule(:digit) { match['0-9'] }
17
+ rule(:integer) { cts digit.repeat(1) }
18
+
19
+ rule(:expression) { infix_expression(integer,
20
+ [mul_op, 2, :left],
21
+ [add_op, 1, :right]) }
22
+ end
23
+
24
+ let(:p) { InfixExpressionParser.new }
25
+ describe '#integer' do
26
+ let(:i) { p.integer }
27
+ it "parses integers" do
28
+ i.should parse('1')
29
+ i.should parse('123')
30
+ end
31
+ it "consumes trailing white space" do
32
+ i.should parse('1 ')
33
+ i.should parse('134 ')
34
+ end
35
+ it "doesn't parse floats" do
36
+ i.should_not parse('1.3')
37
+ end
38
+ end
39
+ describe '#multiplication' do
40
+ let(:m) { p.expression }
41
+ it "parses simple multiplication" do
42
+ m.should parse('1*2').as(l: '1', o: '*', r: '2')
43
+ end
44
+ it "parses simple multiplication with spaces" do
45
+ m.should parse('1 * 2').as(l: '1 ', o: '* ', r: '2')
46
+ end
47
+ it "parses division" do
48
+ m.should parse('1/2')
49
+ end
50
+ end
51
+ describe '#addition' do
52
+ let(:a) { p.expression }
53
+
54
+ it "parses simple addition" do
55
+ a.should parse('1+2')
56
+ end
57
+ it "parses complex addition" do
58
+ a.should parse('1+2+3-4')
59
+ end
60
+ it "parses a single element" do
61
+ a.should parse('1')
62
+ end
63
+ end
64
+
65
+ describe 'mixed operations' do
66
+ let(:mo) { p.expression }
67
+
68
+ describe 'inspection' do
69
+ it 'produces useful expressions' do
70
+ p.expression.parslet.inspect.should ==
71
+ "infix_expression(INTEGER, [MUL_OP, ADD_OP])"
72
+ end
73
+ end
74
+ describe 'right associativity' do
75
+ it 'produces trees that lean right' do
76
+ mo.should parse('1+2+3').as(
77
+ l: '1', o: '+', r: {l: '2', o: '+', r: '3'})
78
+ end
79
+ end
80
+ describe 'left associativity' do
81
+ it 'produces trees that lean left' do
82
+ mo.should parse('1*2*3').as(
83
+ l: {l:'1', o:'*', r:'2'}, o:'*', r:'3')
84
+ end
85
+ end
86
+ describe 'error handling' do
87
+ describe 'incomplete expression' do
88
+ it 'produces the right error' do
89
+ cause = catch_failed_parse {
90
+ mo.parse('1+') }
91
+
92
+ cause.ascii_tree.to_s.should == <<-ERROR
93
+ INTEGER was expected at line 1 char 3.
94
+ `- Failed to match sequence (DIGIT{1, } SPACE{0, }) at line 1 char 3.
95
+ `- Expected at least 1 of DIGIT at line 1 char 3.
96
+ `- Premature end of input at line 1 char 3.
97
+ ERROR
98
+ end
99
+ end
100
+ describe 'invalid operator' do
101
+ it 'produces the right error' do
102
+ cause = catch_failed_parse {
103
+ mo.parse('1%') }
104
+
105
+ cause.ascii_tree.to_s.should == <<-ERROR
106
+ Don't know what to do with "%" at line 1 char 2.
107
+ ERROR
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,314 @@
1
+ # Encoding: UTF-8
2
+
3
+ require 'spec_helper'
4
+
5
+ require 'parslet'
6
+
7
+ describe "Regressions from real examples" do
8
+ # This parser piece produces on the left a subtree that is keyed (a hash)
9
+ # and on the right a subtree that is a repetition of such subtrees. I've
10
+ # for now decided that these would merge into the repetition such that the
11
+ # return value is an array. This avoids maybe loosing keys/values in a
12
+ # hash merge.
13
+ #
14
+ class ArgumentListParser
15
+ include Parslet
16
+
17
+ rule :argument_list do
18
+ expression.as(:argument) >>
19
+ (comma >> expression.as(:argument)).repeat
20
+ end
21
+ rule :expression do
22
+ string
23
+ end
24
+ rule :string do
25
+ str('"') >>
26
+ (
27
+ str('\\') >> any |
28
+ str('"').absent? >> any
29
+ ).repeat.as(:string) >>
30
+ str('"') >> space?
31
+ end
32
+ rule :comma do
33
+ str(',') >> space?
34
+ end
35
+ rule :space? do
36
+ space.maybe
37
+ end
38
+ rule :space do
39
+ match("[ \t]").repeat(1)
40
+ end
41
+
42
+ def parse(str)
43
+ argument_list.parse(str)
44
+ end
45
+ end
46
+ describe ArgumentListParser do
47
+ let(:instance) { ArgumentListParser.new }
48
+ it "should have method expression" do
49
+ instance.should respond_to(:expression)
50
+ end
51
+ it 'should parse "arg1", "arg2"' do
52
+ result = ArgumentListParser.new.parse('"arg1", "arg2"')
53
+
54
+ result.size.should == 2
55
+ result.each do |r|
56
+ r[:argument]
57
+ end
58
+ end
59
+ it 'should parse "arg1", "arg2", "arg3"' do
60
+ result = ArgumentListParser.new.parse('"arg1", "arg2", "arg3"')
61
+
62
+ result.size.should == 3
63
+ result.each do |r|
64
+ r[:argument]
65
+ end
66
+ end
67
+ end
68
+
69
+ class ParensParser < Parslet::Parser
70
+ rule(:balanced) {
71
+ str('(').as(:l) >> balanced.maybe.as(:m) >> str(')').as(:r)
72
+ }
73
+
74
+ root(:balanced)
75
+ end
76
+ describe ParensParser do
77
+ let(:instance) { ParensParser.new }
78
+
79
+ context "statefulness: trying several expressions in sequence" do
80
+ it "should not be stateful" do
81
+ # NOTE: Since you've come here to read this, I'll explain why
82
+ # this is broken and not fixed: You're looking at the tuning branch,
83
+ # which rewrites a bunch of stuff - so I have failing tests to
84
+ # remind me of what is left to be done. And to remind you not to
85
+ # trust this code.
86
+ instance.parse('(())')
87
+ lambda {
88
+ instance.parse('((()))')
89
+ instance.parse('(((())))')
90
+ }.should_not raise_error
91
+ end
92
+ end
93
+ context "expression '(())'" do
94
+ let(:result) { instance.parse('(())') }
95
+
96
+ it "should yield a doubly nested hash" do
97
+ result.should be_a(Hash)
98
+ result.should have_key(:m)
99
+ result[:m].should be_a(Hash) # This was an array earlier
100
+ end
101
+ context "inner hash" do
102
+ let(:inner) { result[:m] }
103
+
104
+ it "should have nil as :m" do
105
+ inner[:m].should be_nil
106
+ end
107
+ end
108
+ end
109
+ end
110
+
111
+ class ALanguage < Parslet::Parser
112
+ root(:expressions)
113
+
114
+ rule(:expressions) { (line >> eol).repeat(1) | line }
115
+ rule(:line) { space? >> an_expression.as(:exp).repeat }
116
+ rule(:an_expression) { str('a').as(:a) >> space? }
117
+
118
+ rule(:eol) { space? >> match["\n\r"].repeat(1) >> space? }
119
+
120
+ rule(:space?) { space.repeat }
121
+ rule(:space) { multiline_comment.as(:multi) | line_comment.as(:line) | str(' ') }
122
+
123
+ rule(:line_comment) { str('//') >> (match["\n\r"].absent? >> any).repeat }
124
+ rule(:multiline_comment) { str('/*') >> (str('*/').absent? >> any).repeat >> str('*/') }
125
+ end
126
+ describe ALanguage do
127
+ def remove_indent(s)
128
+ s.to_s.lines.map { |l| l.chomp.strip }.join("\n")
129
+ end
130
+
131
+ it "should count lines correctly" do
132
+ cause = catch_failed_parse {
133
+ subject.parse('a
134
+ a a a
135
+ aaa // ff
136
+ /*
137
+ a
138
+ */
139
+ b
140
+ ')
141
+ }
142
+
143
+ remove_indent(cause.ascii_tree).should == remove_indent(%q(
144
+ Expected one of [(LINE EOL){1, }, LINE] at line 1 char 1.
145
+ |- Extra input after last repetition at line 7 char 11.
146
+ | `- Failed to match sequence (LINE EOL) at line 7 char 11.
147
+ | `- Failed to match sequence (SPACE? [\n\r]{1, } SPACE?) at line 7 char 11.
148
+ | `- Expected at least 1 of [\n\r] at line 7 char 11.
149
+ | `- Failed to match [\n\r] at line 7 char 11.
150
+ `- Don't know what to do with "\n " at line 1 char 2.).strip)
151
+ end
152
+ end
153
+
154
+ class BLanguage < Parslet::Parser
155
+ root :expression
156
+ rule(:expression) { b.as(:one) >> b.as(:two) }
157
+ rule(:b) { str('b') }
158
+ end
159
+ describe BLanguage do
160
+ it "should parse 'bb'" do
161
+ subject.should parse('bb').as(:one => 'b', :two => 'b')
162
+ end
163
+ it "should transform with binding constraint" do
164
+ transform = Parslet::Transform.new do |t|
165
+ t.rule(:one => simple(:b), :two => simple(:b)) { :ok }
166
+ end
167
+ transform.apply(subject.parse('bb')).should == :ok
168
+ end
169
+ end
170
+
171
+ class UnicodeLanguage < Parslet::Parser
172
+ root :gobble
173
+ rule(:gobble) { any.repeat }
174
+ end
175
+ describe UnicodeLanguage do
176
+ it "should parse UTF-8 strings" do
177
+ subject.should parse('éèäöü').as('éèäöü')
178
+ subject.should parse('RubyKaigi2009のテーマは、「変わる/変える」です。 前回の').as('RubyKaigi2009のテーマは、「変わる/変える」です。 前回の')
179
+ end
180
+ end
181
+
182
+ class UnicodeSentenceLanguage < Parslet::Parser
183
+ rule(:sentence) { (match('[^。]').repeat(1) >> str("。")).as(:sentence) }
184
+ rule(:sentences) { sentence.repeat }
185
+ root(:sentences)
186
+ end
187
+ describe UnicodeSentenceLanguage do
188
+ let(:string) {
189
+ "RubyKaigi2009のテーマは、「変わる/変える」です。 前回の" +
190
+ "RubyKaigi2008のテーマであった「多様性」の言葉の通り、 " +
191
+ "2008年はRubyそのものに関しても、またRubyの活躍する舞台に関しても、 " +
192
+ "ますます多様化が進みつつあります。RubyKaigi2008は、そのような " +
193
+ "Rubyの生態系をあらためて認識する場となりました。 しかし、" +
194
+ "こうした多様化が進む中、異なる者同士が単純に距離を 置いたままでは、" +
195
+ "その違いを認識したところであまり意味がありません。 異なる実装、" +
196
+ "異なる思想、異なる背景といった、様々な多様性を理解しつつ、 " +
197
+ "すり合わせるべきものをすり合わせ、変えていくべきところを " +
198
+ "変えていくことが、豊かな未来へとつながる道に違いありません。"
199
+ }
200
+
201
+ it "should parse sentences" do
202
+ subject.should parse(string)
203
+ end
204
+ end
205
+
206
+ class TwoCharLanguage < Parslet::Parser
207
+ root :twochar
208
+ rule(:twochar) { any >> str('2') }
209
+ end
210
+ describe TwoCharLanguage do
211
+ def di(s)
212
+ s.strip.to_s.lines.map { |l| l.chomp.strip }.join("\n")
213
+ end
214
+
215
+ it "should raise an error" do
216
+ error = catch_failed_parse {
217
+ subject.parse('123') }
218
+ di(error.ascii_tree).should == di(%q(
219
+ Failed to match sequence (. '2') at line 1 char 2.
220
+ `- Don't know what to do with "3" at line 1 char 3.
221
+ ))
222
+ end
223
+ end
224
+
225
+ # Issue #68: Extra input reporting, written by jmettraux
226
+ class RepetitionParser < Parslet::Parser
227
+ rule(:nl) { match('[\s]').repeat(1) }
228
+ rule(:nl?) { nl.maybe }
229
+ rule(:sp) { str(' ').repeat(1) }
230
+ rule(:sp?) { str(' ').repeat(0) }
231
+ rule(:line) { sp >> str('line') }
232
+ rule(:body) { ((line | block) >> nl).repeat(0) }
233
+ rule(:block) { sp? >> str('begin') >> sp >> match('[a-z]') >> nl >>
234
+ body >> sp? >> str('end') }
235
+ rule(:blocks) { nl? >> block >> (nl >> block).repeat(0) >> nl? }
236
+
237
+ root(:blocks)
238
+ end
239
+ describe RepetitionParser do
240
+ def di(s)
241
+ s.strip.to_s.lines.map { |l| l.chomp.strip }.join("\n")
242
+ end
243
+
244
+ it 'parses a block' do
245
+ subject.parse(%q{
246
+ begin a
247
+ end
248
+ })
249
+ end
250
+ it 'parses nested blocks' do
251
+ subject.parse(%q{
252
+ begin a
253
+ begin b
254
+ end
255
+ end
256
+ })
257
+ end
258
+ it 'parses successive blocks' do
259
+ subject.parse(%q{
260
+ begin a
261
+ end
262
+ begin b
263
+ end
264
+ })
265
+ end
266
+ it 'fails gracefully on a missing end' do
267
+ error = catch_failed_parse {
268
+ subject.parse(%q{
269
+ begin a
270
+ begin b
271
+ end
272
+ }) }
273
+
274
+ di(error.ascii_tree).should == di(%q(
275
+ Failed to match sequence (NL? BLOCK (NL BLOCK){0, } NL?) at line 2 char 11.
276
+ `- Failed to match sequence (SP? 'begin' SP [a-z] NL BODY SP? 'end') at line 5 char 9.
277
+ `- Premature end of input at line 5 char 9.
278
+ ))
279
+ end
280
+ it 'fails gracefully on a missing end (2)' do
281
+ error = catch_failed_parse {
282
+ subject.parse(%q{
283
+ begin a
284
+ end
285
+ begin b
286
+ begin c
287
+ end
288
+ }) }
289
+
290
+ di(error.ascii_tree).should == di(%q(
291
+ Failed to match sequence (NL? BLOCK (NL BLOCK){0, } NL?) at line 3 char 14.
292
+ `- Don't know what to do with "begin b\n " at line 4 char 11.
293
+ ))
294
+ end
295
+ it 'fails gracefully on a missing end (deepest reporter)' do
296
+ error = catch_failed_parse {
297
+ subject.parse(%q{
298
+ begin a
299
+ end
300
+ begin b
301
+ begin c
302
+ li
303
+ end
304
+ end
305
+ },
306
+ :reporter => Parslet::ErrorReporter::Deepest.new) }
307
+
308
+ di(error.ascii_tree).should == di(%q(
309
+ Failed to match sequence (NL? BLOCK (NL BLOCK){0, } NL?) at line 3 char 16.
310
+ `- Expected "end", but got "li\n" at line 6 char 17.
311
+ ))
312
+ end
313
+ end
314
+ end