parser gemとパーセントリテラルのバグ
-
^@
とかがデリミタとしてparser gemだと通らない(Rubyだと通る -
1
とかがデリミタとしてparser gemだと通る(Rubyだと通らない
軽く調査したらwhitequark/parserにレポートする
# gen.rb
puts "%Q\0foo\0"
$ ruby gen.rb > test.rb
$ ruby-parse test.rb
warning: parser/current is loading parser/ruby31, which recognizes
warning: 3.1.0-dev-compliant syntax, but you are running 3.1.0.
warning: please see https://github.com/whitequark/parser#compatibility-with-ruby-mri.
test.rb:1:1: fatal: unterminated string meets end of file
test.rb:1: %Q
$ ruby -c test.rb
Syntax OK
$ ruby-parse -e "%w1foo1"
(array
(str "foo"))
$ ruby -ce "%w1foo1"
-e:1: unknown type of %string
%w1foo1
^~~
"\0"
をデリミタにすると死ぬ問題の原因はこれ。
パーセントリテラルのデリミタにc_anyというtokenを使っているのだけど、c_anyはanyから^D
とかを抜いた文字であり、それが良くないっぽい。
any - zlen
を使うのが正しいのでは?多分zlenはEOFぽく見える。
c_any(とany)は結構いたるところで使われていて、眺めるのがだるい。
%w1foo1
が通ってしまうのは別問題として見つけた
openなIssueとPRをざっと見た感じ、該当のIssueはなかった
現実世界の問題としては、hamlit + querlyで問題が起きていた。querlyでHAMLのコードを解析するためにpreprocessorをhamlit compile -
に指定していたのだけど、hamlitがヌル文字をデリミタとしてパーセントリテラル文字列を出力していて、それがparser gemによって解釈できていなかった
all-rubyで試した結果。parser gemは1.8からのサポートなので、特にバージョン分岐は考える必要がなさそう。
^D
は実際には制御文字
v0.99から制御文字をデリミタにしたパーセントリテラルは通る
$ docker run -it --rm rubylang/all-ruby ./all-ruby -e 'p %q^Dfoo^D'
ruby-0.49 -e:1: undefined method `(null)' for "nil"(Nil)
exit 1
ruby-0.50 -e:1: undefined method `%' for "nil"(Nil)
exit 1
ruby-0.51 -e:1: undefined method `%' for "nil"(Nil)
exit 1
ruby-0.54 -e:1:in method `%': undefined method `%' for "nil"(Nil)
exit 1
ruby-0.55 -e:1: undefined method `%' for "nil"(Nil)
exit 1
...
ruby-0.76 -e:1: undefined method `%' for "nil"(Nil)
exit 1
ruby-0.95 -e:1: undefined method `p' for main(Object)
exit 1
ruby-0.99.4-961224 "foo"
...
ruby-3.0.2 "foo"
%1foo1
は、Ruby 0.99からRuby 1.6.5までは通っていた。それ以降は通らない。
$ docker run -it --rm rubylang/all-ruby ./all-ruby -e 'p %q1foo1'
ruby-0.49 -e:1: undefined method `(null)' for "nil"(Nil)
exit 1
ruby-0.50 -e:1: undefined method `%' for "nil"(Nil)
exit 1
ruby-0.51 -e:1: undefined method `%' for "nil"(Nil)
exit 1
ruby-0.54 -e:1:in method `%': undefined method `%' for "nil"(Nil)
exit 1
ruby-0.55 -e:1: undefined method `%' for "nil"(Nil)
exit 1
...
ruby-0.76 -e:1: undefined method `%' for "nil"(Nil)
exit 1
ruby-0.95 -e:1: undefined method `p' for main(Object)
exit 1
ruby-0.99.4-961224 "foo"
...
ruby-1.6.5 "foo"
ruby-1.6.6 -e:1: unknown type of %string
p %q1foo1
^
exit 1
...
ruby-2.4.10 -e:1: unknown type of %string
p %q1foo1
^
exit 1
ruby-2.5.0-preview1 -e:1: unknown type of %string
p %q1foo1
^~~
exit 1
...
ruby-2.6.8 -e:1: unknown type of %string
p %q1foo1
^~~
exit 1
ruby-2.7.0-preview1 -e:1: unknown type of %string
p %q1foo1
^~~
exit 1
ruby-2.7.0-preview2 -e:1: unknown type of %string
p %q1foo1
^~~
exit 1
...
ruby-3.0.2 -e:1: unknown type of %string
p %q1foo1
^~~
exit 1
%q†foo†
もだめだった(parser gemでは通ってしまうけどcrubyでは通らない)
test.rb
(0..127).each do |n|
next if /[a-zA-Z0-9()<>{}\[\]]/ =~ n.chr
eval "%q#{n.chr}foo#{n.chr}"
end
$ docker run -it --rm -v $(pwd)/test.rb:/tmp/test.rb rubylang/all-ruby env ALL_RUBY_SINCE=1.8 ./all-ruby /tmp/test.rb
ruby-1.8.0
...
ruby-2.0.0-p648
ruby-2.1.0-preview1 (eval):1: warning: encountered \r in middle of line, treated as a mere space
...
ruby-2.7.4 (eval):1: warning: encountered \r in middle of line, treated as a mere space
ruby-3.0.0-preview1
...
ruby-3.0.2
MRIはこれが通る
MRIのコードはこの辺
diff --git a/lib/parser/lexer.rl b/lib/parser/lexer.rl
index 8574f95..c266f1e 100644
--- a/lib/parser/lexer.rl
+++ b/lib/parser/lexer.rl
@@ -518,7 +518,8 @@ class Parser::Lexer
c_nl_zlen = c_nl | zlen;
c_line = any - c_nl_zlen;
- c_unicode = c_any - 0x00..0x7f;
+ c_ascii = 0x00..0x7f;
+ c_unicode = c_any - c_ascii;
c_upper = [A-Z];
c_lower = [a-z_] | c_unicode;
c_alpha = c_lower | c_upper;
@@ -1406,7 +1407,7 @@ class Parser::Lexer
':'
=> { fhold; fgoto expr_beg; };
- '%s' c_any
+ '%s' c_ascii - c_alnum
=> {
if version?(23)
type, delimiter = tok[0..-2], tok[-1].chr
@@ -1758,14 +1759,14 @@ class Parser::Lexer
};
# %<string>
- '%' ( any - [A-Za-z] )
+ '%' ( c_ascii - c_alnum )
=> {
type, delimiter = @source_buffer.slice(@ts).chr, tok[-1].chr
fgoto *push_literal(type, delimiter, @ts);
};
# %w(we are the people)
- '%' [A-Za-z]+ c_any
+ '%' [A-Za-z] c_ascii - c_alnum
=> {
type, delimiter = tok[0..-2], tok[-1].chr
fgoto *push_literal(type, delimiter, @ts);
こんな感じのパッチで良いのでは。テスト書くかー。
'%' [A-Za-z]+ c_any
の+
がちょっと謎い。多分正規表現の+
と同じ意味。これ消してもいいと思うけどよくわからんのでPRで聞くか
PRed
似た内容でheredocにもバグがあったのでissueを作った。これは私は困ってないので、自分で手を動かすつもりはとりあえずなし