Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions lib/standard/creates_config_store/detects_lint_roller_plugins.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
class Standard::CreatesConfigStore
class DetectsLintRollerPlugins
def initialize
@determines_class_constant = Standard::Plugin::DeterminesClassConstant.new
end

def call(loaded_gems = Gem.loaded_specs.values)
lint_roller_plugins = []

loaded_gems.each do |gem_spec|
next unless gem_spec.metadata.key?("default_lint_roller_plugin")

plugin_class_name = gem_spec.metadata["default_lint_roller_plugin"]

begin
require gem_spec.name

plugin_class = @determines_class_constant.call(gem_spec.name, {
"plugin_class_name" => plugin_class_name
})

plugin_instance = plugin_class.new({})

lint_roller_plugins << plugin_instance
rescue LoadError => e
warn "[Standard] Failed to load gem '#{gem_spec.name}': #{e.message}" if ENV["DEBUG"]
next
rescue => e
warn "[Standard] Failed to load lint_roller plugin from '#{gem_spec.name}': #{e.message}" if ENV["DEBUG"]
next
end
end

lint_roller_plugins
end
end
end
67 changes: 65 additions & 2 deletions lib/standard/creates_config_store/merges_user_config_extensions.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
require_relative "../file_finder"
require_relative "../plugin"
require_relative "detects_lint_roller_plugins"

class Standard::CreatesConfigStore
class MergesUserConfigExtensions
Expand All @@ -23,15 +24,77 @@ def rules(context)

def initialize
@merges_plugins_into_rubocop_config = Standard::Plugin::MergesPluginsIntoRubocopConfig.new
@detects_lint_roller_plugins = DetectsLintRollerPlugins.new
@creates_runner_context = Standard::Plugin::CreatesRunnerContext.new
end

def call(options_config, standard_config)
return unless standard_config[:extend_config]&.any?

plugins = standard_config[:extend_config].map { |path|
static_plugins = standard_config[:extend_config].map { |path|
ExtendConfigPlugin.new(path)
}
@merges_plugins_into_rubocop_config.call(options_config, standard_config, plugins, permit_merging: false)

needs_lint_roller_detection = should_detect_lint_roller_plugins?(standard_config[:extend_config], standard_config)

@merges_plugins_into_rubocop_config.call(options_config, standard_config, static_plugins, permit_merging: needs_lint_roller_detection)

# Only auto-detect when extend_config contains lint_roller cops to preserve backward compatibility
all_lint_roller_plugins = if needs_lint_roller_detection
@detects_lint_roller_plugins.call(Gem.loaded_specs.values)
else
[]
end

if all_lint_roller_plugins.any?
already_configured_cops = options_config.to_h.keys

runner_context = @creates_runner_context.call(standard_config)
new_plugins = all_lint_roller_plugins.select do |plugin|
rules = plugin.rules(runner_context)
if rules.type == :object
rules.value.keys.any? { |cop| !already_configured_cops.include?(cop) }
elsif rules.type == :path && rules.config_format == :rubocop
begin
plugin_config = YAML.load_file(rules.value) || {}
plugin_cops = plugin_config.keys.select { |key| key.include?("/") }
plugin_cops.none? { |cop| already_configured_cops.include?(cop) }
rescue
true
end
else
true
end
end

if new_plugins.any?
@merges_plugins_into_rubocop_config.call(options_config, standard_config, new_plugins, permit_merging: false)
end
end
end

private

def should_detect_lint_roller_plugins?(extend_config_paths, standard_config)
return true if standard_config[:plugins]&.any?

extend_config_paths.any? do |path|
yaml_path = Standard::FileFinder.new.call(path, Dir.pwd)
next false unless yaml_path && File.exist?(yaml_path)

begin
config_content = YAML.load_file(yaml_path) || {}

has_explicit_plugins = config_content.key?("plugins")
has_namespaced_cops = config_content.keys.any? { |key|
key.include?("/") && !key.start_with?("AllCops")
}

has_explicit_plugins && has_namespaced_cops
rescue
false
end
end
end
end
end
100 changes: 100 additions & 0 deletions test/standard/creates_config_store/detects_lint_roller_plugins_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
require_relative "../../test_helper"

class Standard::CreatesConfigStore::DetectsLintRollerPluginsTest < UnitTest
def setup
@subject = Standard::CreatesConfigStore::DetectsLintRollerPlugins.new
end

def test_returns_empty_array_when_no_lint_roller_plugins
gem_specs = [
create_mock_gem_spec("regular-gem", {}),
create_mock_gem_spec("another-gem", {"some_key" => "value"})
]

result = @subject.call(gem_specs)

assert_equal [], result
end

def test_detects_lint_roller_plugin_with_metadata
# Test that gems with lint_roller metadata are processed
# This test validates the metadata filtering logic
gem_with_metadata = create_mock_gem_spec("has-lint-roller", {
"default_lint_roller_plugin" => "SomePlugin"
})

gem_without_metadata = create_mock_gem_spec("no-lint-roller", {})

# This will fail at the require step, but that's expected for non-existent gems
result = @subject.call([gem_with_metadata, gem_without_metadata])

# We expect empty result because require will fail, but the method should not crash
assert_equal [], result
end

def test_skips_gems_with_load_errors
gem_spec = create_mock_gem_spec("broken-gem", {
"default_lint_roller_plugin" => "NonExistent::Plugin"
})

result = @subject.call([gem_spec])

assert_equal [], result
end

def test_skips_gems_with_instantiation_errors
gem_spec = create_mock_gem_spec("broken-plugin", {
"default_lint_roller_plugin" => "BrokenPlugin"
})

# Define a broken plugin class
broken_plugin_class = Class.new(LintRoller::Plugin) do
def initialize(_config)
raise "Broken plugin"
end
end

stub_const("BrokenPlugin", broken_plugin_class)

result = @subject.call([gem_spec])

assert_equal [], result
end

private

def create_mock_gem_spec(name, metadata)
mock_spec = Class.new do
def initialize(name, metadata)
@name = name
@metadata = metadata
end

attr_reader :name, :metadata

def require_paths
["lib"]
end

def full_gem_path
"/fake/path/#{@name}"
end
end

mock_spec.new(name, metadata)
end

def stub_const(const_name, value)
parts = const_name.split("::")
parent = Object

parts[0..-2].each do |part|
unless parent.const_defined?(part)
parent.const_set(part, Module.new)
end
parent = parent.const_get(part)
end

parent.const_set(parts.last, value)
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -126,4 +126,21 @@ def test_when_three_files_extend_with_monkey_business
}
assert_equal(expected, options_config.to_h)
end

def test_when_extend_config_without_lint_roller_cops
options_config = RuboCop::Config.new({
"AllCops" => {
"TargetRubyVersion" => "3.0"
}
}, "")

# Using fixture that doesn't contain lint_roller cops
@subject.call(options_config, {
extend_config: ["test/fixture/extend_config/all_cops.yml"]
})

# Should not have auto-detected lint_roller plugins (no namespaced cops)
namespaced_cops = options_config.to_h.keys.select { |k| k.include?("/") && !k.start_with?("AllCops") }
assert_equal [], namespaced_cops, "Should not auto-detect namespaced cops when no plugin cops in extend_config"
end
end