From 7b7d947438e6b14671f7b06d4a3c5dee7025c12d Mon Sep 17 00:00:00 2001 From: David Corson-Knowles Date: Wed, 1 Jul 2026 07:18:15 -0700 Subject: [PATCH] Add Rake/RequireOutsideTask cop Adds a new cop, `Rake/RequireOutsideTask`, that flags `require`/`require_relative` calls not inside a task block. ```ruby require 'foo' task :foo do Foo.do_something end namespace :foo do require 'foo' task :bar do Foo.do_something end end task :foo do require 'foo' Foo.do_something end Rake::Task['db:seed'].enhance do require 'foo' Foo.do_something end ``` A rake file's top level - including the body of a `namespace` block - is evaluated every time the file is loaded (i.e. on every `rake` invocation), not only when the task runs. A require placed there is paid on every load. Moving it into the task (or a `Rake::Task#enhance` block) loads the dependency lazily, only when the task that needs it runs. This complements the existing `Rake/MethodDefinitionInTask` / `Rake/ClassDefinitionInTask` family. Rakefiles are excluded by default, since they are the canonical place to require task libraries at the top (e.g. `require "bundler/gem_tasks"`, `require "rspec/core/rake_task"`). Added as `Enabled: pending` per the guidance for new cops. Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 5 ++ config/default.yml | 7 ++ lib/rubocop/cop/rake.rb | 1 + lib/rubocop/cop/rake/require_outside_task.rb | 76 +++++++++++++++++++ spec/lazy_loading_spec.rb | 2 +- .../cop/rake/require_outside_task_spec.rb | 55 ++++++++++++++ 6 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 lib/rubocop/cop/rake/require_outside_task.rb create mode 100644 spec/rubocop/cop/rake/require_outside_task_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index f193878..6633822 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## master (unreleased) +### New features + +* [#64](https://github.com/rubocop/rubocop-rake/pull/64): Add `Rake/RequireOutsideTask` cop. ([@corsonknowles][]) + ### Changes * [#66](https://github.com/rubocop/rubocop-rake/pull/66): Speed up loading rubocop-rake by lazily loading only the cops needed for a run. This requires RuboCop 1.89.0+. ([@koic][]) @@ -82,3 +86,4 @@ [@jaruuuu]: https://github.com/jaruuuu [@koic]: https://github.com/koic [@tejasbubane]: https://github.com/tejasbubane +[@corsonknowles]: https://github.com/corsonknowles diff --git a/config/default.yml b/config/default.yml index ac4b277..bc602b2 100644 --- a/config/default.yml +++ b/config/default.yml @@ -28,3 +28,10 @@ Rake/MethodDefinitionInTask: Description: 'Do not define a method in rake task, because it will be defined to the top level.' Enabled: true VersionAdded: '0.2.0' + +Rake/RequireOutsideTask: + Description: 'Do not `require` outside of a task block, because it is loaded every time the rake file is loaded.' + Enabled: pending + VersionAdded: '<>' + Exclude: + - 'Rakefile' diff --git a/lib/rubocop/cop/rake.rb b/lib/rubocop/cop/rake.rb index ae1ec23..2d56592 100644 --- a/lib/rubocop/cop/rake.rb +++ b/lib/rubocop/cop/rake.rb @@ -14,6 +14,7 @@ module Rake register_cop :DuplicateTask, "#{__dir__}/rake/duplicate_task" register_cop :DuplicateNamespace, "#{__dir__}/rake/duplicate_namespace" register_cop :MethodDefinitionInTask, "#{__dir__}/rake/method_definition_in_task" + register_cop :RequireOutsideTask, "#{__dir__}/rake/require_outside_task" end end end diff --git a/lib/rubocop/cop/rake/require_outside_task.rb b/lib/rubocop/cop/rake/require_outside_task.rb new file mode 100644 index 0000000..402fdb0 --- /dev/null +++ b/lib/rubocop/cop/rake/require_outside_task.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +module RuboCop + module Cop + module Rake + # Detects `require` and `require_relative` calls that are not inside a + # task block. + # + # A rake file's top level - including the body of a `namespace` block - + # is evaluated every time the file is loaded. A require placed there + # therefore runs on every rake invocation, instead of only when the task + # that needs it runs. Move the require inside the task (or a + # `Rake::Task#enhance` block) so the dependency is loaded lazily. + # + # @example + # # bad + # require 'foo' + # + # task :foo do + # Foo.do_something + # end + # + # # bad - the namespace body is still evaluated on load + # namespace :foo do + # require 'foo' + # + # task :bar do + # Foo.do_something + # end + # end + # + # # good + # task :foo do + # require 'foo' + # Foo.do_something + # end + # + # # good + # Rake::Task['db:seed'].enhance do + # require 'foo' + # Foo.do_something + # end + # + class RequireOutsideTask < Base + MSG = 'Do not `require` outside of a task block, because it is loaded every time the rake file is loaded.' + RESTRICT_ON_SEND = %i[require require_relative].freeze + + # @!method task_block?(node) + def_node_matcher :task_block?, <<~PATTERN + (block (send nil? :task ...) ...) + PATTERN + + # @!method task_enhance_block?(node) + def_node_matcher :task_enhance_block?, <<~PATTERN + (block + (send + (send (const (const nil? :Rake) :Task) :[] ...) + :enhance ...) + ...) + PATTERN + + def on_send(node) + return if inside_task_block?(node) + + add_offense(node) + end + + private def inside_task_block?(node) + node.each_ancestor(:block).any? do |ancestor| + task_block?(ancestor) || task_enhance_block?(ancestor) + end + end + end + end + end +end diff --git a/spec/lazy_loading_spec.rb b/spec/lazy_loading_spec.rb index 48944ea..5c337e7 100644 --- a/spec/lazy_loading_spec.rb +++ b/spec/lazy_loading_spec.rb @@ -24,7 +24,7 @@ def run_script(source) puts "loaded_cop_files=\#{loaded.size}" RUBY - expect(output).to include('registered=5', 'loaded_cop_files=0') + expect(output).to include('registered=6', 'loaded_cop_files=0') end it 'resolves every helper file in `lib/rubocop/cop/rake/helper` through an autoload' do diff --git a/spec/rubocop/cop/rake/require_outside_task_spec.rb b/spec/rubocop/cop/rake/require_outside_task_spec.rb new file mode 100644 index 0000000..1ab7952 --- /dev/null +++ b/spec/rubocop/cop/rake/require_outside_task_spec.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +RSpec.describe RuboCop::Cop::Rake::RequireOutsideTask, :config do + it 'registers an offense for `require` at the top level' do + expect_offense(<<~RUBY) + require 'foo' + ^^^^^^^^^^^^^ Do not `require` outside of a task block, because it is loaded every time the rake file is loaded. + + task :foo do + Foo.do_something + end + RUBY + end + + it 'registers an offense for `require_relative` at the top level' do + expect_offense(<<~RUBY) + require_relative 'foo' + ^^^^^^^^^^^^^^^^^^^^^^ Do not `require` outside of a task block, because it is loaded every time the rake file is loaded. + + task :foo do + end + RUBY + end + + it 'registers an offense for `require` inside a namespace but outside a task' do + expect_offense(<<~RUBY) + namespace :foo do + require 'foo' + ^^^^^^^^^^^^^ Do not `require` outside of a task block, because it is loaded every time the rake file is loaded. + + task :bar do + Foo.do_something + end + end + RUBY + end + + it 'does not register an offense for `require` inside a task' do + expect_no_offenses(<<~RUBY) + task :foo do + require 'foo' + Foo.do_something + end + RUBY + end + + it 'does not register an offense for `require` inside a `Rake::Task#enhance` block' do + expect_no_offenses(<<~RUBY) + Rake::Task['db:seed'].enhance do + require 'foo' + Foo.do_something + end + RUBY + end +end