Class: RBS::Test::Hook

Inherits:
Object
  • Object
show all
Defined in:
lib/rbs/test/hook.rb

Defined Under Namespace

Classes: Error

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(env, klass, logger:, raise_on_error: false) ⇒ Hook

Returns a new instance of Hook.



35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# File 'lib/rbs/test/hook.rb', line 35

def initialize(env, klass, logger:, raise_on_error: false)
  @env = env
  @logger = logger
  @klass = klass

  @instance_module = Module.new
  @instance_methods = []

  @singleton_module = Module.new
  @singleton_methods = []

  @errors = []

  @raise_on_error = raise_on_error
end

Instance Attribute Details

#envObject (readonly)

Returns the value of attribute env.



16
17
18
# File 'lib/rbs/test/hook.rb', line 16

def env
  @env
end

#errorsObject (readonly)

Returns the value of attribute errors.



25
26
27
# File 'lib/rbs/test/hook.rb', line 25

def errors
  @errors
end

#instance_methodsObject (readonly)

Returns the value of attribute instance_methods.



20
21
22
# File 'lib/rbs/test/hook.rb', line 20

def instance_methods
  @instance_methods
end

#instance_moduleObject (readonly)

Returns the value of attribute instance_module.



19
20
21
# File 'lib/rbs/test/hook.rb', line 19

def instance_module
  @instance_module
end

#klassObject (readonly)

Returns the value of attribute klass.



24
25
26
# File 'lib/rbs/test/hook.rb', line 24

def klass
  @klass
end

#loggerObject (readonly)

Returns the value of attribute logger.



17
18
19
# File 'lib/rbs/test/hook.rb', line 17

def logger
  @logger
end

#singleton_methodsObject (readonly)

Returns the value of attribute singleton_methods.



22
23
24
# File 'lib/rbs/test/hook.rb', line 22

def singleton_methods
  @singleton_methods
end

#singleton_moduleObject (readonly)

Returns the value of attribute singleton_module.



21
22
23
# File 'lib/rbs/test/hook.rb', line 21

def singleton_module
  @singleton_module
end

Class Method Details

.backtrace(skip: 2) ⇒ Object



260
261
262
263
264
# File 'lib/rbs/test/hook.rb', line 260

def self.backtrace(skip: 2)
  raise
rescue => exn
  exn.backtrace.drop(skip)
end

.inspect_(obj) ⇒ Object



281
282
283
284
285
# File 'lib/rbs/test/hook.rb', line 281

def self.inspect_(obj)
  obj.inspect
rescue
  INSPECT.bind(obj).call()
end

.install(env, klass, logger:) ⇒ Object



72
73
74
# File 'lib/rbs/test/hook.rb', line 72

def self.install(env, klass, logger:)
  new(env, klass, logger: logger).prepend!
end

Instance Method Details

#builderObject



27
28
29
# File 'lib/rbs/test/hook.rb', line 27

def builder
  @builder ||= DefinitionBuilder.new(env: env)
end

#call(receiver, method, *args, &block) ⇒ Object



273
274
275
# File 'lib/rbs/test/hook.rb', line 273

def call(receiver, method, *args, &block)
  method.bind(receiver).call(*args, &block)
end

#delegation(name, method_types, method_name) ⇒ Object



124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
# File 'lib/rbs/test/hook.rb', line 124

def delegation(name, method_types, method_name)
  hook = self

  -> (*args, &block) do
    hook.logger.debug { "#{method_name} receives arguments: #{hook.inspect_(args)}" }

    block_calls = []

    if block
      original_block = block

      block = hook.call(Object.new, INSTANCE_EVAL) do |fresh_obj|
        ->(*as) do
          hook.logger.debug { "#{method_name} receives block arguments: #{hook.inspect_(as)}" }

          ret = if self.equal?(fresh_obj)
                  original_block[*as]
                else
                  hook.call(self, INSTANCE_EXEC, *as, &original_block)
                end

          block_calls << ArgumentsReturn.new(
            arguments: as,
            return_value: ret,
            exception: nil
          )

          hook.logger.debug { "#{method_name} returns from block: #{hook.inspect_(ret)}" }

          ret
        end.ruby2_keywords
      end
    end

    method = hook.call(self, METHOD, name)
    klass = hook.call(self, CLASS)
    singleton_klass = begin
      hook.call(self, SINGLETON_CLASS)
    rescue TypeError
      nil
    end
    prepended = klass.ancestors.include?(hook.instance_module) || singleton_klass&.ancestors&.include?(hook.singleton_module)
    exception = nil
    result = begin
       if prepended
         method.super_method.call(*args, &block)
       else
         # Using refinement
         method.call(*args, &block)
       end
    rescue Exception => e
      exception = e
      nil
    end

    hook.logger.debug { "#{method_name} returns: #{hook.inspect_(result)}" }

    call = CallTrace.new(method_call: ArgumentsReturn.new(arguments: args, return_value: result, exception: exception),
                         block_calls: block_calls,
                         block_given: block != nil)

    method_type_errors = method_types.map do |method_type|
      hook.typecheck.method_call(method_name, method_type, call, errors: [])
    end

    new_errors = []

    if method_type_errors.none?(&:empty?)
      if (best_errors = hook.find_best_errors(method_type_errors))
        new_errors.push(*best_errors)
      else
        new_errors << Errors::UnresolvedOverloadingError.new(
          klass: hook.klass,
          method_name: method_name,
          method_types: method_types
        )
      end
    end

    unless new_errors.empty?
      new_errors.each do |error|
        hook.logger.error Errors.to_string(error)
      end

      hook.errors.push(*new_errors)

      if hook.raise_on_error?
        raise Error.new(new_errors)
      end
    end

    result
  end.ruby2_keywords
end

#disableObject



287
288
289
290
291
# File 'lib/rbs/test/hook.rb', line 287

def disable
  self.instance_module.remove_method(*instance_methods)
  self.singleton_module.remove_method(*singleton_methods)
  self
end

#find_best_errors(errorss) ⇒ Object



240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
# File 'lib/rbs/test/hook.rb', line 240

def find_best_errors(errorss)
  if errorss.size == 1
    errorss[0]
  else
    no_arity_errors = errorss.select do |errors|
      errors.none? do |error|
        error.is_a?(Errors::ArgumentError) ||
          error.is_a?(Errors::BlockArgumentError) ||
          error.is_a?(Errors::MissingBlockError) ||
          error.is_a?(Errors::UnexpectedBlockError)
      end
    end

    unless no_arity_errors.empty?
      # Choose a error set which doesn't include arity error
      return no_arity_errors[0] if no_arity_errors.size == 1
    end
  end
end

#inspect_(obj) ⇒ Object



277
278
279
# File 'lib/rbs/test/hook.rb', line 277

def inspect_(obj)
  Hook.inspect_(obj)
end

#prepend!Object



60
61
62
63
64
65
66
67
68
69
70
# File 'lib/rbs/test/hook.rb', line 60

def prepend!
  klass.prepend @instance_module
  klass.singleton_class.prepend @singleton_module

  if block_given?
    yield
    disable
  end

  self
end

#raise_on_error!(error = true) ⇒ Object



51
52
53
54
# File 'lib/rbs/test/hook.rb', line 51

def raise_on_error!(error = true)
  @raise_on_error = error
  self
end

#raise_on_error?Boolean

Returns:

  • (Boolean)


56
57
58
# File 'lib/rbs/test/hook.rb', line 56

def raise_on_error?
  @raise_on_error
end

#refinementObject



76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/rbs/test/hook.rb', line 76

def refinement
  klass = self.klass
  instance_module = self.instance_module
  singleton_module = self.singleton_module

  Module.new do
    refine klass do
      prepend instance_module
    end

    refine klass.singleton_class do
      prepend singleton_module
    end
  end
end

#runObject



266
267
268
269
270
271
# File 'lib/rbs/test/hook.rb', line 266

def run
  yield
  self
ensure
  disable
end

#typecheckObject



31
32
33
# File 'lib/rbs/test/hook.rb', line 31

def typecheck
  @typecheck ||= TypeCheck.new(self_class: klass, builder: builder)
end

#verify(instance_method: nil, singleton_method: nil, types:) ⇒ Object



219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
# File 'lib/rbs/test/hook.rb', line 219

def verify(instance_method: nil, singleton_method: nil, types:)
  method_types = types.map do |type|
    case type
    when String
      Parser.parse_method_type(type)
    else
      type
    end
  end

  case
  when instance_method
    instance_methods << instance_method
    call(self.instance_module, DEFINE_METHOD, instance_method, &delegation(instance_method, method_types, "##{instance_method}"))
  when singleton_method
    call(self.singleton_module, DEFINE_METHOD, singleton_method, &delegation(singleton_method, method_types, ".#{singleton_method}"))
  end

  self
end

#verify_allObject



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
# File 'lib/rbs/test/hook.rb', line 92

def verify_all
  type_name = Namespace.parse(klass.name).to_type_name.absolute!

  builder.build_instance(type_name).tap do |definition|
    definition.methods.each do |name, method|
      if method.defined_in.name.absolute! == type_name
        unless method.annotations.any? {|a| a.string == "rbs:test:skip" }
          logger.info "Installing a hook on #{type_name}##{name}: #{method.method_types.join(" | ")}"
          verify instance_method: name, types: method.method_types
        else
          logger.info "Skipping test of #{type_name}##{name}"
        end
      end
    end
  end

  builder.build_singleton(type_name).tap do |definition|
    definition.methods.each do |name, method|
      if method.defined_in&.name&.absolute! == type_name || name == :new
        unless method.annotations.any? {|a| a.string == "rbs:test:skip" }
          logger.info "Installing a hook on #{type_name}.#{name}: #{method.method_types.join(" | ")}"
          verify singleton_method: name, types: method.method_types
        else
          logger.info "Skipping test of #{type_name}.#{name}"
        end
      end
    end
  end

  self
end