# frozen_string_literal: truemoduleRuboCopmoduleCopmoduleRails# This cop checks for the use of Time methods without zone.## Built on top of Ruby on Rails style guide (https://rails.rubystyle.guide#time)# and the article http://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_zoneclassTimeZone<BaseincludeConfigurableEnforcedStyleextendAutoCorrectorMSG='Do not use `%<current>s` without zone. Use `%<prefer>s` '\'instead.'MSG_ACCEPTABLE='Do not use `%<current>s` without zone. '\'Use one of %<prefer>s instead.'MSG_LOCALTIME='Do not use `Time.localtime` without '\'offset or zone.'GOOD_METHODS=%i[zone zone_default find_zone find_zone!].freezeDANGEROUS_METHODS=%i[now local new parse at].freezeACCEPTED_METHODS=%i[in_time_zone utc getlocal xmlschema iso8601
jisx0301 rfc3339 httpdate to_i to_f].freezeTIMEZONE_SPECIFIER=/[A-z]/.freezedefon_const(node)mod,klass=*node# we should only check core classes# (`Time` or `::Time`)returnunless(mod.nil?||mod.cbase_type?)&&method_send?(node)check_time_node(klass,node.parent)ifklass==:Timeendprivatedefautocorrect(corrector,node)# add `.zone`: `Time.at` => `Time.zone.at`corrector.insert_after(node.children[0].source_range,'.zone')casenode.method_namewhen:current# replace `Time.zone.current` => `Time.zone.now`corrector.replace(node.loc.selector,'now')when:newautocorrect_time_new(node,corrector)end# prefer `Time` over `DateTime` classcorrector.replace(node.children.first.source_range,'Time')ifstrict?remove_redundant_in_time_zone(corrector,node)enddefautocorrect_time_new(node,corrector)ifnode.arguments?corrector.replace(node.loc.selector,'local')elsecorrector.replace(node.loc.selector,'now')endend# remove redundant `.in_time_zone` from `Time.zone.now.in_time_zone`defremove_redundant_in_time_zone(corrector,node)time_methods_called=extract_method_chain(node)returnunlesstime_methods_called.include?(:in_time_zone)||time_methods_called.include?(:zone)whilenode&.send_type?ifnode.children.last==:in_time_zonein_time_zone_with_dot=node.loc.selector.adjust(begin_pos: -1)corrector.remove(in_time_zone_with_dot)endnode=node.parentendenddefcheck_time_node(klass,node)returnifattach_timezone_specifier?(node.first_argument)chain=extract_method_chain(node)returnifnot_danger_chain?(chain)returncheck_localtime(node)ifneed_check_localtime?(chain)method_name=(chain&DANGEROUS_METHODS).join('.')returnifoffset_provided?(node)message=build_message(klass,method_name,node)add_offense(node.loc.selector,message: message)do|corrector|autocorrect(corrector,node)endenddefattach_timezone_specifier?(date)date.respond_to?(:value)&&TIMEZONE_SPECIFIER.match?(date.value.to_s[-1])enddefbuild_message(klass,method_name,node)ifflexible?format(MSG_ACCEPTABLE,current: "#{klass}.#{method_name}",prefer: acceptable_methods(klass,method_name,node).join(', '))elsesafe_method_name=safe_method(method_name,node)format(MSG,current: "#{klass}.#{method_name}",prefer: "Time.zone.#{safe_method_name}")endenddefextract_method_chain(node)chain=[]while!node.nil?&&node.send_type?chain<<node.method_nameifmethod_from_time_class?(node)node=node.parentendchainend# Only add the method to the chain if the method being# called is part of the time class.defmethod_from_time_class?(node)receiver,method_name,*_args=*nodeif(receiver.is_a?RuboCop::AST::Node)&&!receiver.cbase_type?method_from_time_class?(receiver)elsemethod_name==:Timeendend# checks that parent node of send_type# and receiver is the given nodedefmethod_send?(node)returnfalseunlessnode.parent&.send_type?node.parent.receiver==nodeenddefsafe_method(method_name,node)if%w[new current].include?(method_name)node.arguments??'local':'now'elsemethod_nameendenddefcheck_localtime(node)selector_node=nodewhilenode&.send_type?breakifnode.method?(:localtime)node=node.parentendreturnifnode.arguments?add_offense(selector_node.loc.selector,message: MSG_LOCALTIME)do|corrector|autocorrect(corrector,selector_node)endenddefnot_danger_chain?(chain)(chain&DANGEROUS_METHODS).empty?||!(chain&good_methods).empty?enddefneed_check_localtime?(chain)flexible?&&chain.include?(:localtime)enddefflexible?style==:flexibleenddefstrict?style==:strictenddefgood_methodsifstrict?GOOD_METHODSelseGOOD_METHODS+[:current]+ACCEPTED_METHODSendenddefacceptable_methods(klass,method_name,node)acceptable=["`Time.zone.#{safe_method(method_name,node)}`","`#{klass}.current`"]ACCEPTED_METHODS.eachdo|am|acceptable<<"`#{klass}.#{method_name}.#{am}`"endacceptableend# 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")defoffset_provided?(node)node.arguments.size>=7endendendendend