Skip to content

Commit

Permalink
Support asdf's .tool-versions file
Browse files Browse the repository at this point in the history
Augments the TargetRuby class with a ToolVersionsFile class, giving
asdf [1] users the option to specify the target Ruby syntax through its
default config file .tool-versions [2].

The file detection and content matching logic is the same whether
.tool-versions or .ruby-version is in use. The only differences between
the two cases are the file name and pattern used to extract the version
number. So, have ToolVersionsFile subclass RubyVersionFile, and modify
find_version to get file name and pattern from available instance
methods.

Since the author is in the process of replacing rbenv with asdf in their
dev environment, it felt sensible to stipulate that ToolVersionsFile
would be more authoritative than RubyVersionFile (but still less than
RuboCopConfig).

In the absence of a .tool-versions file, or when it doesn't happen to
specify a ruby version, revert to the original logic and allow both
rbenv and asdf (in asdf-ruby legacy mode) to be used.

[1]
asdf is a plugin-based runtime installer/manager - think RVM or
rbenv, but not limited to Ruby: github.com/asdf-vm/asdf

Ruby support for asdf is provided by the asdf-ruby plugin:
github.com/asdf-vm/asdf-ruby

[2]
Each line in .tool-versions identifies the runtimes and the version
needed. For example, here's the contents of the .tool-versions in the
author's home dir in their personal laptop:

```
nodejs 14.9.0
ruby 3.0.0
yarn 1.22.5
```
  • Loading branch information
noon-ng authored and bbatsov committed Jan 4, 2021
1 parent c460feb commit 1b8388f
Show file tree
Hide file tree
Showing 4 changed files with 88 additions and 18 deletions.
1 change: 1 addition & 0 deletions changelog/new_support_asdfs_toolversions_file.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* [#9319](https://github.com/rubocop-hq/rubocop/pull/9319): Support asdf's .tool-versions file. ([@noon-ng][])
13 changes: 7 additions & 6 deletions config/default.yml
Original file line number Diff line number Diff line change
Expand Up @@ -132,12 +132,13 @@ AllCops:
# If a value is specified for TargetRubyVersion then it is used. Acceptable
# values are specificed as a float (i.e. 2.5); the teeny version of Ruby
# should not be included. If the project specifies a Ruby version in the
# .ruby-version file, Gemfile or gems.rb file, RuboCop will try to determine
# the desired version of Ruby by inspecting the .ruby-version file first,
# followed by the Gemfile.lock or gems.locked file. (Although the Ruby version
# is specified in the Gemfile or gems.rb file, RuboCop reads the final value
# from the lock file.) If the Ruby version is still unresolved, RuboCop will
# use the oldest officially supported Ruby version (currently Ruby 2.4).
# .tool-versions or .ruby-version files, Gemfile or gems.rb file, RuboCop will
# try to determine the desired version of Ruby by inspecting the
# .tool-versions file first, then .ruby-version, followed by the Gemfile.lock
# or gems.locked file. (Although the Ruby version is specified in the Gemfile
# or gems.rb file, RuboCop reads the final value from the lock file.) If the
# Ruby version is still unresolved, RuboCop will use the oldest officially
# supported Ruby version (currently Ruby 2.4).
TargetRubyVersion: ~
# Determines if a notification for extension libraries should be shown when
# rubocop is run. Keys are the name of the extension, and values are an array
Expand Down
58 changes: 47 additions & 11 deletions lib/rubocop/target_ruby.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,33 +44,61 @@ def find_version
# The target ruby version may be found in a .ruby-version file.
# @api private
class RubyVersionFile < Source
FILENAME = '.ruby-version'
RUBY_VERSION_FILENAME = '.ruby-version'
RUBY_VERSION_PATTERN = /\A(?:ruby-)?(?<version>\d+\.\d+)/.freeze

def name
"`#{FILENAME}`"
"`#{RUBY_VERSION_FILENAME}`"
end

private

def filename
RUBY_VERSION_FILENAME
end

def pattern
RUBY_VERSION_PATTERN
end

def find_version
file = ruby_version_file
file = version_file
return unless file && File.file?(file)

# rubocop:disable Lint/MixedRegexpCaptureTypes
# `(ruby-)` is not a capture type.
File.read(file).match(/\A(ruby-)?(?<version>\d+\.\d+)/) do |md|
# rubocop:enable Lint/MixedRegexpCaptureTypes
File.read(file).match(pattern) do |md|
md[:version].to_f
end
end

def ruby_version_file
@ruby_version_file ||=
@config.find_file_upwards(FILENAME,
def version_file
@version_file ||=
@config.find_file_upwards(filename,
@config.base_dir_for_path_parameters)
end
end

# The target ruby version may be found in a .tool-versions file, in a line
# starting with `ruby`.
# @api private
class ToolVersionsFile < RubyVersionFile
TOOL_VERSIONS_FILENAME = '.tool-versions'
TOOL_VERSIONS_PATTERN = /\Aruby (?:ruby-)?(?<version>\d+\.\d+)/.freeze

def name
"`#{TOOL_VERSIONS_FILENAME}`"
end

private

def filename
TOOL_VERSIONS_FILENAME
end

def pattern
TOOL_VERSIONS_PATTERN
end
end

# The lock file of Bundler may identify the target ruby version.
# @api private
class BundlerLockFile < Source
Expand Down Expand Up @@ -194,7 +222,15 @@ def self.supported_versions
KNOWN_RUBIES
end

SOURCES = [RuboCopConfig, RubyVersionFile, BundlerLockFile, GemspecFile, Default].freeze
SOURCES = [
RuboCopConfig,
RubyVersionFile,
ToolVersionsFile,
BundlerLockFile,
GemspecFile,
Default
].freeze

private_constant :SOURCES

def initialize(config)
Expand Down
34 changes: 33 additions & 1 deletion spec/rubocop/target_ruby_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -96,13 +96,45 @@
end
end

it 'does not read Gemfile.lock or gems.locked' do
it 'does not read .tool-versions, Gemfile.lock or gems.locked' do
expect(File).not_to receive(:file?).with('.tool-versions')
expect(File).not_to receive(:file?).with('Gemfile')
expect(File).not_to receive(:file?).with('gems.locked')
target_ruby.version
end
end

context 'when .tool-versions is present' do
before do
dir = configuration.base_dir_for_path_parameters
create_file(File.join(dir, '.tool-versions'), tool_versions)
end

context 'when .tool-versions contains a ruby version' do
let(:tool_versions) { ['ruby 3.0.0', 'nodejs 14.9.0'] }
let(:ruby_version_to_f) { 3.0 }

it 'reads it to determine the target ruby version' do
expect(target_ruby.version).to eq ruby_version_to_f
end

it 'does not read Gemfile.lock, gems.locked' do
expect(File).not_to receive(:file?).with(/Gemfile/)
expect(File).not_to receive(:file?).with(/gems\.locked/)
target_ruby.version
end
end

context 'when .tool-versions does not contain a ruby version' do
let(:tool_versions) { ['nodejs 14.9.0'] }
let(:ruby_version_to_f) { 3.0 }

it 'uses the default ruby version' do
expect(target_ruby.version).to eq default_version
end
end
end

context 'when .ruby-version is not present' do
['Gemfile.lock', 'gems.locked'].each do |file_name|
context "and #{file_name} exists" do
Expand Down

0 comments on commit 1b8388f

Please sign in to comment.