Use Gitlab::Utils::Override
over defined?(super)
This commit is contained in:
parent
29749f92b7
commit
8139895b43
6 changed files with 299 additions and 9 deletions
|
@ -87,9 +87,9 @@ still having access the class's implementation with `super`.
|
|||
|
||||
There are a few gotchas with it:
|
||||
|
||||
- you should always add a `raise NotImplementedError unless defined?(super)`
|
||||
guard clause in the "overrider" method to ensure that if the method gets
|
||||
renamed in CE, the EE override won't be silently forgotten.
|
||||
- you should always `extend ::Gitlab::Utils::Override` and use `override` to
|
||||
guard the "overrider" method to ensure that if the method gets renamed in
|
||||
CE, the EE override won't be silently forgotten.
|
||||
- when the "overrider" would add a line in the middle of the CE
|
||||
implementation, you should refactor the CE method and split it in
|
||||
smaller methods. Or create a "hook" method that is empty in CE,
|
||||
|
@ -134,6 +134,9 @@ There are a few gotchas with it:
|
|||
guards:
|
||||
``` ruby
|
||||
module EE::Base
|
||||
extend ::Gitlab::Utils::Override
|
||||
|
||||
override :do_something
|
||||
def do_something
|
||||
# Follow the above pattern to call super and extend it
|
||||
end
|
||||
|
@ -174,10 +177,11 @@ implementation:
|
|||
|
||||
```ruby
|
||||
module EE
|
||||
class ApplicationController
|
||||
def after_sign_out_path_for(resource)
|
||||
raise NotImplementedError unless defined?(super)
|
||||
module ApplicationController
|
||||
extend ::Gitlab::Utils::Override
|
||||
|
||||
override :after_sign_out_path_for
|
||||
def after_sign_out_path_for(resource)
|
||||
if Gitlab::Geo.secondary?
|
||||
Gitlab::Geo.primary_node.oauth_logout_url(@geo_logout_state)
|
||||
else
|
||||
|
@ -209,7 +213,6 @@ In EE, the implementation `ee/app/models/ee/users.rb` would be:
|
|||
|
||||
```ruby
|
||||
def full_private_access?
|
||||
raise NotImplementedError unless defined?(super)
|
||||
super || auditor?
|
||||
end
|
||||
```
|
||||
|
|
111
lib/gitlab/utils/override.rb
Normal file
111
lib/gitlab/utils/override.rb
Normal file
|
@ -0,0 +1,111 @@
|
|||
module Gitlab
|
||||
module Utils
|
||||
module Override
|
||||
class Extension
|
||||
def self.verify_class!(klass, method_name)
|
||||
instance_method_defined?(klass, method_name) ||
|
||||
raise(
|
||||
NotImplementedError.new(
|
||||
"#{klass}\##{method_name} doesn't exist!"))
|
||||
end
|
||||
|
||||
def self.instance_method_defined?(klass, name, include_super: true)
|
||||
klass.instance_methods(include_super).include?(name) ||
|
||||
klass.private_instance_methods(include_super).include?(name)
|
||||
end
|
||||
|
||||
attr_reader :subject
|
||||
|
||||
def initialize(subject)
|
||||
@subject = subject
|
||||
end
|
||||
|
||||
def add_method_name(method_name)
|
||||
method_names << method_name
|
||||
end
|
||||
|
||||
def add_class(klass)
|
||||
classes << klass
|
||||
end
|
||||
|
||||
def verify!
|
||||
classes.each do |klass|
|
||||
index = klass.ancestors.index(subject)
|
||||
parents = klass.ancestors.drop(index + 1)
|
||||
|
||||
method_names.each do |method_name|
|
||||
parents.any? do |parent|
|
||||
self.class.instance_method_defined?(
|
||||
parent, method_name, include_super: false)
|
||||
end ||
|
||||
raise(
|
||||
NotImplementedError.new(
|
||||
"#{klass}\##{method_name} doesn't exist!"))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def method_names
|
||||
@method_names ||= []
|
||||
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:
|
||||
# https://gitlab.com/gitlab-org/gitlab-ee/issues/1819
|
||||
def override(method_name)
|
||||
return unless ENV['STATIC_VERIFICATION']
|
||||
|
||||
if is_a?(Class)
|
||||
Extension.verify_class!(self, method_name)
|
||||
else # We delay the check for modules
|
||||
Override.extensions[self] ||= Extension.new(self)
|
||||
Override.extensions[self].add_method_name(method_name)
|
||||
end
|
||||
end
|
||||
|
||||
def included(base = nil)
|
||||
return super if base.nil? # Rails concern, ignoring it
|
||||
|
||||
super
|
||||
|
||||
if base.is_a?(Class) # We could check for Class in `override`
|
||||
# This could be `nil` if `override` was never called
|
||||
Override.extensions[self]&.add_class(base)
|
||||
end
|
||||
end
|
||||
|
||||
alias_method :prepended, :included
|
||||
|
||||
def self.extensions
|
||||
@extensions ||= {}
|
||||
end
|
||||
|
||||
def self.verify!
|
||||
extensions.values.each(&:verify!)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -7,4 +7,9 @@ namespace :dev do
|
|||
Rake::Task["gitlab:setup"].invoke
|
||||
Rake::Task["gitlab:shell:setup"].invoke
|
||||
end
|
||||
|
||||
desc "GitLab | Eager load application"
|
||||
task load: :environment do
|
||||
Rails.application.eager_load!
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,5 +1,17 @@
|
|||
unless Rails.env.production?
|
||||
namespace :lint do
|
||||
task :static_verification_env do
|
||||
ENV['STATIC_VERIFICATION'] = 'true'
|
||||
end
|
||||
|
||||
desc "GitLab | lint | Static verification"
|
||||
task static_verification: %w[
|
||||
lint:static_verification_env
|
||||
dev:load
|
||||
] do
|
||||
Gitlab::Utils::Override.verify!
|
||||
end
|
||||
|
||||
desc "GitLab | lint | Lint JavaScript files using ESLint"
|
||||
task :javascript do
|
||||
Rake::Task['eslint'].invoke
|
||||
|
|
|
@ -10,9 +10,10 @@ tasks = [
|
|||
%w[bundle exec license_finder],
|
||||
%w[yarn run eslint],
|
||||
%w[bundle exec rubocop --parallel],
|
||||
%w[scripts/lint-conflicts.sh],
|
||||
%w[bundle exec rake gettext:lint],
|
||||
%w[scripts/lint-changelog-yaml]
|
||||
%w[bundle exec rake lint:static_verification],
|
||||
%w[scripts/lint-changelog-yaml],
|
||||
%w[scripts/lint-conflicts.sh]
|
||||
]
|
||||
|
||||
failed_tasks = tasks.reduce({}) do |failures, task|
|
||||
|
|
158
spec/lib/gitlab/utils/override_spec.rb
Normal file
158
spec/lib/gitlab/utils/override_spec.rb
Normal file
|
@ -0,0 +1,158 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Gitlab::Utils::Override do
|
||||
let(:base) { Struct.new(:good) }
|
||||
|
||||
let(:derived) { Class.new(base).tap { |m| m.extend described_class } }
|
||||
let(:extension) { Module.new.tap { |m| m.extend described_class } }
|
||||
|
||||
let(:prepending_class) { base.tap { |m| m.prepend extension } }
|
||||
let(:including_class) { base.tap { |m| m.include extension } }
|
||||
|
||||
let(:klass) { subject }
|
||||
|
||||
def good(mod)
|
||||
mod.module_eval do
|
||||
override :good
|
||||
def good
|
||||
super.succ
|
||||
end
|
||||
end
|
||||
|
||||
mod
|
||||
end
|
||||
|
||||
def bad(mod)
|
||||
mod.module_eval do
|
||||
override :bad
|
||||
def bad
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
mod
|
||||
end
|
||||
|
||||
shared_examples 'checking as intended' do
|
||||
it 'checks ok for overriding method' do
|
||||
good(subject)
|
||||
result = klass.new(0).good
|
||||
|
||||
expect(result).to eq(1)
|
||||
described_class.verify!
|
||||
end
|
||||
|
||||
it 'raises NotImplementedError when it is not overriding anything' do
|
||||
expect do
|
||||
bad(subject)
|
||||
klass.new(0).bad
|
||||
described_class.verify!
|
||||
end.to raise_error(NotImplementedError)
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'nothing happened' do
|
||||
it 'does not complain when it is overriding something' do
|
||||
good(subject)
|
||||
result = klass.new(0).good
|
||||
|
||||
expect(result).to eq(1)
|
||||
described_class.verify!
|
||||
end
|
||||
|
||||
it 'does not complain when it is not overriding anything' do
|
||||
bad(subject)
|
||||
result = klass.new(0).bad
|
||||
|
||||
expect(result).to eq(true)
|
||||
described_class.verify!
|
||||
end
|
||||
end
|
||||
|
||||
before do
|
||||
# Make sure we're not touching the internal cache
|
||||
allow(described_class).to receive(:extensions).and_return({})
|
||||
end
|
||||
|
||||
describe '#override' do
|
||||
context 'when STATIC_VERIFICATION is set' do
|
||||
before do
|
||||
stub_env('STATIC_VERIFICATION', 'true')
|
||||
end
|
||||
|
||||
context 'when subject is a class' do
|
||||
subject { derived }
|
||||
|
||||
it_behaves_like 'checking as intended'
|
||||
end
|
||||
|
||||
context 'when subject is a module, and class is prepending it' do
|
||||
subject { extension }
|
||||
let(:klass) { prepending_class }
|
||||
|
||||
it_behaves_like 'checking as intended'
|
||||
end
|
||||
|
||||
context 'when subject is a module, and class is including it' do
|
||||
subject { extension }
|
||||
let(:klass) { including_class }
|
||||
|
||||
it 'raises NotImplementedError because it is not overriding it' do
|
||||
expect do
|
||||
good(subject)
|
||||
klass.new(0).good
|
||||
described_class.verify!
|
||||
end.to raise_error(NotImplementedError)
|
||||
end
|
||||
|
||||
it 'raises NotImplementedError when it is not overriding anything' do
|
||||
expect do
|
||||
bad(subject)
|
||||
klass.new(0).bad
|
||||
described_class.verify!
|
||||
end.to raise_error(NotImplementedError)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when STATIC_VERIFICATION is not set' do
|
||||
before do
|
||||
stub_env('STATIC_VERIFICATION', nil)
|
||||
end
|
||||
|
||||
context 'when subject is a class' do
|
||||
subject { derived }
|
||||
|
||||
it_behaves_like 'nothing happened'
|
||||
end
|
||||
|
||||
context 'when subject is a module, and class is prepending it' do
|
||||
subject { extension }
|
||||
let(:klass) { prepending_class }
|
||||
|
||||
it_behaves_like 'nothing happened'
|
||||
end
|
||||
|
||||
context 'when subject is a module, and class is including it' do
|
||||
subject { extension }
|
||||
let(:klass) { including_class }
|
||||
|
||||
it 'does not complain when it is overriding something' do
|
||||
good(subject)
|
||||
result = klass.new(0).good
|
||||
|
||||
expect(result).to eq(0)
|
||||
described_class.verify!
|
||||
end
|
||||
|
||||
it 'does not complain when it is not overriding anything' do
|
||||
bad(subject)
|
||||
result = klass.new(0).bad
|
||||
|
||||
expect(result).to eq(true)
|
||||
described_class.verify!
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue