-
Notifications
You must be signed in to change notification settings - Fork 25
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[#499] Implement custom Rubocop rule to enforce class template
- Loading branch information
Showing
7 changed files
with
560 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
# frozen_string_literal: true | ||
|
||
require_relative 'class_template/expression_category' | ||
|
||
module CustomCops | ||
class ClassTemplate < RuboCop::Cop::Base | ||
include CustomCops::ExpressionCategory | ||
|
||
EXPRESSION_TYPE_ORDER = { | ||
extend: 0, | ||
include: 1, | ||
constant_assignment: 2, | ||
attribute_macro: 3, | ||
other_macro: 4, | ||
self_class_block: 5, | ||
public_class_method: 5, | ||
initialization: 6, | ||
public_instance_method: 7, | ||
alias: 8, | ||
alias_method: 8, | ||
protected_instance_method: 9, | ||
private_instance_method: 10 | ||
}.freeze | ||
|
||
def on_class(node) | ||
expressions = top_level_expressions(node) | ||
|
||
state = { function_visibility: :public, expression_types: [] } | ||
expressions.each_with_object(state) { |expression, acc| process_expression(expression, acc) } | ||
|
||
validate_expressions_order(state[:expression_types]) | ||
end | ||
|
||
private | ||
|
||
def top_level_expressions(class_node) | ||
return [] unless class_node.body | ||
|
||
# Multi-expression body | ||
return class_node.body.children if class_node.body.type == :begin | ||
|
||
# Single-expression body | ||
[class_node.body] | ||
end | ||
|
||
def process_expression(expression, acc) | ||
category = categorize(expression) | ||
|
||
if %i[protected private].include?(category) | ||
acc[:function_visibility] = category | ||
else | ||
category = "#{acc[:function_visibility]}_#{category}".to_sym if category == :instance_method | ||
acc[:expression_types] << { category: category, expression: expression } | ||
end | ||
end | ||
|
||
def validate_expressions_order(expressions) | ||
expressions = expressions.filter { _1[:category] != :unknown } | ||
|
||
expressions.each_cons(2) do |first, second| | ||
next unless EXPRESSION_TYPE_ORDER[first[:category]] > EXPRESSION_TYPE_ORDER[second[:category]] | ||
|
||
add_offense( | ||
second[:expression], | ||
message: error_message(second[:category], expressions.map { _1[:category] }) | ||
) | ||
end | ||
end | ||
|
||
# Find the correct spot for the out of order expression | ||
# rubocop:disable Metrics/MethodLength | ||
def error_message(out_of_order_expression, expression_types) | ||
expression_types.filter { _1 != out_of_order_expression } | ||
.each_with_index do |expression, index| | ||
error = if index.zero? | ||
before_first_expression(expression, out_of_order_expression) | ||
elsif index == expression_types.size - 1 | ||
after_last_expression(expression, out_of_order_expression) | ||
else | ||
previous_expression = expression_types[index - 1] | ||
in_between_expressions(previous_expression, expression, out_of_order_expression) | ||
end | ||
|
||
return error if error | ||
end | ||
end | ||
# rubocop:enable Metrics/MethodLength | ||
|
||
def before_first_expression(current_expression, out_of_order_expression) | ||
return unless EXPRESSION_TYPE_ORDER[out_of_order_expression] < EXPRESSION_TYPE_ORDER[current_expression] | ||
|
||
"#{out_of_order_expression} should come before #{current_expression}." | ||
end | ||
|
||
def after_last_expression(current_expression, out_of_order_expression) | ||
return unless EXPRESSION_TYPE_ORDER[out_of_order_expression] > EXPRESSION_TYPE_ORDER[current_expression] | ||
|
||
"#{out_of_order_expression} should come after #{current_expression}." | ||
end | ||
|
||
def in_between_expressions(previous_expression, current_expression, out_of_order_expression) | ||
return unless EXPRESSION_TYPE_ORDER[out_of_order_expression] < EXPRESSION_TYPE_ORDER[current_expression] && | ||
EXPRESSION_TYPE_ORDER[out_of_order_expression] > EXPRESSION_TYPE_ORDER[previous_expression] | ||
|
||
"#{out_of_order_expression} should come after #{previous_expression} and before #{current_expression}." | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
# frozen_string_literal: true | ||
|
||
require 'rubocop' | ||
|
||
module CustomCops | ||
module ExpressionCategory | ||
extend RuboCop::NodePattern::Macros | ||
|
||
ATTRIBUTE_MACROS = %i[attr_reader attr_writer attr_accessor].to_set | ||
|
||
CATEGORIES = %i[ | ||
extend include constant_assignment attribute_macro alias_method protected private | ||
other_macro self_class_block public_class_method initialization instance_method alias | ||
].freeze | ||
|
||
# extend SomeModule | ||
def_node_matcher :extend?, <<~PATTERN | ||
(send {nil? | self} :extend const) | ||
PATTERN | ||
|
||
# include SomeModule | ||
def_node_matcher :include?, <<~PATTERN | ||
(send {nil? | self} :include const) | ||
PATTERN | ||
|
||
# HELLO = 'world'.freeze | ||
def_node_matcher :constant_assignment?, <<~PATTERN | ||
(casgn nil? _ _) | ||
PATTERN | ||
|
||
# attr_reader :foo, :bar | ||
# attr_writer :baz | ||
def_node_matcher :attribute_macro?, <<~PATTERN | ||
(send {nil? | self} ATTRIBUTE_MACROS sym+) | ||
PATTERN | ||
|
||
# validates :foo, presence: true | ||
def_node_matcher :other_macro?, <<~PATTERN | ||
(send {nil? | self} _ _+) | ||
PATTERN | ||
|
||
# class << self | ||
# def foo | ||
# 'bar' | ||
# end | ||
# end | ||
def_node_matcher :self_class_block?, <<~PATTERN | ||
(sclass self _) | ||
PATTERN | ||
|
||
# def self.foo | ||
# "bar" | ||
# end | ||
def_node_matcher :public_class_method?, <<~PATTERN | ||
(defs self _ args _) | ||
PATTERN | ||
|
||
# def initialize(a, b) | ||
# end | ||
def_node_matcher :initialization?, <<~PATTERN | ||
(def :initialize args _) | ||
PATTERN | ||
|
||
# def method(a, b) | ||
# end | ||
def_node_matcher :instance_method?, <<~PATTERN | ||
(def _ args _) | ||
PATTERN | ||
|
||
# alias foo bar | ||
# alias :foo :bar | ||
def_node_matcher :alias?, <<~PATTERN | ||
(alias sym sym) | ||
PATTERN | ||
|
||
# alias_method :foo, :bar | ||
# alias_method 'foo', 'bar' | ||
def_node_matcher :alias_method?, <<~PATTERN | ||
(send {nil? | self} :alias_method _ _) | ||
PATTERN | ||
|
||
# protected | ||
def_node_matcher :protected?, <<~PATTERN | ||
(send {nil? | self} :protected) | ||
PATTERN | ||
|
||
# private | ||
def_node_matcher :private?, <<~PATTERN | ||
(send {nil? | self} :private) | ||
PATTERN | ||
|
||
def categorize(expression) | ||
CATEGORIES.find { |category| send("#{category}?", expression) } || :unknown | ||
end | ||
end | ||
end |
Oops, something went wrong.