# encoding: utf-8 require 'rails_best_practices/reviews/review' module RailsBestPractices module Reviews # Review a route file to make sure all auto-generated routes have corresponding actions in controller. # # See the best practice details here http://rails-bestpractices.com/posts/86-restrict-auto-generated-routes # # Implementation: # # Review process: # check all resources and resource method calls, # compare the generated routes and corresponding actions in controller, # if there is a route generated, but there is not action in that controller, # then you should restrict your routes. class RestrictAutoGeneratedRoutesReview < Review interesting_nodes :command, :command_call, :method_add_block interesting_files ROUTE_FILES url "http://rails-bestpractices.com/posts/86-restrict-auto-generated-routes" RESOURCE_METHODS = ["show", "new", "create", "edit", "update", "destroy"] RESOURCES_METHODS = RESOURCE_METHODS + ["index"] def initialize super @namespaces = [] @resource_controllers = [] end # check if the generated routes have the corresponding actions in controller for rails routes. add_callback :start_command, :start_command_call do |node| if "resources" == node.message.to_s check_resources(node) @resource_controllers << node.arguments.all.first.to_s elsif "resource" == node.message.to_s check_resource(node) @resource_controllers << node.arguments.all.first.to_s end end add_callback :end_command do |node| if "resources" == node.message.to_s @resource_controllers.pop elsif "resource" == node.message.to_s @resource_controllers.pop end end # remember the namespace. add_callback :start_method_add_block do |node| case node.message.to_s when "namespace" @namespaces << node.arguments.all.first.to_s if check_method_add_block?(node) when "resources", "resource" @resource_controllers << node.arguments.all.first.to_s if check_method_add_block?(node) else end end # end of namespace call. add_callback :end_method_add_block do |node| if check_method_add_block?(node) case node.message.to_s when "namespace" @namespaces.pop when "resources", "resource" @resource_controllers.pop end end end def check_method_add_block?(node) :command == node[1].sexp_type || (:command_call == node[1].sexp_type && "map" != node.receiver.to_s) end private # check resources call, if the routes generated by resources does not exist in the controller. def check_resources(node) controller_name = controller_name(node) return unless Prepares.controllers.include? controller_name resources_methods = resources_methods(node) unless resources_methods.all? { |meth| Prepares.controller_methods.has_method?(controller_name, meth) } prepared_method_names = Prepares.controller_methods.get_methods(controller_name).map(&:method_name) only_methods = (resources_methods & prepared_method_names).map { |meth| ":#{meth}" }.join(", ") add_error "restrict auto-generated routes #{friendly_route_name(node)} (only: [#{only_methods}])" end end # check resource call, if the routes generated by resources does not exist in the controller. def check_resource(node) controller_name = controller_name(node) return unless Prepares.controllers.include? controller_name resource_methods = resource_methods(node) unless resource_methods.all? { |meth| Prepares.controller_methods.has_method?(controller_name, meth) } prepared_method_names = Prepares.controller_methods.get_methods(controller_name).map(&:method_name) only_methods = (resource_methods & prepared_method_names).map { |meth| ":#{meth}" }.join(", ") add_error "restrict auto-generated routes #{friendly_route_name(node)} (only: [#{only_methods}])" end end # get the controller name. def controller_name(node) if option_with_hash(node) option_node = node.arguments.all[1] if hash_key_exist?(option_node,"controller") name = option_node.hash_value("controller").to_s else name = node.arguments.all.first.to_s.gsub("::", "").tableize end else name = node.arguments.all.first.to_s.gsub("::", "").tableize end namespaced_class_name(name) end # get the class name with namespace. def namespaced_class_name(name) class_name = "#{name.split("/").map(&:camelize).join("::")}Controller" if @namespaces.empty? class_name else @namespaces.map { |namespace| "#{namespace.camelize}::" }.join("") + class_name end end # get the route actions that should be generated by resources call. def resources_methods(node) methods = RESOURCES_METHODS if option_with_hash(node) option_node = node.arguments.all[1] if hash_key_exist?(option_node, "only") option_node.hash_value("only").to_s == "none" ? [] : Array(option_node.hash_value("only").to_object) elsif hash_key_exist?(option_node, "except") if option_node.hash_value("except").to_s == "all" [] else (methods - Array(option_node.hash_value("except").to_object)) end else methods end else methods end end # get the route actions that should be generated by resource call. def resource_methods(node) methods = RESOURCE_METHODS if option_with_hash(node) option_node = node.arguments.all[1] if hash_key_exist?(option_node, "only") option_node.hash_value("only").to_s == "none" ? [] : Array(option_node.hash_value("only").to_object) elsif hash_key_exist?(option_node, "except") if option_node.hash_value("except").to_s == "all" [] else (methods - Array(option_node.hash_value("except").to_object)) end else methods end else methods end end def option_with_hash(node) node.arguments.all.size > 1 && :bare_assoc_hash == node.arguments.all[1].sexp_type end def hash_key_exist?(node, key) node.hash_keys && node.hash_keys.include?(key) end def friendly_route_name(node) if @resource_controllers.last == node.arguments.to_s [@namespaces.join("/"), @resource_controllers.join("/")].delete_if(&:blank?).join("/") else [@namespaces.join("/"), @resource_controllers.join("/"), node.arguments.to_s].delete_if(&:blank?).join("/") end end end end end