<commit_before>require_relative 'gem_manager'

require 'active_support/descendants_tracker'
require 'active_support/core_ext/class/subclasses'
require 'active_support/core_ext/string/strip'

module RailsRoutesAnalyzer
  MAX_ACTION_LENGTH = 30 # Don't align action names longer than this

  class ActionMethod < Hash
    # is_inherited - action is inherited from a parent controller
    # from_gem     - action is implemented in a gem
    # from_module  - action implementation is in a module
    %i[
      controller_name
      action_name
      route_missing
      source_location
      is_inherited
      from_gem
      from_module
      owner
    ].each do |name|
      define_method(name) { self[name] }
    end

    def initialize(opts={})
      self.update(opts)
    end

    def controller_class
      @controller_class ||= controller_name.constantize
    end

    def needs_reporting?(report_duplicates:, report_gems:, report_modules:, report_routed:, **)
      (route_missing?     || report_routed)     \
        && (!inherited?   || report_duplicates) \
        && (!from_gem?    || report_gems)       \
        && (!from_module? || report_modules)
    end

    def pretty(max_action_length: MAX_ACTION_LENGTH, metadata: false, **)
      ("%-#{max_action_length}s @ %s" % [ action_name, source_location ]).tap do |result|
        if metadata
          result << " "
          result << pretty_metadata
        end
      end
    end

    def pretty_metadata
      [
        route_missing? ? "no-route"                : nil,
        inherited?     ? "inherited:#{owner.name}" : nil,
        from_gem?      ? "gem:#{from_gem}"         : nil,
        from_module?   ? "module:#{owner.name}"    : nil,
      ].compact.join(' ')
    end

    alias_method :inherited?,     :is_inherited
    alias_method :route_missing?, :route_missing
    alias_method :from_gem?,      :from_gem
    alias_method :from_module?,   :from_module
  end

  class ActionAnalysis

    attr_reader :all_action_methods, :unused_controllers, :options

    # Options:
    #  report_duplicates - report actions which the parent controller also has
    #  report_gems       - report actions which are implemented by a gem
    #  report_modules    - report actions inherited from modules
    #  report_routed     - report all actions including those with a route
    #  full_path         - skips shortening file paths
    #  metadata          - include discovered metadata about actions
    def initialize(route_analysis: RailsRoutesAnalyzer::RouteAnalysis.new, **options)
      @options = options

      @all_action_methods = analyze_action_methods(route_analysis: route_analysis)

      @by_controller = @all_action_methods.group_by(&:controller_class)
      @unused_controllers = find_unused_controllers
    end

    def unused_actions_present?
      @all_action_methods.any? do |action|
        action.needs_reporting?(**options)
      end
    end

    def report_actions
      @controller_reporting_cache = {}

      report_actions_recursive
    end

    def actions_for(controller)
      @by_controller[controller] || []
    end

    def actions_to_report(controller)
      actions_for(controller).select do |action|
        action.needs_reporting?(**options)
      end.sort_by(&:action_name)
    end

    def report_actions_recursive(controllers = ActionController::Base.subclasses, level = 0)
      controllers.each do |controller|
        next unless controller_needs_reporting?(controller)

        puts "#{'  ' * level}#{controller.name}"

        if (actions = actions_to_report(controller)).any?
          action_level = level + 1

          if controller.subclasses.any? { |subclass| controller_needs_reporting?(subclass) }
            puts "#{'  ' * action_level}Actions:"
            action_level += 1
          end

          max_action_length = [MAX_ACTION_LENGTH, actions.map {|a| a.action_name.size }.max].min

          actions.each do |action|
            puts "#{'  '*action_level}#{action.pretty(max_action_length: max_action_length, **options)}"
          end
        end

        report_actions_recursive(controller.subclasses.sort_by(&:name), level + 1)
      end
    end

    def controller_likely_from_gem?(controller)
      actions_for(controller).all? { |action| action.from_gem? || action.from_module? || action.inherited? }
    end

    def controller_needs_reporting?(controller)
      if @controller_reporting_cache.has_key?(controller)
        return @controller_reporting_cache[controller]
      end

      @controller_reporting_cache[controller] = \
        actions_to_report(controller).any? \
        || controller.subclasses.any? { |subclass| controller_needs_reporting?(subclass) }
    end

    def find_unused_controllers
      ActionController::Base.descendants.select do |controller|
        controller_has_no_routes?(controller) && !(controller <= ::Rails::ApplicationController) \
          && (options[:report_gems] || !controller_likely_from_gem?(controller))
      end
    end

    def no_direct_routes_to?(controller)
      actions_for(controller).all?(&:route_missing?)
    end

    def controller_has_no_routes?(controller)
      no_direct_routes_to?(controller) \
        && controller.subclasses.all? { |c| controller_has_no_routes?(c) }
    end

    def analyze_action_methods(route_analysis:)
      implemented_routes = Set.new(route_analysis.implemented_routes)<commit_msg>Don't rely on Rails::ApplicationController existing<commit_after>require_relative 'gem_manager'

require 'active_support/descendants_tracker'
require 'active_support/core_ext/class/subclasses'
require 'active_support/core_ext/string/strip'

module RailsRoutesAnalyzer
  MAX_ACTION_LENGTH = 30 # Don't align action names longer than this

  class ActionMethod < Hash
    # is_inherited - action is inherited from a parent controller
    # from_gem     - action is implemented in a gem
    # from_module  - action implementation is in a module
    %i[
      controller_name
      action_name
      route_missing
      source_location
      is_inherited
      from_gem
      from_module
      owner
    ].each do |name|
      define_method(name) { self[name] }
    end

    def initialize(opts={})
      self.update(opts)
    end

    def controller_class
      @controller_class ||= controller_name.constantize
    end

    def needs_reporting?(report_duplicates:, report_gems:, report_modules:, report_routed:, **)
      (route_missing?     || report_routed)     \
        && (!inherited?   || report_duplicates) \
        && (!from_gem?    || report_gems)       \
        && (!from_module? || report_modules)
    end

    def pretty(max_action_length: MAX_ACTION_LENGTH, metadata: false, **)
      ("%-#{max_action_length}s @ %s" % [ action_name, source_location ]).tap do |result|
        if metadata
          result << " "
          result << pretty_metadata
        end
      end
    end

    def pretty_metadata
      [
        route_missing? ? "no-route"                : nil,
        inherited?     ? "inherited:#{owner.name}" : nil,
        from_gem?      ? "gem:#{from_gem}"         : nil,
        from_module?   ? "module:#{owner.name}"    : nil,
      ].compact.join(' ')
    end

    alias_method :inherited?,     :is_inherited
    alias_method :route_missing?, :route_missing
    alias_method :from_gem?,      :from_gem
    alias_method :from_module?,   :from_module
  end

  class ActionAnalysis

    attr_reader :all_action_methods, :unused_controllers, :options

    # Options:
    #  report_duplicates - report actions which the parent controller also has
    #  report_gems       - report actions which are implemented by a gem
    #  report_modules    - report actions inherited from modules
    #  report_routed     - report all actions including those with a route
    #  full_path         - skips shortening file paths
    #  metadata          - include discovered metadata about actions
    def initialize(route_analysis: RailsRoutesAnalyzer::RouteAnalysis.new, **options)
      @options = options

      @all_action_methods = analyze_action_methods(route_analysis: route_analysis)

      @by_controller = @all_action_methods.group_by(&:controller_class)
      @unused_controllers = find_unused_controllers
    end

    def unused_actions_present?
      @all_action_methods.any? do |action|
        action.needs_reporting?(**options)
      end
    end

    def report_actions
      @controller_reporting_cache = {}

      report_actions_recursive
    end

    def actions_for(controller)
      @by_controller[controller] || []
    end

    def actions_to_report(controller)
      actions_for(controller).select do |action|
        action.needs_reporting?(**options)
      end.sort_by(&:action_name)
    end

    def report_actions_recursive(controllers = ActionController::Base.subclasses, level = 0)
      controllers.each do |controller|
        next unless controller_needs_reporting?(controller)

        puts "#{'  ' * level}#{controller.name}"

        if (actions = actions_to_report(controller)).any?
          action_level = level + 1

          if controller.subclasses.any? { |subclass| controller_needs_reporting?(subclass) }
            puts "#{'  ' * action_level}Actions:"
            action_level += 1
          end

          max_action_length = [MAX_ACTION_LENGTH, actions.map {|a| a.action_name.size }.max].min

          actions.each do |action|
            puts "#{'  '*action_level}#{action.pretty(max_action_length: max_action_length, **options)}"
          end
        end

        report_actions_recursive(controller.subclasses.sort_by(&:name), level + 1)
      end
    end

    def controller_likely_from_gem?(controller)
      actions_for(controller).all? { |action| action.from_gem? || action.from_module? || action.inherited? }
    end

    def controller_needs_reporting?(controller)
      if @controller_reporting_cache.has_key?(controller)
        return @controller_reporting_cache[controller]
      end

      @controller_reporting_cache[controller] = \
        actions_to_report(controller).any? \
        || controller.subclasses.any? { |subclass| controller_needs_reporting?(subclass) }
    end

    def find_unused_controllers
      ActionController::Base.descendants.select do |controller|
        controller_has_no_routes?(controller) \
          && !(defined?(::Rails::ApplicationController) && controller <= ::Rails::ApplicationController) \
          && (options[:report_gems] || !controller_likely_from_gem?(controller))
      end
    end

    def no_direct_routes_to?(controller)
      actions_for(controller).all?(&:route_missing?)
    end

    def controller_has_no_routes?(controller)
      no_direct_routes_to?(controller) \
        && controller.subclasses.all? { |c| controller_has_no_routes?(c) }
    end

    def analyze_action_methods(route_analysis:)
      implemented_routes = Set.new(route_analysis.implemented_routes)
{'fraction_non_alphanumeric': 0.09438635112823335, 'fraction_numerical': 0.001100715465052284, 'mean_word_length': 2.9719489981785063, 'pattern_counts': {'":': 0, '<': 15, '<?xml version=': 0, '>': 3, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2130332', 'n_tokens_mistral': 3230, 'n_tokens_neox': 3141, 'n_words': 638}