parser-prism 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 87ed9acec576c353ec93af84fbcdc73bebdfba153194e64fccad45be90a768e8
4
+ data.tar.gz: 4c7263d22aedd730a2859e7e52390f10c25b4f540baccedff8138fa478327cd3
5
+ SHA512:
6
+ metadata.gz: 4451a6a3234c60e424b85b1c182a211c310846c73ed4abf6e18ee14ef673ccacfffa948268ce07d1b1ae9667c773a7535d8acb834beee85b7877eb37cafe0c3a
7
+ data.tar.gz: 596ff68ecba5813cd45ef83434a0b94e0146c6cb6ac40444992899f70481e8f181233bb5d6f96e1e63fe0dc290faeb89da667f44bd1e891d569f36a3886bc953
@@ -0,0 +1,18 @@
1
+ name: Main
2
+
3
+ on:
4
+ - push
5
+ - pull_request
6
+
7
+ jobs:
8
+ ci:
9
+ name: CI
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@master
13
+ - uses: ruby/setup-ruby@v1
14
+ with:
15
+ ruby-version: '3.2'
16
+ bundler-cache: true
17
+ - name: Test
18
+ run: bundle exec rake test
data/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ pkg
2
+ test.rb
3
+ tmp
data/.rubocop.yml ADDED
@@ -0,0 +1,4 @@
1
+ AllCops:
2
+ NewCops: enable
3
+ SuggestExtensions: false
4
+ TargetRubyVersion: 80_82_73_83_77.33
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "benchmark-ips"
6
+ gem "prism"
7
+ gem "rake"
8
+ gem "rubocop"
9
+ gem "test-unit"
10
+
11
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,57 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ parser-prism (0.1.0)
5
+ parser
6
+ prism
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ ast (2.4.2)
12
+ benchmark-ips (2.12.0)
13
+ json (2.6.3)
14
+ language_server-protocol (3.17.0.3)
15
+ parallel (1.23.0)
16
+ parser (3.2.2.4)
17
+ ast (~> 2.4.1)
18
+ racc
19
+ power_assert (2.0.3)
20
+ prism (0.19.0)
21
+ racc (1.7.3)
22
+ rainbow (3.1.1)
23
+ rake (13.1.0)
24
+ regexp_parser (2.8.2)
25
+ rexml (3.2.6)
26
+ rubocop (1.57.2)
27
+ json (~> 2.3)
28
+ language_server-protocol (>= 3.17.0)
29
+ parallel (~> 1.10)
30
+ parser (>= 3.2.2.4)
31
+ rainbow (>= 2.2.2, < 4.0)
32
+ regexp_parser (>= 1.8, < 3.0)
33
+ rexml (>= 3.2.5, < 4.0)
34
+ rubocop-ast (>= 1.28.1, < 2.0)
35
+ ruby-progressbar (~> 1.7)
36
+ unicode-display_width (>= 2.4.0, < 3.0)
37
+ rubocop-ast (1.30.0)
38
+ parser (>= 3.2.1.0)
39
+ ruby-progressbar (1.13.0)
40
+ test-unit (3.6.1)
41
+ power_assert
42
+ unicode-display_width (2.5.0)
43
+
44
+ PLATFORMS
45
+ arm64-darwin-22
46
+ x86_64-linux
47
+
48
+ DEPENDENCIES
49
+ benchmark-ips
50
+ parser-prism!
51
+ prism
52
+ rake
53
+ rubocop
54
+ test-unit
55
+
56
+ BUNDLED WITH
57
+ 2.4.13
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2023-present Kevin Newton
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,96 @@
1
+ # parser-prism
2
+
3
+ This gem provides a new backend for the [whitequark/parser](https://github.com/whitequark/parser) gem's syntax tree that uses the [prism](https://github.com/ruby/prism) parser.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem "parser-prism"
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install parser-prism
20
+
21
+ ## Usage
22
+
23
+ The `parser` gem provides multiple parsers to support different versions of the Ruby grammar. This includes all of the Ruby versions going back to 1.8, as well as third-party parsers like MacRuby and RubyMotion. The `parser-prism` gem provides another parser that uses the `prism` parser to build the syntax tree.
24
+
25
+ You can use the `prism` parser like you would any other. After requiring the parser, you should be able to call any of the regular `Parser::Base` APIs that you would normally use.
26
+
27
+ ```ruby
28
+ require "parser/prism"
29
+
30
+ Parser::Prism.parse_file("path/to/file.rb")
31
+ ```
32
+
33
+ ### RuboCop
34
+
35
+ To run RuboCop using the `parser-prism` gem as the parser, you will need to require the `parser/prism/rubocop` file. This file injects `prism` into the known options for both `rubocop` and `rubocop-ast`, such that you can specify it in your `.rubocop.yml` file. Unfortunately `rubocop` doesn't support any direct way to do this, so we have to get a bit hacky.
36
+
37
+ First, set the `TargetRubyVersion` in your RuboCop configuration file to `80_82_73_83_77.33`. This is the version of Ruby that `prism` reports itself as. (The leading numbers are the ASCII values for `PRISM`.)
38
+
39
+ ```yaml
40
+ AllCops:
41
+ TargetRubyVersion: 80_82_73_83_77.33
42
+ ```
43
+
44
+ Now when you run `rubocop` you will need to require the `parser/prism/rubocop` file before executing so that it can inject the `prism` parser into the known options.
45
+
46
+ ```
47
+ bundle exec ruby -rparser/prism/rubocop $(bundle exec which rubocop)
48
+ ```
49
+
50
+ This should run RuboCop using the `prism` parser.
51
+
52
+ ## Benchmarks
53
+
54
+ As a whole, this parser should be significantly faster than the `parser` gem. The `bin/bench` script in this repository compares the performance of `Parser::CurrentRuby` and `Parser::Prism`. Running against a large file like `lib/parser/prism/compiler.rb` yields:
55
+
56
+ ```
57
+ Warming up --------------------------------------
58
+ Parser::CurrentRuby 1.000 i/100ms
59
+ Parser::Prism 6.000 i/100ms
60
+ Calculating -------------------------------------
61
+ Parser::CurrentRuby 16.642 (± 0.0%) i/s - 84.000 in 5.052021s
62
+ Parser::Prism 64.951 (± 3.1%) i/s - 330.000 in 5.088147s
63
+
64
+ Comparison:
65
+ Parser::Prism: 65.0 i/s
66
+ Parser::CurrentRuby: 16.6 i/s - 3.90x slower
67
+ ```
68
+
69
+ When running with `--yjit`, the comparison is even more stark:
70
+
71
+ ```
72
+ Warming up --------------------------------------
73
+ Parser::CurrentRuby 1.000 i/100ms
74
+ Parser::Prism 9.000 i/100ms
75
+ Calculating -------------------------------------
76
+ Parser::CurrentRuby 20.062 (± 0.0%) i/s - 101.000 in 5.034389s
77
+ Parser::Prism 112.823 (± 9.7%) i/s - 558.000 in 5.009460s
78
+
79
+ Comparison:
80
+ Parser::Prism: 112.8 i/s
81
+ Parser::CurrentRuby: 20.1 i/s - 5.62x slower
82
+ ```
83
+
84
+ These benchmarks were run on a single laptop without a lot of control for other processes, so take them with a grain of salt.
85
+
86
+ ## Development
87
+
88
+ Run `bundle exec rake test` to run the tests. This runs tests exported from the `parser` gem into their own fixture files.
89
+
90
+ ## Contributing
91
+
92
+ Bug reports and pull requests are welcome on GitHub at https://github.com/kddnewton/parser-prism.
93
+
94
+ ## License
95
+
96
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << "test"
8
+ t.libs << "lib"
9
+ t.test_files = FileList["test/**/*_test.rb"]
10
+ end
11
+
12
+ task default: :test
13
+
14
+ # This namespace's purpose is to extract the examples from the whitequark/parser
15
+ # gem and generate a test file that we can use to ensure that our parser
16
+ # generates equivalent syntax trees when translating. To do this, it runs the
17
+ # parser's test suite but overrides the `assert_parses` method to collect the
18
+ # examples into a hash. Then, it writes out the hash to a file that we can use
19
+ # to generate our own tests.
20
+ #
21
+ # To run the test suite, it's important to note that we have to mirror both any
22
+ # APIs provided to the test suite (for example the ParseHelper module below).
23
+ # This is obviously relatively brittle, but it's effective for now.
24
+ namespace :whitequark do
25
+ desc "Ensure there's a local copy of whitequark/parser"
26
+ file "tmp/whitequark" do
27
+ sh "git clone --depth=1 https://github.com/whitequark/parser tmp/whitequark"
28
+ end
29
+
30
+ desc "Ensure we have a fixtures directory for the whitequark/parser tests"
31
+ directory "test/fixtures"
32
+
33
+ desc "Import the whitequark/parser tests"
34
+ task import: ["tmp/whitequark", "test/fixtures"] do
35
+ cp "tmp/whitequark/LICENSE.txt", "test/fixtures/LICENSE"
36
+
37
+ mkdir_p "tmp/whitequark/scratch"
38
+ touch "tmp/whitequark/scratch/helper.rb"
39
+ touch "tmp/whitequark/scratch/parse_helper.rb"
40
+ $:.unshift("tmp/whitequark/scratch")
41
+
42
+ require "ast"
43
+ module ParseHelper
44
+ include AST::Sexp
45
+
46
+ # This object is going to collect all of the examples from the parser gem
47
+ # into a hash that we can use to generate our own tests.
48
+ COLLECTED = Hash.new { |hash, key| hash[key] = [] }
49
+ ALL_VERSIONS = %w[3.1 3.2]
50
+
51
+ private
52
+
53
+ def assert_context(*)
54
+ end
55
+
56
+ def assert_diagnoses(*)
57
+ end
58
+
59
+ def assert_diagnoses_many(*)
60
+ end
61
+
62
+ def refute_diagnoses(*)
63
+ end
64
+
65
+ def with_versions(*)
66
+ end
67
+
68
+ def assert_parses(_ast, code, _source_maps = "", versions = ALL_VERSIONS)
69
+ # We're going to skip any examples that are for older Ruby versions
70
+ # that we do not support.
71
+ return if (versions & %w[3.1 3.2]).empty?
72
+
73
+ entry = caller.find { _1.include?("test_parser.rb") }
74
+ _, name = *entry.match(/\d+:in `(?:block in )?(?:test_|assert_parses_)?(.+)'/)
75
+
76
+ COLLECTED[name] << code
77
+ end
78
+ end
79
+
80
+ require "parser/current"
81
+ require "minitest/autorun"
82
+ require_relative "tmp/whitequark/test/test_parser"
83
+
84
+ Minitest.after_run do
85
+ ParseHelper::COLLECTED.each do |(name, codes)|
86
+ File.write("test/fixtures/#{name}.rb", "#{codes.sort.join("\n\n")}\n")
87
+ end
88
+ end
89
+ end
90
+
91
+ desc "Clean up tmp files related to whitequark/parser"
92
+ task :clean do
93
+ rm_rf "tmp/whitequark"
94
+ rm_rf "test/fixtures"
95
+ end
96
+ end
data/bin/bench ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "benchmark/ips"
6
+ require "parser/prism"
7
+ require "parser/current"
8
+
9
+ filepath = ARGV.fetch(0) { File.expand_path("../lib/parser/prism/compiler.rb", __dir__) }
10
+
11
+ Benchmark.ips do |x|
12
+ x.report("Parser::CurrentRuby") { Parser::CurrentRuby.parse_file(filepath) }
13
+ x.report("Parser::Prism") { Parser::Prism.parse_file(filepath) }
14
+ x.compare!
15
+ end
data/bin/parse ADDED
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ $:.unshift(File.expand_path("../lib", __dir__))
6
+
7
+ require "parser/prism"
8
+ require "parser/prism/compare"
9
+
10
+ if ARGV[0].nil?
11
+ filepaths = Dir[File.expand_path("../test/fixtures/*.rb", __dir__)]
12
+ failures = 0
13
+
14
+ filepaths.each do |filepath|
15
+ failures += 1 unless Parser::Prism.compare(filepath)
16
+ end
17
+
18
+ puts "#{filepaths.size - failures}/#{filepaths.size} tests passed"
19
+ elsif ARGV[0] == "-e"
20
+ puts "match!" if Parser::Prism.compare("-e", ARGV[1])
21
+ else
22
+ puts "match!" if Parser::Prism.compare(ARGV[0])
23
+ end
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "parser/current"
4
+
5
+ # First, opt in to every AST feature.
6
+ Parser::Builders::Default.modernize
7
+
8
+ # Modify the source map == check so that it doesn't check against the node
9
+ # itself so we don't get into a recursive loop.
10
+ Parser::Source::Map.prepend(
11
+ Module.new {
12
+ def ==(other)
13
+ self.class == other.class &&
14
+ (instance_variables - %i[@node]).map do |ivar|
15
+ instance_variable_get(ivar) == other.instance_variable_get(ivar)
16
+ end.reduce(:&)
17
+ end
18
+ }
19
+ )
20
+
21
+ # Next, ensure that we're comparing the nodes and also comparing the source
22
+ # ranges so that we're getting all of the necessary information.
23
+ Parser::AST::Node.prepend(
24
+ Module.new {
25
+ def ==(other)
26
+ super && (location == other.location)
27
+ end
28
+ }
29
+ )
30
+
31
+ module Parser
32
+ class Prism
33
+ # Compare the ASTs between the translator and the whitequark/parser gem.
34
+ def self.compare(filepath, source = nil, compare_tokens: true)
35
+ buffer = Source::Buffer.new(filepath, 1)
36
+ buffer.source = source || File.read(filepath)
37
+
38
+ parser = CurrentRuby.default_parser
39
+ parser.diagnostics.consumer = ->(*) {}
40
+ parser.diagnostics.all_errors_are_fatal = true
41
+
42
+ expected_ast, expected_comments, expected_tokens =
43
+ begin
44
+ parser.tokenize(buffer)
45
+ rescue ArgumentError, SyntaxError
46
+ return true
47
+ end
48
+
49
+ actual_ast, actual_comments, actual_tokens = Prism.new.tokenize(buffer)
50
+
51
+ if expected_ast != actual_ast
52
+ puts filepath
53
+ queue = [[expected_ast, actual_ast]]
54
+
55
+ while (left, right = queue.shift)
56
+ if left.type != right.type
57
+ puts "expected:"
58
+ pp left
59
+
60
+ puts "actual:"
61
+ pp right
62
+
63
+ return false
64
+ end
65
+
66
+ if left.location != right.location
67
+ puts "expected:"
68
+ pp left
69
+ pp left.location
70
+
71
+ puts "actual:"
72
+ pp right
73
+ pp right.location
74
+
75
+ return false
76
+ end
77
+
78
+ if left.type == :str && left.children[0] != right.children[0]
79
+ puts "expected:"
80
+ pp left
81
+
82
+ puts "actual:"
83
+ pp right
84
+
85
+ return false
86
+ end
87
+
88
+ left.children.zip(right.children).each do |left_child, right_child|
89
+ queue << [left_child, right_child] if left_child.is_a?(Parser::AST::Node)
90
+ end
91
+ end
92
+
93
+ return false
94
+ end
95
+
96
+ if compare_tokens && expected_tokens != actual_tokens
97
+ expected_index = 0
98
+ actual_index = 0
99
+
100
+ while expected_index < expected_tokens.length
101
+ expected_token = expected_tokens[expected_index]
102
+ actual_token = actual_tokens[actual_index]
103
+
104
+ expected_index += 1
105
+ actual_index += 1
106
+
107
+ if expected_token[0] == :tSPACE && actual_token[0] == :tSTRING_END
108
+ expected_index += 1
109
+ next
110
+ end
111
+
112
+ case actual_token[0]
113
+ when :kDO
114
+ actual_token[0] = expected_token[0] if %i[kDO_BLOCK kDO_LAMBDA].include?(expected_token[0])
115
+ when :tLPAREN
116
+ actual_token[0] = expected_token[0] if expected_token[0] == :tLPAREN2
117
+ when :tLCURLY
118
+ actual_token[0] = expected_token[0] if %i[tLBRACE tLBRACE_ARG].include?(expected_token[0])
119
+ when :tPOW
120
+ actual_token[0] = expected_token[0] if expected_token[0] == :tDSTAR
121
+ end
122
+
123
+ if expected_token != actual_token
124
+ puts "expected:"
125
+ pp expected_token
126
+
127
+ puts "actual:"
128
+ pp actual_token
129
+
130
+ return false
131
+ end
132
+ end
133
+ end
134
+
135
+ if expected_comments != actual_comments
136
+ puts "expected:"
137
+ pp expected_comments
138
+
139
+ puts "actual:"
140
+ pp actual_comments
141
+
142
+ return false
143
+ end
144
+
145
+ true
146
+ end
147
+ end
148
+ end