2019-02-15 15:39:39 +05:30
# frozen_string_literal: true
2019-03-02 22:35:43 +05:30
require_dependency 'gitlab/utils'
2018-03-17 18:26:18 +05:30
module Gitlab
module Utils
module Override
class Extension
2019-02-15 15:39:39 +05:30
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 )
2018-03-17 18:26:18 +05:30
end
attr_reader :subject
def initialize ( subject )
@subject = subject
end
2019-02-15 15:39:39 +05:30
def parents_for ( klass )
index = klass . ancestors . index ( subject )
klass . ancestors . drop ( index + 1 )
2018-03-17 18:26:18 +05:30
end
def verify!
classes . each do | klass |
2019-02-15 15:39:39 +05:30
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 )
2018-03-17 18:26:18 +05:30
end
end
end
2019-02-15 15:39:39 +05:30
def verify_method! ( klass : , parents : , method_name : , sub_method_arity : )
overridden_parent = parents . find do | parent |
instance_method_defined? ( parent , method_name )
end
2021-06-08 01:23:25 +05:30
raise NotImplementedError , " #{ klass } \# #{ method_name } doesn't exist! " unless overridden_parent
2019-02-15 15:39:39 +05:30
super_method_arity = find_direct_method ( overridden_parent , method_name ) . arity
unless arity_compatible? ( sub_method_arity , super_method_arity )
2021-06-08 01:23:25 +05:30
raise NotImplementedError , " #{ subject } \# #{ method_name } has arity of #{ sub_method_arity } , but #{ overridden_parent } \# #{ method_name } has arity of #{ super_method_arity } "
2019-02-15 15:39:39 +05:30
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
2018-03-17 18:26:18 +05:30
private
2019-02-15 15:39:39 +05:30
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
2018-03-17 18:26:18 +05:30
def method_names
2019-02-15 15:39:39 +05:30
@method_names || = { }
2018-03-17 18:26:18 +05:30
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-12-04 20:38:33 +05:30
# https://gitlab.com/gitlab-org/gitlab/issues/1819
2018-03-17 18:26:18 +05:30
def override ( method_name )
return unless ENV [ 'STATIC_VERIFICATION' ]
2019-02-15 15:39:39 +05:30
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
2018-03-17 18:26:18 +05:30
if is_a? ( Class )
2019-02-15 15:39:39 +05:30
Extension . verify_class! ( self , method_name , method_arity )
2018-03-17 18:26:18 +05:30
else # We delay the check for modules
2019-02-15 15:39:39 +05:30
Override . extensions [ self ] . add_method_name ( method_name , method_arity )
2018-03-17 18:26:18 +05:30
end
end
def included ( base = nil )
2018-11-08 19:23:39 +05:30
super
2018-12-05 23:21:45 +05:30
queue_verification ( base ) if base
2018-11-08 19:23:39 +05:30
end
2018-03-17 18:26:18 +05:30
2018-12-05 23:21:45 +05:30
def prepended ( base = nil )
super
2020-01-01 13:55:28 +05:30
# prepend can override methods, thus we need to verify it like classes
queue_verification ( base , verify : true ) if base
2018-12-05 23:21:45 +05:30
end
2018-11-08 19:23:39 +05:30
2018-12-05 23:21:45 +05:30
def extended ( mod = nil )
2018-03-17 18:26:18 +05:30
super
2021-03-11 19:13:27 +05:30
# 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-11-08 19:23:39 +05:30
end
2020-01-01 13:55:28 +05:30
def queue_verification ( base , verify : false )
2018-11-08 19:23:39 +05:30
return unless ENV [ 'STATIC_VERIFICATION' ]
2020-01-01 13:55:28 +05:30
# 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
2018-03-17 18:26:18 +05:30
Override . extensions [ self ] & . add_class ( base )
end
end
def self . extensions
@extensions || = { }
end
def self . verify!
2021-03-11 19:13:27 +05:30
extensions . each_value ( & :verify! )
2018-03-17 18:26:18 +05:30
end
end
end
end