2018-11-19 21:01:13 -05:00
# frozen_string_literal: true
2019-01-21 14:20:38 -05:00
require_dependency 'gitlab/utils'
2017-12-25 05:20:50 -05:00
module Gitlab
module Utils
module Override
class Extension
2018-12-01 16:52:43 -05:00
def self . verify_class! ( klass , method_name , arity )
extension = new ( klass )
parents = extension . parents_for ( klass )
extension . verify_method! (
klass : klass , parents : parents , method_name : method_name , sub_method_arity : arity )
2017-12-25 05:20:50 -05:00
end
attr_reader :subject
def initialize ( subject )
@subject = subject
end
2018-12-01 16:52:43 -05:00
def parents_for ( klass )
index = klass . ancestors . index ( subject )
klass . ancestors . drop ( index + 1 )
2017-12-25 05:20:50 -05:00
end
def verify!
classes . each do | klass |
2018-12-01 16:52:43 -05:00
parents = parents_for ( klass )
method_names . each_pair do | method_name , arity |
verify_method! (
klass : klass ,
parents : parents ,
method_name : method_name ,
sub_method_arity : arity )
2017-12-25 05:20:50 -05:00
end
end
end
2018-12-01 16:52:43 -05:00
def verify_method! ( klass : , parents : , method_name : , sub_method_arity : )
overridden_parent = parents . find do | parent |
instance_method_defined? ( parent , method_name )
end
raise NotImplementedError . new ( " #{ klass } \# #{ method_name } doesn't exist! " ) unless overridden_parent
super_method_arity = find_direct_method ( overridden_parent , method_name ) . arity
unless arity_compatible? ( sub_method_arity , super_method_arity )
raise NotImplementedError . new ( " #{ subject } \# #{ method_name } has arity of #{ sub_method_arity } , but #{ overridden_parent } \# #{ method_name } has arity of #{ super_method_arity } " )
end
end
def add_method_name ( method_name , arity = nil )
method_names [ method_name ] = arity
end
def add_class ( klass )
classes << klass
end
def verify_override? ( method_name )
method_names . has_key? ( method_name )
end
2017-12-25 05:20:50 -05:00
private
2018-12-01 16:52:43 -05:00
def instance_method_defined? ( klass , name )
klass . instance_methods ( false ) . include? ( name ) ||
klass . private_instance_methods ( false ) . include? ( name )
end
def find_direct_method ( klass , name )
method = klass . instance_method ( name )
method = method . super_method until method && klass == method . owner
method
end
def arity_compatible? ( sub_method_arity , super_method_arity )
if sub_method_arity > = 0 && super_method_arity > = 0
# Regular arguments
sub_method_arity == super_method_arity
else
# It's too complex to check this case, just allow sub-method having negative arity
# But we don't allow sub_method_arity > 0 yet super_method_arity < 0
sub_method_arity < 0
end
end
2017-12-25 05:20:50 -05:00
def method_names
2018-12-01 16:52:43 -05:00
@method_names || = { }
2017-12-25 05:20:50 -05:00
end
def classes
@classes || = [ ]
end
end
# Instead of writing patterns like this:
#
# def f
# raise NotImplementedError unless defined?(super)
#
# true
# end
#
# We could write it like:
#
# extend ::Gitlab::Utils::Override
#
# override :f
# def f
# true
# end
#
# This would make sure we're overriding something. See:
2019-09-18 10:02:45 -04:00
# https://gitlab.com/gitlab-org/gitlab/issues/1819
2017-12-25 05:20:50 -05:00
def override ( method_name )
return unless ENV [ 'STATIC_VERIFICATION' ]
2018-12-01 16:52:43 -05:00
Override . extensions [ self ] || = Extension . new ( self )
Override . extensions [ self ] . add_method_name ( method_name )
end
def method_added ( method_name )
super
return unless ENV [ 'STATIC_VERIFICATION' ]
return unless Override . extensions [ self ] & . verify_override? ( method_name )
method_arity = instance_method ( method_name ) . arity
2017-12-25 05:20:50 -05:00
if is_a? ( Class )
2018-12-01 16:52:43 -05:00
Extension . verify_class! ( self , method_name , method_arity )
2017-12-25 05:20:50 -05:00
else # We delay the check for modules
2018-12-01 16:52:43 -05:00
Override . extensions [ self ] . add_method_name ( method_name , method_arity )
2017-12-25 05:20:50 -05:00
end
end
def included ( base = nil )
2018-06-04 10:54:50 -04:00
super
2018-08-31 13:23:22 -04:00
queue_verification ( base ) if base
2018-06-04 10:54:50 -04:00
end
2017-12-25 05:20:50 -05:00
2018-08-30 07:51:32 -04:00
def prepended ( base = nil )
super
2019-12-17 19:08:09 -05:00
# prepend can override methods, thus we need to verify it like classes
queue_verification ( base , verify : true ) if base
2018-08-30 07:51:32 -04:00
end
2018-06-04 10:54:50 -04:00
2018-08-30 07:51:32 -04:00
def extended ( mod = nil )
2017-12-25 05:20:50 -05:00
super
2021-01-29 13:09:17 -05:00
# Hack to resolve https://gitlab.com/gitlab-org/gitlab/-/issues/23932
is_not_concern_hack =
( mod . is_a? ( Class ) || ! name & . end_with? ( '::ClassMethods' ) )
if mod && is_not_concern_hack
queue_verification ( mod . singleton_class )
end
2018-06-04 10:54:50 -04:00
end
2019-12-17 19:08:09 -05:00
def queue_verification ( base , verify : false )
2018-06-04 10:54:50 -04:00
return unless ENV [ 'STATIC_VERIFICATION' ]
2019-12-17 19:08:09 -05:00
# We could check for Class in `override`
# This could be `nil` if `override` was never called.
# We also force verification for prepend because it can also override
# a method like a class, but not the cases for include or extend.
# This includes Rails helpers but not limited to.
if base . is_a? ( Class ) || verify
2017-12-25 05:20:50 -05:00
Override . extensions [ self ] & . add_class ( base )
end
end
def self . extensions
@extensions || = { }
end
def self . verify!
2021-01-29 13:09:17 -05:00
extensions . each_value ( & :verify! )
2017-12-25 05:20:50 -05:00
end
end
end
end