rubocop-hq/rubocop

View on GitHub
docs/modules/ROOT/pages/v1_upgrade_notes.adoc

Summary

Maintainability
Test Coverage
= v1 Upgrade Notes
:doctype: book

== Cop Upgrade guide

Your custom cops should continue to work in v1.

Nevertheless it is suggested that you tweak them to use the v1 API by following the following steps:

1) Your class should inherit from `RuboCop::Cop::Base` instead of `RuboCop::Cop::Cop`.

2) Locate your calls to `add_offense` and make sure that you pass as the first argument either an `AST::Node`, a `::Parser::Source::Comment` or a `::Parser::Source::Range`, and no `location:` named parameter.

[discrete]
==== Example:

[source,ruby]
----
# Before
class MySillyCop < Cop
  def on_send(node)
    if node.method_name == :-
      add_offense(node, location: :selector, message: "Be positive")
    end
  end
end

# After
class MySillyCop < Base
  def on_send(node)
    if node.method_name == :-
      add_offense(node.loc.selector, message: "Be positive")
    end
  end
end
----

=== If your class supports autocorrection

Your class must `extend AutoCorrector`.

The `corrector` is now yielded from `add_offense`. Move the code of your method `autocorrect` in that block and do not wrap your correction in a lambda. `Corrector` are more powerful and can now be `merge`d.

==== Example:

[source,ruby]
----
# Before
class MySillyCorrectingCop < Cop
  def on_send(node)
    if node.method_name == :-
      add_offense(node, location: :selector, message: 'Be positive')
    end
  end

  def autocorrect(node)
    lambda do |corrector|
      corrector.replace(node.loc.selector, '+')
    end
  end
end

# After
class MySillyCorrectingCop < Base
  extend AutoCorrector

  def on_send(node)
    if node.method_name == :-
      add_offense(node.loc.selector, message: 'Be positive') do |corrector|
        corrector.replace(node.loc.selector, '+')
      end
    end
  end
end
----

=== Instance variables

Do not use RuboCop's internal instance variables. If you used `@processed_source`, use `processed_source`. If you have a need to access an instance variable, open an issue with your use case.

By default, a Cop instance will be called only once for a given `processed_source`, so instance variables will be uninitialized when the investigation starts. Using `@cache ||= ...` is fine. If you want to initialize some instance variable, the callback `on_new_investigation` is the best place to do so.

[source,ruby]
----
class MyCachingCop < Base
  def on_send(node)
    if my_cached_data[node]
      @counts(node.method_name) += 1
      #...
    end
  end

  # One way:
  def my_cached_data
    @data ||= processed_source.comments.map { # ... }
  end

  # Another way:
  def on_new_investigation
    @counts = Hash.new(0)
    super  # Be nice and call super for callback
  end
end
----

=== Other API changes

If your cop uses `investigate`, `investigate_post_walk`, `join_force?`, or internal classes like `Corrector`, `Commissioner`, `Team`, these have changed. See the <<Detailed API Changes>>.

=== Upgrading specs

It is highly recommended you use `expect_offense` / `expect_correction` / `expect_no_offense` in your specs, e.g.:

[source,ruby]
----
require 'rubocop/rspec/support'

RSpec.describe RuboCop::Cop::Custom::MySillyCorrectingCop, :config do
  # No need for `let(:cop)`
  it 'is positive' do
    expect_offense(<<~RUBY)
      42 + 2 - 2
             ^ Be positive
    RUBY

    expect_correction(<<~RUBY)
      42 + 2 + 2
    RUBY
  end

  it 'does not register an offense for calls to `despair`' do
    expect_no_offenses(<<~RUBY)
      "don't".despair
    RUBY
  end
end
----

In the unlikely case where you use the class `RuboCop::Cop::Corrector` directly, it has changed a bit but you can ease your transition with `RuboCop::Cop::Legacy::Corrector` that is meant to be somewhat backwards compatible. You will need to `require 'rubocop/cop/legacy/corrector'`.

== Detailed API Changes

This section lists all changes (big or small) to the API. It is meant for maintainers of the nuts & bolts of RuboCop; most cop writers will not be impacted by these and are thus not the target audience.

=== Base class

_Legacy_: Cops inherit from `Cop::Cop`.

_Current_: Cops inherit from `Cop::Base`. Having a different base class makes the implementation much cleaner and makes it easy to signal which API is being used. `Cop::Cop` inherits from `Cop::Base` and refines some methods for backward compatibility.

=== `add_offense` API

==== arguments

_Legacy:_ interface allowed for a `node`, with an optional `location` (symbol or range) or a range with a mandatory range as the location. Some cops were abusing the `node` argument and passing very different things.

_Current:_ pass a range (or node as a shortcut for `node.loc.expression`), no `location:`. No abuse tolerated.

==== deduping changes

Both dedupe on `range` and won't process the duplicated offenses at all.

_Legacy:_ if offenses on same `node` but different `range`: considered as multiple offenses but a single autocorrect call.

_Current:_ not applicable and not needed with autocorrection's API.

==== yield

Both yield under the same conditions (unless cop is disabled for that line), but:

_Legacy:_ yields after offense added to `#offenses`

_Current:_ yields before offense is added to `#offenses`.

Even the legacy mode yields a corrector, but if a developer uses it an error will be raised asking her to inherit from `Cop::Base` instead.

=== Autocorrection

==== `#autocorrect`

_Legacy:_ calls `autocorrect` unless it is disabled / autocorrect is off.

_Current:_ yields a corrector unless it is disabled. The corrector will be ignored if autocorrecting is off, etc. No support for `autocorrect` method, but a warning is issued if that method is still defined.

==== Empty corrections

_Legacy:_ `autocorrect` could return `nil` / `false` in cases where it couldn't actually make a correction.

_Current:_ No special API. Cases where no corrections are made are automatically detected.

==== Correction timing

_Legacy:_ the lambda was called only later in the process, and only under specific conditions (if the autocorrect setting is turned on, etc.)

_Current:_ correction is built immediately (assuming the cop isn't disabled for the line) and applied later in the process.

==== Exception handling

Both: `Commissioner` will rescue all ``StandardError``s during analysis (unless `option[:raise_error]`) and store a corresponding `ErrorWithAnalyzedFileLocation` in its error list. This is done when calling the cop's `on_send` & al., or when calling `investigate` / `investigate_post_walk` callback.

_Legacy:_ autocorrecting cops were treating errors differently depending on when they occurred. Some errors were silently ignored. Others were rescued as above. Others crashed. Some code in `Team` would rescue errors and add them to the list of errors but I don't think the code worked.

_Current:_ `Team` no longer has any special error handling to do as potential exceptions happen when `Commissioner` is running.

==== Other error handling

_Legacy:_ Clobbering errors are silently ignored. Calling `insert_before` with ranges that extend beyond the source code was silently fixed.

_Current:_ Such errors are not ignored. It is still ok that a given Cop's corrections clobber another Cop's, but any given Cop should not issue corrections that clobber each other, or with invalid ranges, otherwise these will be listed in the processing errors.

==== `#corrections`

_Legacy:_ Corrections were held in `#corrections` as an array of lambdas. A proxy was written to maintain compatibility with `+cop.corrections << ...+`, `+cop.corrections.concat ...+`, etc.

_Current:_ Corrections are held in `current_corrector`, a `Corrector` which inherits from `Source::TreeRewriter`.

==== `#support_autocorrect?`

_Legacy:_ was an instance method.

_Current:_ now a class method.

==== Joining forces

_Legacy:_ `join_force?(force_class)` was called with every force class

_Current:_ `self.joining_forces` is now used to return the force (or an array of forces) to join.

=== Cop persistence

Cops can now be persisted between files. By default new cop instances are created for each source. See `support_multiple_source?` documentation.

=== Internal classes

==== `Corrector`

_Legacy:_ `initialize` accepted a second argument (an array of lambdas). Available through `Legacy::Corrector` if needed.

_Current:_ derives from `parser`'s `TreeRewriter`. No second argument to `initialize`; not needed as correctors can be merged.

==== `Commissioner` & `Team`

Refactored for better separation of concern, being reusable, better result reporting and better error handling.

=== Misc API changes

* internal API clarified for Commissioner. It calls `begin_investigation` and receives the results in `complete_investigation`.
* New method `add_global_offense` for offenses that are not attached to a location in particular; it's used for Syntax errors only right now.
* `#offenses`: No longer accessible.
* Callbacks `investigate(processed_source)` and `investigate_post_walk(processed_source)` are renamed `on_new_investigation` and `on_investigation_end` and don't accept an argument; all `on_` callbacks should rely on `processed_source`.
* `#find_location` is deprecated.
* `Correction` is deprecated.
* A few registry access methods were moved from `Cop` to `Registry` both for correctness (e.g. `MyCop.qualified_cop_name` did not work nor made sense) and so that `Cop::Cop` no longer holds any necessary code anymore. Backwards compatibility is maintained.
 ** `Cop.registry` \=> `Registry.global`
 ** `Cop.all` \=> `Registry.all`
 ** `Cop.qualified_cop_name` \=> `Registry.qualified_cop_name`
* The `ConfigurableMax` mixin for tracking exclude limits of configuration options is deprecated. Use `exclude_limit ParameterName` instead.