# bundle exec rubocop -d -r ./path/to/this/cop.rb --only DarkFinger/ModelStructure ./app/models/foo.rb # # TODO: # # * Constructors # * Handle "Misc" stuff require File.dirname(__FILE__) + '/active_model_node_decorator' module RuboCop module Cop module DarkFinger class ModelStructure < ::RuboCop::Cop::Cop ASSOCIATION = :association ATTRIBUTES = :attribute CALLBACK = :callback CLASS_METHOD = :class_method CONSTANT = :constant ENUM = :enum INCLUDE = :include INSTANCE_METHOD = :instance_method MODULE = :module SCOPE = :scope VALIDATION = :validation DEFAULT_REQUIRED_ORDER = [ MODULE, INCLUDE, ENUM, CONSTANT, ASSOCIATION, VALIDATION, SCOPE, ATTRIBUTES, CALLBACK, CLASS_METHOD, INSTANCE_METHOD, ] DEFAULT_REQUIRED_COMMENTS = { ASSOCIATION => 'Relationships', ATTRIBUTES => 'Attributes', CALLBACK => 'Callbacks', CONSTANT => 'Constants', ENUM => 'Enums', INCLUDE => 'Includes', MODULE => 'Modules', SCOPE => 'Scopes', VALIDATION => 'Validations' } attr_reader :required_order, :required_comments def initialize(*args, required_order: nil, required_comments: nil, **_) super @class_elements_seen = [] @required_order = required_order || cop_config['required_order'] || DEFAULT_REQUIRED_ORDER @required_comments = required_comments || cop_config['required_comments'] || DEFAULT_REQUIRED_COMMENTS # symbolize order/comments @required_order.map!(&:to_sym) @required_comments = Hash[ @required_comments.map {|k,v| [k.to_sym, v]} ] end def on_send(node) process_node(node) end def on_casgn(node) process_node(node, seen_element: CONSTANT) end def on_module(node) process_node(node, seen_element: MODULE) end def on_def(node) process_node(node, seen_element: INSTANCE_METHOD) end def on_defs(node) process_node(node, seen_element: CLASS_METHOD) end private attr_reader :class_elements_seen def process_node(node, seen_element: nil) return if @order_violation_reported node = ActiveModelNodeDecorator.new(node) seen_element ||= node.node_type return unless seen_element return if node.ignore_due_to_nesting? if first_time_seeing?(seen_element) detect_comment_violation(node, seen_element) end seen(seen_element) detect_order_violation(node) end def seen(class_element) return unless required_order.include?(class_element) class_elements_seen << class_element class_elements_seen.compact! end def first_time_seeing?(class_element) !class_elements_seen.include?(class_element) end def detect_comment_violation(node, class_element) return false unless required_comments[class_element] comment = node.preceeding_comment(processed_source) unless comment && comment =~ Regexp.new(%Q(^\s*##\s*#{required_comments[class_element]} ##$)) add_offense(node, message: "Expected preceeding comment: \"## #{required_comments[class_element]} ##\"") end end def detect_order_violation(node) if order_violation_detected? @order_violation_reported = true add_offense( node, message: "Model elements must appear in order: \n\n* #{required_order.join("\n* ")}\n".freeze ) end end def order_violation_detected? required_order_for_elements_seen != class_elements_seen end def required_order_for_elements_seen class_elements_seen.sort do |class_elem_1, class_elem_2| required_order.index(class_elem_1) <=> required_order.index(class_elem_2) end end end end end end