Skip to content

Commit

Permalink
Add new Rails/I18nLazyLookup cop
Browse files Browse the repository at this point in the history
  • Loading branch information
fatkodima committed Aug 12, 2020
1 parent d6cd35f commit c0b9a10
Show file tree
Hide file tree
Showing 7 changed files with 275 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

* [#52](https://github.com/rubocop-hq/rubocop-rails/issues/52): Add new `Rails/AfterCommitOverride` cop. ([@fatkodima][])
* [#274](https://github.com/rubocop-hq/rubocop-rails/pull/274): Add new `Rails/WhereNot` cop. ([@fatkodima][])
* [#326](https://github.com/rubocop-hq/rubocop-rails/pull/326): Add new `Rails/I18nLazyLookup` cop. ([@fatkodima][])
* [#311](https://github.com/rubocop-hq/rubocop-rails/issues/311): Make `Rails/HelperInstanceVariable` aware of memoization. ([@koic][])

### Bug fixes
Expand Down
8 changes: 8 additions & 0 deletions config/default.yml
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,14 @@ Rails/HttpStatus:
- numeric
- symbolic

Rails/I18nLazyLookup:
Description: 'Checks for places where I18n "lazy" lookup can be used.'
StyleGuide: 'https://rails.rubystyle.guide/#lazy-lookup'
Enabled: pending
VersionAdded: '2.8'
Include:
- 'controllers/**/*'

Rails/IgnoredSkipActionFilterOption:
Description: 'Checks that `if` and `only` (or `except`) are not used together as options of `skip_*` action filter.'
Reference: 'https://api.rubyonrails.org/classes/AbstractController/Callbacks/ClassMethods.html#method-i-_normalize_callback_options'
Expand Down
1 change: 1 addition & 0 deletions docs/modules/ROOT/pages/cops.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ based on the https://rails.rubystyle.guide/[Rails Style Guide].
* xref:cops_rails.adoc#railshelperinstancevariable[Rails/HelperInstanceVariable]
* xref:cops_rails.adoc#railshttppositionalarguments[Rails/HttpPositionalArguments]
* xref:cops_rails.adoc#railshttpstatus[Rails/HttpStatus]
* xref:cops_rails.adoc#railsi18nlazylookup[Rails/I18nLazyLookup]
* xref:cops_rails.adoc#railsignoredskipactionfilteroption[Rails/IgnoredSkipActionFilterOption]
* xref:cops_rails.adoc#railsindexby[Rails/IndexBy]
* xref:cops_rails.adoc#railsindexwith[Rails/IndexWith]
Expand Down
55 changes: 55 additions & 0 deletions docs/modules/ROOT/pages/cops_rails.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -1622,6 +1622,61 @@ redirect_to root_url, status: 301
| `numeric`, `symbolic`
|===

== Rails/I18nLazyLookup

|===
| Enabled by default | Safe | Supports autocorrection | VersionAdded | VersionChanged

| Pending
| Yes
| Yes
| 2.8
| -
|===

This cop checks for places where I18n "lazy" lookup can be used.

=== Examples

[source,ruby]
----
# en.yml
# en:
# books:
# create:
# success: Book created!
# bad
class BooksController < ApplicationController
def create
# ...
redirect_to books_url, notice: t('books.create.success')
end
end
# good
class BooksController < ApplicationController
def create
# ...
redirect_to books_url, notice: t('.success')
end
end
----

=== Configurable attributes

|===
| Name | Default value | Configurable values

| Include
| `controllers/**/*`
| Array
|===

=== References

* https://rails.rubystyle.guide/#lazy-lookup

== Rails/IgnoredSkipActionFilterOption

|===
Expand Down
95 changes: 95 additions & 0 deletions lib/rubocop/cop/rails/i18n_lazy_lookup.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# frozen_string_literal: true

module RuboCop
module Cop
module Rails
# This cop checks for places where I18n "lazy" lookup can be used.
#
# @example
# # en.yml
# # en:
# # books:
# # create:
# # success: Book created!
#
# # bad
# class BooksController < ApplicationController
# def create
# # ...
# redirect_to books_url, notice: t('books.create.success')
# end
# end
#
# # good
# class BooksController < ApplicationController
# def create
# # ...
# redirect_to books_url, notice: t('.success')
# end
# end
#
class I18nLazyLookup < Base
include VisibilityHelp
extend AutoCorrector

MSG = 'Use "lazy" lookup for the texts used in controllers.'

def_node_matcher :translate_call?, <<~PATTERN
(send nil? {:translate :t} ${sym_type? str_type?} ...)
PATTERN

def on_send(node)
translate_call?(node) do |key_node|
controller, action = controller_and_action(node)
return unless controller && action

key = key_node.value
scoped_key = get_scoped_key(key_node, controller, action)
return unless key == scoped_key

add_offense(key_node) do |corrector|
unscoped_key = key_node.value.to_s.split('.').last
corrector.replace(key_node, "'.#{unscoped_key}'")
end
end
end

private

def controller_and_action(node)
controller = nil
action = nil

def_node = node.each_ancestor(:def).first
action = def_node if def_node && node_visibility(def_node) == :public

class_node = node.each_ancestor(:class).first
controller = class_node if class_node && class_node.identifier.source.end_with?('Controller')

[controller, action]
end

def get_scoped_key(key_node, controller, action)
path = controller_path(controller).tr('/', '.')
action_name = action.method_name
key = key_node.value.to_s.split('.').last

"#{path}.#{action_name}.#{key}"
end

def controller_path(controller)
module_name = controller.parent_module_name
controller_name = controller.identifier.source

path = if module_name == 'Object'
controller_name
else
"#{module_name}::#{controller_name}"
end

path.delete_suffix('Controller').underscore
end
end
end
end
end
1 change: 1 addition & 0 deletions lib/rubocop/cop/rails_cops.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
require_relative 'rails/helper_instance_variable'
require_relative 'rails/http_positional_arguments'
require_relative 'rails/http_status'
require_relative 'rails/i18n_lazy_lookup'
require_relative 'rails/ignored_skip_action_filter_option'
require_relative 'rails/index_by'
require_relative 'rails/index_with'
Expand Down
114 changes: 114 additions & 0 deletions spec/rubocop/cop/rails/i18n_lazy_lookup_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# frozen_string_literal: true

RSpec.describe RuboCop::Cop::Rails::I18nLazyLookup do
subject(:cop) { described_class.new }

it 'registers an offense and corrects when using translation helpers with the key scoped to controller and action' do
expect_offense(<<~RUBY)
class FooController
def action
t 'foo.action.key'
^^^^^^^^^^^^^^^^ Use "lazy" lookup for the texts used in controllers.
translate 'foo.action.key'
^^^^^^^^^^^^^^^^ Use "lazy" lookup for the texts used in controllers.
end
end
RUBY

expect_correction(<<~RUBY)
class FooController
def action
t '.key'
translate '.key'
end
end
RUBY
end

it 'does not register an offense when translation methods scoped to `I18n`' do
expect_no_offenses(<<~RUBY)
class FooController
def action
I18n.t 'foo.action.key'
I18n.translate 'foo.action.key'
end
end
RUBY
end

it 'does not register an offense when not inside controller' do
expect_no_offenses(<<~RUBY)
class FooService
def do_something
t 'foo_service.do_something.key'
end
end
RUBY
end

it 'does not register an offense when not inside controller action' do
expect_no_offenses(<<~RUBY)
class FooController
private
def action
t 'foo.action.key'
end
end
RUBY
end

it 'does not register an offense when translating key not scoped to controller and action' do
expect_no_offenses(<<~RUBY)
class FooController
def action
t 'one.two.key'
end
end
RUBY
end

it 'does not register an offense when using "lazy" translation' do
expect_no_offenses(<<~RUBY)
class FooController
def action
t '.key'
end
end
RUBY
end

it 'does not register an offense when translation key is not a string nor a symbol' do
expect_no_offenses(<<~RUBY)
class FooController
def action
t ['foo.action.key']
t key
end
end
RUBY
end

it 'handles scoped controllers' do
expect_offense(<<~RUBY)
module Bar
class FooController
def action
t 'bar.foo.action.key'
^^^^^^^^^^^^^^^^^^^^ Use "lazy" lookup for the texts used in controllers.
end
end
end
RUBY

expect_correction(<<~RUBY)
module Bar
class FooController
def action
t '.key'
end
end
end
RUBY
end
end

0 comments on commit c0b9a10

Please sign in to comment.