Skip to content

Commit

Permalink
Add new Rails/MailerPreviews cop
Browse files Browse the repository at this point in the history
  • Loading branch information
fatkodima committed Nov 15, 2023
1 parent dc6cebc commit 26cc1cd
Show file tree
Hide file tree
Showing 15 changed files with 238 additions and 23 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ rdoc
doc
.yardoc

tmp

# bundler
.bundle
Gemfile.lock
Expand Down
1 change: 1 addition & 0 deletions changelog/new_add_mailer_previews_cop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* [#654](https://github.com/rubocop/rubocop-rails/issues/654): Add new `Rails/MailerPreviews` cop. ([@fatkodima][])
9 changes: 9 additions & 0 deletions config/default.yml
Original file line number Diff line number Diff line change
Expand Up @@ -663,6 +663,15 @@ Rails/MailerName:
Include:
- app/mailers/**/*.rb

Rails/MailerPreviews:
Description: 'Checks for existence of mailer previews.'
Enabled: pending
VersionAdded: '<<next>>'
PreviewPaths:
- test/mailers/previews
Include:
- app/mailers/**/*.rb

Rails/MatchRoute:
Description: >-
Don't use `match` to define any routes unless there is a need to map multiple request types
Expand Down
1 change: 1 addition & 0 deletions lib/rubocop-rails.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
require_relative 'rubocop/rails'
require_relative 'rubocop/rails/version'
require_relative 'rubocop/rails/inject'
require_relative 'rubocop/cop/mixin/parsing_helper'
require_relative 'rubocop/rails/schema_loader'
require_relative 'rubocop/rails/schema_loader/schema'

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
module RuboCop
module Cop
# A mixin to return all of the class send nodes.
module ClassSendNodeHelper
module ClassElementsHelper
def class_send_nodes(class_node)
class_def = class_node.body

Expand All @@ -15,6 +15,18 @@ def class_send_nodes(class_node)
class_def.each_child_node(:send)
end
end

def class_def_nodes(class_node)
class_def = class_node.body

return [] unless class_def

if class_def.def_type?
[class_def]
else
class_def.each_child_node(:def)
end
end
end
end
end
21 changes: 21 additions & 0 deletions lib/rubocop/cop/mixin/parent_class_matchers.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# frozen_string_literal: true

module RuboCop
module Cop
# A mixin with predefined parent classes matchers
module ParentClassMatchers
extend NodePattern::Macros

def_node_matcher :mailer_base_class?, <<~PATTERN
{
(const (const {nil? cbase} :ActionMailer) :Base)
(const {nil? cbase} :ApplicationMailer)
}
PATTERN

def_node_matcher :mailer_preview_base_class?, <<~PATTERN
(const (const nil? :ActionMailer) :Preview)
PATTERN
end
end
end
19 changes: 19 additions & 0 deletions lib/rubocop/cop/mixin/parsing_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# frozen_string_literal: true

module RuboCop
module Cop
# A mixin with helpers related to source code parsing
module ParsingHelper
def parse(path, target_ruby_version)
klass_name = :"Ruby#{target_ruby_version.to_s.sub('.', '')}"
klass = ::Parser.const_get(klass_name)
parser = klass.new(RuboCop::AST::Builder.new)

buffer = Parser::Source::Buffer.new(path, 1)
buffer.source = path.read

parser.parse(buffer)
end
end
end
end
2 changes: 1 addition & 1 deletion lib/rubocop/cop/rails/after_commit_override.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ module Rails
# after_update_commit :log_update_action
#
class AfterCommitOverride < Base
include ClassSendNodeHelper
include ClassElementsHelper

MSG = 'There can only be one `after_*_commit :%<name>s` hook defined for a model.'

Expand Down
2 changes: 1 addition & 1 deletion lib/rubocop/cop/rails/duplicate_association.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ module Rails
class DuplicateAssociation < Base
include RangeHelp
extend AutoCorrector
include ClassSendNodeHelper
include ClassElementsHelper
include ActiveRecordHelper

MSG = "Association `%<name>s` is defined multiple times. Don't repeat associations."
Expand Down
2 changes: 1 addition & 1 deletion lib/rubocop/cop/rails/duplicate_scope.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ module Rails
# scope :hidden, -> { where(visible: false) }
#
class DuplicateScope < Base
include ClassSendNodeHelper
include ClassElementsHelper

MSG = 'Multiple scopes share this same where clause.'

Expand Down
8 changes: 1 addition & 7 deletions lib/rubocop/cop/rails/mailer_name.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,10 @@ module Rails
#
class MailerName < Base
extend AutoCorrector
include ParentClassMatchers

MSG = 'Mailer should end with `Mailer` suffix.'

def_node_matcher :mailer_base_class?, <<~PATTERN
{
(const (const {nil? cbase} :ActionMailer) :Base)
(const {nil? cbase} :ApplicationMailer)
}
PATTERN

def_node_matcher :class_definition?, <<~PATTERN
(class $(const _ !#mailer_suffix?) #mailer_base_class? ...)
PATTERN
Expand Down
78 changes: 78 additions & 0 deletions lib/rubocop/cop/rails/mailer_previews.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# frozen_string_literal: true

module RuboCop
module Cop
module Rails
# Enforces the existence of mailer previews.
#
# @example
# # bad
# # app/mailer/user_mailer.rb
# class UserMailer < ApplicationMailer
# def welcome_email
# end
# end
#
# # No file exists in mailer previews directory.
#
# # good
# # app/mailer/user_mailer.rb
# class UserMailer < ApplicationMailer
# def welcome_email
# end
# end
#
# # test/mailers/previews/user_mailer_preview.rb
# class UserMailer < ActionMailer::Preview
# def welcome_email
# end
# end
#
class MailerPreviews < Base
include ParentClassMatchers
include ClassElementsHelper
include ParsingHelper
include VisibilityHelp

MSG = 'Add a mailer preview for `%<action_name>s`.'

def on_class(node)
return unless mailer_base_class?(node.parent_class)

actions(node).each do |action_node|
mailer_name = node.identifier.source
action_name = action_node.method_name
message = format(MSG, action_name: action_name)

add_offense(action_node, message: message) unless preview_action_exists?(mailer_name, action_name)
end
end

private

def preview_action_exists?(mailer_name, action_name)
preview_files(mailer_name).any? do |preview_path|
if preview_path.exist?
node = parse(preview_path, target_ruby_version)

node&.class_type? &&
mailer_preview_base_class?(node.parent_class) &&
actions(node).map(&:method_name).include?(action_name)
end
end
end

def actions(class_node)
class_def_nodes(class_node).select { |def_node| node_visibility(def_node) == :public }
end

def preview_files(class_name)
path = Pathname.pwd
Array(cop_config['PreviewPaths']).map do |preview_path|
path.join(preview_path, "#{class_name.underscore}_preview.rb")
end
end
end
end
end
end
4 changes: 3 additions & 1 deletion lib/rubocop/cop/rails_cops.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@

require_relative 'mixin/active_record_helper'
require_relative 'mixin/active_record_migrations_helper'
require_relative 'mixin/class_send_node_helper'
require_relative 'mixin/class_elements_helper'
require_relative 'mixin/database_type_resolvable'
require_relative 'mixin/enforce_superclass'
require_relative 'mixin/index_method'
require_relative 'mixin/migrations_helper'
require_relative 'mixin/parent_class_matchers'
require_relative 'mixin/target_rails_version'

require_relative 'rails/action_controller_flash_before_render'
Expand Down Expand Up @@ -74,6 +75,7 @@
require_relative 'rails/lexically_scoped_action_filter'
require_relative 'rails/link_to_blank'
require_relative 'rails/mailer_name'
require_relative 'rails/mailer_previews'
require_relative 'rails/match_route'
require_relative 'rails/migration_class_name'
require_relative 'rails/negate_include'
Expand Down
12 changes: 1 addition & 11 deletions lib/rubocop/rails/schema_loader.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ module Rails
# It loads db/schema.rb and return Schema object.
# Cops refers database schema information with this module.
module SchemaLoader
include Cop::ParsingHelper
extend self

# It parses `db/schema.rb` and return it.
Expand Down Expand Up @@ -45,17 +46,6 @@ def load!(target_ruby_version)
ast = parse(path, target_ruby_version)
Schema.new(ast) if ast
end

def parse(path, target_ruby_version)
klass_name = :"Ruby#{target_ruby_version.to_s.sub('.', '')}"
klass = ::Parser.const_get(klass_name)
parser = klass.new(RuboCop::AST::Builder.new)

buffer = Parser::Source::Buffer.new(path, 1)
buffer.source = path.read

parser.parse(buffer)
end
end
end
end
86 changes: 86 additions & 0 deletions spec/rubocop/cop/rails/mailer_previews_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# frozen_string_literal: true

RSpec.describe RuboCop::Cop::Rails::MailerPreviews, :config do
include FileHelper

let(:cop_config) { { 'PreviewPaths' => 'tmp/mailers/previews' } }

after { FileUtils.rm_rf('tmp') }

it 'registers an offense when there is no mailer preview file' do
expect_offense(<<~RUBY)
class UserMailer < ApplicationMailer
def welcome_email
^^^^^^^^^^^^^^^^^ Add a mailer preview for `welcome_email`.
end
end
RUBY
end

it 'registers an offense when there is no mailer preview method' do
create_preview(<<~RUBY)
class UserMailerPreview < ActionMailer::Preview
end
RUBY

expect_offense(<<~RUBY)
class UserMailer < ApplicationMailer
def welcome_email
^^^^^^^^^^^^^^^^^ Add a mailer preview for `welcome_email`.
end
end
RUBY
end

it 'registers an offense when there is a private mailer preview method' do
create_preview(<<~RUBY)
class UserMailerPreview < ActionMailer::Preview
private
def welcome_email
end
end
RUBY

expect_offense(<<~RUBY)
class UserMailer < ApplicationMailer
def welcome_email
^^^^^^^^^^^^^^^^^ Add a mailer preview for `welcome_email`.
end
end
RUBY
end

it 'registers an offense when there is no mailer preview in the file' do
create_preview

expect_offense(<<~RUBY)
class UserMailer < ApplicationMailer
def welcome_email
^^^^^^^^^^^^^^^^^ Add a mailer preview for `welcome_email`.
end
end
RUBY
end

it 'does not register an offense when there is mailer preview' do
create_preview(<<~RUBY)
class UserMailerPreview < ActionMailer::Preview
def welcome_email
end
end
RUBY

expect_no_offenses(<<~RUBY)
class UserMailer < ApplicationMailer
def welcome_email
end
end
RUBY
end

private

def create_preview(content = '')
create_file('tmp/mailers/previews/user_mailer_preview.rb', content)
end
end

0 comments on commit 26cc1cd

Please sign in to comment.