Skip to content

Commit

Permalink
[Fix #5465] Fix Layout/ClassStructure to allow grouping macros by the…
Browse files Browse the repository at this point in the history
…ir visibility (#5468)
  • Loading branch information
gPrado authored and bbatsov committed Mar 17, 2019
1 parent e1eb1f9 commit 2133a5f
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 53 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -74,6 +74,7 @@
* [#6382](https://github.com/rubocop-hq/rubocop/issues/6382): Fix `Layout/IndentationWidth` with `Layout/EndAlignment` set to start_of_line. ([@dischorde][], [@siegfault][], [@mhelmetag][])
* [#6710](https://github.com/rubocop-hq/rubocop/issues/6710): Fix `Naming/MemoizedInstanceVariableName` on method starts with underscore. ([@pocke][])
* [#6722](https://github.com/rubocop-hq/rubocop/issues/6722): Fix an error for `Style/OneLineConditional` when `then` branch has no body. ([@koic][])
* [#5465](https://github.com/rubocop-hq/rubocop/issues/5465): Fix `Layout/ClassStructure` to allow grouping macros by their visibility. ([@gprado][])
* [#6702](https://github.com/rubocop-hq/rubocop/pull/6702): Fix `TrailingComma` regression where heredoc with commas caused false positives. ([@abrom][])
* [#6737](https://github.com/rubocop-hq/rubocop/issues/6737): Fix an incorrect auto-correct for `Rails/LinkToBlank` when `link_to` method arguments are enclosed in parentheses. ([@koic][])
* [#6720](https://github.com/rubocop-hq/rubocop/issues/6720): Fix detection of `:native` line ending for `Layout/EndOfLine` on JRuby. ([@enkessler][])
Expand Down
87 changes: 59 additions & 28 deletions lib/rubocop/cop/layout/class_structure.rb
Expand Up @@ -8,46 +8,61 @@ module Layout
# `Categories` allows us to map macro names into a category.
#
# Consider an example of code style that covers the following order:
# - Module inclusion (include, prepend, extend)
# - Constants
# - Associations (has_one, has_many)
# - Attributes (attr_accessor, attr_writer, attr_reader)
# - Public attribute macros (attr_accessor, attr_writer, attr_reader)
# - Other macros (validates, validate)
# - Public class methods
# - Initializer
# - Instance methods
# - Protected methods
# - Private methods
# - Public instance methods
# - Protected attribute macros (attr_accessor, attr_writer, attr_reader)
# - Protected instance methods
# - Private attribute macros (attr_accessor, attr_writer, attr_reader)
# - Private instance methods
#
# You can configure the following order:
#
# ```yaml
# Layout/ClassStructure:
# Categories:
# module_inclusion:
# - include
# - prepend
# - extend
# ExpectedOrder:
# - module_inclusion
# - constants
# - public_class_methods
# - initializer
# - public_methods
# - protected_methods
# - private_methods
#
# - module_inclusion
# - constants
# - association
# - public_attribute_macros
# - public_delegate
# - macros
# - public_class_methods
# - initializer
# - public_methods
# - protected_attribute_macros
# - protected_methods
# - private_attribute_macros
# - private_delegate
# - private_methods
# ```
#
# Instead of putting all literals in the expected order, is also
# possible to group categories of macros.
# possible to group categories of macros. Visibility levels are handled
# automatically.
#
# ```yaml
# Layout/ClassStructure:
# Categories:
# association:
# - has_many
# - has_one
# attribute:
# attribute_macros:
# - attr_accessor
# - attr_reader
# - attr_writer
# macros:
# - validates
# - validate
# module_inclusion:
# - include
# - prepend
# - extend
# ```
#
# @example
Expand All @@ -73,12 +88,15 @@ module Layout
# # constants are next
# SOME_CONSTANT = 20
#
# # afterwards we have attribute macros
# # afterwards we have public attribute macros
# attr_reader :name
#
# # followed by other macros (if any)
# validates :name
#
# # then we have public delegate macros
# delegate :to_s, to: :name
#
# # public class methods are next in line
# def self.some_method
# end
Expand All @@ -91,14 +109,22 @@ module Layout
# def some_method
# end
#
# # protected and private methods are grouped near the end
# # protected attribute macros and methods go next
# protected
#
# attr_reader :protected_name
#
# def some_protected_method
# end
#
# # private attribute macros, delegate macros and methods
# # are grouped near the end
# private
#
# attr_reader :private_name
#
# delegate :some_private_delegate, to: :name
#
# def some_private_method
# end
# end
Expand All @@ -108,7 +134,8 @@ class ClassStructure < Cop
HUMANIZED_NODE_TYPE = {
casgn: :constants,
defs: :class_methods,
def: :public_methods
def: :public_methods,
sclass: :class_singleton
}.freeze

VISIBILITY_SCOPES = %i[private protected public].freeze
Expand Down Expand Up @@ -167,19 +194,23 @@ def classify(node)
when :block
classify(node.send_node)
when :send
find_category(node.method_name)
find_category(node)
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
# Categorize a node according to the {expected_order}
# Try to match {categories} values against the node's method_name given
# also its visibility.
# @param node to be analysed.
# @return [String] with the key category or the `method_name` as string
def find_category(method_name)
name = method_name.to_s
def find_category(node)
name = node.method_name.to_s
category, = categories.find { |_, names| names.include?(name) }
category || name
key = category || name
visibility_key = "#{node_visibility(node)}_#{key}"
expected_order.include?(visibility_key) ? visibility_key : key
end

def walk_over_nested_class_definition(class_node)
Expand Down
68 changes: 47 additions & 21 deletions manual/cops_layout.md
Expand Up @@ -552,46 +552,61 @@ 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:
- Module inclusion (include, prepend, extend)
- Constants
- Associations (has_one, has_many)
- Attributes (attr_accessor, attr_writer, attr_reader)
- Public attribute macros (attr_accessor, attr_writer, attr_reader)
- Other macros (validates, validate)
- Public class methods
- Initializer
- Instance methods
- Protected methods
- Private methods
- Public instance methods
- Protected attribute macros (attr_accessor, attr_writer, attr_reader)
- Protected instance methods
- Private attribute macros (attr_accessor, attr_writer, attr_reader)
- Private instance methods

You can configure the following order:

```yaml
Layout/ClassStructure:
Categories:
module_inclusion:
- include
- prepend
- extend
ExpectedOrder:
- module_inclusion
- constants
- public_class_methods
- initializer
- public_methods
- protected_methods
- private_methods

- module_inclusion
- constants
- association
- public_attribute_macros
- public_delegate
- macros
- public_class_methods
- initializer
- public_methods
- protected_attribute_macros
- protected_methods
- private_attribute_macros
- private_delegate
- private_methods
```

Instead of putting all literals in the expected order, is also
possible to group categories of macros.
possible to group categories of macros. Visibility levels are handled
automatically.

```yaml
Layout/ClassStructure:
Categories:
association:
- has_many
- has_one
attribute:
attribute_macros:
- attr_accessor
- attr_reader
- attr_writer
macros:
- validates
- validate
module_inclusion:
- include
- prepend
- extend
```

### Examples
Expand Down Expand Up @@ -619,12 +634,15 @@ class Person
# constants are next
SOME_CONSTANT = 20

# afterwards we have attribute macros
# afterwards we have public attribute macros
attr_reader :name

# followed by other macros (if any)
validates :name

# then we have public delegate macros
delegate :to_s, to: :name

# public class methods are next in line
def self.some_method
end
Expand All @@ -637,14 +655,22 @@ class Person
def some_method
end

# protected and private methods are grouped near the end
# protected attribute macros and methods go next
protected

attr_reader :protected_name

def some_protected_method
end

# private attribute macros, delegate macros and methods
# are grouped near the end
private

attr_reader :private_name

delegate :some_private_delegate, to: :name

def some_private_method
end
end
Expand Down
33 changes: 29 additions & 4 deletions spec/rubocop/cop/layout/class_structure_spec.rb
Expand Up @@ -10,17 +10,32 @@
module_inclusion
constants
attribute_macros
delegate
macros
public_class_methods
initializer
public_methods
protected_attribute_macros
protected_methods
private_attribute_macros
private_delegate
private_methods
],
'Categories' => {
'macros' => %w[validates validate],
'module_inclusion' => %w[prepend extend include],
'attribute_macros' => %w[attr_accessor attr_reader attr_writer]
'attribute_macros' => %w[
attr_accessor
attr_reader
attr_writer
],
'macros' => %w[
validates
validate
],
'module_inclusion' => %w[
prepend
extend
include
]
}
}
)
Expand All @@ -43,6 +58,9 @@ class Person
# afterwards we have attribute macros
attr_reader :name
# then we have public delegate macros
delegate :to_s, to: :name
# followed by other macros (if any)
validates :name
Expand All @@ -58,14 +76,21 @@ def initialize
def some_method
end
# protected and private methods are grouped near the end
# protected attribute macros and methods go next
protected
attr_reader :protected_name
def some_protected_method
end
# private attribute macros, delegate macros and methods are grouped near the end
private
attr_reader :private_name
delegate :some_private_delegate, to: :name
def some_private_method
end
end
Expand Down

0 comments on commit 2133a5f

Please sign in to comment.