class Modelist::CircularRefChecker

Public Class Methods

test_model(model_class, results = nil, model_and_association_names = []) click to toggle source

Get hash of circular reference information about associations tree on model specified, e.g.

Modelist::CircularRefChecker.test_model(:my_model)

Also can take model class:

Modelist::CircularRefChecker.test_model(MyModel)
# File lib/modelist/circular_ref_checker.rb, line 113
def self.test_model(model_class, results = nil, model_and_association_names = [])
  model_class = model_class.to_s.camelize.constantize unless model_class.is_a?(Class)

  results ||= {}
  results[:offenders] ||= []
  results[:circles_sorted] ||= []
  results[:circles] ||= []
  results[:selected_offenders] ||= []

  model_class.reflections.collect {|association_name, reflection|
    puts "warning: #{model_class}'s association #{reflection.name}'s foreign_key was nil. can't check." unless reflection.foreign_key || Modelist.quiet?
    assc_sym = reflection.name.to_sym
    
    begin
      next_class = reflection.class_name.constantize
    rescue => e
      puts "Problem in #{model_class.name} with association: #{reflection.macro} #{assc_sym.inspect} which refers to class #{reflection.class_name}" unless Modelist.quiet?
      raise e
    end

    has_presence_validator = model_class.validators_on(assc_sym).collect{|v|v.class}.include?(ActiveModel::Validations::PresenceValidator)
    required = false
    if reflection.macro == :belongs_to
      # note: supports composite_primary_keys gem which stores primary_key as an array
      foreign_key_is_also_primary_key = Array.wrap(model_class.primary_key).collect{|pk|pk.to_sym}.include?(reflection.foreign_key.to_sym)
      is_not_null_fkey_that_is_not_primary_key = model_class.columns.any?{|c| !c.null && c.name.to_sym == reflection.foreign_key.to_sym && !foreign_key_is_also_primary_key}
      required = is_not_null_fkey_that_is_not_primary_key || has_presence_validator
    else
      # no nullable metadata on column if no foreign key in this table. we'd figure out the null requirement on the column if inspecting the child model
      required = has_presence_validator
    end

    if required
      key = [model_class.table_name.to_sym, reflection.foreign_key.to_sym, next_class.table_name.to_sym]
      if model_and_association_names.include?(key)
        results[:offenders] << model_and_association_names.last unless results[:offenders].include?(model_and_association_names.last)
        short = model_and_association_names.dup
        # drop all preceding keys that have nothing to do with the circle
        (short.index(key)).times {short.delete_at(0)}
        sorted = short.sort
        unless results[:circles_sorted].include?(sorted)
          results[:circles_sorted] << sorted
          results[:circles] << "#{(short + [key]).collect{|b|"#{b[0]}.#{b[1]}"}.join(' -> ')}".to_sym
        end
      else
        model_and_association_names << key
        test_model(next_class, results, model_and_association_names)
      end
    end
  }

  model_and_association_names.pop

  results
end
test_models(*args) click to toggle source

Check refs on all models or models specified, e.g.

Modelist::CircularRefChecker.test_models

or

Modelist::CircularRefChecker.test_models(:my_model, :some_other_model)
# File lib/modelist/circular_ref_checker.rb, line 8
def self.test_models(*args)
  # less-dependent extract_options!
  options = args.last.is_a?(Hash) ? args.pop : {}
  results = {}
  models = []
  included_models = args.compact.collect{|m|m.to_sym}
  puts "Checking models: #{included_models.collect{|m|m.inspect}.join(', ')}" if !Modelist.quiet? && included_models.size > 0
  Dir[File.join('app','models','*.rb').to_s].each do |filename|
    model_name = File.basename(filename).sub(/.rb$/, '')
    next if included_models.size > 0 && !included_models.include?(model_name.to_sym)
    load File.join('app','models',"#{model_name}.rb")
    
    begin
      model_class = model_name.camelize.constantize
    rescue => e
      puts "Problem in #{model_name.camelize}" unless Modelist.quiet?
      raise e
    end

    next unless model_class.ancestors.include?(ActiveRecord::Base)
    models << model_class
  end

  models.each do |model_class|
    test_model(model_class, results)
  end

  if results[:circles].nil? || results[:circles].size == 0
    unless Modelist.quiet?
      puts
      puts "No circular dependencies."
      puts
    end
    return true
  end

  if !Modelist.quiet? || options[:output_file]
    totals = {}
    results[:circles_sorted].each do |arr|
      arr.each do |key|
        totals[key] = 0 unless totals[key]
        totals[key] = totals[key] + 1
      end
    end

    unless Modelist.quiet?
      puts  "The following non-nullable foreign keys used in ActiveRecord model associations are involved in circular dependencies:"
      results[:circles].sort.each do |c|
        puts
        puts "#{c}"
      end
      puts
      puts
      puts "Distinct foreign keys involved in a circular dependency:"
      puts
      results[:offenders].sort.each do |c|
        puts "#{c[0]}.#{c[1]}"
      end
      puts
      puts
      puts "Foreign keys by number of circular dependency chains involved with:"
      puts
      totals.sort_by {|k,v| v}.reverse.each do |arr|
        c = arr[0]
        t = arr[1]
        puts "#{t} (out of #{results[:circles_sorted].size}): #{c[0]}.#{c[1]} -> #{c[2]}"
      end
      puts
    end

    if options[:output_file]
      File.open(options[:output_file], "w") do |f|
        f.puts  "The following non-nullable foreign keys used in ActiveRecord model associations are involved in circular dependencies:"
        results[:circles].sort.each do |c|
          f.puts
          f.puts "#{c}"
        end
        f.puts
        f.puts
        f.puts "Distinct foreign keys involved in a circular dependency:"
        f.puts
        results[:offenders].sort.each do |c|
          f.puts "#{c[0]}.#{c[1]}"
        end
        f.puts
        f.puts
        f.puts "Foreign keys by number of circular dependency chains involved with:"
        f.puts
        totals.sort_by {|k,v| v}.reverse.each do |arr|
          c = arr[0]
          t = arr[1]
          f.puts "#{t} (out of #{results[:circles_sorted].size}): #{c[0]}.#{c[1]} -> #{c[2]}"
        end
        f.puts
      end
    end
  end

  return false
end