diff --git a/CHANGELOG.md b/CHANGELOG.md index 6027d5abef8..27cdfd97b15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### New features * [#8322](https://github.com/rubocop-hq/rubocop/pull/8322): Support autocorrect for `Style/CaseEquality` cop. ([@fatkodima][]) +* [#8376](https://github.com/rubocop-hq/rubocop/pull/8376): Add new `Lint/MissingSuper` cop. ([@fatkodima][]) * [#8339](https://github.com/rubocop-hq/rubocop/pull/8339): Add `Config#for_badge` as an efficient way to get a cop's config merged with its department's. ([@marcandre][]) ### Bug fixes @@ -20,6 +21,7 @@ ### Changes +* [#8376](https://github.com/rubocop-hq/rubocop/pull/8376): `Style/MethodMissingSuper` cop is removed in favor of new `Lint/MissingSuper` cop. ([@fatkodima][]) * [#8350](https://github.com/rubocop-hq/rubocop/pull/8350): Set default max line length to 120 for `Style/MultilineMethodSignature`. ([@koic][]) * [#8338](https://github.com/rubocop-hq/rubocop/pull/8338): **potentially breaking**. Config#for_department now returns only the config specified for that department; the 'Enabled' attribute is no longer calculated. ([@marcandre][]) diff --git a/config/default.yml b/config/default.yml index 9a86c01f6e9..16d35298324 100644 --- a/config/default.yml +++ b/config/default.yml @@ -1556,6 +1556,13 @@ Lint/MissingCopEnableDirective: # .inf for any size MaximumRangeSize: .inf +Lint/MissingSuper: + Description: >- + This cop checks for the presence of constructors and lifecycle callbacks + without calls to `super`'. + Enabled: pending + VersionAdded: '0.89' + Lint/MixedRegexpCaptureTypes: Description: 'Do not mix named captures and numbered captures in a Regexp literal.' Enabled: pending @@ -3219,12 +3226,6 @@ Style/MethodDefParentheses: - require_no_parentheses - require_no_parentheses_except_multiline -Style/MethodMissingSuper: - Description: Checks for `method_missing` to call `super`. - StyleGuide: '#no-method-missing' - Enabled: true - VersionAdded: '0.56' - Style/MinMax: Description: >- Use `Enumerable#minmax` instead of `Enumerable#min` diff --git a/docs/modules/ROOT/pages/cops.adoc b/docs/modules/ROOT/pages/cops.adoc index 17a9797ca3f..b6cab055641 100644 --- a/docs/modules/ROOT/pages/cops.adoc +++ b/docs/modules/ROOT/pages/cops.adoc @@ -216,6 +216,7 @@ In the following section you find all available cops: * xref:cops_lint.adoc#lintliteralininterpolation[Lint/LiteralInInterpolation] * xref:cops_lint.adoc#lintloop[Lint/Loop] * xref:cops_lint.adoc#lintmissingcopenabledirective[Lint/MissingCopEnableDirective] +* xref:cops_lint.adoc#lintmissingsuper[Lint/MissingSuper] * xref:cops_lint.adoc#lintmixedregexpcapturetypes[Lint/MixedRegexpCaptureTypes] * xref:cops_lint.adoc#lintmultiplecomparison[Lint/MultipleComparison] * xref:cops_lint.adoc#lintnestedmethoddefinition[Lint/NestedMethodDefinition] @@ -395,7 +396,6 @@ In the following section you find all available cops: * xref:cops_style.adoc#stylemethodcallwithoutargsparentheses[Style/MethodCallWithoutArgsParentheses] * xref:cops_style.adoc#stylemethodcalledondoendblock[Style/MethodCalledOnDoEndBlock] * xref:cops_style.adoc#stylemethoddefparentheses[Style/MethodDefParentheses] -* xref:cops_style.adoc#stylemethodmissingsuper[Style/MethodMissingSuper] * xref:cops_style.adoc#styleminmax[Style/MinMax] * xref:cops_style.adoc#stylemissingelse[Style/MissingElse] * xref:cops_style.adoc#stylemissingrespondtomissing[Style/MissingRespondToMissing] diff --git a/docs/modules/ROOT/pages/cops_lint.adoc b/docs/modules/ROOT/pages/cops_lint.adoc index decf1d39c8a..d08b312aa4e 100644 --- a/docs/modules/ROOT/pages/cops_lint.adoc +++ b/docs/modules/ROOT/pages/cops_lint.adoc @@ -1693,6 +1693,56 @@ x += 1 | Float |=== +== Lint/MissingSuper + +|=== +| Enabled by default | Safe | Supports autocorrection | VersionAdded | VersionChanged + +| Pending +| Yes +| No +| 0.89 +| - +|=== + +This cop checks for the presence of constructors and lifecycle callbacks +without calls to `super`. + +=== Examples + +[source,ruby] +---- +# bad +class Employee < Person + def initialize(name, salary) + @salary = salary + end +end + +# good +class Employee < Person + def initialize(name, salary) + super(name) + @salary = salary + end +end + +# bad +class Parent + def self.inherited(base) + do_something + end +end + +# good +class Parent + def self.inherited(base) + super + do_something + end +end +---- + == Lint/MixedRegexpCaptureTypes |=== diff --git a/docs/modules/ROOT/pages/cops_style.adoc b/docs/modules/ROOT/pages/cops_style.adoc index fef5d0893cb..151c688bfb0 100644 --- a/docs/modules/ROOT/pages/cops_style.adoc +++ b/docs/modules/ROOT/pages/cops_style.adoc @@ -4815,42 +4815,6 @@ end * https://rubystyle.guide#method-parens -== Style/MethodMissingSuper - -|=== -| Enabled by default | Safe | Supports autocorrection | VersionAdded | VersionChanged - -| Enabled -| Yes -| No -| 0.56 -| - -|=== - -This cop checks for the presence of `method_missing` without -falling back on `super`. - -=== Examples - -[source,ruby] ----- -#bad -def method_missing(name, *args) - # ... -end - -#good - -def method_missing(name, *args) - # ... - super -end ----- - -=== References - -* https://rubystyle.guide#no-method-missing - == Style/MinMax |=== diff --git a/lib/rubocop.rb b/lib/rubocop.rb index 4092de4cea6..0c8cb96d33b 100644 --- a/lib/rubocop.rb +++ b/lib/rubocop.rb @@ -273,6 +273,7 @@ require_relative 'rubocop/cop/lint/literal_in_interpolation' require_relative 'rubocop/cop/lint/loop' require_relative 'rubocop/cop/lint/missing_cop_enable_directive' +require_relative 'rubocop/cop/lint/missing_super' require_relative 'rubocop/cop/lint/mixed_regexp_capture_types' require_relative 'rubocop/cop/lint/multiple_comparison' require_relative 'rubocop/cop/lint/nested_method_definition' @@ -443,7 +444,6 @@ require_relative 'rubocop/cop/style/method_call_with_args_parentheses/require_parentheses' require_relative 'rubocop/cop/style/method_called_on_do_end_block' require_relative 'rubocop/cop/style/method_def_parentheses' -require_relative 'rubocop/cop/style/method_missing_super' require_relative 'rubocop/cop/style/min_max' require_relative 'rubocop/cop/style/missing_else' require_relative 'rubocop/cop/style/missing_respond_to_missing' diff --git a/lib/rubocop/cli/command/base.rb b/lib/rubocop/cli/command/base.rb index 89304378c13..b4fbb66b198 100644 --- a/lib/rubocop/cli/command/base.rb +++ b/lib/rubocop/cli/command/base.rb @@ -13,6 +13,7 @@ class << self attr_accessor :command_name def inherited(subclass) + super @subclasses << subclass end diff --git a/lib/rubocop/config_obsoletion.rb b/lib/rubocop/config_obsoletion.rb index 0c8d81766e6..fcc7ef930fb 100644 --- a/lib/rubocop/config_obsoletion.rb +++ b/lib/rubocop/config_obsoletion.rb @@ -84,7 +84,9 @@ class ConfigObsoletion 'Lint/InvalidCharacterLiteral' => 'it was never being actually triggered', 'Lint/SpaceBeforeFirstArg' => 'it was a duplicate of `Layout/SpaceBeforeFirstArg`. Please use ' \ - '`Layout/SpaceBeforeFirstArg` instead' + '`Layout/SpaceBeforeFirstArg` instead', + 'Style/MethodMissingSuper' => 'it has been superseded by `Lint/MissingSuper`. Please use ' \ + '`Lint/MissingSuper` instead' }.map do |cop_name, reason| [cop_name, "The `#{cop_name}` cop has been removed since #{reason}."] end diff --git a/lib/rubocop/cop/base.rb b/lib/rubocop/cop/base.rb index c25c9a7e936..3fafc5b0f8b 100644 --- a/lib/rubocop/cop/base.rb +++ b/lib/rubocop/cop/base.rb @@ -137,6 +137,7 @@ def external_dependency_checksum end def self.inherited(subclass) + super Registry.global.enlist(subclass) end diff --git a/lib/rubocop/cop/force.rb b/lib/rubocop/cop/force.rb index aafcbe99d86..bfb7566af4c 100644 --- a/lib/rubocop/cop/force.rb +++ b/lib/rubocop/cop/force.rb @@ -11,6 +11,7 @@ def self.all end def self.inherited(subclass) + super all << subclass end diff --git a/lib/rubocop/cop/lint/missing_super.rb b/lib/rubocop/cop/lint/missing_super.rb new file mode 100644 index 00000000000..bdb0882dbed --- /dev/null +++ b/lib/rubocop/cop/lint/missing_super.rb @@ -0,0 +1,99 @@ +# frozen_string_literal: true + +module RuboCop + module Cop + module Lint + # This cop checks for the presence of constructors and lifecycle callbacks + # without calls to `super`. + # + # @example + # # bad + # class Employee < Person + # def initialize(name, salary) + # @salary = salary + # end + # end + # + # # good + # class Employee < Person + # def initialize(name, salary) + # super(name) + # @salary = salary + # end + # end + # + # # bad + # class Parent + # def self.inherited(base) + # do_something + # end + # end + # + # # good + # class Parent + # def self.inherited(base) + # super + # do_something + # end + # end + # + class MissingSuper < Base + CONSTRUCTOR_MSG = 'Call `super` to initialize state of the parent class.' + CALLBACK_MSG = 'Call `super` to invoke callback defined in the parent class.' + + STATELESS_CLASSES = %w[BasicObject Object].freeze + + OBJECT_LIFECYCLE_CALLBACKS = %i[method_missing respond_to_missing?].freeze + CLASS_LIFECYCLE_CALLBACKS = %i[inherited].freeze + MODULE_LIFECYCLE_CALLBACKS = %i[included extended prepended].freeze + + def on_def(node) + return unless offender?(node) + + if node.method?(:initialize) + add_offense(node, message: CONSTRUCTOR_MSG) if inside_class_with_stateful_parent?(node) + elsif callback_method_def?(node) + add_offense(node, message: CALLBACK_MSG) + end + end + + def on_defs(node) + return if !callback_method_def?(node) || contains_super?(node) + + add_offense(node, message: CALLBACK_MSG) + end + + private + + def offender?(node) + (node.method?(:initialize) || callback_method_def?(node)) && !contains_super?(node) + end + + def callback_method_def?(node) + method_name = node.method_name + + if OBJECT_LIFECYCLE_CALLBACKS.include?(method_name) || + CLASS_LIFECYCLE_CALLBACKS.include?(method_name) + + node.each_ancestor(:class, :sclass, :module).first + elsif MODULE_LIFECYCLE_CALLBACKS.include?(method_name) + node.each_ancestor(:module).first + end + end + + def contains_super?(node) + node.each_descendant(:super, :zsuper).any? + end + + def inside_class_with_stateful_parent?(node) + class_node = node.each_ancestor(:class).first + class_node&.parent_class && !stateless_class?(class_node.parent_class) + end + + def stateless_class?(node) + STATELESS_CLASSES.include?(node.const_name) + end + end + end + end +end diff --git a/lib/rubocop/cop/mixin/enforce_superclass.rb b/lib/rubocop/cop/mixin/enforce_superclass.rb index e490149d8a2..6a1fbc31757 100644 --- a/lib/rubocop/cop/mixin/enforce_superclass.rb +++ b/lib/rubocop/cop/mixin/enforce_superclass.rb @@ -5,6 +5,8 @@ module Cop # Common functionality for enforcing a specific superclass module EnforceSuperclass def self.included(base) + super + base.def_node_matcher :class_definition, <<~PATTERN (class (const _ !:#{base::SUPERCLASS}) #{base::BASE_PATTERN} ...) PATTERN diff --git a/lib/rubocop/cop/style/method_missing_super.rb b/lib/rubocop/cop/style/method_missing_super.rb deleted file mode 100644 index 4f1a483a942..00000000000 --- a/lib/rubocop/cop/style/method_missing_super.rb +++ /dev/null @@ -1,34 +0,0 @@ -# frozen_string_literal: true - -module RuboCop - module Cop - module Style - # This cop checks for the presence of `method_missing` without - # falling back on `super`. - # - # @example - # #bad - # def method_missing(name, *args) - # # ... - # end - # - # #good - # - # def method_missing(name, *args) - # # ... - # super - # end - class MethodMissingSuper < Cop - MSG = 'When using `method_missing`, fall back on `super`.' - - def on_def(node) - return unless node.method?(:method_missing) - return if node.descendants.any?(&:zsuper_type?) - - add_offense(node) - end - alias on_defs on_def - end - end - end -end diff --git a/lib/rubocop/cop/variable_force/branch.rb b/lib/rubocop/cop/variable_force/branch.rb index 4ceb21cce0e..9d23bb562ea 100644 --- a/lib/rubocop/cop/variable_force/branch.rb +++ b/lib/rubocop/cop/variable_force/branch.rb @@ -49,6 +49,7 @@ def self.classes end def self.inherited(subclass) + super classes << subclass end diff --git a/lib/rubocop/error.rb b/lib/rubocop/error.rb index e989b23cc12..30d7c4b8718 100644 --- a/lib/rubocop/error.rb +++ b/lib/rubocop/error.rb @@ -12,6 +12,7 @@ class ValidationError < Error; end # A wrapper to display errored location of analyzed file. class ErrorWithAnalyzedFileLocation < Error def initialize(cause:, node:, cop:) + super() @cause = cause @cop = cop @location = node.is_a?(RuboCop::AST::Node) ? node.loc : node diff --git a/lib/rubocop/formatter/formatter_set.rb b/lib/rubocop/formatter/formatter_set.rb index 473c1a03b43..ec0e65f375d 100644 --- a/lib/rubocop/formatter/formatter_set.rb +++ b/lib/rubocop/formatter/formatter_set.rb @@ -35,6 +35,7 @@ class FormatterSet < Array end def initialize(options = {}) + super() @options = options # CLI options end diff --git a/lib/rubocop/rake_task.rb b/lib/rubocop/rake_task.rb index 809be7f0057..dee73f59e15 100644 --- a/lib/rubocop/rake_task.rb +++ b/lib/rubocop/rake_task.rb @@ -15,6 +15,7 @@ class RakeTask < ::Rake::TaskLib attr_accessor :name, :verbose, :fail_on_error, :patterns, :formatters, :requires, :options def initialize(name = :rubocop, *args, &task_block) + super() setup_ivars(name) desc 'Run RuboCop' unless ::Rake.application.last_description diff --git a/spec/rubocop/config_obsoletion_spec.rb b/spec/rubocop/config_obsoletion_spec.rb index b6e84691370..447ca104afc 100644 --- a/spec/rubocop/config_obsoletion_spec.rb +++ b/spec/rubocop/config_obsoletion_spec.rb @@ -69,6 +69,7 @@ 'Layout/SpaceAfterControlKeyword' => { 'Enabled': true }, 'Layout/SpaceBeforeModifierKeyword' => { 'Enabled': true }, 'Lint/InvalidCharacterLiteral' => { 'Enabled': true }, + 'Style/MethodMissingSuper' => { 'Enabled': true }, 'Lint/RescueWithoutErrorClass' => { 'Enabled': true }, 'Lint/SpaceBeforeFirstArg' => { 'Enabled': true }, 'Style/SpaceAfterControlKeyword' => { 'Enabled': true }, @@ -196,6 +197,8 @@ (obsolete configuration found in example/.rubocop.yml, please update it) The `Lint/SpaceBeforeFirstArg` cop has been removed since it was a duplicate of `Layout/SpaceBeforeFirstArg`. Please use `Layout/SpaceBeforeFirstArg` instead. (obsolete configuration found in example/.rubocop.yml, please update it) + The `Style/MethodMissingSuper` cop has been removed since it has been superseded by `Lint/MissingSuper`. Please use `Lint/MissingSuper` instead. + (obsolete configuration found in example/.rubocop.yml, please update it) The `Style/MethodMissing` cop has been split into `Style/MethodMissingSuper` and `Style/MissingRespondToMissing`. (obsolete configuration found in example/.rubocop.yml, please update it) OUTPUT diff --git a/spec/rubocop/cop/lint/missing_super_spec.rb b/spec/rubocop/cop/lint/missing_super_spec.rb new file mode 100644 index 00000000000..464f3f46860 --- /dev/null +++ b/spec/rubocop/cop/lint/missing_super_spec.rb @@ -0,0 +1,138 @@ +# frozen_string_literal: true + +RSpec.describe RuboCop::Cop::Lint::MissingSuper do + subject(:cop) { described_class.new } + + context 'constructor' do + it 'registers an offense when no `super` call' do + expect_offense(<<~RUBY) + class Child < Parent + def initialize + ^^^^^^^^^^^^^^ Call `super` to initialize state of the parent class. + end + end + RUBY + end + + it 'does not register an offense for the class without parent class' do + expect_no_offenses(<<~RUBY) + class Child + def initialize + end + end + RUBY + end + + it 'does not register an offense for the class with stateless parent class' do + expect_no_offenses(<<~RUBY) + class Child < Object + def initialize + end + end + RUBY + end + + it 'does not register an offense for the constructor-like method defined outside of a class' do + expect_no_offenses(<<~RUBY) + module M + def initialize + end + end + RUBY + end + + it 'does not register an offense when there is a `super` call' do + expect_no_offenses(<<~RUBY) + class Child < Parent + def initialize + super + end + end + RUBY + end + end + + context 'callbacks' do + it 'registers an offense when module callback without `super` call' do + expect_offense(<<~RUBY) + module M + def self.included(base) + ^^^^^^^^^^^^^^^^^^^^^^^ Call `super` to invoke callback defined in the parent class. + end + end + RUBY + end + + it 'registers an offense when class callback without `super` call' do + expect_offense(<<~RUBY) + class Foo + def self.inherited(base) + ^^^^^^^^^^^^^^^^^^^^^^^^ Call `super` to invoke callback defined in the parent class. + end + end + RUBY + end + + it 'registers an offense when class callback within `self << class` and without `super` call' do + expect_offense(<<~RUBY) + class Foo + class << self + def inherited(base) + ^^^^^^^^^^^^^^^^^^^ Call `super` to invoke callback defined in the parent class. + end + end + end + RUBY + end + + it 'registers an offense for instance level `method_missing?` with no `super` call' do + expect_offense(<<~RUBY) + class Foo + def method_missing(*args) + ^^^^^^^^^^^^^^^^^^^^^^^^^ Call `super` to invoke callback defined in the parent class. + end + end + RUBY + end + + it 'registers an offense for class level `method_missing?` with no `super` call' do + expect_offense(<<~RUBY) + class Foo + def self.method_missing(*args) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Call `super` to invoke callback defined in the parent class. + end + end + RUBY + end + + it 'does not register an offense when `method_missing?` contains `super` call' do + expect_no_offenses(<<~RUBY) + class Foo + def method_missing(*args) + super + do_something + end + end + RUBY + end + + it 'does not register an offense when class has instance method named as callback' do + expect_no_offenses(<<~RUBY) + class Foo + def included(base) + end + end + RUBY + end + + it 'does not register an offense when callback has a `super` call' do + expect_no_offenses(<<~RUBY) + class Foo + def self.inherited(base) + super + end + end + RUBY + end + end +end diff --git a/spec/rubocop/cop/style/method_missing_super_spec.rb b/spec/rubocop/cop/style/method_missing_super_spec.rb deleted file mode 100644 index 5fddc3c2f89..00000000000 --- a/spec/rubocop/cop/style/method_missing_super_spec.rb +++ /dev/null @@ -1,49 +0,0 @@ -# frozen_string_literal: true - -RSpec.describe RuboCop::Cop::Style::MethodMissingSuper do - subject(:cop) { described_class.new } - - describe 'method_missing defined as an instance method' do - it 'registers an offense when super is not called.' do - expect_offense(<<~RUBY) - class Test - def method_missing - ^^^^^^^^^^^^^^^^^^ When using `method_missing`, fall back on `super`. - end - end - RUBY - end - - it 'allows method_missing when super is called' do - expect_no_offenses(<<-RUBY) - class Test - def method_missing - super - end - end - RUBY - end - end - - describe 'method_missing defined as a class method' do - it 'registers an offense when super is not called.' do - expect_offense(<<~RUBY) - class Test - def self.method_missing - ^^^^^^^^^^^^^^^^^^^^^^^ When using `method_missing`, fall back on `super`. - end - end - RUBY - end - - it 'allows method_missing when super is called' do - expect_no_offenses(<<-RUBY) - class Test - def self.method_missing - super - end - end - RUBY - end - end -end diff --git a/tasks/spec_runner.rake b/tasks/spec_runner.rake index 3fd4720f0b1..1e14c9789e0 100644 --- a/tasks/spec_runner.rake +++ b/tasks/spec_runner.rake @@ -94,6 +94,7 @@ module RuboCop # implicitly reading ARGV. class Framework < ::TestQueue::TestFramework::RSpec def initialize(rspec_args) + super() @rspec_args = rspec_args end