Skip to content

Commit

Permalink
Add new Style/TopLevelInclude cop
Browse files Browse the repository at this point in the history
Checks that `include`, `extend` and `prepend` exists at the top level.
Using these at the top level affects the behavior of `Object`.

```console
% cat app/models/example.rb

include M

class Example < ApplicationRecord
end
```

```console
% rubocop app/models/example.rb
Inspecting 1 file
C

Offenses:

/tmp/example.rb:3:1: C: include is used at the top level. Use inside
class or module.
include M
^^^^^^^^^

1 file inspected, 1 offense detected
```

Since it is difficult to automatically determine the position of
`include` for class including multiple modules, autocorrect is not
provided.
  • Loading branch information
koic committed Oct 17, 2017
1 parent afbb06f commit a383739
Show file tree
Hide file tree
Showing 7 changed files with 204 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -12,6 +12,7 @@
* Add `IndentationWidth` configuration for `Layout/Tab` cop. ([@rrosenblum][])
* [#4854](https://github.com/bbatsov/rubocop/pull/4854): Add new `Lint/RegexpAsCondition` cop. ([@pocke][])
* [#4862](https://github.com/bbatsov/rubocop/pull/4862): Add `MethodDefineMacros` option to `Naming/PredicateName` cop. ([@koic][])
* [#4840](https://github.com/bbatsov/rubocop/pull/4840): Add new `Style/MixinUsage` cop. ([@koic][])

### Bug fixes

Expand Down
4 changes: 4 additions & 0 deletions config/enabled.yml
Expand Up @@ -996,6 +996,10 @@ Style/TernaryParentheses:
Description: 'Checks for use of parentheses around ternary conditions.'
Enabled: true

Style/MixinUsage:
Description: 'Checks that `include`, `extend` and `prepend` exists at the top level.'
Enabled: true

Style/TrailingCommaInArguments:
Description: 'Checks for trailing comma in argument lists.'
StyleGuide: '#no-trailing-params-comma'
Expand Down
1 change: 1 addition & 0 deletions lib/rubocop.rb
Expand Up @@ -400,6 +400,7 @@
require_relative 'rubocop/cop/style/min_max'
require_relative 'rubocop/cop/style/missing_else'
require_relative 'rubocop/cop/style/mixin_grouping'
require_relative 'rubocop/cop/style/mixin_usage'
require_relative 'rubocop/cop/style/module_function'
require_relative 'rubocop/cop/style/multiline_block_chain'
require_relative 'rubocop/cop/style/multiline_if_then'
Expand Down
73 changes: 73 additions & 0 deletions lib/rubocop/cop/style/mixin_usage.rb
@@ -0,0 +1,73 @@
# frozen_string_literal: true

module RuboCop
module Cop
module Style
# This cop checks that `include`, `extend` and `prepend` exists at
# the top level.
# Using these at the top level affects the behavior of `Object`.
# There will not be using `include`, `extend` and `prepend` at
# the top level. Let's use it inside `class` or `module`.
#
# @example
# # bad
# include M
#
# class C
# end
#
# # bad
# extend M
#
# class C
# end
#
# # bad
# prepend M
#
# class C
# end
#
# # good
# class C
# include M
# end
#
# # good
# class C
# extend M
# end
#
# # good
# class C
# prepend M
# end
class MixinUsage < Cop
MSG = '`%<statement>s` is used at the top level. Use inside `class` ' \
'or `module`.'.freeze

def_node_matcher :include_statement, <<-PATTERN
(send nil? ${:include :extend :prepend}
(const nil? _))
PATTERN

def on_send(node)
return unless (statement = include_statement(node))
return unless top_level_node?(node)

add_offense(node, message: format(MSG, statement: statement))
end

private

def top_level_node?(node)
if node.parent.parent.nil?
node.sibling_index.zero?
else
top_level_node?(node.parent)
end
end
end
end
end
end
1 change: 1 addition & 0 deletions manual/cops.md
Expand Up @@ -408,6 +408,7 @@ In the following section you find all available cops:
* [Style/MinMax](cops_style.md#styleminmax)
* [Style/MissingElse](cops_style.md#stylemissingelse)
* [Style/MixinGrouping](cops_style.md#stylemixingrouping)
* [Style/MixinUsage](cops_style.md#stylemixinusage)
* [Style/ModuleFunction](cops_style.md#stylemodulefunction)
* [Style/MultilineBlockChain](cops_style.md#stylemultilineblockchain)
* [Style/MultilineIfModifier](cops_style.md#stylemultilineifmodifier)
Expand Down
49 changes: 49 additions & 0 deletions manual/cops_style.md
Expand Up @@ -2051,6 +2051,55 @@ SupportedStyles | separated, grouped

* [https://github.com/bbatsov/ruby-style-guide#mixin-grouping](https://github.com/bbatsov/ruby-style-guide#mixin-grouping)

## Style/MixinUsage

Enabled by default | Supports autocorrection
--- | ---
Enabled | No

This cop checks that `include`, `extend` and `prepend` exists at
the top level.
Using these at the top level affects the behavior of `Object`.
There will not be using `include`, `extend` and `prepend` at
the top level. Let's use it inside `class` or `module`.

### Example

```ruby
# bad
include M

class C
end

# bad
extend M

class C
end

# bad
prepend M

class C
end

# good
class C
include M
end

# good
class C
extend M
end

# good
class C
prepend M
end
```

## Style/ModuleFunction

Enabled by default | Supports autocorrection
Expand Down
75 changes: 75 additions & 0 deletions spec/rubocop/cop/style/mixin_usage_spec.rb
@@ -0,0 +1,75 @@
# frozen_string_literal: true

describe RuboCop::Cop::Style::MixinUsage do
let(:config) { RuboCop::Config.new }
subject(:cop) { described_class.new(config) }

context 'include' do
it 'registers an offense when using outside class' do
expect_offense(<<-RUBY.strip_indent)
include M
^^^^^^^^^ `include` is used at the top level. Use inside `class` or `module`.
class C
end
RUBY
end

it 'does not register an offense when using inside class' do
expect_no_offenses(<<-RUBY.strip_indent)
class C
include M
end
RUBY
end
end

context 'extend' do
it 'registers an offense when using outside class' do
expect_offense(<<-RUBY.strip_indent)
extend M
^^^^^^^^ `extend` is used at the top level. Use inside `class` or `module`.
class C
end
RUBY
end

it 'does not register an offense when using inside class' do
expect_no_offenses(<<-RUBY.strip_indent)
class C
extend M
end
RUBY
end
end

context 'prepend' do
it 'registers an offense when using outside class' do
expect_offense(<<-RUBY.strip_indent)
prepend M
^^^^^^^^^ `prepend` is used at the top level. Use inside `class` or `module`.
class C
end
RUBY
end

it 'does not register an offense when using inside class' do
expect_no_offenses(<<-RUBY.strip_indent)
class C
prepend M
end
RUBY
end
end

it 'does not register an offense when using inside nested module' do
expect_no_offenses(<<-RUBY.strip_indent)
module M1
include M2
class C
include M3
end
end
RUBY
end
end

0 comments on commit a383739

Please sign in to comment.