debian-mirror-gitlab/lib/gitlab/utils/override.rb

188 lines
5.4 KiB
Ruby
Raw Normal View History

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