Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ Style/ArgumentsForwarding:
Style/Documentation:
Enabled: false

Style/FormatStringToken:
Exclude:
- spec/rubocop/**/*

Style/HashSyntax:
EnforcedShorthandSyntax: never

Expand Down
25 changes: 25 additions & 0 deletions lib/rubocop/code0/zero_track/file_helpers.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# frozen_string_literal: true

module RuboCop
module Code0
module ZeroTrack
module FileHelpers
def dirname(node)
File.dirname(filepath(node))
end

def basename(node)
File.basename(filepath(node))
end

def filepath(node)
node.location.expression.source_buffer.name
end

def in_migration?(node)
dirname(node).end_with?('db/migrate')
end
end
end
end
end
31 changes: 31 additions & 0 deletions lib/rubocop/cop/code0/zero_track/logs/rails_logger.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# frozen_string_literal: true

module RuboCop
module Cop
module Code0
module ZeroTrack
module Logs
# Cop that checks if 'timestamps' method is called with timezone information.
class RailsLogger < RuboCop::Cop::Base
MSG = 'Do not use `Rails.logger` directly, include `Code0::ZeroTrack::Loggable` instead'
LOG_METHODS = %i[debug error fatal info warn].freeze
LOG_METHODS_PATTERN = LOG_METHODS.map(&:inspect).join(' ').freeze

def_node_matcher :rails_logger_log?, <<~PATTERN
(send
(send (const nil? :Rails) :logger)
{#{LOG_METHODS_PATTERN}} ...
)
PATTERN

def on_send(node)
return unless rails_logger_log?(node)

add_offense(node)
end
end
end
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# frozen_string_literal: true

require_relative '../../../../code0/zero_track/file_helpers'

module RuboCop
module Cop
module Code0
module ZeroTrack
module Migration
class CreateTableWithTimestamps < RuboCop::Cop::Base
include RuboCop::Code0::ZeroTrack::FileHelpers

MSG = 'Add timestamps when creating a new table.'
RESTRICT_ON_SEND = %i[create_table].freeze

def_node_matcher :create_table_with_timestamps_proc?, <<~PATTERN
(send nil? :create_table (sym _) ... (block-pass (sym :timestamps_with_timezone)))
PATTERN

def_node_search :timestamps_included?, <<~PATTERN
(send _var :timestamps_with_timezone ...)
PATTERN

def_node_search :created_at_included?, <<~PATTERN
(send _var :datetime_with_timezone
{(sym :created_at)(str "created_at")}
...)
PATTERN

def_node_search :updated_at_included?, <<~PATTERN
(send _var :datetime_with_timezone
{(sym :updated_at)(str "updated_at")}
...)
PATTERN

def_node_matcher :create_table_with_block?, <<~PATTERN
(block
(send nil? :create_table ...)
(args (arg _var)+)
_)
PATTERN

def on_send(node)
return unless in_migration?(node)
return unless node.command?(:create_table)

parent = node.parent

if create_table_with_block?(parent)
add_offense(parent) if parent.body.nil? || !time_columns_included?(parent.body)
elsif create_table_with_timestamps_proc?(node)
# nothing to do
else
add_offense(node)
end
end

private

def time_columns_included?(node)
timestamps_included?(node) || created_at_and_updated_at_included?(node)
end

def created_at_and_updated_at_included?(node)
created_at_included?(node) && updated_at_included?(node)
end
end
end
end
end
end
end
53 changes: 53 additions & 0 deletions lib/rubocop/cop/code0/zero_track/migration/datetime.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# frozen_string_literal: true

require_relative '../../../../code0/zero_track/file_helpers'

module RuboCop
module Cop
module Code0
module ZeroTrack
module Migration
# Cop that checks if datetime data type is added with timezone information.
class Datetime < RuboCop::Cop::Base
include RuboCop::Code0::ZeroTrack::FileHelpers
extend AutoCorrector

MSG = 'Do not use the `%s` data type, use `datetime_with_timezone` instead'

# Check methods in table creation.
def on_def(node)
return unless in_migration?(node)

node.each_descendant(:send) do |send_node|
method_name = send_node.children[1]

next unless %i[datetime timestamp].include?(method_name)

add_offense(send_node.loc.selector, message: format(MSG, method_name)) do |corrector|
corrector.replace(send_node.loc.selector, 'datetime_with_timezone')
end
end
end

# Check methods.
def on_send(node)
return unless in_migration?(node)

node.each_descendant do |descendant|
next unless descendant.type == :sym

last_argument = descendant.children.last

next unless %i[datetime timestamp].include?(last_argument)

add_offense(descendant, message: format(MSG, last_argument)) do |corrector|
corrector.replace(descendant, ':datetime_with_timezone')
end
end
end
end
end
end
end
end
end
38 changes: 38 additions & 0 deletions lib/rubocop/cop/code0/zero_track/migration/timestamps.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# frozen_string_literal: true

require_relative '../../../../code0/zero_track/file_helpers'

module RuboCop
module Cop
module Code0
module ZeroTrack
module Migration
# Cop that checks if 'timestamps' method is called with timezone information.
class Timestamps < RuboCop::Cop::Base
include RuboCop::Code0::ZeroTrack::FileHelpers
extend AutoCorrector

MSG = 'Do not use `timestamps`, use `timestamps_with_timezone` instead'

# Check methods in table creation.
def on_def(node)
return unless in_migration?(node)

node.each_descendant(:send) do |send_node|
next unless method_name(send_node) == :timestamps

add_offense(send_node.loc.selector) do |corrector|
corrector.replace(send_node.loc.selector, 'timestamps_with_timezone')
end
end
end

def method_name(node)
node.children[1]
end
end
end
end
end
end
end
93 changes: 93 additions & 0 deletions lib/rubocop/cop/code0/zero_track/migration/versioned_class.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# frozen_string_literal: true

require_relative '../../../../code0/zero_track/file_helpers'

module RuboCop
module Cop
module Code0
module ZeroTrack
module Migration
class VersionedClass < RuboCop::Cop::Base
include RuboCop::Code0::ZeroTrack::FileHelpers
extend AutoCorrector

MIGRATION_CLASS = 'Code0::ZeroTrack::Database::Migration'

# rubocop:disable Layout/LineLength
MSG_WRONG_BASE_CLASS = "Don't use `%<base_class>s`. Use `#{MIGRATION_CLASS}` instead.".freeze
MSG_WRONG_VERSION = "Don't use version `%<current_version>s` of `#{MIGRATION_CLASS}`. Use version `%<allowed_version>s` instead.".freeze
# rubocop:enable Layout/LineLength

def on_class(node)
return unless in_migration?(node)

return on_zerotrack_migration(node) if zerotrack_migration?(node)

add_offense(
node.parent_class,
message: format(MSG_WRONG_BASE_CLASS, base_class: superclass(node))
) do |corrector|
corrector.replace(node.parent_class, "#{MIGRATION_CLASS}[#{find_allowed_versions(node).last}]")
end
end

private

def on_zerotrack_migration(node)
return if cop_config['AllowedVersions'].nil? # allow all versions if nothing configured
return if correct_migration_version?(node)

current_version = get_migration_version(node)
allowed_version = find_allowed_versions(node).last

version_node = get_migration_version_node(node)

add_offense(
version_node,
message: format(MSG_WRONG_VERSION, current_version: current_version, allowed_version: allowed_version)
) do |corrector|
corrector.replace(version_node, find_allowed_versions(node).last.to_s)
end
end

def zerotrack_migration?(node)
superclass(node) == MIGRATION_CLASS
end

def superclass(class_node)
_, *others = class_node.descendants

others.find { |node| node.const_type? && node.const_name != 'Types' }&.const_name
end

def correct_migration_version?(node)
find_allowed_versions(node).include?(get_migration_version(node))
end

def get_migration_version_node(node)
node.parent_class.arguments[0]
end

def get_migration_version(node)
get_migration_version_node(node).value
end

def find_allowed_versions(node)
migration_version = basename(node).split('_').first.to_i
allowed_versions.select do |range, _|
range.include?(migration_version)
end.values
end

def allowed_versions
cop_config['AllowedVersions'].transform_keys do |range|
range_ints = range.split('..').map(&:to_i)
range_ints[0]..range_ints[1]
end
end
end
end
end
end
end
end
5 changes: 5 additions & 0 deletions lib/rubocop/zero_track.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# frozen_string_literal: true

# rubocop:disable Lint/RedundantDirGlobSort
Dir[File.join(__dir__, 'cop', '**', '*.rb')].sort.each { |file| require file }
# rubocop:enable Lint/RedundantDirGlobSort
17 changes: 17 additions & 0 deletions rubocop-zero_track.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
require:
- ./lib/rubocop/zero_track

Code0/ZeroTrack/Logs/RailsLogger:
Enabled: false

Code0/ZeroTrack/Migration/CreateTableWithTimestamps:
Enabled: false

Code0/ZeroTrack/Migration/Datetime:
Enabled: false

Code0/ZeroTrack/Migration/Timestamps:
Enabled: false

Code0/ZeroTrack/Migration/VersionedClass:
Enabled: false
29 changes: 29 additions & 0 deletions spec/rubocop/cop/code0/zero_track/logs/rails_logger_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe RuboCop::Cop::Code0::ZeroTrack::Logs::RailsLogger, type: :rubocop do
described_class::LOG_METHODS.each do |method|
it "flags the use of Rails.logger.#{method} with a constant receiver" do
node = "Rails.logger.#{method}('some error')"
msg = 'Do not use `Rails.logger` directly, include `Code0::ZeroTrack::Loggable` instead'

expect_offense(<<~CODE, node: node, msg: msg)
%{node}
^{node} %{msg}
CODE
end
end

it 'does not flag the use of Rails.logger with a constant that is not Rails' do
expect_no_offenses("AppLogger.error('some error')")
end

it 'does not flag the use of logger with a send receiver' do
expect_no_offenses("file_logger.info('important info')")
end

it 'does not flag the use of Rails.logger.level' do
expect_no_offenses('Rails.logger.level')
end
end
Loading