# frozen_string_literal: true module RailsBestPractices module Prepares # Remember models and model associations. class ModelPrepare < Core::Check include Core::Check::Classable include Core::Check::Accessable interesting_nodes :class, :def, :defs, :command, :alias interesting_files MODEL_FILES ASSOCIATION_METHODS = %w[ belongs_to has_one has_many has_and_belongs_to_many embeds_many embeds_one embedded_in many one ].freeze def initialize @models = Prepares.models @model_associations = Prepares.model_associations @model_attributes = Prepares.model_attributes @methods = Prepares.model_methods end # remember the class name. add_callback :start_class do |_node| if current_extend_class_name != 'ActionMailer::Base' @models << @klass end end # check def node to remember all methods. # # the remembered methods (@methods) are like # { # "Post" => { # "save" => {"file" => "app/models/post.rb", "line_number" => 10, "unused" => false, "unused" => false}, # "find" => {"file" => "app/models/post.rb", "line_number" => 10, "unused" => false, "unused" => false} # }, # "Comment" => { # "create" => {"file" => "app/models/comment.rb", "line_number" => 10, "unused" => false, "unused" => false}, # } # } add_callback :start_def do |node| if @klass && current_extend_class_name != 'ActionMailer::Base' && (classable_modules.empty? || klasses.any?) method_name = node.method_name.to_s @methods.add_method( current_class_name, method_name, { 'file' => node.file, 'line_number' => node.line_number }, current_access_control ) end end # check defs node to remember all static methods. # # the remembered methods (@methods) are like # { # "Post" => { # "save" => {"file" => "app/models/post.rb", "line_number" => 10, "unused" => false, "unused" => false}, # "find" => {"file" => "app/models/post.rb", "line_number" => 10, "unused" => false, "unused" => false} # }, # "Comment" => { # "create" => {"file" => "app/models/comment.rb", "line_number" => 10, "unused" => false, "unused" => false}, # } # } add_callback :start_defs do |node| if @klass && current_extend_class_name != 'ActionMailer::Base' method_name = node.method_name.to_s @methods.add_method( current_class_name, method_name, { 'file' => node.file, 'line_number' => node.line_number }, current_access_control ) end end # check command node to remember all assoications or named_scope/scope methods. # # the remembered association names (@associations) are like # { # "Project" => { # "categories" => {"has_and_belongs_to_many" => "Category"}, # "project_manager" => {"has_one" => "ProjectManager"}, # "portfolio" => {"belongs_to" => "Portfolio"}, # "milestones => {"has_many" => "Milestone"} # } # } add_callback :start_command do |node| case node.message.to_s when 'named_scope', 'scope', 'alias_method' method_name = node.arguments.all.first.to_s @methods.add_method( current_class_name, method_name, { 'file' => node.file, 'line_number' => node.line_number }, current_access_control ) when 'alias_method_chain' method, feature = *node.arguments.all.map(&:to_s) @methods.add_method( current_class_name, "#{method}_with_#{feature}", { 'file' => node.file, 'line_number' => node.line_number }, current_access_control ) @methods.add_method( current_class_name, method.to_s, { 'file' => node.file, 'line_number' => node.line_number }, current_access_control ) when 'field' arguments = node.arguments.all attribute_name = arguments.first.to_s attribute_type = arguments.last.hash_value('type').present? ? arguments.last.hash_value('type').to_s : 'String' @model_attributes.add_attribute(current_class_name, attribute_name, attribute_type) when 'key' attribute_name, attribute_type = node.arguments.all.map(&:to_s) @model_attributes.add_attribute(current_class_name, attribute_name, attribute_type) when *ASSOCIATION_METHODS remember_association(node) end end # check alias node to remembr the alias methods. add_callback :start_alias do |node| method_name = node.new_method.to_s @methods.add_method( current_class_name, method_name, { 'file' => node.file, 'line_number' => node.line_number }, current_access_control ) end # after prepare process, fix incorrect associations' class_name. add_callback :after_check do @model_associations.each do |model, model_associations| model_associations.each do |_association_name, association_meta| unless @models.include?(association_meta['class_name']) if @models.include?("#{model}::#{association_meta['class_name']}") association_meta['class_name'] = "#{model}::#{association_meta['class_name']}" elsif @models.include?(model.gsub(/::\w+$/, '')) association_meta['class_name'] = model.gsub(/::\w+$/, '') end end end end end private # remember associations, with class to association names. def remember_association(node) association_meta = node.message.to_s association_name = node.arguments.all.first.to_s arguments_node = node.arguments.all.last if arguments_node.hash_value('class_name').present? association_class = arguments_node.hash_value('class_name').to_s end association_class ||= association_name.classify @model_associations.add_association(current_class_name, association_name, association_meta, association_class) end end end end