Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Gem::Requirement.new in required_ruby_version raises BadRequirementError when version is read from elsewhere #12579

Closed
CamJN opened this issue Dec 28, 2023 · 10 comments
Labels

Comments

@CamJN
Copy link

CamJN commented Dec 28, 2023

Expected behavior

Rubocop should evaluate the gemspec file rather than try to parse the unevaluated Ruby.

Actual behavior

rake aborted!
Gem::Requirement::BadRequirementError: Illformed requirement [">= \#{ruby_version}.0"] (Gem::Requirement::BadRequirementError)

      raise BadRequirementError, "Illformed requirement [#{obj.inspect}]"
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/Users/runner/work/getargv.rb/getargv.rb/getargv_ruby/vendor/bundle/ruby/3.3.0/gems/rubocop-1.59.0/lib/rubocop/target_ruby.rb:216:in `new'
/Users/runner/work/getargv.rb/getargv.rb/getargv_ruby/vendor/bundle/ruby/3.3.0/gems/rubocop-1.59.0/lib/rubocop/target_ruby.rb:216:in `find_default_minimal_known_ruby'
/Users/runner/work/getargv.rb/getargv.rb/getargv_ruby/vendor/bundle/ruby/3.3.0/gems/rubocop-1.59.0/lib/rubocop/target_ruby.rb:178:in `find_version'
/Users/runner/work/getargv.rb/getargv.rb/getargv_ruby/vendor/bundle/ruby/3.3.0/gems/rubocop-1.59.0/lib/rubocop/target_ruby.rb:29:in `initialize'
/Users/runner/work/getargv.rb/getargv.rb/getargv_ruby/vendor/bundle/ruby/3.3.0/gems/rubocop-1.59.0/lib/rubocop/target_ruby.rb:257:in `new'
/Users/runner/work/getargv.rb/getargv.rb/getargv_ruby/vendor/bundle/ruby/3.3.0/gems/rubocop-1.59.0/lib/rubocop/target_ruby.rb:257:in `block in source'
/Users/runner/work/getargv.rb/getargv.rb/getargv_ruby/vendor/bundle/ruby/3.3.0/gems/rubocop-1.59.0/lib/rubocop/target_ruby.rb:257:in `each'
/Users/runner/work/getargv.rb/getargv.rb/getargv_ruby/vendor/bundle/ruby/3.3.0/gems/standard-1.33.0/lib/standard/creates_config_store.rb:19:in `call'
/Users/runner/work/getargv.rb/getargv.rb/getargv_ruby/vendor/bundle/ruby/3.3.0/gems/standard-1.33.0/lib/standard/builds_config.rb:32:in `call'
/Users/runner/work/getargv.rb/getargv.rb/getargv_ruby/vendor/bundle/ruby/3.3.0/gems/standard-1.33.0/lib/standard/cli.rb:13:in `run'
/Users/runner/work/getargv.rb/getargv.rb/getargv_ruby/vendor/bundle/ruby/3.3.0/gems/standard-1.33.0/lib/standard/rake.rb:17:in `block in <top (required)>'
/Users/runner/work/getargv.rb/getargv.rb/getargv_ruby/vendor/bundle/ruby/3.3.0/gems/rake-13.1.0/exe/rake:27:in `<top (required)>'
/Users/runner/hostedtoolcache/Ruby/3.3.0/x64/bin/bundle:25:in `load'
/Users/runner/hostedtoolcache/Ruby/3.3.0/x64/bin/bundle:25:in `<main>'
Tasks: TOP => default => standard
(See full trace by running task with --trace)
Error: Process completed with exit code 1.

Steps to reproduce the problem

context 'when file contains `required_ruby_version` as a interpolated string' do
  let(:base_path) { configuration.base_dir_for_path_parameters }
  let(:gemspec_file_path) { File.join(base_path, 'example.gemspec') }
  it 'sets target_ruby to the minimal version satisfying the requirements' do
    content = <<~HEREDOC
      ruby_version = "2.6"
      Gem::Specification.new do |s|
        s.name = 'test'
        s.required_ruby_version = ">= #{ruby_version}.0"
        s.licenses = ['MIT']
      end
    HEREDOC
    create_file(gemspec_file_path, content)
    expect(target_ruby.version).to eq 2.6
  end
end

RuboCop version

$ bundle exec rubocop -V
1.59.0 (using Parser 3.2.2.4, rubocop-ast 1.30.0, running on ruby 3.3.0) [x86_64-darwin23]
@koic koic added the bug label Dec 29, 2023
@capripot
Copy link

capripot commented Mar 2, 2024

With Ruby 2.7.8 / bundler 2.4.22, it was still working

> bundle exec rubucop -V
1.60.2 (using Parser 3.3.0.5, rubocop-ast 1.31.1, running on ruby 2.7.8) [arm64-darwin22]
  - rubocop-performance 1.20.2
  - rubocop-rake 0.6.0
  - rubocop-rspec 2.26.1

With Ruby 2.7.8 / bundler 2.4.22, these version updates

Using rubocop 1.61.0 (was 1.60.2)
Bundle updated!

it stopped working with the following error:

Illformed requirement [">= \#{File.read(File.expand_path(\".ruby-version\", __dir__)).strip}"]

@Earlopain
Copy link
Contributor

Earlopain commented Mar 6, 2024

Can you check if this is still an issue on 1.62.0? I suspect #12732 has fixed. Also see #12744.

@davidrunger
Copy link
Contributor

davidrunger commented Mar 7, 2024

Thank you for providing that pointer, @Earlopain . You're correct that my PR #12732 is definitely relevant to this issue.

I think that #12732 does indeed essentially fix the original problem raised by @CamJN . It does avoid the exception that @CamJN reported. That being said, though, the spec provided by @CamJN fails, even on latest master, including #12732.

First of all, we should slightly tweak @CamJN's spec, to this:

context 'when file contains `required_ruby_version` as an interpolated string' do
  it 'sets target_ruby to the minimal version satisfying the requirements' do
    content = <<~HEREDOC
      ruby_version = "2.6"
      Gem::Specification.new do |s|
        s.name = 'test'
        s.required_ruby_version = ">= \#{ruby_version}.0"
        s.licenses = ['MIT']
      end
    HEREDOC
    create_file(gemspec_file_path, content)
    expect(target_ruby.version).to eq 2.6
  end
end

The most notable change there is that I've added a backslash to escape the interpolation within the heredoc: \#{ruby_version}. Otherwise, we actually interpolate the default ruby_version from here into the spec's example gemspec:

let(:ruby_version) do
# The minimum version Prism can parse is 3.3.
ENV['PARSER_ENGINE'] == 'parser_prism' ? 3.3 : RuboCop::TargetRuby::DEFAULT_VERSION
end

On latest master (6a4cf81), that modified spec fails with this error:

  1) RuboCop::TargetRuby when TargetRubyVersion is not set when gemspec file is present when file contains `required_ruby_version` as an interpolated string sets target_ruby to the minimal version satisfying the requirements
     Failure/Error: expect(target_ruby.version).to eq 2.6

       expected: 2.6
            got: 2.7

This is because the current RuboCop code doesn't determine a target Ruby version based on ruby_version = "2.6" and s.required_ruby_version = ">= #{ruby_version}.0" in the gemspec and instead returns RuboCop::TargetRuby::DEFAULT_VERSION, which is currently 2.7. In my opinion, I think that is okay. I don't know that it's reasonably possible for RuboCop to actually parse the gemspec, in order to figure out the target Ruby version. Personally, I think that it's fine to fall back to trying to use the other finder classes defined within RuboCop::TargetRuby to try to find a target Ruby version (such as from the RuboCop config or in a .ruby-version file), and, if a target Ruby version cannot be found in some other way, to use the default target Ruby version. I think the important thing is that the error that @CamJN had been seeing is indeed fixed by #12732.

@davidrunger
Copy link
Contributor

davidrunger commented Mar 7, 2024

Regarding the error mentioned by @capripot , unfortunately, that is not fixed by #12732, because @capripot's example has an array wrapping the Ruby version specification in the gemspec, which is not something that #12732 handles.

This spec fails on latest master (6a4cf81):

context 'when file contains `required_ruby_version` as an interpolated string in an array' do
  it 'uses the default target ruby version' do
    content = <<~HEREDOC
      Gem::Specification.new do |s|
        s.name = 'test'
        s.required_ruby_version = [">= \#{File.read(File.expand_path(".ruby-version", __dir__)).strip}"]
        s.licenses = ['MIT']
      end
    HEREDOC
    create_file(gemspec_file_path, content)
    expect(target_ruby.version).to eq default_version
  end
end

with this error

  1) RuboCop::TargetRuby when TargetRubyVersion is not set when gemspec file is present when file contains `required_ruby_version` as an interpolated string in an array uses the default target ruby version
     Failure/Error: requirement = Gem::Requirement.new(version)

     Gem::Requirement::BadRequirementError:
       Illformed requirement [">= \#{File.read(File.expand_path(\".ruby-version\", __dir__)).strip}"]
     # ./lib/rubocop/target_ruby.rb:123:in `new'
     # ./lib/rubocop/target_ruby.rb:123:in `find_default_minimal_known_ruby'
     # ./lib/rubocop/target_ruby.rb:83:in `find_version'
     # ./lib/rubocop/target_ruby.rb:29:in `initialize'
     # ./lib/rubocop/target_ruby.rb:259:in `new'
[...]

However, that spec will pass if we remove the array wrapping around the s.required_ruby_version value. So, @capripot , I believe that removing the array wrapping from around the required_ruby_version value in your gemspec and updating to RuboCop 1.62.0 (which includes #12732) should, at least, serve as a workaround for the issue that you're facing.

@Earlopain
Copy link
Contributor

Earlopain commented Mar 7, 2024

Ah right, my bad, I only checked the example from OP. Good point with the interpolation happening, I didn't catch that. Do you want to make a PR for this, since you've already fixed that other case?

@davidrunger
Copy link
Contributor

@Earlopain : Yes, I'm working up a PR right now. Thanks for asking!

bbatsov pushed a commit that referenced this issue Mar 7, 2024
This fixes an issue mentioned in [this
comment](#12579 (comment))
on issue #12579. (I believe that the main/original issue mentioned in
issue #12579 has already been fixed by #12732.)

This change is a related/similar followup to #12732 (5026393). That PR
avoided an exception that was occurring in the
`RuboCop::TargetRuby::GemspecFile` target Ruby finder when a project's
gemspec determines the value of `required_ruby_version` in some dynamic
way (such as by reading from a `.ruby-version` file).

However, #12732 did not handle a case where the dynamic value for the
gemspec's `required_ruby_version` is wrapped within an array, and, in
such a case, currently, an exception like the following will occur:

```
Gem::Requirement::BadRequirementError:
  Illformed requirement [">= \#{File.read('.ruby-version').rstrip}"]
  ./lib/rubocop/target_ruby.rb:123:in `new'
  ./lib/rubocop/target_ruby.rb:123:in `find_default_minimal_known_ruby'
  ./lib/rubocop/target_ruby.rb:83:in `find_version'
  ./lib/rubocop/target_ruby.rb:29:in `initialize'
  ./lib/rubocop/target_ruby.rb:259:in `new'
```

(As with the scenario that _was_ fixed by #12732, I believe that this is
essentially a "latent" issue that is now occurring in more projects than
previously, due to #12645 having increased the precedence of the
`GemspecFile` target Ruby finder in the `RuboCop::TargetRuby::SOURCES`
list.)

This change avoids this exception by only attempting to parse a target
Ruby version from an array value for a gemspec's `required_ruby_version`
if every value in the array is a string. When the values within the
`required_ruby_version` array are _not_ all strings, now, rather than
raising an unhandled exception, the `GemspecFile` finder will return
`nil` for the target Ruby version, and so we will continue on to the
other TargetRuby finder classes, in order to find a target Ruby version.
@CamJN
Copy link
Author

CamJN commented Mar 12, 2024

After updating my checkout of this repo to afddf65 which was HEAD at the time, and re-adding my test (with the #escaped, thanks @davidrunger) the test still fails.

      context 'when file contains `required_ruby_version` as a interpolated string' do
        let(:base_path) { configuration.base_dir_for_path_parameters }
        let(:gemspec_file_path) { File.join(base_path, 'example.gemspec') }

        it 'sets target_ruby to the minimal version satisfying the requirements' do
          content = <<~HEREDOC
            ruby_version = "2.6"
            Gem::Specification.new do |s|
              s.name = 'test'
              s.required_ruby_version = ">= \#{ruby_version}.0"
              s.licenses = ['MIT']
            end
          HEREDOC

          create_file(gemspec_file_path, content)
          expect(target_ruby.version).to eq 2.6
        end
      end

Here's the rspec output.

 $ bundle exec rake spec SPEC=./spec/rubocop/target_ruby_spec.rb:96 SPEC_OPTS='-e "as a interpolated string"'
Starting test-queue master (/tmp/test_queue_46518_6040.sock)

==> Summary (12 workers in 6.9118s)

    [ 1]                              0 examples, 0 failures        59 suites in 6.7476s      (pid 46544 exit 0 )
    [ 2]                              0 examples, 0 failures        53 suites in 6.8023s      (pid 46545 exit 0 )
    [ 3]                              0 examples, 0 failures        61 suites in 6.8014s      (pid 46546 exit 0 )
    [ 4]                              0 examples, 0 failures        46 suites in 6.7997s      (pid 46547 exit 0 )
    [ 5]                              0 examples, 0 failures        55 suites in 6.7950s      (pid 46548 exit 0 )
    [ 6]                              0 examples, 0 failures        50 suites in 6.7902s      (pid 46549 exit 0 )
    [ 7]                              0 examples, 0 failures        56 suites in 6.7860s      (pid 46550 exit 0 )
    [ 8]                              0 examples, 0 failures        75 suites in 6.7825s      (pid 46551 exit 0 )
    [ 9]                                1 example, 1 failure        39 suites in 6.8649s      (pid 46552 exit 1 )
    [10]                              0 examples, 0 failures        52 suites in 6.8592s      (pid 46553 exit 0 )
    [11]                              0 examples, 0 failures        68 suites in 6.8551s      (pid 46554 exit 0 )
    [12]                              0 examples, 0 failures        54 suites in 6.8403s      (pid 46555 exit 0 )

==> Failures

  1) RuboCop::TargetRuby when TargetRubyVersion is not set when gemspec file is present when file contains `required_ruby_version` as a interpolated string sets target_ruby to the minimal version satisfying the requirements
     Failure/Error: expect(target_ruby.version).to eq 2.6
     
       expected: 2.6
            got: 2.7
     
       (compared using ==)
     # ./spec/rubocop/target_ruby_spec.rb:96:in `block (5 levels) in <top (required)>'
     # ./lib/rubocop/rspec/shared_contexts.rb:44:in `block (2 levels) in <top (required)>'
     # ./lib/rubocop/rspec/shared_contexts.rb:29:in `block (4 levels) in <top (required)>'
     # ./lib/rubocop/rspec/shared_contexts.rb:29:in `chdir'
     # ./lib/rubocop/rspec/shared_contexts.rb:29:in `block (3 levels) in <top (required)>'
     # ./lib/rubocop/rspec/shared_contexts.rb:7:in `block (2 levels) in <top (required)>'
     # tasks/spec_runner.rake:83:in `run_worker'
     # tasks/spec_runner.rake:38:in `block in run_specs'
     # tasks/spec_runner.rake:52:in `with_encoding'
     # tasks/spec_runner.rake:36:in `run_specs'
     # tasks/spec_runner.rake:164:in `block in <top (required)>'


==> Failed Examples

rspec  # RuboCop::TargetRuby when TargetRubyVersion is not set when gemspec file is present when file contains `required_ruby_version` as a interpolated string sets target_ruby to the minimal version satisfying the requirements

@davidrunger
Copy link
Contributor

davidrunger commented Mar 12, 2024

@CamJN Thanks for re-running your spec against the latest HEAD.

The spec failure that you are seeing now is different than the one that you were seeing before, and that new failure is expected. I mentioned why in my previous comment:

[The spec fails (getting 2.7 rather than 2.6)] because the current RuboCop code doesn't determine a target Ruby version based on ruby_version = "2.6" and s.required_ruby_version = ">= #{ruby_version}.0" in the gemspec and instead returns RuboCop::TargetRuby::DEFAULT_VERSION, which is currently 2.7. In my opinion, I think that is okay. I don't know that it's reasonably possible for RuboCop to actually parse the gemspec, in order to figure out the target Ruby version.

If you want to specify a RuboCop target Ruby version of 2.6 in your project, then you should be able to do so, via one of the means of specifying a target Ruby version other than via the gemspec. I recently (in #12756) tweaked/clarified the relevant documentation; hopefully that is helpful!

@CamJN
Copy link
Author

CamJN commented Mar 12, 2024

Fair enough, I do think trying to parse the gemspec file rather than evaluate it is fighting against what the file is (a ruby program, not a config file), but it is very much the rubocop way of thinking about things to try to parse rather than evaluate.

@CamJN CamJN closed this as completed Mar 12, 2024
@davidrunger
Copy link
Contributor

davidrunger commented Mar 12, 2024

@CamJN : Yeah, I hear your point, and, while working on this, I did sort of wonder whether it would be possible to somehow evaluate rather than parse the gemspec file. But that might have issues of its own. For example, it's conceivable that (since it is a Ruby program, as you mentioned) actually evaluating the gemspec might produce some sort of side effect, such as writing a file on the local machine, or really anything else (such as making a request to a remote server). In this sense, RuboCop might (perhaps rightly) consider that it would be overstepping its bounds to actually evaluate the gemspec, in order to try to extract a target Ruby version from it. So, in that paradigm, we are confined to merely trying to parse the gemspec file. Thus, somewhat unfortunately, but maybe for the best, overall, we thereby limit ourselves to only being able to parse some manually specified, finite subset of the infinite possible ways that the gemspec could be written.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

5 participants