-
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
6 changed files
with
567 additions
and
0 deletions.
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,94 @@ | ||
# 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: 7, | ||
alias_method: 7, | ||
protected_instance_method: 8, | ||
private_instance_method: 9 | ||
}.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 = category == :instance_method ? "#{acc[:function_visibility]}_#{category}".to_sym : category | ||
acc[:expression_types] << { category:, 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 | ||
def error_message(out_of_order_expression, expression_types) | ||
expression_types = expression_types.filter { _1 != out_of_order_expression } | ||
out_of_order = EXPRESSION_TYPE_ORDER[out_of_order_expression] | ||
|
||
expression_types.each_with_index do |expression, index| | ||
# Before first expression | ||
if index.zero? && (out_of_order < EXPRESSION_TYPE_ORDER[expression]) | ||
return "#{out_of_order_expression} should come before #{expression}." | ||
end | ||
|
||
# After last expression | ||
if index == expression_types.size - 1 && (out_of_order > EXPRESSION_TYPE_ORDER[expression]) | ||
return "#{out_of_order_expression} should come after #{expression}." | ||
end | ||
|
||
previous_expression = expression_types[index - 1] | ||
next unless out_of_order < EXPRESSION_TYPE_ORDER[expression] && | ||
out_of_order > EXPRESSION_TYPE_ORDER[previous_expression] | ||
|
||
return "#{out_of_order_expression} should come after #{previous_expression} and before #{expression}." | ||
end | ||
end | ||
end | ||
end |
117 changes: 117 additions & 0 deletions
117
rubocop/custom_cops/class_template/expression_category.rb
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,117 @@ | ||
# 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 | ||
|
||
# extend SomeModule | ||
def_node_matcher :extend_expr?, <<~PATTERN | ||
(send {nil? | self} :extend const) | ||
PATTERN | ||
|
||
# include SomeModule | ||
def_node_matcher :include_expr?, <<~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 :initialize_method?, <<~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) | ||
return :extend if extend_expr?(expression) | ||
|
||
return :include if include_expr?(expression) | ||
|
||
return :constant_assignment if constant_assignment?(expression) | ||
|
||
return :attribute_macro if attribute_macro?(expression) | ||
|
||
return :alias_method if alias_method?(expression) | ||
|
||
return :protected if protected?(expression) | ||
|
||
return :private if private?(expression) | ||
|
||
return :other_macro if other_macro?(expression) | ||
|
||
return :self_class_block if self_class_block?(expression) | ||
|
||
return :public_class_method if public_class_method?(expression) | ||
|
||
return :initialization if initialize_method?(expression) | ||
|
||
return :instance_method if instance_method?(expression) | ||
|
||
return :alias if alias?(expression) | ||
|
||
:unknown | ||
end | ||
end | ||
end |
Oops, something went wrong.