forked from rubocop/rubocop
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Fix rubocop#1575] Add Layout/ClassStructure cop
- Loading branch information
Jônatas Davi Paganini
committed
Nov 23, 2017
1 parent
b1a74d7
commit 6f4c533
Showing
8 changed files
with
646 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
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,302 @@ | ||
# frozen_string_literal: true | ||
|
||
module RuboCop | ||
module Cop | ||
module Layout | ||
# Checks if the code style follows the ExpectedOrder configuration: | ||
# | ||
# `Categories` allows us to map macro names into a category. | ||
# | ||
# Consider an example of code style that covers the following order: | ||
# - Constants | ||
# - Associations (has_one, has_many) | ||
# - Attributes (attr_accessor, attr_writer, attr_reader) | ||
# - Initialize | ||
# - Instance methods | ||
# - Protected methods | ||
# - Private methods | ||
# | ||
# You can configure the following order: | ||
# | ||
# ```yaml | ||
# Layout/ClassStructure: | ||
# ExpectedOrder: | ||
# - constant | ||
# - association | ||
# - attribute | ||
# - initialize | ||
# - instance_method | ||
# - protected_method | ||
# - private_method | ||
# | ||
# ``` | ||
# Instead of putting all literals in the expected order, is also | ||
# possible to group categories of macros. | ||
# | ||
# ```yaml | ||
# Layout/ClassStructure: | ||
# Categories: | ||
# association: | ||
# - has_many | ||
# - has_one | ||
# attribute: | ||
# - attr_accessor | ||
# - attr_reader | ||
# - attr_writer | ||
# ``` | ||
# | ||
# @example bad code | ||
# | ||
# # bad: Expect extend be before constant | ||
# class Person < ApplicationRecord | ||
# has_many : | ||
# ANSWER = 42 | ||
# | ||
# extend SomeModule | ||
# include AnotherModule | ||
# end | ||
# | ||
# @example of good code | ||
# | ||
# class Person | ||
# # extend and include go first | ||
# extend SomeModule | ||
# include AnotherModule | ||
# | ||
# # inner classes | ||
# CustomError = Class.new(StandardError) | ||
# | ||
# # constants are next | ||
# SOME_CONSTANT = 20 | ||
# | ||
# # afterwards we have attribute macros | ||
# attr_reader :name | ||
# | ||
# # followed by other macros (if any) | ||
# validates :name | ||
# | ||
# # public class methods are next in line | ||
# def self.some_method | ||
# end | ||
# | ||
# # initialization goes between class methods and instance methods | ||
# def initialize | ||
# end | ||
# | ||
# # followed by other public instance methods | ||
# def some_method | ||
# end | ||
# | ||
# # protected and private methods are grouped near the end | ||
# protected | ||
# | ||
# def some_protected_method | ||
# end | ||
# | ||
# private | ||
# | ||
# def some_private_method | ||
# end | ||
# end | ||
# | ||
# @see https://github.com/bbatsov/ruby-style-guide#consistent-classes | ||
class ClassStructure < Cop | ||
HUMANIZED_NODE_TYPE = { | ||
casgn: :constant, | ||
defs: :class_method, | ||
def: :instance_method | ||
}.freeze | ||
|
||
VISIBILITY_SCOPES = %i[private protected public].freeze | ||
MSG = '`%s` is supposed to appear before `%s`.'.freeze | ||
|
||
def_node_matcher :visibility_block?, <<-PATTERN | ||
(send nil? { :private :protected :public }) | ||
PATTERN | ||
|
||
# Validates code style on class declaration. | ||
# Add offense when find a node out of expected order. | ||
def on_class(class_node) | ||
previous = -1 | ||
walk_over_nested_class_definition(class_node) do |node, category| | ||
index = expected_order.index(category) | ||
if index < previous | ||
message = format(MSG, category, expected_order[previous]) | ||
add_offense(node, message: message) | ||
end | ||
previous = index | ||
end | ||
end | ||
|
||
# Autocorrect by swapping between two nodes autocorrecting them | ||
def autocorrect(node) | ||
node_classification = classify(node) | ||
previous = left_siblings_of(node).find do |sibling| | ||
classification = classify(sibling) | ||
!ignore?(classification) && node_classification != classification | ||
end | ||
|
||
current_range = source_range_with_comment(node) | ||
previous_range = source_range_with_comment(previous) | ||
|
||
lambda do |corrector| | ||
corrector.insert_before(previous_range, current_range.source) | ||
corrector.remove(current_range) | ||
end | ||
end | ||
|
||
private | ||
|
||
# Classifies a node to match with something in the {expected_order} | ||
# @param node to be analysed | ||
# @return String when the node type is a `:block` then | ||
# {classify} recursively with the first children | ||
# @return String when the node type is a `:send` then {find_category} | ||
# by method name | ||
# @return String otherwise trying to {humanize_node} of the current node | ||
def classify(node) | ||
return node.to_s unless node.respond_to?(:type) | ||
case node.type | ||
when :block | ||
classify(node.send_node) | ||
when :send | ||
find_category(node.method_name) | ||
else | ||
humanize_node(node) | ||
end.to_s | ||
end | ||
|
||
# Categorize a method_name according to the {expected_order} | ||
# @param method_name try to match {categories} values | ||
# @return [String] with the key category or the `method_name` as string | ||
def find_category(method_name) | ||
name = method_name.to_s | ||
category, = categories.find { |_, names| names.include?(name) } | ||
category || name | ||
end | ||
|
||
def walk_over_nested_class_definition(class_node) | ||
class_elements(class_node).each do |node| | ||
classification = classify(node) | ||
next if ignore?(classification) | ||
yield node, classification | ||
end | ||
end | ||
|
||
def class_elements(class_node) | ||
*, class_def = class_node.children | ||
return [] unless class_def | ||
if class_def.def_type? || class_def.send_type? | ||
[class_def] | ||
else | ||
class_def.children.compact | ||
end | ||
end | ||
|
||
def ignore?(classification) | ||
classification.nil? || | ||
classification.to_s.end_with?('=') || | ||
expected_order.index(classification).nil? | ||
end | ||
|
||
def node_visibility(node) | ||
_, method_name, = *find_visibility_start(node) | ||
method_name || :public | ||
end | ||
|
||
def find_visibility_start(node) | ||
left_siblings_of(node) | ||
.reverse | ||
.find(&method(:visibility_block?)) | ||
end | ||
|
||
# Navigate to find the last protected method | ||
def find_visibility_end(node) | ||
possible_visibilities = VISIBILITY_SCOPES - [node_visibility(node)] | ||
right = right_siblings_of(node) | ||
right.find do |child_node| | ||
possible_visibilities.include?(node_visibility(child_node)) | ||
end || right.last | ||
end | ||
|
||
def siblings_of(node) | ||
node.parent.children | ||
end | ||
|
||
def right_siblings_of(node) | ||
siblings_of(node)[node.sibling_index..-1] | ||
end | ||
|
||
def left_siblings_of(node) | ||
siblings_of(node)[0, node.sibling_index] | ||
end | ||
|
||
def humanize_node(node) | ||
method_name, = *node | ||
if node.def_type? | ||
return :initialize if method_name == :initialize | ||
return "#{node_visibility(node)}_method" | ||
end | ||
HUMANIZED_NODE_TYPE[node.type] || node.type | ||
end | ||
|
||
def source_range_with_comment(node) | ||
begin_pos, end_pos = | ||
if node.def_type? | ||
start_node = find_visibility_start(node) || node | ||
end_node = find_visibility_end(node) || node | ||
[begin_pos_with_comment(start_node), | ||
end_position_for(end_node) + 1] | ||
else | ||
[begin_pos_with_comment(node), end_position_for(node)] | ||
end | ||
|
||
Parser::Source::Range.new(buffer, begin_pos, end_pos) | ||
end | ||
|
||
def end_position_for(node) | ||
end_line = buffer.line_for_position(node.loc.expression.end_pos) | ||
buffer.line_range(end_line).end_pos | ||
end | ||
|
||
def begin_pos_with_comment(node) | ||
annotation_line = node.loc.line - 1 | ||
first_comment = nil | ||
|
||
comments_before_line(annotation_line).reverse_each do |comment| | ||
if comment.location.line == annotation_line | ||
first_comment = comment | ||
annotation_line -= 1 | ||
end | ||
end | ||
|
||
start_line_position(first_comment || node) | ||
end | ||
|
||
def start_line_position(node) | ||
buffer.line_range(node.loc.line).begin_pos - 1 | ||
end | ||
|
||
def comments_before_line(line) | ||
processed_source.comments.select { |c| c.location.line <= line } | ||
end | ||
|
||
def buffer | ||
processed_source.buffer | ||
end | ||
|
||
# Load expected order from `ExpectedOrder` config. | ||
# Define new terms in the expected order by adding new {categories}. | ||
def expected_order | ||
cop_config['ExpectedOrder'] | ||
end | ||
|
||
# Setting categories hash allow you to group methods in group to match | ||
# in the {expected_order}. | ||
def categories | ||
cop_config['Categories'] | ||
end | ||
end | ||
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
Oops, something went wrong.