Skip to content

Commit

Permalink
Merge pull request #512 from nimblehq/release/5.15.0
Browse files Browse the repository at this point in the history
Release - 5.15.0
  • Loading branch information
malparty authored May 9, 2024
2 parents 097d77a + 07447c3 commit 8fc0a41
Show file tree
Hide file tree
Showing 13 changed files with 593 additions and 23 deletions.
5 changes: 5 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ require:
- rubocop-rspec
- rubocop-performance
- ./rubocop/custom_cops/required_inverse_of_relations.rb
- ./rubocop/custom_cops/class_template.rb

AllCops:
Exclude:
Expand All @@ -16,6 +17,7 @@ AllCops:
- 'node_modules/**/*'
- 'config/**/*'
- 'tmp/**/*'
- 'rubocop/**/*'
TargetRubyVersion: 3
NewCops: enable

Expand Down Expand Up @@ -60,3 +62,6 @@ CustomCops/RequiredInverseOfRelations:
Include:
# Only Rails model files
- !ruby/regexp /models\//

CustomCops/ClassTemplate:
Enabled: true
2 changes: 2 additions & 0 deletions .template/addons/custom_cops/template.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@
require 'fileutils'

copy_file 'rubocop/custom_cops/required_inverse_of_relations.rb', mode: :preserve
directory 'rubocop/custom_cops/class_template', mode: :preserve
copy_file 'rubocop/custom_cops/class_template.rb', mode: :preserve
6 changes: 3 additions & 3 deletions .template/spec/base/config/environments/development_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
end

it 'configures the mailer asset host' do
expect(subject).to contain("config.action_mailer.asset_host = ENV.fetch('MAILER_DEFAULT_HOST')")
expect(subject).to contain("config.action_mailer.asset_host = ENV.fetch('MAILER_DEFAULT_HOST', 'localhost')")
end

it 'configures the mailer default url options' do
Expand All @@ -24,8 +24,8 @@
def mailer_default_url_config
<<~RUBY
config.action_mailer.default_url_options = {
host: ENV.fetch('MAILER_DEFAULT_HOST'),
port: ENV.fetch('MAILER_DEFAULT_PORT')
host: ENV.fetch('MAILER_DEFAULT_HOST', 'localhost'),
port: ENV.fetch('MAILER_DEFAULT_PORT', '3000')
}
RUBY
end
Expand Down
12 changes: 6 additions & 6 deletions .template/spec/base/config/environments/production_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
subject { file('config/environments/production.rb') }

it 'configures the mailer asset host' do
expect(subject).to contain("config.action_mailer.asset_host = ENV.fetch('MAILER_DEFAULT_HOST')")
expect(subject).to contain("config.action_mailer.asset_host = ENV.fetch('MAILER_DEFAULT_HOST', 'localhost')")
end

it 'configures the mailer default url options' do
Expand All @@ -24,22 +24,22 @@
def mailer_default_url_config
<<~RUBY
config.action_mailer.default_url_options = {
host: ENV.fetch('MAILER_DEFAULT_HOST'),
port: ENV.fetch('MAILER_DEFAULT_PORT')
host: ENV.fetch('MAILER_DEFAULT_HOST', 'localhost'),
port: ENV.fetch('MAILER_DEFAULT_PORT', '3000')
}
RUBY
end

def i18n_config
<<~RUBY
# eg: AVAILABLE_LOCALES = 'en,th'
config.i18n.available_locales = ENV.fetch('AVAILABLE_LOCALES').split(',')
config.i18n.available_locales = ENV.fetch('AVAILABLE_LOCALES', 'en').split(',')
# eg: DEFAULT_LOCALE = 'en'
config.i18n.default_locale = ENV.fetch('DEFAULT_LOCALE')
config.i18n.default_locale = ENV.fetch('DEFAULT_LOCALE', 'en')
# eg: FALLBACK_LOCALES = 'en,th'
config.i18n.fallbacks = ENV.fetch('FALLBACK_LOCALES').split(',')
config.i18n.fallbacks = ENV.fetch('FALLBACK_LOCALES', 'en').split(',')
RUBY
end
end
4 changes: 2 additions & 2 deletions .template/spec/base/config/environments/test_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@
def mailer_default_url_config
<<~RUBY
config.action_mailer.default_url_options = {
host: ENV.fetch('MAILER_DEFAULT_HOST'),
port: ENV.fetch('MAILER_DEFAULT_PORT')
host: ENV.fetch('MAILER_DEFAULT_HOST', 'localhost'),
port: ENV.fetch('MAILER_DEFAULT_PORT', '3000')
}
RUBY
end
Expand Down
6 changes: 3 additions & 3 deletions config/environments/development.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
config.action_mailer.delivery_method = :letter_opener
config.action_mailer.asset_host = ENV.fetch('MAILER_DEFAULT_HOST')
config.action_mailer.asset_host = ENV.fetch('MAILER_DEFAULT_HOST', 'localhost')
config.action_mailer.default_url_options = {
host: ENV.fetch('MAILER_DEFAULT_HOST'),
port: ENV.fetch('MAILER_DEFAULT_PORT')
host: ENV.fetch('MAILER_DEFAULT_HOST', 'localhost'),
port: ENV.fetch('MAILER_DEFAULT_PORT', '3000')
}
RUBY
end
Expand Down
12 changes: 6 additions & 6 deletions config/environments/production.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@
insert_into_file 'config/environments/production.rb', after: /config.action_mailer.perform_caching.+\n/ do
<<~RUBY.indent(2)
config.action_mailer.asset_host = ENV.fetch('MAILER_DEFAULT_HOST')
config.action_mailer.asset_host = ENV.fetch('MAILER_DEFAULT_HOST', 'localhost')
config.action_mailer.default_url_options = {
host: ENV.fetch('MAILER_DEFAULT_HOST'),
port: ENV.fetch('MAILER_DEFAULT_PORT')
host: ENV.fetch('MAILER_DEFAULT_HOST', 'localhost'),
port: ENV.fetch('MAILER_DEFAULT_PORT', '3000')
}
RUBY
end
Expand All @@ -23,13 +23,13 @@
environment do
<<~RUBY
# eg: AVAILABLE_LOCALES = 'en,th'
config.i18n.available_locales = ENV.fetch('AVAILABLE_LOCALES').split(',')
config.i18n.available_locales = ENV.fetch('AVAILABLE_LOCALES', 'en').split(',')
# eg: DEFAULT_LOCALE = 'en'
config.i18n.default_locale = ENV.fetch('DEFAULT_LOCALE')
config.i18n.default_locale = ENV.fetch('DEFAULT_LOCALE', 'en')
# eg: FALLBACK_LOCALES = 'en,th'
config.i18n.fallbacks = ENV.fetch('FALLBACK_LOCALES').split(',')
config.i18n.fallbacks = ENV.fetch('FALLBACK_LOCALES', 'en').split(',')
RUBY
end
4 changes: 2 additions & 2 deletions config/environments/test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
<<~RUBY.indent(2)
config.action_mailer.default_url_options = {
host: ENV.fetch('MAILER_DEFAULT_HOST'),
port: ENV.fetch('MAILER_DEFAULT_PORT')
host: ENV.fetch('MAILER_DEFAULT_HOST', 'localhost'),
port: ENV.fetch('MAILER_DEFAULT_PORT', '3000')
}
RUBY
end
Expand Down
117 changes: 117 additions & 0 deletions rubocop/custom_cops/class_template.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# frozen_string_literal: true

require_relative 'class_template/expression_category'

module CustomCops
class ClassTemplate < RuboCop::Cop::Base
include CustomCops::ExpressionCategory

EXPRESSION_TYPE_ORDER = {
extend: 0,
include: 1,
constant_assignment: 2,
attribute_macro: 3,
other_macro: 4,
self_class_block: 5,
public_class_method: 5,
initialization: 6,
public_instance_method: 7,
alias: 8,
alias_method: 8,
protected_instance_method: 9,
private_instance_method: 10
}.freeze

# Scan class body
def on_class(node)
expressions = top_level_expressions(node)

state = { function_visibility: :public, expression_types: [] }
expressions.each_with_object(state) { |expression, acc| process_expression(expression, acc) }

validate_expressions_order(state[:expression_types])
end

private

def top_level_expressions(class_node)
return [] unless class_node.body

# Multi-expression body
return class_node.body.children if class_node.body.type == :begin

# Single-expression body
[class_node.body]
end

def process_expression(expression, acc)
category = categorize(expression)

if %i[protected private].include?(category)
acc[:function_visibility] = category
else
category = "#{acc[:function_visibility]}_#{category}".to_sym if category == :instance_method
acc[:expression_types] << { category: category, expression: expression }
end
end

def validate_expressions_order(expressions)
expressions = expressions.filter { _1[:category] != :unknown }

expressions.each_cons(2) do |first, second|
next unless EXPRESSION_TYPE_ORDER[first[:category]] > EXPRESSION_TYPE_ORDER[second[:category]]

add_offense(
second[:expression],
message: error_message(second[:category], expressions)
)
end
end

# Find the correct spot for the out of order expression
# rubocop:disable Metrics/MethodLength
def error_message(out_of_order_expression, expressions)
expressions.filter { _1[:category] != out_of_order_expression }
.each_with_index do |expression, index|
error = if index.zero?
before_first_expression(expression, out_of_order_expression)
elsif index == expressions.size - 1
after_last_expression(expression, out_of_order_expression)
else
previous_expression = expressions[index - 1]
in_between_expressions(previous_expression, expression, out_of_order_expression)
end

return error if error
end
end
# rubocop:enable Metrics/MethodLength

def before_first_expression(current_expression, out_of_order_expression)
unless EXPRESSION_TYPE_ORDER[out_of_order_expression] < EXPRESSION_TYPE_ORDER[current_expression[:category]]
return
end

"#{out_of_order_expression} should come before `#{current_expression[:expression].source}`."
end

def after_last_expression(current_expression, out_of_order_expression)
unless EXPRESSION_TYPE_ORDER[out_of_order_expression] > EXPRESSION_TYPE_ORDER[current_expression[:category]]
return
end

"#{out_of_order_expression} should come after `#{current_expression[:expression].source}`."
end

def in_between_expressions(previous_expression, current_expression, out_of_order_expression)
unless EXPRESSION_TYPE_ORDER[out_of_order_expression] < EXPRESSION_TYPE_ORDER[current_expression[:category]] &&
EXPRESSION_TYPE_ORDER[out_of_order_expression] > EXPRESSION_TYPE_ORDER[previous_expression[:category]]
return
end

"#{out_of_order_expression} should come after " \
"`#{previous_expression[:expression].source}` " \
"and before `#{current_expression[:expression].source}`."
end
end
end
97 changes: 97 additions & 0 deletions rubocop/custom_cops/class_template/expression_category.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# frozen_string_literal: true

require 'rubocop'

module CustomCops
module ExpressionCategory
extend RuboCop::NodePattern::Macros

ATTRIBUTE_MACROS = %i[attr_reader attr_writer attr_accessor].to_set

CATEGORIES = %i[
extend include constant_assignment attribute_macro alias_method protected private
other_macro self_class_block public_class_method initialization instance_method alias
].freeze

# extend SomeModule
def_node_matcher :extend?, <<~PATTERN
(send {nil? | self} :extend const)
PATTERN

# include SomeModule
def_node_matcher :include?, <<~PATTERN
(send {nil? | self} :include const)
PATTERN

# HELLO = 'world'.freeze
def_node_matcher :constant_assignment?, <<~PATTERN
(casgn nil? _ _)
PATTERN

# attr_reader :foo, :bar
# attr_writer :baz
def_node_matcher :attribute_macro?, <<~PATTERN
(send {nil? | self} ATTRIBUTE_MACROS sym+)
PATTERN

# validates :foo, presence: true
def_node_matcher :other_macro?, <<~PATTERN
(send {nil? | self} _ _+)
PATTERN

# class << self
# def foo
# 'bar'
# end
# end
def_node_matcher :self_class_block?, <<~PATTERN
(sclass self _)
PATTERN

# def self.foo
# "bar"
# end
def_node_matcher :public_class_method?, <<~PATTERN
(defs self _ args _)
PATTERN

# def initialize(a, b)
# end
def_node_matcher :initialization?, <<~PATTERN
(def :initialize args _)
PATTERN

# def method(a, b)
# end
def_node_matcher :instance_method?, <<~PATTERN
(def _ args _)
PATTERN

# alias foo bar
# alias :foo :bar
def_node_matcher :alias?, <<~PATTERN
(alias sym sym)
PATTERN

# alias_method :foo, :bar
# alias_method 'foo', 'bar'
def_node_matcher :alias_method?, <<~PATTERN
(send {nil? | self} :alias_method _ _)
PATTERN

# protected
def_node_matcher :protected?, <<~PATTERN
(send {nil? | self} :protected)
PATTERN

# private
def_node_matcher :private?, <<~PATTERN
(send {nil? | self} :private)
PATTERN

# Categorize the expression of the class body
def categorize(expression)
CATEGORIES.find { |category| send(:"#{category}?", expression) } || :unknown
end
end
end
Loading

0 comments on commit 8fc0a41

Please sign in to comment.