diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c81fce48c3..6da67d197e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -53,6 +53,22 @@ jobs: - name: internal_investigation run: bundle exec rake internal_investigation + prism: + runs-on: ubuntu-latest + name: Prism + steps: + - uses: actions/checkout@v4 + - name: set up Ruby + uses: ruby/setup-ruby@v1 + with: + # Specify the minimum Ruby version 2.7 required for Prism to run. + ruby-version: 2.7 + bundler-cache: true + - name: spec + env: + PARSER_ENGINE: parser_prism + run: bundle exec rake prism_spec + documentation_checks: runs-on: ubuntu-latest name: Check documentation syntax diff --git a/Gemfile b/Gemfile index b958bb4b69..745286abfb 100644 --- a/Gemfile +++ b/Gemfile @@ -7,6 +7,7 @@ git_source(:github) { |repo| "https://github.com/#{repo}.git" } gemspec gem 'bump', require: false +gem 'prism' gem 'rake' gem 'rspec' gem 'rubocop', github: 'rubocop/rubocop' diff --git a/Rakefile b/Rakefile index 6067b454e2..1bbb91061a 100644 --- a/Rakefile +++ b/Rakefile @@ -27,6 +27,11 @@ task :spec do end end +desc 'Run RSpec with Prism' +task :prism_spec do + sh('PARSER_ENGINE=parser_prism bundle exec rake spec') +end + desc 'Run RSpec with code coverage' task :coverage do ENV['COVERAGE'] = 'true' @@ -36,7 +41,7 @@ end desc 'Run RuboCop over itself' RuboCop::RakeTask.new(:internal_investigation) -task default: %i[documentation_syntax_check spec internal_investigation] +task default: %i[documentation_syntax_check spec prism_spec internal_investigation] desc 'Generate a new cop template' task :new_cop, [:cop] do |_task, args| diff --git a/changelog/new_support_prism.md b/changelog/new_support_prism.md new file mode 100644 index 0000000000..c2a7bf2b62 --- /dev/null +++ b/changelog/new_support_prism.md @@ -0,0 +1 @@ +* [#1245](https://github.com/rubocop/rubocop-rails/pull/1245): Support Prism as a Ruby parser. ([@koic][]) diff --git a/lib/rubocop/cop/mixin/active_record_helper.rb b/lib/rubocop/cop/mixin/active_record_helper.rb index e5b5b5903c..5cfa171373 100644 --- a/lib/rubocop/cop/mixin/active_record_helper.rb +++ b/lib/rubocop/cop/mixin/active_record_helper.rb @@ -39,7 +39,12 @@ def external_dependency_checksum end def schema - RuboCop::Rails::SchemaLoader.load(target_ruby_version) + # For compatibility with RuboCop 1.61.0 or lower. + if respond_to?(:parser_engine) + RuboCop::Rails::SchemaLoader.load(target_ruby_version, parser_engine) + else + RuboCop::Rails::SchemaLoader.load(target_ruby_version, :parser_whitequark) + end end def table_name(class_node) diff --git a/lib/rubocop/rails/schema_loader.rb b/lib/rubocop/rails/schema_loader.rb index a3f457f2c2..591d1dd362 100644 --- a/lib/rubocop/rails/schema_loader.rb +++ b/lib/rubocop/rails/schema_loader.rb @@ -12,10 +12,10 @@ module SchemaLoader # So a cop that uses the loader should handle `nil` properly. # # @return [Schema, nil] - def load(target_ruby_version) + def load(target_ruby_version, parser_engine) return @load if defined?(@load) - @load = load!(target_ruby_version) + @load = load!(target_ruby_version, parser_engine) end def reset! @@ -38,23 +38,13 @@ def db_schema_path private - def load!(target_ruby_version) + def load!(target_ruby_version, parser_engine) path = db_schema_path return unless path - ast = parse(path, target_ruby_version) - Schema.new(ast) if ast - end - - def parse(path, target_ruby_version) - klass_name = :"Ruby#{target_ruby_version.to_s.sub('.', '')}" - klass = ::Parser.const_get(klass_name) - parser = klass.new(RuboCop::AST::Builder.new) + ast = RuboCop::ProcessedSource.new(File.read(path), target_ruby_version, path, parser_engine: parser_engine).ast - buffer = Parser::Source::Buffer.new(path, 1) - buffer.source = path.read - - parser.parse(buffer) + Schema.new(ast) if ast end end end diff --git a/rubocop-rails.gemspec b/rubocop-rails.gemspec index 2bccd94c71..99e503272c 100644 --- a/rubocop-rails.gemspec +++ b/rubocop-rails.gemspec @@ -36,5 +36,5 @@ Gem::Specification.new do |s| # introduced in rack 1.1 s.add_runtime_dependency 'rack', '>= 1.1' s.add_runtime_dependency 'rubocop', '>= 1.33.0', '< 2.0' - s.add_runtime_dependency 'rubocop-ast', '>= 1.30.0', '< 2.0' + s.add_runtime_dependency 'rubocop-ast', '>= 1.31.1', '< 2.0' end diff --git a/spec/rubocop/cop/rails/blank_spec.rb b/spec/rubocop/cop/rails/blank_spec.rb index 6b53515624..c5fab52ae5 100644 --- a/spec/rubocop/cop/rails/blank_spec.rb +++ b/spec/rubocop/cop/rails/blank_spec.rb @@ -2,7 +2,9 @@ RSpec.describe RuboCop::Cop::Rails::Blank, :config do shared_examples 'offense' do |source, correction, message| - it 'registers an offense and corrects' do + # FIXME: `undefined method `[]' for nil` occurs Prism 0.24.0. It has been resolved in + # the development line. This will be resolved in Prism > 0.24.0 and higher releases. + it 'registers an offense and corrects', broken_on: :prism do expect_offense(<<~RUBY, source: source, message: message) #{source} ^{source} #{message} diff --git a/spec/rubocop/cop/rails/exit_spec.rb b/spec/rubocop/cop/rails/exit_spec.rb index b593fa45d4..2bafd54efe 100644 --- a/spec/rubocop/cop/rails/exit_spec.rb +++ b/spec/rubocop/cop/rails/exit_spec.rb @@ -8,7 +8,9 @@ RUBY end - it 'registers an offense for an exit! call with no receiver' do + # FIXME: `undefined method `[]' for nil` occurs Prism 0.24.0. It has been resolved in + # the development line. This will be resolved in Prism > 0.24.0 and higher releases. + it 'registers an offense for an exit! call with no receiver', broken_on: :prism do expect_offense(<<~RUBY) exit! ^^^^^ Do not use `exit` in Rails applications. diff --git a/spec/rubocop/cop/rails/index_by_spec.rb b/spec/rubocop/cop/rails/index_by_spec.rb index c3c48165c0..01afddca49 100644 --- a/spec/rubocop/cop/rails/index_by_spec.rb +++ b/spec/rubocop/cop/rails/index_by_spec.rb @@ -175,7 +175,7 @@ end end - context 'when using Ruby 2.5 or older', :ruby25 do + context 'when using Ruby 2.5 or older', :ruby25, unsupported_on: :prism do it 'does not register an offense for `to_h { ... }`' do expect_no_offenses(<<~RUBY) x.to_h { |el| [el.to_sym, el] } diff --git a/spec/rubocop/cop/rails/index_with_spec.rb b/spec/rubocop/cop/rails/index_with_spec.rb index 142d0e5377..32fb4e06ff 100644 --- a/spec/rubocop/cop/rails/index_with_spec.rb +++ b/spec/rubocop/cop/rails/index_with_spec.rb @@ -148,7 +148,7 @@ end end - context 'when using Ruby 2.5 or older', :ruby25 do + context 'when using Ruby 2.5 or older', :ruby25, unsupported_on: :prism do it 'does not register an offense for `to_h { ... }`' do expect_no_offenses(<<~RUBY) x.to_h { |el| [el, el.to_sym] } diff --git a/spec/rubocop/cop/rails/present_spec.rb b/spec/rubocop/cop/rails/present_spec.rb index a547de99a6..ebb3d3d69b 100644 --- a/spec/rubocop/cop/rails/present_spec.rb +++ b/spec/rubocop/cop/rails/present_spec.rb @@ -2,7 +2,9 @@ RSpec.describe RuboCop::Cop::Rails::Present, :config do shared_examples 'offense' do |source, correction, message| - it 'registers an offense and corrects' do + # FIXME: `undefined method `[]' for nil` occurs Prism 0.24.0. It has been resolved in + # the development line. This will be resolved in Prism > 0.24.0 and higher releases. + it 'registers an offense and corrects', broken_on: :prism do expect_offense(<<~RUBY, source: source, message: message) #{source} ^{source} #{message} diff --git a/spec/rubocop/cop/rails/root_pathname_methods_spec.rb b/spec/rubocop/cop/rails/root_pathname_methods_spec.rb index 6fd8272cc1..fdeeeba603 100644 --- a/spec/rubocop/cop/rails/root_pathname_methods_spec.rb +++ b/spec/rubocop/cop/rails/root_pathname_methods_spec.rb @@ -46,7 +46,7 @@ end end - context 'when using `Dir.glob` on Ruby 2.4 or lower', :ruby24 do + context 'when using `Dir.glob` on Ruby 2.4 or lower', :ruby24, unsupported_on: :prism do it 'does not registers an offense' do expect_no_offenses(<<~RUBY) Dir.glob(Rails.root.join('**/*.rb')) diff --git a/spec/rubocop/cop/rails/safe_navigation_spec.rb b/spec/rubocop/cop/rails/safe_navigation_spec.rb index 190ece7993..f699b147d1 100644 --- a/spec/rubocop/cop/rails/safe_navigation_spec.rb +++ b/spec/rubocop/cop/rails/safe_navigation_spec.rb @@ -28,7 +28,7 @@ it_behaves_like 'accepts', 'non try! method calls', 'join' - context 'target_ruby_version < 2.3', :ruby22 do + context 'target_ruby_version < 2.3', :ruby22, unsupported_on: :prism do it_behaves_like 'accepts', 'try! with a single parameter', 'try!(:join)' it_behaves_like 'accepts', 'try! with a multiple parameters', 'try!(:join, ",")' it_behaves_like 'accepts', 'try! with a block', 'try!(:map) { |e| e.some_method }' @@ -93,7 +93,7 @@ context 'convert try and try!' do let(:cop_config) { { 'ConvertTry' => true } } - context 'target_ruby_version < 2.3', :ruby22 do + context 'target_ruby_version < 2.3', :ruby22, unsupported_on: :prism do it_behaves_like 'accepts', 'try! with a single parameter', 'try!(:join)' it_behaves_like 'accepts', 'try! with a multiple parameters', 'try!(:join, ",")' it_behaves_like 'accepts', 'try! with a block', 'try!(:map) { |e| e.some_method }' @@ -119,7 +119,12 @@ it_behaves_like 'autocorrect', 'try! with 2 parameters', '[1, 2].try!(:join, ",")', '[1, 2]&.join(",")' it_behaves_like 'autocorrect', 'try! with multiple parameters', '[1, 2].try!(:join, bar, baz)', '[1, 2]&.join(bar, baz)' - it_behaves_like 'autocorrect', 'try! without receiver', 'try!(:join)', 'self&.join' + # FIXME: `undefined method `[]' for nil` occurs Prism 0.24.0. It has been resolved in + # the development line. This will be resolved in Prism > 0.24.0 and higher releases. + context 'skip test when parser engine is prism', broken_on: :prism do + it_behaves_like 'autocorrect', 'try! without receiver', 'try!(:join)', 'self&.join' + end + it_behaves_like 'autocorrect', 'try! with a block', ['[foo, bar].try!(:map) do |e|', ' e.some_method', diff --git a/spec/rubocop/cop/rails/save_bang_spec.rb b/spec/rubocop/cop/rails/save_bang_spec.rb index a74704889d..b19701aee1 100644 --- a/spec/rubocop/cop/rails/save_bang_spec.rb +++ b/spec/rubocop/cop/rails/save_bang_spec.rb @@ -74,7 +74,9 @@ end end - it "when using #{method} without arguments" do + # FIXME: `undefined method `[]' for nil` occurs Prism 0.24.0. It has been resolved in + # the development line. This will be resolved in Prism > 0.24.0 and higher releases. + it "when using #{method} without arguments", broken_on: :prism do expect_offense(<<~RUBY, method: method) #{method} ^{method} Use `#{method}!` instead of `#{method}` if the return value is not checked. diff --git a/spec/rubocop/cop/rails/skips_model_validations_spec.rb b/spec/rubocop/cop/rails/skips_model_validations_spec.rb index 4e430f23c4..4a19f6cca3 100644 --- a/spec/rubocop/cop/rails/skips_model_validations_spec.rb +++ b/spec/rubocop/cop/rails/skips_model_validations_spec.rb @@ -66,21 +66,27 @@ RUBY end - it "registers an offense for #{method} with `:returning` keyword argument" do + # FIXME: `undefined method `[]' for nil` occurs Prism 0.24.0. It has been resolved in + # the development line. This will be resolved in Prism > 0.24.0 and higher releases. + it "registers an offense for #{method} with `:returning` keyword argument", broken_on: :prism do expect_offense(<<~RUBY, method: method) %{method}(attributes, returning: false) ^{method} Avoid using `#{method}` because it skips validations. RUBY end - it "registers an offense for #{method} with `:unique_by` keyword argument" do + # FIXME: `undefined method `[]' for nil` occurs Prism 0.24.0. It has been resolved in + # the development line. This will be resolved in Prism > 0.24.0 and higher releases. + it "registers an offense for #{method} with `:unique_by` keyword argument", broken_on: :prism do expect_offense(<<~RUBY, method: method) %{method}(attributes, unique_by: :username) ^{method} Avoid using `#{method}` because it skips validations. RUBY end - it "registers an offense for #{method} with both `:returning` and `:unique_by` keyword arguments" do + # FIXME: `undefined method `[]' for nil` occurs Prism 0.24.0. It has been resolved in + # the development line. This will be resolved in Prism > 0.24.0 and higher releases. + it "registers an offense for #{method} with both `:returning` and `:unique_by` keyword arguments", broken_on: :prism do # rubocop:disable Layout/LineLength expect_offense(<<~RUBY, method: method) %{method}(attributes, returning: false, unique_by: :username) ^{method} Avoid using `#{method}` because it skips validations. diff --git a/spec/rubocop/cop/rails/strip_heredoc_spec.rb b/spec/rubocop/cop/rails/strip_heredoc_spec.rb index b965a82fa9..f502b49a86 100644 --- a/spec/rubocop/cop/rails/strip_heredoc_spec.rb +++ b/spec/rubocop/cop/rails/strip_heredoc_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true RSpec.describe RuboCop::Cop::Rails::StripHeredoc, :config do - context 'Ruby <= 2.2', :ruby22 do + context 'Ruby <= 2.2', :ruby22, unsupported_on: :prism do it 'does not register an offense when using `strip_heredoc`' do expect_no_offenses(<<~RUBY) <<-EOS.strip_heredoc diff --git a/spec/rubocop/cop/rails/where_exists_spec.rb b/spec/rubocop/cop/rails/where_exists_spec.rb index 1fade2409c..d47b29c460 100644 --- a/spec/rubocop/cop/rails/where_exists_spec.rb +++ b/spec/rubocop/cop/rails/where_exists_spec.rb @@ -59,7 +59,9 @@ RUBY end - it 'registers an offense when using implicit receiver and arg' do + # FIXME: `undefined method `[]' for nil` occurs Prism 0.24.0. It has been resolved in + # the development line. This will be resolved in Prism > 0.24.0 and higher releases. + it 'registers an offense when using implicit receiver and arg', broken_on: :prism do expect_offense(<<~RUBY) where('name = ?', 'john').exists? ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer `exists?(['name = ?', 'john'])` over `where('name = ?', 'john').exists?`. @@ -153,7 +155,9 @@ RUBY end - it 'registers an offense when using implicit receiver and arg' do + # FIXME: `undefined method `[]' for nil` occurs Prism 0.24.0. It has been resolved in + # the development line. This will be resolved in Prism > 0.24.0 and higher releases. + it 'registers an offense when using implicit receiver and arg', broken_on: :prism do expect_offense(<<~RUBY) exists?('name = ?', 'john') ^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer `where('name = ?', 'john').exists?` over `exists?('name = ?', 'john')`. diff --git a/spec/rubocop/rails/schema_loader_spec.rb b/spec/rubocop/rails/schema_loader_spec.rb index 4dfd7c0743..c7dc4fc378 100644 --- a/spec/rubocop/rails/schema_loader_spec.rb +++ b/spec/rubocop/rails/schema_loader_spec.rb @@ -2,8 +2,11 @@ RSpec.describe RuboCop::Rails::SchemaLoader do describe '.load' do - require 'parser/ruby27' - let(:target_ruby_version) { 2.7 } + let(:target_ruby_version) do + # The minimum version Prism can parse is 3.3. + ENV['PARSER_ENGINE'] == 'parser_prism' ? 3.3 : RuboCop::TargetRuby::DEFAULT_VERSION + end + let(:parser_engine) { ENV.fetch('PARSER_ENGINE', :parser_whitequark).to_sym } around do |example| described_class.reset! @@ -13,13 +16,13 @@ context 'without schema.rb' do it do - expect(described_class.load(target_ruby_version).nil?).to be(true) + expect(described_class.load(target_ruby_version, parser_engine).nil?).to be(true) end end context 'with schema.rb' do subject(:loaded_schema) do - described_class.load(target_ruby_version) + described_class.load(target_ruby_version, parser_engine) end let(:rails_root) { Pathname(Dir.mktmpdir) } diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index ad47cf804b..0093673020 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -19,6 +19,11 @@ config.shared_context_metadata_behavior = :apply_to_host_groups config.filter_run_when_matching :focus + config.filter_run_excluding broken_on: :prism if ENV['PARSER_ENGINE'] == 'parser_prism' + + # Prism supports Ruby 3.3+ parsing. + config.filter_run_excluding unsupported_on: :prism if ENV['PARSER_ENGINE'] == 'parser_prism' + config.example_status_persistence_file_path = 'spec/examples.txt' config.disable_monkey_patching! config.warnings = true