CrackedRuby logo

CrackedRuby

Strict Unused Block

Overview

Strict unused block warnings represent Ruby's approach to detecting when blocks are passed to methods that never use them. Introduced in Ruby 3.4.0, this warning category helps developers identify potential bugs, API misuse, and unnecessary block passing that could indicate logic errors or outdated method calls.

Ruby's implementation centers around the Warning[:strict_unused_block] setting and the -W:strict_unused_block command-line flag. When enabled, Ruby analyzes method calls at runtime to determine whether passed blocks are actually utilized through yielding or explicit block handling. The warning system distinguishes between methods that legitimately accept blocks but conditionally use them versus methods that completely ignore blocks.

Warning[:strict_unused_block] = true

def process_data(data)
  # Method never yields or uses block_given?
  data.upcase
end

# This will trigger a warning
process_data("hello") { |x| puts x }
# Warning: the block passed to 'Object#process_data' may be ignored

The warning detection occurs during method execution, not at parse time. Ruby tracks whether methods yield to blocks, check block_given?, or explicitly handle block parameters. Methods that accept blocks through formal parameters (&block) but never use them will still trigger warnings in certain scenarios.

def conditional_yield(flag)
  yield if flag  # No warning - conditionally uses block
end

def accepts_block(&block)
  # No warning - explicitly declares block parameter
end

def checks_block
  puts block_given?  # Warning - checks but never uses
end

The primary use case involves catching regressions where method APIs change and previously block-accepting methods no longer utilize blocks. This commonly occurs during library updates or refactoring when method signatures change but calling code remains unchanged.

Basic Usage

Enabling strict unused block warnings requires either command-line flags or runtime configuration. The feature operates as an opt-in warning category due to potential false positives in complex codebases.

# Runtime configuration
Warning[:strict_unused_block] = true

# Command line configuration
ruby -W:strict_unused_block script.rb

# Combined with other warning categories
ruby -W:deprecated -W:strict_unused_block -W:performance script.rb

Environment variable configuration provides persistent warning settings across Ruby invocations:

# Enable multiple warning categories
export RUBYOPT="-W:deprecated -W:strict_unused_block -W:performance"

# Verify current settings
echo $RUBYOPT

The warning system analyzes several block usage patterns during method execution. Methods that never interact with blocks trigger warnings immediately:

Warning[:strict_unused_block] = true

def simple_method(data)
  data.to_s
end

# Triggers warning
simple_method(42) { puts "unused" }
# Output: warning: the block passed to 'Object#simple_method' may be ignored

Methods that conditionally yield or check for block presence avoid warnings:

def conditional_processor(data, condition)
  result = data.process
  yield(result) if condition && block_given?
  result
end

# No warning - method conditionally uses block
conditional_processor(data, true) { |r| puts r }
conditional_processor(data, false) { |r| puts r }

Block parameter declaration without usage creates nuanced warning behavior:

def explicit_block_handler(&block)
  # Method declares block parameter but never calls it
  perform_operation
end

# No warning in most cases - explicit parameter declaration
explicit_block_handler { puts "declared but unused" }

def block_checker(&block)
  puts "Block given: #{block_given?}"
  # Never actually calls the block
end

# May trigger warning - checks but doesn't use
block_checker { puts "checked but unused" }

Integration with existing warning systems allows granular control over different warning types:

# Disable all warnings except strict unused block
Warning[:deprecated] = false
Warning[:experimental] = false
Warning[:strict_unused_block] = true

# Enable only in development/test environments
if Rails.env.development? || Rails.env.test?
  Warning[:strict_unused_block] = true
end

Error Handling & Debugging

Strict unused block warnings excel at catching subtle bugs that emerge during API changes or incorrect method usage. The warning system helps identify mismatched expectations between block-passing calling code and block-ignoring method implementations.

Common debugging scenarios involve methods that previously accepted blocks but changed behavior during updates:

# Version 1.0 - method used blocks
def process_items(items)
  items.each { |item| yield(item) }
end

# Version 2.0 - method signature changed
def process_items(items)
  items.map(&:process)  # No longer yields
end

# Calling code remains unchanged, but block is now ignored
Warning[:strict_unused_block] = true
process_items(data) { |item| puts item.debug_info }
# Warning reveals the API mismatch

Method name confusion represents another debugging target where similarly named methods have different block expectations:

# Intended method that uses blocks
def time_operation
  start_time = Time.now
  yield
  puts "Duration: #{Time.now - start_time}"
end

# Similar method that ignores blocks
def timed_operation
  puts "Operation completed at #{Time.now}"
end

# Accidental method call with block
Warning[:strict_unused_block] = true
timed_operation { heavy_computation }  # Warning indicates wrong method

Exception handling during block execution creates complex warning scenarios. Methods that handle exceptions from block evaluation may trigger warnings if exception paths don't actually execute blocks:

def safe_execution
  begin
    yield if block_given?
  rescue StandardError => e
    puts "Error: #{e.message}"
    # Block was attempted, so no warning
  end
end

def unsafe_execution
  begin
    perform_operation
    # yield never called, even in rescue
  rescue StandardError => e
    puts "Error: #{e.message}"
  end
end

# This triggers warning - block never used despite rescue logic
unsafe_execution { puts "This won't run" }

Debugging warning output requires understanding the message format and location reporting:

# Warning format: file:line: warning: the block passed to 'ClassName#method_name'
# defined at file:line may be ignored

def problematic_method
  42
end

# Call from line 15 of script.rb
problematic_method { puts "unused" }
# script.rb:15: warning: the block passed to 'Object#problematic_method'
# defined at script.rb:10 may be ignored

Conditional logic debugging involves tracing execution paths that bypass block usage:

def conditional_block_user(condition1, condition2)
  if condition1 && condition2
    yield  # Block only used when both conditions true
  else
    puts "Conditions not met"
  end
end

# Warning appears when conditions prevent block usage
Warning[:strict_unused_block] = true
conditional_block_user(true, false) { puts "This won't execute" }
# No warning - method could use block under different conditions

def impossible_block_user
  if false  # Dead code path
    yield
  else
    puts "Block never reachable"
  end
end

# Warning appears - yield is never reachable
impossible_block_user { puts "Dead code" }

Common Pitfalls

Understanding the nuances of strict unused block warnings requires recognizing several categories of false positives and edge cases that can confuse developers. Block parameter declaration creates the most frequent source of unexpected warning behavior.

Methods accepting explicit block parameters through &block syntax generally avoid warnings, but exceptions occur when the block is captured but never executed:

def captures_block(&block)
  @stored_block = block  # Stores but doesn't execute
  puts "Block captured"
end

# Usually no warning, but implementation-dependent
captures_block { puts "stored for later" }

def conditional_block_storage(&block)
  @block = block if some_condition?
  # Block stored conditionally but never executed
end

# May generate warning depending on condition evaluation
conditional_block_storage { puts "might be stored" }

The block_given? method creates subtle warning conditions. Methods that check for block presence but never actually use blocks trigger warnings:

def block_aware_method
  if block_given?
    puts "Block provided but not used"
    # Warning: checks but never yields
  end
  perform_regular_operation
end

# Triggers warning despite block_given? check
block_aware_method { puts "checked but unused" }

# Correct usage that avoids warnings
def proper_block_usage
  if block_given?
    puts "Using block:"
    yield  # Actually uses the block
  else
    puts "No block provided"
  end
end

Metaprogramming and dynamic method calls create complex warning scenarios. Methods defined through define_method or similar mechanisms may not trigger warnings consistently:

class DynamicMethods
  define_method(:generated_method) do |arg|
    arg.to_s  # Never uses blocks
  end

  # Method created through metaprogramming
  %w[first second third].each do |name|
    define_method("process_#{name}") do |data|
      data.send(name.to_sym)  # May or may not use blocks
    end
  end
end

obj = DynamicMethods.new
# Warning behavior varies with metaprogrammed methods
obj.generated_method("test") { puts "unused?" }

Inheritance and method overriding introduce warning inconsistencies. Parent class methods that use blocks may be overridden by child class methods that ignore blocks:

class Parent
  def process_data(data)
    data.each { |item| yield(item) }
  end
end

class Child < Parent
  def process_data(data)
    data.map(&:to_s)  # Overrides parent, ignores blocks
  end
end

# Warning depends on actual method called at runtime
parent = Parent.new
child = Child.new

parent.process_data([1, 2, 3]) { |x| puts x }  # No warning
child.process_data([1, 2, 3]) { |x| puts x }   # Warning - overridden method ignores block

Module inclusion and method forwarding create warning propagation issues:

module Processor
  def enhanced_process(data)
    basic_process(data)  # Forwards to basic_process
  end
end

class BasicHandler
  include Processor

  def basic_process(data)
    data.upcase  # Never uses blocks
  end
end

handler = BasicHandler.new
# Warning occurs but attribution may be confusing
handler.enhanced_process("test") { puts "forwarded but unused" }

Thread and fiber contexts affect warning generation. Blocks passed to methods executing in different threads may not trigger warnings due to execution context differences:

def threaded_operation(data)
  Thread.new do
    # Block passed to outer method, not this thread
    data.process
  end.join
end

# Warning behavior varies with threading
threaded_operation("data") { puts "thread context confusion" }

def fiber_operation(data)
  fiber = Fiber.new do
    data.process  # Block not available in fiber context
  end
  fiber.resume
end

# Similar context issues with fibers
fiber_operation("data") { puts "fiber context issues" }

Library integration creates false positive scenarios where established patterns conflict with strict warning requirements:

# Common Rails pattern - block may be used by framework
def around_action_method(&block)
  # Framework may call block through metaprogramming
  Rails.application.executor.wrap(&block)
end

# May trigger warnings despite legitimate framework usage
around_action_method { perform_user_action }

Production Patterns

Implementing strict unused block warnings in production environments requires careful consideration of deployment strategies, performance implications, and integration with existing code quality tools. Production usage typically focuses on development and continuous integration environments rather than live application servers.

Development environment configuration provides the safest introduction point for strict unused block warnings:

# config/environments/development.rb
if Rails.env.development?
  Warning[:strict_unused_block] = true
  Warning[:deprecated] = true
  Warning[:performance] = true
end

# Custom warning handler for development
module DevelopmentWarningHandler
  def self.setup
    Warning.process do |message|
      if message.include?('strict_unused_block')
        Rails.logger.warn "[BLOCK WARNING] #{message}"
      end
      # Continue normal warning processing
      warn message
    end
  end
end

DevelopmentWarningHandler.setup if Rails.env.development?

Continuous integration pipeline integration ensures warning detection during automated testing:

# .github/workflows/ci.yml
- name: Run tests with strict warnings
  run: |
    RUBYOPT="-W:strict_unused_block" bundle exec rspec
  env:
    RAILS_ENV: test

- name: Check for unused block warnings
  run: |
    output=$(RUBYOPT="-W:strict_unused_block" bundle exec rspec 2>&1)
    if echo "$output" | grep -q "may be ignored"; then
      echo "Unused block warnings detected"
      exit 1
    fi

Custom warning processors enable sophisticated handling of unused block warnings in production-adjacent environments:

class UnusedBlockWarningProcessor
  def self.install
    @warnings = []

    Warning.process do |message|
      if message.match?(/the block passed.*may be ignored/)
        record_unused_block_warning(message)
      end

      # Send to normal warning processing
      warn message unless Rails.env.production?
    end
  end

  def self.record_unused_block_warning(message)
    location = extract_location(message)
    method_name = extract_method_name(message)

    @warnings << {
      message: message,
      location: location,
      method: method_name,
      timestamp: Time.current
    }

    # Report to monitoring system
    if defined?(Honeybadger)
      Honeybadger.notify("Unused block warning",
        context: { location: location, method: method_name }
      )
    end
  end

  def self.generate_report
    @warnings.group_by { |w| w[:method] }
             .transform_values(&:count)
             .sort_by { |_, count| -count }
  end

  private

  def self.extract_location(message)
    message.match(/^([^:]+:\d+)/)[1] rescue "unknown"
  end

  def self.extract_method_name(message)
    message.match(/'([^']+)'/)[1] rescue "unknown"
  end
end

# Initialize in application.rb or initializers
UnusedBlockWarningProcessor.install unless Rails.env.production?

Library and gem development benefits significantly from strict unused block warnings during development and testing phases:

# lib/my_gem/base.rb
module MyGem
  class Base
    def self.configure_warnings_for_development
      return unless development_mode?

      Warning[:strict_unused_block] = true

      # Custom handler for gem-specific warnings
      Warning.process do |message|
        if message.include?('MyGem') && message.include?('may be ignored')
          log_gem_warning(message)
        end
        warn message
      end
    end

    private

    def self.development_mode?
      ENV['GEM_ENV'] == 'development' ||
      (defined?(Rails) && Rails.env.development?)
    end

    def self.log_gem_warning(message)
      File.open('unused_blocks.log', 'a') do |f|
        f.puts "[#{Time.now}] #{message}"
      end
    end
  end
end

# Activate during gem loading in development
MyGem::Base.configure_warnings_for_development

Code review integration assists teams in maintaining block usage discipline:

# scripts/check_unused_blocks.rb
#!/usr/bin/env ruby

require 'open3'
require 'json'

class UnusedBlockChecker
  def self.run_on_changed_files(base_branch = 'main')
    changed_files = get_changed_ruby_files(base_branch)
    return if changed_files.empty?

    warnings = []

    changed_files.each do |file|
      file_warnings = check_file_for_unused_blocks(file)
      warnings.concat(file_warnings)
    end

    generate_review_report(warnings)
  end

  private

  def self.get_changed_ruby_files(base_branch)
    cmd = "git diff --name-only #{base_branch}..HEAD | grep '\\.rb$'"
    stdout, _, _ = Open3.capture3(cmd)
    stdout.split("\n")
  end

  def self.check_file_for_unused_blocks(file)
    return [] unless File.exist?(file)

    cmd = "ruby -W:strict_unused_block -c #{file}"
    _, stderr, _ = Open3.capture3(cmd)

    stderr.split("\n")
          .select { |line| line.include?('may be ignored') }
          .map { |warning| { file: file, warning: warning } }
  end

  def self.generate_review_report(warnings)
    if warnings.any?
      puts "🚨 Unused block warnings detected in changed files:"
      warnings.each do |w|
        puts "  #{w[:file]}: #{w[:warning]}"
      end
      exit 1
    else
      puts "✅ No unused block warnings in changed files"
    end
  end
end

# Run as part of CI or git hooks
UnusedBlockChecker.run_on_changed_files(ARGV[0] || 'main')

Performance monitoring in staging environments helps assess warning overhead:

class WarningPerformanceMonitor
  def self.benchmark_warning_overhead
    iterations = 10_000

    # Disable warnings
    Warning[:strict_unused_block] = false
    without_warnings = Benchmark.measure do
      iterations.times { sample_method_with_unused_block }
    end

    # Enable warnings
    Warning[:strict_unused_block] = true
    with_warnings = Benchmark.measure do
      iterations.times { sample_method_with_unused_block }
    end

    overhead = ((with_warnings.real - without_warnings.real) / without_warnings.real * 100)

    {
      without_warnings: without_warnings.real,
      with_warnings: with_warnings.real,
      overhead_percent: overhead.round(2)
    }
  end

  private

  def self.sample_method_with_unused_block
    [1, 2, 3].map(&:to_s) { |x| x.upcase }
  end
end

# Monitor periodically in staging
performance_data = WarningPerformanceMonitor.benchmark_warning_overhead
Rails.logger.info "Warning overhead: #{performance_data[:overhead_percent]}%"

Reference

Warning Configuration Methods

Method Parameters Returns Description
Warning[:strict_unused_block] = value value (Boolean) Boolean Enable/disable strict unused block warnings
Warning[:strict_unused_block] None Boolean or nil Current setting for strict unused block warnings
Warning.categories None Array<Symbol> List all available warning categories
Warning.process(&block) block (Proc) Proc Install custom warning processor

Command Line Options

Option Description Example Usage
-W:strict_unused_block Enable strict unused block warnings ruby -W:strict_unused_block script.rb
-W:no-strict_unused_block Explicitly disable warnings ruby -W:no-strict_unused_block script.rb
-w -W:strict_unused_block Enable all warnings plus strict unused block ruby -w -W:strict_unused_block script.rb

Environment Variables

Variable Format Description
RUBYOPT "-W:strict_unused_block" Persistent warning configuration
RUBYOPT "-w -W:strict_unused_block -W:deprecated" Multiple warning categories

Block Usage Detection Rules

Scenario Warning Triggered Reason
Method never yields Yes Block completely unused
Method yields conditionally No Block may be used in some execution paths
Method checks block_given? but never yields Yes Block checked but never executed
Method accepts &block parameter Usually No Explicit block parameter declaration
Method stores block without calling Implementation-dependent May warn depending on usage pattern
super calls with block passing No Automatic block forwarding to parent method

Warning Message Format

filename:line_number: warning: the block passed to 'ClassName#method_name' defined at filename:line_number may be ignored

Warning Categories Hierarchy

Category Default State Ruby Version Purpose
:deprecated Enabled with -w 2.7+ Deprecated feature warnings
:experimental Enabled with -w 2.7+ Experimental feature warnings
:performance Disabled 3.3+ Performance-related warnings
:strict_unused_block Disabled 3.4+ Unused block detection

Common Method Patterns

# No warning - method yields
def yields_block
  yield
end

# No warning - conditional yielding
def conditional_yield(condition)
  yield if condition
end

# No warning - explicit block parameter
def explicit_block(&block)
  # Even if block unused
end

# Warning - checks but never uses
def checks_block
  puts block_given?
end

# Warning - completely ignores block
def ignores_block
  puts "done"
end

# No warning - block_given? with yield
def proper_usage
  yield if block_given?
end

Integration Examples

# RSpec configuration
RSpec.configure do |config|
  config.before(:suite) do
    Warning[:strict_unused_block] = true if ENV['STRICT_WARNINGS']
  end
end

# Rails initializer
# config/initializers/warnings.rb
if Rails.env.development? || Rails.env.test?
  Warning[:strict_unused_block] = true
end

# Gem development
# In gemspec or lib file
if $PROGRAM_NAME.end_with?('rspec') || ENV['GEM_DEVELOPMENT']
  Warning[:strict_unused_block] = true
end

Performance Characteristics

Context Overhead Impact Recommendation
Development ~2-5% Minimal Always enable
Testing ~1-3% Negligible Enable for thorough testing
Staging <1% Acceptable for debugging Enable selectively
Production Not recommended Potential performance impact Disable