diff --git a/CHANGELOG.md b/CHANGELOG.md index 625bdd93d13..7387ae519af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/config/enabled.yml b/config/enabled.yml index 004eaf04c35..2ffef85fbcc 100644 --- a/config/enabled.yml +++ b/config/enabled.yml @@ -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' diff --git a/lib/rubocop.rb b/lib/rubocop.rb index aec8d3cfe7e..e473f9628ff 100644 --- a/lib/rubocop.rb +++ b/lib/rubocop.rb @@ -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' diff --git a/lib/rubocop/cop/style/mixin_usage.rb b/lib/rubocop/cop/style/mixin_usage.rb new file mode 100644 index 00000000000..1ad42caa317 --- /dev/null +++ b/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 = '`%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 diff --git a/manual/cops.md b/manual/cops.md index a7d7b436ac0..c246cb7b055 100644 --- a/manual/cops.md +++ b/manual/cops.md @@ -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) diff --git a/manual/cops_style.md b/manual/cops_style.md index 51501df0be3..1366f93626b 100644 --- a/manual/cops_style.md +++ b/manual/cops_style.md @@ -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 diff --git a/spec/rubocop/cop/style/mixin_usage_spec.rb b/spec/rubocop/cop/style/mixin_usage_spec.rb new file mode 100644 index 00000000000..f557038d1c7 --- /dev/null +++ b/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