parser-prism 0.1.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.
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