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

Support Bundler-like namespaced feature on require config #10845

Merged
merged 1 commit into from Aug 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog/change_support_namespaced_feature_on_require.md
@@ -0,0 +1 @@
* [#10845](https://github.com/rubocop/rubocop/pull/10845): Support Bundler-like namespaced feature on require config. ([@r7kamura][])
3 changes: 3 additions & 0 deletions docs/modules/ROOT/pages/extensions.adoc
Expand Up @@ -19,6 +19,9 @@ NOTE: The paths are directly passed to `Kernel.require`. If your
extension file is not in `$LOAD_PATH`, you need to specify the path as
relative path prefixed with `./` explicitly or absolute path. Paths
starting with a `.` are resolved relative to `.rubocop.yml`.
If a path containing `-` is given, it will be used as is, but if we
cannot find the file to load, we will replace `-` with `/` and try it
again as when Bundler loads gems.

== Extension Suggestions

Expand Down
1 change: 1 addition & 0 deletions lib/rubocop.rb
Expand Up @@ -690,6 +690,7 @@
require_relative 'rubocop/config_obsoletion'
require_relative 'rubocop/config_store'
require_relative 'rubocop/config_validator'
require_relative 'rubocop/feature_loader'
require_relative 'rubocop/lockfile'
require_relative 'rubocop/target_finder'
require_relative 'rubocop/directive_comment'
Expand Down
6 changes: 1 addition & 5 deletions lib/rubocop/config_loader_resolver.rb
Expand Up @@ -11,11 +11,7 @@ def resolve_requires(path, hash)
config_dir = File.dirname(path)
hash.delete('require').tap do |loaded_features|
Array(loaded_features).each do |feature|
if feature.start_with?('.')
require(File.join(config_dir, feature))
else
require(feature)
end
FeatureLoader.load(config_directory_path: config_dir, feature: feature)
end
end
end
Expand Down
88 changes: 88 additions & 0 deletions lib/rubocop/feature_loader.rb
@@ -0,0 +1,88 @@
# frozen_string_literal: true

module RuboCop
# This class handles loading files (a.k.a. features in Ruby) specified
# by `--require` command line option and `require` directive in the config.
#
# Normally, the given string is directly passed to `require`. If a string
# beginning with `.` is given, it is assumed to be relative to the given
# directory.
#
# If a string containing `-` is given, it will be used as is, but if we
# cannot find the file to load, we will replace `-` with `/` and try it
# again as when Bundler loads gems.
#
# @api private
class FeatureLoader
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This class can probably use more documentation itself.

class << self
# @param [String] config_directory_path
# @param [String] feature
def load(config_directory_path:, feature:)
new(config_directory_path: config_directory_path, feature: feature).load
end
end

# @param [String] config_directory_path
# @param [String] feature
def initialize(config_directory_path:, feature:)
@config_directory_path = config_directory_path
@feature = feature
end

def load
::Kernel.require(target)
rescue ::LoadError => e
raise if e.path != target

begin
::Kernel.require(namespaced_target)
rescue ::LoadError => error_for_namespaced_target
raise e if error_for_namespaced_target.path == namespaced_target

raise error_for_namespaced_target
end
end

private

# @return [String]
def namespaced_feature
@feature.tr('-', '/')
end

# @return [String]
def namespaced_target
if relative?
relative(namespaced_feature)
else
namespaced_feature
end
end

# @param [String]
# @return [String]
def relative(feature)
::File.join(@config_directory_path, feature)
end

# @return [Boolean]
def relative?
@feature.start_with?('.')
end

# @param [LoadError] error
# @return [Boolean]
def seems_cannot_load_such_file_error?(error)
error.path == target
end

# @return [String]
def target
if relative?
relative(@feature)
else
@feature
end
end
end
end
97 changes: 97 additions & 0 deletions spec/rubocop/feature_loader_spec.rb
@@ -0,0 +1,97 @@
# frozen_string_literal: true

RSpec.describe RuboCop::FeatureLoader do
describe '.load' do
subject(:load) do
described_class.load(config_directory_path: config_directory_path, feature: feature)
end

let(:config_directory_path) do
'path-to-config'
end

let(:feature) do
'feature'
end

context 'with normally lodable feature' do
before do
allow(Kernel).to receive(:require)
end

it 'loads it normally' do
expect(Kernel).to receive(:require).with('feature')
load
end
end

context 'with dot-prefixed lodable feature' do
before do
allow(Kernel).to receive(:require)
end

let(:feature) do
'./path/to/feature'
end

it 'loads it as relative path' do
expect(Kernel).to receive(:require).with('path-to-config/./path/to/feature')
load
end
end

context 'with namespaced feature' do
before do
allow(Kernel).to receive(:require).with('feature-foo').and_call_original
allow(Kernel).to receive(:require).with('feature/foo')
end

let(:feature) do
'feature-foo'
end

it 'loads it as namespaced feature' do
expect(Kernel).to receive(:require).with('feature/foo')
load
end
end

context 'with dot-prefixed namespaced feature' do
before do
allow(Kernel).to receive(:require).with('path-to-config/./feature-foo').and_call_original
allow(Kernel).to receive(:require).with('path-to-config/./feature/foo')
end

let(:feature) do
'./feature-foo'
end

it 'loads it as namespaced feature' do
expect(Kernel).to receive(:require).with('path-to-config/./feature/foo')
load
end
end

context 'with unexpected LoadError from require' do
before do
allow(Kernel).to receive(:require).and_raise(LoadError)
end

it 'raises LoadError' do
expect { load }.to raise_error(LoadError)
end
end

context 'with unloadable namespaced feature' do
let(:feature) do
'feature-foo'
end

# In normal Ruby, the message starts with "cannot load such file",
# but in JRuby it seems to start with "no such file to load".
it 'raises LoadError with preferred message' do
expect { load }.to raise_error(LoadError, /feature-foo/)
end
end
end
end