summaryrefslogtreecommitdiff
path: root/lib/gitlab/utils/override.rb
blob: d00921e6cdc28ec6c4805a940eb400a0c609d5b5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
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)
        super

        queue_verification(base) if base
      end

      def prepended(base = nil)
        super

        queue_verification(base) if base
      end

      def extended(mod = nil)
        super

        queue_verification(mod.singleton_class) if mod
      end

      def queue_verification(base)
        return unless ENV['STATIC_VERIFICATION']

        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

      def self.extensions
        @extensions ||= {}
      end

      def self.verify!
        extensions.values.each(&:verify!)
      end
    end
  end
end