summaryrefslogtreecommitdiff
path: root/lib/gitlab/utils
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-09-21 09:12:21 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2021-09-21 09:12:21 +0000
commit63a014fe28a4d5cbcc4ac2e89abcc6ab40c0df12 (patch)
treef3088c179faaca35048ecdb10c0c73bd43a9c3ff /lib/gitlab/utils
parentc84d80849d0ce96d4464a8d60e519e4bfe044185 (diff)
downloadgitlab-ce-63a014fe28a4d5cbcc4ac2e89abcc6ab40c0df12.tar.gz
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'lib/gitlab/utils')
-rw-r--r--lib/gitlab/utils/delegator_override.rb48
-rw-r--r--lib/gitlab/utils/delegator_override/error.rb23
-rw-r--r--lib/gitlab/utils/delegator_override/validator.rb99
3 files changed, 170 insertions, 0 deletions
diff --git a/lib/gitlab/utils/delegator_override.rb b/lib/gitlab/utils/delegator_override.rb
new file mode 100644
index 00000000000..15ba29d3916
--- /dev/null
+++ b/lib/gitlab/utils/delegator_override.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Utils
+ # This module is to validate that delegator classes (`SimpleDelegator`) do not
+ # accidentally override important logic on the fabricated object.
+ module DelegatorOverride
+ def delegator_target(target_class)
+ return unless ENV['STATIC_VERIFICATION']
+
+ unless self < ::SimpleDelegator
+ raise ArgumentError, "'#{self}' is not a subclass of 'SimpleDelegator' class."
+ end
+
+ DelegatorOverride.validator(self).add_target(target_class)
+ end
+
+ def delegator_override(*names)
+ return unless ENV['STATIC_VERIFICATION']
+ raise TypeError unless names.all? { |n| n.is_a?(Symbol) }
+
+ DelegatorOverride.validator(self).add_allowlist(names)
+ end
+
+ def delegator_override_with(mod)
+ return unless ENV['STATIC_VERIFICATION']
+ raise TypeError unless mod.is_a?(Module)
+
+ DelegatorOverride.validator(self).add_allowlist(mod.instance_methods)
+ end
+
+ def self.validator(delegator_class)
+ validators[delegator_class] ||= Validator.new(delegator_class)
+ end
+
+ def self.validators
+ @validators ||= {}
+ end
+
+ def self.verify!
+ validators.each_value do |validator|
+ validator.expand_on_ancestors(validators)
+ validator.validate_overrides!
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/utils/delegator_override/error.rb b/lib/gitlab/utils/delegator_override/error.rb
new file mode 100644
index 00000000000..dfe8d5468b4
--- /dev/null
+++ b/lib/gitlab/utils/delegator_override/error.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Utils
+ module DelegatorOverride
+ class Error
+ attr_accessor :method_name, :target_class, :target_location, :delegator_class, :delegator_location
+
+ def initialize(method_name, target_class, target_location, delegator_class, delegator_location)
+ @method_name = method_name
+ @target_class = target_class
+ @target_location = target_location
+ @delegator_class = delegator_class
+ @delegator_location = delegator_location
+ end
+
+ def to_s
+ "#{delegator_class}##{method_name} is overriding #{target_class}##{method_name}. delegator_location: #{delegator_location} target_location: #{target_location}"
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/utils/delegator_override/validator.rb b/lib/gitlab/utils/delegator_override/validator.rb
new file mode 100644
index 00000000000..825b3efa203
--- /dev/null
+++ b/lib/gitlab/utils/delegator_override/validator.rb
@@ -0,0 +1,99 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Utils
+ module DelegatorOverride
+ class Validator
+ UnexpectedDelegatorOverrideError = Class.new(StandardError)
+
+ attr_reader :delegator_class, :target_classes
+
+ OVERRIDE_ERROR_MESSAGE = <<~EOS
+ We've detected that the delegator is overriding a specific method(s) on the target class.
+ Please make sure if it's intentional and handle this error accordingly.
+ See https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/presenters/README.md#validate-accidental-overrides for more information.
+ EOS
+
+ def initialize(delegator_class)
+ @delegator_class = delegator_class
+ @target_classes = []
+ end
+
+ def add_allowlist(names)
+ allowed_method_names.concat(names)
+ end
+
+ def allowed_method_names
+ @allowed_method_names ||= []
+ end
+
+ def add_target(target_class)
+ @target_classes << target_class if target_class
+ end
+
+ # This will make sure allowlist we put into ancestors are all included
+ def expand_on_ancestors(validators)
+ delegator_class.ancestors.each do |ancestor|
+ next if delegator_class == ancestor # ancestor includes itself
+
+ validator_ancestor = validators[ancestor]
+
+ next unless validator_ancestor
+
+ add_allowlist(validator_ancestor.allowed_method_names)
+ end
+ end
+
+ def validate_overrides!
+ return if target_classes.empty?
+
+ errors = []
+
+ # Workaround to fully load the instance methods in the target class.
+ # See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/69823#note_678887402
+ target_classes.map(&:new) rescue nil
+
+ (delegator_class.instance_methods - allowlist).each do |method_name|
+ target_classes.each do |target_class|
+ next unless target_class.instance_methods.include?(method_name)
+
+ errors << generate_error(method_name, target_class, delegator_class)
+ end
+ end
+
+ return if errors.empty?
+
+ details = errors.map { |error| "- #{error}" }.join("\n")
+
+ raise UnexpectedDelegatorOverrideError,
+ <<~TEXT
+ #{OVERRIDE_ERROR_MESSAGE}
+ Here are the conflict details.
+
+ #{details}
+ TEXT
+ end
+
+ private
+
+ def generate_error(method_name, target_class, delegator_class)
+ target_location = extract_location(target_class, method_name)
+ delegator_location = extract_location(delegator_class, method_name)
+ Error.new(method_name, target_class, target_location, delegator_class, delegator_location)
+ end
+
+ def extract_location(klass, method_name)
+ klass.instance_method(method_name).source_location&.join(':') || 'unknown'
+ end
+
+ def allowlist
+ [].tap do |allowed|
+ allowed.concat(allowed_method_names)
+ allowed.concat(Object.instance_methods)
+ allowed.concat(::Delegator.instance_methods)
+ end
+ end
+ end
+ end
+ end
+end