Skip to content

Commit 90d5831

Browse files
authored
Merge pull request #360 from koic/lazy_load_cops
Load cops lazily
2 parents d42c72c + cdbd01c commit 90d5831

10 files changed

Lines changed: 182 additions & 66 deletions

File tree

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ jobs:
8888
run: |
8989
sed -e "/gem 'rubocop', github: 'rubocop\/rubocop'/d" -i Gemfile
9090
cat << EOF > Gemfile.local
91-
gem 'rubocop', '1.75.0' # Specify the oldest supported RuboCop version
91+
gem 'rubocop', '1.89.0' # Specify the oldest supported RuboCop version
9292
EOF
9393
- name: set up Ruby
9494
uses: ruby/setup-ruby@v1

.rubocop.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
inherit_from: .rubocop_todo.yml
44

5+
inherit_mode:
6+
merge:
7+
- Exclude
8+
59
plugins:
610
- rubocop-internal_affairs
711
- rubocop-minitest
@@ -50,6 +54,10 @@ Naming/InclusiveLanguage:
5054
Suggestions:
5155
- does_not_register
5256

57+
Style/Documentation:
58+
Exclude:
59+
- lib/rubocop/cop/minitest.rb
60+
5361
Style/FormatStringToken:
5462
# Because we parse a lot of source codes from strings. Percent arrays
5563
# look like unannotated format string tokens to this cop.

Rakefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ task :new_cop, [:cop] do |_task, args|
4444

4545
generator.write_source
4646
generator.write_test
47-
generator.inject_require(root_file_path: 'lib/rubocop/cop/minitest_cops.rb')
47+
generator.inject_registration
4848
generator.inject_config(config_file_path: 'config/default.yml')
4949

5050
puts generator.todo
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* [#360](https://github.com/rubocop/rubocop-minitest/pull/360): Speed up loading rubocop-minitest by lazily loading only the cops and mixins needed for a run. This requires RuboCop 1.89.0+. ([@koic][])

lib/rubocop-minitest.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,4 @@
55
require_relative 'rubocop/minitest'
66
require_relative 'rubocop/minitest/version'
77
require_relative 'rubocop/minitest/plugin'
8-
require_relative 'rubocop/cop/minitest_cops'
8+
require_relative 'rubocop/cop/minitest'

lib/rubocop/cop/minitest.rb

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# frozen_string_literal: true
2+
3+
module RuboCop
4+
module Cop
5+
# Mixins are autoloaded because they're used only when the relevant cop class is loaded.
6+
autoload :ArgumentRangeHelper, "#{__dir__}/mixin/argument_range_helper"
7+
autoload :InDeltaMixin, "#{__dir__}/mixin/in_delta_mixin"
8+
autoload :MinitestCopRule, "#{__dir__}/mixin/minitest_cop_rule"
9+
autoload :MinitestExplorationHelpers, "#{__dir__}/mixin/minitest_exploration_helpers"
10+
11+
# Cops for the `Minitest` department. The department's cops are registered for lazy loading and their files,
12+
# along with the mixins they use, are loaded on demand.
13+
module Minitest
14+
autoload :InstanceOfAssertionHandleable, "#{__dir__}/mixin/instance_of_assertion_handleable"
15+
autoload :NilAssertionHandleable, "#{__dir__}/mixin/nil_assertion_handleable"
16+
autoload :PredicateAssertionHandleable, "#{__dir__}/mixin/predicate_assertion_handleable"
17+
18+
extend LazyLoader
19+
20+
register_cop :AssertEmpty, "#{__dir__}/minitest/assert_empty"
21+
register_cop :AssertEmptyLiteral, "#{__dir__}/minitest/assert_empty_literal"
22+
register_cop :AssertEqual, "#{__dir__}/minitest/assert_equal"
23+
register_cop :AssertInDelta, "#{__dir__}/minitest/assert_in_delta"
24+
register_cop :AssertOperator, "#{__dir__}/minitest/assert_operator"
25+
register_cop :AssertPredicate, "#{__dir__}/minitest/assert_predicate"
26+
register_cop :AssertRaisesCompoundBody, "#{__dir__}/minitest/assert_raises_compound_body"
27+
register_cop :AssertRaisesWithRegexpArgument, "#{__dir__}/minitest/assert_raises_with_regexp_argument"
28+
register_cop :AssertWithExpectedArgument, "#{__dir__}/minitest/assert_with_expected_argument"
29+
register_cop :AssertionInLifecycleHook, "#{__dir__}/minitest/assertion_in_lifecycle_hook"
30+
register_cop :AssertKindOf, "#{__dir__}/minitest/assert_kind_of"
31+
register_cop :AssertNil, "#{__dir__}/minitest/assert_nil"
32+
register_cop :AssertIncludes, "#{__dir__}/minitest/assert_includes"
33+
register_cop :AssertInstanceOf, "#{__dir__}/minitest/assert_instance_of"
34+
register_cop :AssertMatch, "#{__dir__}/minitest/assert_match"
35+
register_cop :AssertOutput, "#{__dir__}/minitest/assert_output"
36+
register_cop :AssertPathExists, "#{__dir__}/minitest/assert_path_exists"
37+
register_cop :AssertRespondTo, "#{__dir__}/minitest/assert_respond_to"
38+
register_cop :AssertSame, "#{__dir__}/minitest/assert_same"
39+
register_cop :AssertSilent, "#{__dir__}/minitest/assert_silent"
40+
register_cop :AssertTruthy, "#{__dir__}/minitest/assert_truthy"
41+
register_cop :DuplicateTestRun, "#{__dir__}/minitest/duplicate_test_run"
42+
register_cop :EmptyLineBeforeAssertionMethods, "#{__dir__}/minitest/empty_line_before_assertion_methods"
43+
register_cop :Focus, "#{__dir__}/minitest/focus"
44+
register_cop :NonExecutableTestMethod, "#{__dir__}/minitest/non_executable_test_method"
45+
register_cop :RedundantMessageArgument, "#{__dir__}/minitest/redundant_message_argument"
46+
register_cop :ReturnInTestMethod, "#{__dir__}/minitest/return_in_test_method"
47+
register_cop :TestFileName, "#{__dir__}/minitest/test_file_name"
48+
register_cop :GlobalExpectations, "#{__dir__}/minitest/global_expectations"
49+
register_cop :LifecycleHooksOrder, "#{__dir__}/minitest/lifecycle_hooks_order"
50+
register_cop :LiteralAsActualArgument, "#{__dir__}/minitest/literal_as_actual_argument"
51+
register_cop :MultipleAssertions, "#{__dir__}/minitest/multiple_assertions"
52+
register_cop :NoAssertions, "#{__dir__}/minitest/no_assertions"
53+
register_cop :NoTestCases, "#{__dir__}/minitest/no_test_cases"
54+
register_cop :NonPublicTestMethod, "#{__dir__}/minitest/non_public_test_method"
55+
register_cop :RefuteEmpty, "#{__dir__}/minitest/refute_empty"
56+
register_cop :RefuteFalse, "#{__dir__}/minitest/refute_false"
57+
register_cop :RefuteEqual, "#{__dir__}/minitest/refute_equal"
58+
register_cop :RefuteInDelta, "#{__dir__}/minitest/refute_in_delta"
59+
register_cop :RefuteIncludes, "#{__dir__}/minitest/refute_includes"
60+
register_cop :RefuteInstanceOf, "#{__dir__}/minitest/refute_instance_of"
61+
register_cop :RefuteKindOf, "#{__dir__}/minitest/refute_kind_of"
62+
register_cop :RefuteMatch, "#{__dir__}/minitest/refute_match"
63+
register_cop :RefuteNil, "#{__dir__}/minitest/refute_nil"
64+
register_cop :RefuteOperator, "#{__dir__}/minitest/refute_operator"
65+
register_cop :RefutePathExists, "#{__dir__}/minitest/refute_path_exists"
66+
register_cop :RefutePredicate, "#{__dir__}/minitest/refute_predicate"
67+
register_cop :RefuteRespondTo, "#{__dir__}/minitest/refute_respond_to"
68+
register_cop :RefuteSame, "#{__dir__}/minitest/refute_same"
69+
register_cop :SkipEnsure, "#{__dir__}/minitest/skip_ensure"
70+
register_cop :SkipWithoutReason, "#{__dir__}/minitest/skip_without_reason"
71+
register_cop :TestMethodName, "#{__dir__}/minitest/test_method_name"
72+
register_cop :UnreachableAssertion, "#{__dir__}/minitest/unreachable_assertion"
73+
register_cop :UnspecifiedException, "#{__dir__}/minitest/unspecified_exception"
74+
register_cop :UselessAssertion, "#{__dir__}/minitest/useless_assertion"
75+
end
76+
end
77+
end

lib/rubocop/cop/minitest_cops.rb

Lines changed: 4 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,6 @@
11
# frozen_string_literal: true
22

3-
require_relative 'mixin/argument_range_helper'
4-
require_relative 'mixin/in_delta_mixin'
5-
require_relative 'mixin/instance_of_assertion_handleable'
6-
require_relative 'mixin/minitest_cop_rule'
7-
require_relative 'mixin/minitest_exploration_helpers'
8-
require_relative 'mixin/nil_assertion_handleable'
9-
require_relative 'mixin/predicate_assertion_handleable'
10-
require_relative 'minitest/assert_empty'
11-
require_relative 'minitest/assert_empty_literal'
12-
require_relative 'minitest/assert_equal'
13-
require_relative 'minitest/assert_in_delta'
14-
require_relative 'minitest/assert_operator'
15-
require_relative 'minitest/assert_predicate'
16-
require_relative 'minitest/assert_raises_compound_body'
17-
require_relative 'minitest/assert_raises_with_regexp_argument'
18-
require_relative 'minitest/assert_with_expected_argument'
19-
require_relative 'minitest/assertion_in_lifecycle_hook'
20-
require_relative 'minitest/assert_kind_of'
21-
require_relative 'minitest/assert_nil'
22-
require_relative 'minitest/assert_includes'
23-
require_relative 'minitest/assert_instance_of'
24-
require_relative 'minitest/assert_match'
25-
require_relative 'minitest/assert_output'
26-
require_relative 'minitest/assert_path_exists'
27-
require_relative 'minitest/assert_respond_to'
28-
require_relative 'minitest/assert_same'
29-
require_relative 'minitest/assert_silent'
30-
require_relative 'minitest/assert_truthy'
31-
require_relative 'minitest/duplicate_test_run'
32-
require_relative 'minitest/empty_line_before_assertion_methods'
33-
require_relative 'minitest/focus'
34-
require_relative 'minitest/non_executable_test_method'
35-
require_relative 'minitest/redundant_message_argument'
36-
require_relative 'minitest/return_in_test_method'
37-
require_relative 'minitest/test_file_name'
38-
require_relative 'minitest/global_expectations'
39-
require_relative 'minitest/lifecycle_hooks_order'
40-
require_relative 'minitest/literal_as_actual_argument'
41-
require_relative 'minitest/multiple_assertions'
42-
require_relative 'minitest/no_assertions'
43-
require_relative 'minitest/no_test_cases'
44-
require_relative 'minitest/non_public_test_method'
45-
require_relative 'minitest/refute_empty'
46-
require_relative 'minitest/refute_false'
47-
require_relative 'minitest/refute_equal'
48-
require_relative 'minitest/refute_in_delta'
49-
require_relative 'minitest/refute_includes'
50-
require_relative 'minitest/refute_instance_of'
51-
require_relative 'minitest/refute_kind_of'
52-
require_relative 'minitest/refute_match'
53-
require_relative 'minitest/refute_nil'
54-
require_relative 'minitest/refute_operator'
55-
require_relative 'minitest/refute_path_exists'
56-
require_relative 'minitest/refute_predicate'
57-
require_relative 'minitest/refute_respond_to'
58-
require_relative 'minitest/refute_same'
59-
require_relative 'minitest/skip_ensure'
60-
require_relative 'minitest/skip_without_reason'
61-
require_relative 'minitest/test_method_name'
62-
require_relative 'minitest/unreachable_assertion'
63-
require_relative 'minitest/unspecified_exception'
64-
require_relative 'minitest/useless_assertion'
3+
# @deprecated This file is deprecated. Cops are registered for lazy loading in
4+
# `rubocop/cop/minitest`; this file is kept for compatibility with code that
5+
# requires it directly.
6+
require_relative 'minitest'

rubocop-minitest.gemspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,6 @@ Gem::Specification.new do |spec|
3434
spec.require_paths = ['lib']
3535

3636
spec.add_dependency 'lint_roller', '~> 1.1'
37-
spec.add_dependency 'rubocop', '>= 1.75.0', '< 2.0'
37+
spec.add_dependency 'rubocop', '>= 1.89.0', '< 2.0'
3838
spec.add_dependency 'rubocop-ast', '>= 1.38.0', '< 2.0'
3939
end

test/lazy_loading_test.rb

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# frozen_string_literal: true
2+
3+
require_relative 'test_helper'
4+
require 'tmpdir'
5+
6+
class LazyLoadingTest < Minitest::Test
7+
def test_cops_are_registered_without_loading_their_files
8+
output = run_script(<<~RUBY)
9+
require 'rubocop-minitest'
10+
11+
registry = RuboCop::Cop::Registry.global
12+
loaded_cop_files = $LOADED_FEATURES.grep(%r{/rubocop/cop/minitest/})
13+
loaded_mixin_files = $LOADED_FEATURES.grep(%r{\#{Regexp.escape('#{lib_dir}')}/rubocop/cop/mixin/})
14+
15+
puts "registered=\#{registry.names.grep(%r{\\AMinitest/}).size}"
16+
puts "loaded_cop_files=\#{loaded_cop_files.size}"
17+
puts "loaded_mixin_files=\#{loaded_mixin_files.size}"
18+
RUBY
19+
20+
assert_includes(output, 'registered=55')
21+
assert_includes(output, 'loaded_cop_files=0')
22+
assert_includes(output, 'loaded_mixin_files=0')
23+
end
24+
25+
def test_mixins_are_autoloaded_when_a_cop_class_is_loaded
26+
output = run_script(<<~RUBY)
27+
require 'rubocop-minitest'
28+
29+
assert_empty_cop = RuboCop::Cop::Registry.global.find_by_cop_name('Minitest/AssertEmpty')
30+
assert_nil_cop = RuboCop::Cop::Registry.global.find_by_cop_name('Minitest/AssertNil')
31+
loaded_mixin_files = $LOADED_FEATURES.grep(%r{\#{Regexp.escape('#{lib_dir}')}/rubocop/cop/mixin/})
32+
33+
puts "define_rule_applied=\#{assert_empty_cop.include?(RuboCop::Cop::ArgumentRangeHelper) &&
34+
assert_empty_cop.singleton_class.include?(RuboCop::Cop::MinitestCopRule)}"
35+
puts "minitest_mixin_applied=\#{assert_nil_cop.include?(RuboCop::Cop::Minitest::NilAssertionHandleable)}"
36+
puts "loaded_mixin_files=\#{loaded_mixin_files.map { |path| File.basename(path) }.sort.join(',')}"
37+
RUBY
38+
39+
assert_includes(output, 'define_rule_applied=true')
40+
assert_includes(output, 'minitest_mixin_applied=true')
41+
expected_mixin_files = 'argument_range_helper.rb,minitest_cop_rule.rb,nil_assertion_handleable.rb'
42+
assert_includes(output, "loaded_mixin_files=#{expected_mixin_files}")
43+
end
44+
45+
def test_a_cop_is_not_registered_twice_when_its_file_is_required_directly
46+
output = run_script(<<~RUBY)
47+
require 'rubocop-minitest'
48+
49+
before = RuboCop::Cop::Registry.global.length
50+
require 'rubocop/cop/minitest/assert_empty'
51+
after = RuboCop::Cop::Registry.global.length
52+
53+
puts "stable=\#{before == after}"
54+
puts "class=\#{RuboCop::Cop::Registry.global.find_by_cop_name('Minitest/AssertEmpty')}"
55+
RUBY
56+
57+
assert_includes(output, 'stable=true')
58+
assert_includes(output, 'class=RuboCop::Cop::Minitest::AssertEmpty')
59+
end
60+
61+
private
62+
63+
def run_script(source)
64+
Dir.mktmpdir do |dir|
65+
script = File.join(dir, 'script.rb')
66+
File.write(script, source)
67+
output = `#{RbConfig.ruby} -I #{lib_dir} #{script} 2>&1`
68+
raise "script failed:\n#{output}" unless $CHILD_STATUS.success?
69+
70+
output
71+
end
72+
end
73+
74+
def lib_dir
75+
File.expand_path('../lib', __dir__)
76+
end
77+
end

test/project_test.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,17 @@ def setup
1111
load_feature_entries
1212
end
1313

14+
def test_every_cop_file_is_registered_exactly_once
15+
cop_root = File.expand_path('../lib/rubocop/cop', __dir__)
16+
files = Dir[File.join(cop_root, 'minitest', '*.rb')].sort
17+
18+
registered = RuboCop::Cop::Registry.global.cops_for_department(:Minitest).map do |cop|
19+
Object.const_source_location(cop.name).first
20+
end.sort
21+
22+
assert_equal(files, registered)
23+
end
24+
1425
def test_changelog_has_newline_at_end_of_file
1526
assert(@changelog.end_with?("\n"))
1627
end

0 commit comments

Comments
 (0)