PageRenderTime 31ms CodeModel.GetById 2ms app.highlight 26ms RepoModel.GetById 1ms app.codeStats 0ms

/lib/rails_best_practices/reviews/restrict_auto_generated_routes_review.rb

http://github.com/flyerhzm/rails_best_practices
Ruby | 195 lines | 155 code | 21 blank | 19 comment | 38 complexity | 5d50bc0e13a55957b4c80c692ae0b01b MD5 | raw file
  1# frozen_string_literal: true
  2
  3module RailsBestPractices
  4  module Reviews
  5    # Review a route file to make sure all auto-generated routes have corresponding actions in controller.
  6    #
  7    # See the best practice details here https://rails-bestpractices.com/posts/2011/08/19/restrict-auto-generated-routes/
  8    #
  9    # Implementation:
 10    #
 11    # Review process:
 12    #   check all resources and resource method calls,
 13    #   compare the generated routes and corresponding actions in controller,
 14    #   if there is a route generated, but there is not action in that controller,
 15    #   then you should restrict your routes.
 16    class RestrictAutoGeneratedRoutesReview < Review
 17      interesting_nodes :command, :command_call, :method_add_block
 18      interesting_files ROUTE_FILES
 19      url 'https://rails-bestpractices.com/posts/2011/08/19/restrict-auto-generated-routes/'
 20
 21      def resource_methods
 22        if Prepares.configs['config.api_only']
 23          %w[show create update destroy]
 24        else
 25          %w[show new create edit update destroy]
 26        end
 27      end
 28
 29      def resources_methods
 30        resource_methods + ['index']
 31      end
 32
 33      def initialize(options = {})
 34        super(options)
 35        @namespaces = []
 36        @resource_controllers = []
 37      end
 38
 39      # check if the generated routes have the corresponding actions in controller for rails routes.
 40      add_callback :start_command, :start_command_call do |node|
 41        if node.message.to_s == 'resources'
 42          if (mod = module_option(node))
 43            @namespaces << mod
 44          end
 45          check_resources(node)
 46          @resource_controllers << node.arguments.all.first.to_s
 47        elsif node.message.to_s == 'resource'
 48          check_resource(node)
 49          @resource_controllers << node.arguments.all.first.to_s
 50        end
 51      end
 52
 53      add_callback :end_command do |node|
 54        if node.message.to_s == 'resources'
 55          @resource_controllers.pop
 56          @namespaces.pop if module_option(node)
 57        elsif node.message.to_s == 'resource'
 58          @resource_controllers.pop
 59        end
 60      end
 61
 62      # remember the namespace.
 63      add_callback :start_method_add_block do |node|
 64        case node.message.to_s
 65        when 'namespace'
 66          @namespaces << node.arguments.all.first.to_s if check_method_add_block?(node)
 67        when 'resources', 'resource'
 68          @resource_controllers << node.arguments.all.first.to_s if check_method_add_block?(node)
 69        when 'scope'
 70          if check_method_add_block?(node) && (mod = module_option(node))
 71            @namespaces << mod
 72          end
 73        end
 74      end
 75
 76      # end of namespace call.
 77      add_callback :end_method_add_block do |node|
 78        if check_method_add_block?(node)
 79          case node.message.to_s
 80          when 'namespace'
 81            @namespaces.pop
 82          when 'resources', 'resource'
 83            @resource_controllers.pop
 84          when 'scope'
 85            if check_method_add_block?(node) && module_option(node)
 86              @namespaces.pop
 87            end
 88          end
 89        end
 90      end
 91
 92      def check_method_add_block?(node)
 93        node[1].sexp_type == :command || (node[1].sexp_type == :command_call && node.receiver.to_s != 'map')
 94      end
 95
 96      private
 97
 98      # check resources call, if the routes generated by resources does not exist in the controller.
 99      def check_resources(node)
100        _check(node, resources_methods)
101      end
102
103      # check resource call, if the routes generated by resources does not exist in the controller.
104      def check_resource(node)
105        _check(node, resource_methods)
106      end
107
108      # get the controller name.
109      def controller_name(node)
110        if option_with_hash(node)
111          option_node = node.arguments.all[1]
112          name =
113            if hash_key_exist?(option_node, 'controller')
114              option_node.hash_value('controller').to_s
115            else
116              node.arguments.all.first.to_s.gsub('::', '').tableize
117            end
118        else
119          name = node.arguments.all.first.to_s.gsub('::', '').tableize
120        end
121        namespaced_class_name(name)
122      end
123
124      # get the class name with namespace.
125      def namespaced_class_name(name)
126        class_name = "#{name.split('/').map(&:camelize).join('::')}Controller"
127        if @namespaces.empty?
128          class_name
129        else
130          @namespaces.map { |namespace| "#{namespace.camelize}::" }.join('') + class_name
131        end
132      end
133
134      def _check(node, methods)
135        controller_name = controller_name(node)
136        return unless Prepares.controllers.include? controller_name
137
138        _methods = _methods(node, methods)
139        unless _methods.all? { |meth| Prepares.controller_methods.has_method?(controller_name, meth) }
140          prepared_method_names = Prepares.controller_methods.get_methods(controller_name).map(&:method_name)
141          only_methods = (_methods & prepared_method_names).map { |meth| ":#{meth}" }
142          routes_message =
143            if only_methods.size > 3
144              "except: [#{(methods.map { |meth| ':' + meth } - only_methods).join(', ')}]"
145            else
146              "only: [#{only_methods.join(', ')}]"
147            end
148          add_error "restrict auto-generated routes #{friendly_route_name(node)} (#{routes_message})"
149        end
150      end
151
152      def _methods(node, methods)
153        if option_with_hash(node)
154          option_node = node.arguments.all[1]
155          if hash_key_exist?(option_node, 'only')
156            option_node.hash_value('only').to_s == 'none' ? [] : Array(option_node.hash_value('only').to_object)
157          elsif hash_key_exist?(option_node, 'except')
158            if option_node.hash_value('except').to_s == 'all'
159              []
160            else
161              (methods - Array(option_node.hash_value('except').to_object))
162            end
163          else
164            methods
165          end
166        else
167          methods
168        end
169      end
170
171      def module_option(node)
172        option_node = node.arguments[1].last
173        if option_node && option_node.sexp_type == :bare_assoc_hash && hash_key_exist?(option_node, 'module')
174          option_node.hash_value('module').to_s
175        end
176      end
177
178      def option_with_hash(node)
179        node.arguments.all.size > 1 && node.arguments.all[1].sexp_type == :bare_assoc_hash
180      end
181
182      def hash_key_exist?(node, key)
183        node.hash_keys&.include?(key)
184      end
185
186      def friendly_route_name(node)
187        if @resource_controllers.last == node.arguments.to_s
188          [@namespaces.join('/'), @resource_controllers.join('/')].delete_if(&:blank?).join('/')
189        else
190          [@namespaces.join('/'), @resource_controllers.join('/'), node.arguments.to_s].delete_if(&:blank?).join('/')
191        end
192      end
193    end
194  end
195end