Module: Tuile::Final

Included in:
Component
Defined in:
lib/tuile/final.rb,
sig/tuile.rbs

Overview

Ruby's missing final keyword. A class extends this, marks the methods a subclass may not redefine, and calls #verify_final! from its own initialize:

class Component
extend Final

final :children, :parent, :add_child

def initialize = Component.verify_final!(self.class)
end

Class.new(Component) { def children = [] }.new   # => Tuile::Error

Marking a method says only that it is final. Why — the invariant an override would break, and the seam to use instead — belongs in that method's own rdoc, which is where the raise sends the reader.

Implementation details

The check resolves each final method's owner instead of hooking method_added, which fires earlier but sees only def and define_method — an override arriving through an include or a prepend slips past it. Nothing fires late enough to place the check automatically, which is why the extending class owes the explicit #verify_final! call.

Instance Method Summary collapse

Instance Method Details

#final(*names) ⇒ Symbol, ::Array[Symbol]

Marks each of names non-overridable by subclasses. Declare them in one call near the top of the class; the names may be forward references, since nothing is resolved until #verify_final! runs.

final :parent, :children, :add_child

It reads better as a keyword on the definition (final def foo, which parses — def hands back its name), but don't: YARD has no handler for the macro, so a decorated def loses its parameter list and sord then generates def foo: () -> void into sig/, silently.

@param names — method names.

@return — the names — a bare Symbol when exactly one was given.

Parameters:

  • names (::Array[(Symbol | ::Array[Symbol])])

Returns:

  • (Symbol, ::Array[Symbol])


44
45
46
47
48
# File 'lib/tuile/final.rb', line 44

def final(*names)
  names = names.flatten
  (@final_methods ||= []).concat(names)
  names.one? ? names.first : names
end

#final_methods::Array[Symbol]

@return — the methods marked #final on this class.

Returns:

  • (::Array[Symbol])


51
# File 'lib/tuile/final.rb', line 51

def final_methods = @final_methods || []

#verify_final!(klass) ⇒ void

This method returns an undefined value.

Raises unless klass inherits every #final_methods entry from this class. Memoized per class, so a construction-time call costs one hash lookup after the first instance.

@param klass — the class being instantiated.

Parameters:

  • klass (Class)


60
61
62
63
64
65
66
67
68
69
70
71
72
73
# File 'lib/tuile/final.rb', line 60

def verify_final!(klass)
  @final_verified ||= {}
  return if @final_verified.key?(klass)

  overridden = final_methods.reject { klass.instance_method(_1).owner == self }
  unless overridden.empty?
    raise Error, "#{klass} overrides #{overridden.join(", ")}, which #{overridden.one? ? "is" : "are"} final " \
                 "on #{self} and may not be redefined. See the rdoc of " \
                 "#{overridden.map { "#{self}##{_1}" }.join(", ")} for the invariant at stake and the seam " \
                 "to use instead."
  end

  @final_verified[klass] = true
end