class RuboCop::Cop::Rails::TimeZone

This cop checks for the use of Time methods without zone.

Built on top of Ruby on Rails style guide (rails.rubystyle.guide#time) and the article danilenko.org/2012/7/6/rails_timezones/

Two styles are supported for this cop. When `EnforcedStyle` is 'strict' then only use of `Time.zone` is allowed.

When EnforcedStyle is 'flexible' then it's also allowed to use `Time#in_time_zone`.

@example

# bad
Time.now
Time.parse('2015-03-02T19:05:37')

# good
Time.current
Time.zone.now
Time.zone.parse('2015-03-02T19:05:37')
Time.zone.parse('2015-03-02T19:05:37Z') # Respect ISO 8601 format with timezone specifier.

@example EnforcedStyle: strict

# `strict` means that `Time` should be used with `zone`.

# bad
Time.at(timestamp).in_time_zone

@example EnforcedStyle: flexible (default)

# `flexible` allows usage of `in_time_zone` instead of `zone`.

# good
Time.at(timestamp).in_time_zone

Constants

ACCEPTED_METHODS
DANGEROUS_METHODS
GOOD_METHODS
MSG
MSG_ACCEPTABLE
MSG_LOCALTIME
TIMEZONE_SPECIFIER

Public Instance Methods

on_const(node) click to toggle source
# File lib/rubocop/cop/rails/time_zone.rb, line 61
def on_const(node)
  mod, klass = *node
  # we should only check core classes
  # (`Time` or `::Time`)
  return unless (mod.nil? || mod.cbase_type?) && method_send?(node)

  check_time_node(klass, node.parent) if klass == :Time
end

Private Instance Methods

acceptable_methods(klass, method_name, node) click to toggle source
# File lib/rubocop/cop/rails/time_zone.rb, line 226
def acceptable_methods(klass, method_name, node)
  acceptable = [
    "`Time.zone.#{safe_method(method_name, node)}`",
    "`#{klass}.current`"
  ]

  ACCEPTED_METHODS.each do |am|
    acceptable << "`#{klass}.#{method_name}.#{am}`"
  end

  acceptable
end
attach_timezone_specifier?(date) click to toggle source
# File lib/rubocop/cop/rails/time_zone.rb, line 131
def attach_timezone_specifier?(date)
  date.respond_to?(:value) && TIMEZONE_SPECIFIER.match?(date.value.to_s[-1])
end
autocorrect(corrector, node) click to toggle source
# File lib/rubocop/cop/rails/time_zone.rb, line 72
def autocorrect(corrector, node)
  # add `.zone`: `Time.at` => `Time.zone.at`
  corrector.insert_after(node.children[0].source_range, '.zone')

  case node.method_name
  when :current
    # replace `Time.zone.current` => `Time.zone.now`
    corrector.replace(node.loc.selector, 'now')
  when :new
    autocorrect_time_new(node, corrector)
  end

  # prefer `Time` over `DateTime` class
  corrector.replace(node.children.first.source_range, 'Time') if strict?
  remove_redundant_in_time_zone(corrector, node)
end
autocorrect_time_new(node, corrector) click to toggle source
# File lib/rubocop/cop/rails/time_zone.rb, line 89
def autocorrect_time_new(node, corrector)
  if node.arguments?
    corrector.replace(node.loc.selector, 'local')
  else
    corrector.replace(node.loc.selector, 'now')
  end
end
build_message(klass, method_name, node) click to toggle source
# File lib/rubocop/cop/rails/time_zone.rb, line 135
def build_message(klass, method_name, node)
  if flexible?
    format(
      MSG_ACCEPTABLE,
      current: "#{klass}.#{method_name}",
      prefer: acceptable_methods(klass, method_name, node).join(', ')
    )
  else
    safe_method_name = safe_method(method_name, node)
    format(MSG,
           current: "#{klass}.#{method_name}",
           prefer: "Time.zone.#{safe_method_name}")
  end
end
check_localtime(node) click to toggle source
# File lib/rubocop/cop/rails/time_zone.rb, line 186
def check_localtime(node)
  selector_node = node

  while node&.send_type?
    break if node.method?(:localtime)

    node = node.parent
  end

  return if node.arguments?

  add_offense(selector_node.loc.selector, message: MSG_LOCALTIME) do |corrector|
    autocorrect(corrector, selector_node)
  end
end
check_time_node(klass, node) click to toggle source
# File lib/rubocop/cop/rails/time_zone.rb, line 113
def check_time_node(klass, node)
  return if attach_timezone_specifier?(node.first_argument)

  chain = extract_method_chain(node)
  return if not_danger_chain?(chain)
  return check_localtime(node) if need_check_localtime?(chain)

  method_name = (chain & DANGEROUS_METHODS).join('.')

  return if offset_provided?(node)

  message = build_message(klass, method_name, node)

  add_offense(node.loc.selector, message: message) do |corrector|
    autocorrect(corrector, node)
  end
end
extract_method_chain(node) click to toggle source
# File lib/rubocop/cop/rails/time_zone.rb, line 150
def extract_method_chain(node)
  chain = []
  while !node.nil? && node.send_type?
    chain << node.method_name if method_from_time_class?(node)
    node = node.parent
  end
  chain
end
flexible?() click to toggle source
# File lib/rubocop/cop/rails/time_zone.rb, line 210
def flexible?
  style == :flexible
end
good_methods() click to toggle source
# File lib/rubocop/cop/rails/time_zone.rb, line 218
def good_methods
  if strict?
    GOOD_METHODS
  else
    GOOD_METHODS + [:current] + ACCEPTED_METHODS
  end
end
method_from_time_class?(node) click to toggle source

Only add the method to the chain if the method being called is part of the time class.

# File lib/rubocop/cop/rails/time_zone.rb, line 161
def method_from_time_class?(node)
  receiver, method_name, *_args = *node
  if (receiver.is_a? RuboCop::AST::Node) && !receiver.cbase_type?
    method_from_time_class?(receiver)
  else
    method_name == :Time
  end
end
method_send?(node) click to toggle source

checks that parent node of send_type and receiver is the given node

# File lib/rubocop/cop/rails/time_zone.rb, line 172
def method_send?(node)
  return false unless node.parent&.send_type?

  node.parent.receiver == node
end
need_check_localtime?(chain) click to toggle source
# File lib/rubocop/cop/rails/time_zone.rb, line 206
def need_check_localtime?(chain)
  flexible? && chain.include?(:localtime)
end
not_danger_chain?(chain) click to toggle source
# File lib/rubocop/cop/rails/time_zone.rb, line 202
def not_danger_chain?(chain)
  (chain & DANGEROUS_METHODS).empty? || !(chain & good_methods).empty?
end
offset_provided?(node) click to toggle source

Time.new can be called with a time zone offset When it is, that should be considered safe Example: Time.new(1988, 3, 15, 3, 0, 0, “-05:00”)

# File lib/rubocop/cop/rails/time_zone.rb, line 243
def offset_provided?(node)
  node.arguments.size >= 7
end
remove_redundant_in_time_zone(corrector, node) click to toggle source

remove redundant `.in_time_zone` from `Time.zone.now.in_time_zone`

# File lib/rubocop/cop/rails/time_zone.rb, line 98
def remove_redundant_in_time_zone(corrector, node)
  time_methods_called = extract_method_chain(node)
  return unless time_methods_called.include?(:in_time_zone) ||
                time_methods_called.include?(:zone)

  while node&.send_type?
    if node.children.last == :in_time_zone
      in_time_zone_with_dot =
        node.loc.selector.adjust(begin_pos: -1)
      corrector.remove(in_time_zone_with_dot)
    end
    node = node.parent
  end
end
safe_method(method_name, node) click to toggle source
# File lib/rubocop/cop/rails/time_zone.rb, line 178
def safe_method(method_name, node)
  if %w[new current].include?(method_name)
    node.arguments? ? 'local' : 'now'
  else
    method_name
  end
end
strict?() click to toggle source
# File lib/rubocop/cop/rails/time_zone.rb, line 214
def strict?
  style == :strict
end