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