adornment-core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +34 -0
- data/LICENSE +21 -0
- data/README.md +669 -0
- data/lib/adornment/core/annotation.rb +326 -0
- data/lib/adornment/core/base.rb +116 -0
- data/lib/adornment/core/diagnostics/lint.rb +150 -0
- data/lib/adornment/core/errors.rb +62 -0
- data/lib/adornment/core/hooks.rb +77 -0
- data/lib/adornment/core/immutable.rb +56 -0
- data/lib/adornment/core/private/annotation_methods.rb +61 -0
- data/lib/adornment/core/private/attachment.rb +122 -0
- data/lib/adornment/core/private/pending.rb +142 -0
- data/lib/adornment/core/private/query.rb +136 -0
- data/lib/adornment/core/private/site.rb +45 -0
- data/lib/adornment/core/private/state.rb +117 -0
- data/lib/adornment/core/private/utils.rb +40 -0
- data/lib/adornment/core/reflection.rb +146 -0
- data/lib/adornment/core/version.rb +8 -0
- data/lib/adornment/core.rb +200 -0
- metadata +70 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Adornment
|
|
4
|
+
module Core
|
|
5
|
+
# Passes the reflection API on to includers, prependers and extenders of a module: prepended to
|
|
6
|
+
# activated modules at activation, and by {Attachment.grant} to plain modules that include or
|
|
7
|
+
# prepend one (Ruby calls `included` only on the module included directly, so each module in
|
|
8
|
+
# the chain must pass it on). Classes never get it: nothing includes a class.
|
|
9
|
+
# @api private
|
|
10
|
+
module Propagation
|
|
11
|
+
# Grants reflection to the includer, then calls the next `included`. The arguments are
|
|
12
|
+
# forwarded unchanged, so an `included do ... end` DSL defined on the module (called with no
|
|
13
|
+
# base and a block) keeps working.
|
|
14
|
+
# @param args [Array] the includer, as Ruby passes it (or nothing, for a DSL block call)
|
|
15
|
+
# @return [Object] whatever the next `included` returns
|
|
16
|
+
def included(*args, &)
|
|
17
|
+
Attachment.grant(args[0], :include) if args[0].is_a?(Module)
|
|
18
|
+
super
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Grants reflection to the prepender, then calls the next `prepended`.
|
|
22
|
+
# @param args [Array] the prepender, as Ruby passes it
|
|
23
|
+
# @return [Object] whatever the next `prepended` returns
|
|
24
|
+
def prepended(*args, &)
|
|
25
|
+
Attachment.grant(args[0], :prepend) if args[0].is_a?(Module)
|
|
26
|
+
super
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Grants reflection to the extender (the extended object's singleton side), then calls the
|
|
30
|
+
# next `extended`.
|
|
31
|
+
# @param args [Array] the extended object, as Ruby passes it
|
|
32
|
+
# @return [Object] whatever the next `extended` returns
|
|
33
|
+
def extended(*args, &)
|
|
34
|
+
Attachment.grant(args[0], :extend) if args[0].is_a?(Module)
|
|
35
|
+
super
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private :included, :prepended, :extended
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Definition hooks prepended to an activated owner's singleton class (once per ancestry:
|
|
42
|
+
# subclasses inherit them). A definition takes the owner's pending declarations; a definition
|
|
43
|
+
# with nothing pending does nothing, so ordinary definitions cost one fiber-local lookup.
|
|
44
|
+
# Like any Ruby hook, an owner's own `method_added` must call `super`.
|
|
45
|
+
# @api private
|
|
46
|
+
module Hooks
|
|
47
|
+
# An instance method entry was added: it takes the owner's pending declarations, if any.
|
|
48
|
+
# @param name [Symbol]
|
|
49
|
+
# @return [Object] whatever the next `method_added` returns
|
|
50
|
+
# @raise [Adornment::InvalidTarget, Adornment::DefinitionError] from the attachment checks
|
|
51
|
+
def method_added(name)
|
|
52
|
+
Attachment.added(self, name, :instance) if Pending.for?(self)
|
|
53
|
+
super
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# A singleton method entry was added: it takes the owner's pending declarations, if any.
|
|
57
|
+
# @param name [Symbol]
|
|
58
|
+
# @return [Object] whatever the next `singleton_method_added` returns
|
|
59
|
+
# @raise [Adornment::InvalidTarget, Adornment::DefinitionError] from the attachment checks
|
|
60
|
+
def singleton_method_added(name)
|
|
61
|
+
Attachment.added(self, name, :singleton) if Pending.for?(self)
|
|
62
|
+
super
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# A constant was added to the owner: pending declarations can no longer attach.
|
|
66
|
+
# @param name [Symbol]
|
|
67
|
+
# @return [Object] whatever the next `const_added` returns
|
|
68
|
+
# @raise [Adornment::LeftoverAnnotations] when the owner has pending declarations
|
|
69
|
+
def const_added(name)
|
|
70
|
+
Pending.const_added(self, name)
|
|
71
|
+
super
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private :method_added, :singleton_method_added, :const_added
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Adornment
|
|
4
|
+
module Core
|
|
5
|
+
# Deep-frozen copies of annotation values (public as {Adornment.immutable_copy}).
|
|
6
|
+
#
|
|
7
|
+
# Array, Hash, Set and String values are copied and frozen, recursively; the caller's objects
|
|
8
|
+
# are never mutated or frozen. Other objects are kept by reference. A value that is already
|
|
9
|
+
# deeply immutable (`Ractor.shareable?`, which CRuby answers in O(1) once an object is known
|
|
10
|
+
# to be shareable) is returned as it is, since nobody can mutate it. The identity table that
|
|
11
|
+
# preserves cycles and shared subgraphs is allocated only when a container is traversed.
|
|
12
|
+
# @api private
|
|
13
|
+
module Immutable
|
|
14
|
+
extend self
|
|
15
|
+
|
|
16
|
+
# @param value [Object]
|
|
17
|
+
# @param seen [Hash{Object => Object}, nil] originals to their copies (identity), shared by a
|
|
18
|
+
# traversal so cycles and shared subgraphs are copied once
|
|
19
|
+
# @return [Object] a deep-frozen copy of a String, Array, Hash or Set; `value` itself when it
|
|
20
|
+
# is already deeply immutable or of any other class
|
|
21
|
+
def copy(value, seen = nil)
|
|
22
|
+
case value
|
|
23
|
+
when String
|
|
24
|
+
value.frozen? ? value : value.dup.freeze
|
|
25
|
+
when Array, Hash, Set
|
|
26
|
+
return value if Ractor.shareable?(value)
|
|
27
|
+
|
|
28
|
+
container(value, seen || {}.compare_by_identity)
|
|
29
|
+
else
|
|
30
|
+
value
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Copies an Array, Hash or Set whose elements may need copying.
|
|
35
|
+
# @param value [Array, Hash, Set]
|
|
36
|
+
# @param seen [Hash{Object => Object}] see {copy}
|
|
37
|
+
# @return [Array, Hash, Set] a frozen copy (a Hash keeps `compare_by_identity`)
|
|
38
|
+
def container(value, seen)
|
|
39
|
+
return seen[value] if seen.key?(value)
|
|
40
|
+
|
|
41
|
+
case value
|
|
42
|
+
when Array
|
|
43
|
+
seen[value] = out = []
|
|
44
|
+
value.each { |v| out << copy(v, seen) }
|
|
45
|
+
when Hash
|
|
46
|
+
seen[value] = out = value.compare_by_identity? ? {}.compare_by_identity : {}
|
|
47
|
+
value.each { |k, v| out[copy(k, seen)] = copy(v, seen) }
|
|
48
|
+
else # Set
|
|
49
|
+
seen[value] = out = Set.new
|
|
50
|
+
value.each { |v| out << copy(v, seen) }
|
|
51
|
+
end
|
|
52
|
+
out.freeze
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Adornment
|
|
4
|
+
module Core
|
|
5
|
+
# An owner's annotation methods: one per annotation type, named after it (`Traced(...)`),
|
|
6
|
+
# declaring an annotation of that type for the next definition. Extended into the owner, so
|
|
7
|
+
# its subclasses inherit them and nothing is defined globally.
|
|
8
|
+
# @api private
|
|
9
|
+
class AnnotationMethods < Module
|
|
10
|
+
# @return [Hash{Symbol => Class}] frozen; method name => annotation type
|
|
11
|
+
attr_reader :types
|
|
12
|
+
|
|
13
|
+
# @param owner [Module]
|
|
14
|
+
# @return [AnnotationMethods] the owner's own module, created and extended into the owner on
|
|
15
|
+
# first use (a subclass of an activated class gets its own on its first `use_annotations`)
|
|
16
|
+
def self.for(owner)
|
|
17
|
+
state = State.for(owner)
|
|
18
|
+
methods = state.annotation_methods || state.lock.synchronize { state.annotation_methods ||= new(owner) }
|
|
19
|
+
owner.extend(methods) unless owner.singleton_class.include?(methods) # outside the lock
|
|
20
|
+
methods
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @param owner [Module]
|
|
24
|
+
# @return [Hash{Symbol => Class}] the annotation methods usable on `owner` (its own and
|
|
25
|
+
# inherited ones; nearer modules win)
|
|
26
|
+
def self.types_of(owner)
|
|
27
|
+
owner.singleton_class.ancestors.grep(AnnotationMethods).reverse_each.inject({}) { |all, d| all.merge(d.types) }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @param owner [Module] the owner this module belongs to (shown by {#inspect})
|
|
31
|
+
def initialize(owner)
|
|
32
|
+
super()
|
|
33
|
+
@owner = owner
|
|
34
|
+
@types = {}.freeze
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Defines the method `name`, which declares `type.new(*args, **kwargs)` for the next
|
|
38
|
+
# definition of the receiver. Adding the same name and type again does nothing; another type
|
|
39
|
+
# under the same name replaces the method.
|
|
40
|
+
# @param name [Symbol]
|
|
41
|
+
# @param type [Class] an annotation type
|
|
42
|
+
# @return [void]
|
|
43
|
+
def add(name, type)
|
|
44
|
+
return if @types[name].equal?(type) # activated again with the same vocabulary
|
|
45
|
+
|
|
46
|
+
remove_method(name) if @types.key?(name)
|
|
47
|
+
@types = @types.merge(name => type).freeze
|
|
48
|
+
define_method(name) do |*args, **kwargs, &block|
|
|
49
|
+
raise ArgumentError, "#{name}(...) takes no block" if block
|
|
50
|
+
|
|
51
|
+
location = caller_locations(1, 1).first # the user's declaration, captured once
|
|
52
|
+
Attachment.declare(self, type.new(*args, **kwargs), location)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# @return [String] `#<Adornment::Core::AnnotationMethods Owner>`
|
|
57
|
+
def inspect = "#<Adornment::Core::AnnotationMethods #{@owner.inspect}>"
|
|
58
|
+
alias to_s inspect
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Adornment
|
|
4
|
+
module Core
|
|
5
|
+
# Declaration, definitions and verification, plus the activation check and the reflection grant
|
|
6
|
+
# for includers. Activation itself is {Target.extended}.
|
|
7
|
+
# @api private
|
|
8
|
+
module Attachment
|
|
9
|
+
# --- activation and propagation ------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
extend self
|
|
12
|
+
|
|
13
|
+
# @param owner [Object]
|
|
14
|
+
# @return [Boolean] whether `owner` is a class or module with the definition hooks
|
|
15
|
+
def activated?(owner) = owner.is_a?(Module) && owner.singleton_class.include?(Hooks)
|
|
16
|
+
|
|
17
|
+
# Includers, prependers and extenders of activated modules get the read-only reflection API,
|
|
18
|
+
# as activated owners do. Modules among the includers and prependers pass it on to their own
|
|
19
|
+
# includers.
|
|
20
|
+
# @param base [Module] the includer, prepender or extender
|
|
21
|
+
# @param kind [:include, :prepend, :extend]
|
|
22
|
+
# @return [void]
|
|
23
|
+
def grant(base, kind)
|
|
24
|
+
base.extend(Reflection) unless base.singleton_class.include?(Reflection)
|
|
25
|
+
return if kind == :extend || base.is_a?(Class)
|
|
26
|
+
|
|
27
|
+
base.singleton_class.prepend(Propagation) unless base.singleton_class.include?(Propagation)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# --- declaration -----------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
# The shared enqueue of every declaration path (declaration methods and `adorn`).
|
|
33
|
+
#
|
|
34
|
+
# @param owner [Module] the class or module declaring
|
|
35
|
+
# @param annotation [Adornment::Annotation]
|
|
36
|
+
# @param location [Thread::Backtrace::Location, nil] the user's call, captured once by the
|
|
37
|
+
# public entry point with `caller_locations(1, 1)` and formatted lazily
|
|
38
|
+
# @return [nil]
|
|
39
|
+
# @raise [ArgumentError] when `annotation` is not an annotation instance
|
|
40
|
+
# @raise [Adornment::DuplicateAnnotation, Adornment::LeftoverAnnotations] see {Pending.push!}
|
|
41
|
+
def declare(owner, annotation, location)
|
|
42
|
+
check_instance!(annotation)
|
|
43
|
+
Pending.push!(owner, annotation, location)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Raises unless `annotation` is a method annotation instance.
|
|
47
|
+
# @param annotation [Object]
|
|
48
|
+
# @return [void]
|
|
49
|
+
# @raise [ArgumentError] naming `Type.new(...)` when `annotation` is a type
|
|
50
|
+
def check_instance!(annotation)
|
|
51
|
+
return if annotation.is_a?(Annotation)
|
|
52
|
+
|
|
53
|
+
if annotation.is_a?(Class) && annotation < Annotation
|
|
54
|
+
raise ArgumentError, "#{annotation.inspect} is an annotation type, not an instance; " \
|
|
55
|
+
"pass #{annotation.short_name}.new(...)"
|
|
56
|
+
end
|
|
57
|
+
raise ArgumentError, "expected an Adornment annotation instance, got #{annotation.inspect}"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# --- definitions -----------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
# A new entry for `name` on `owner`, which has pending declarations: it takes them,
|
|
63
|
+
# replacing the entry's annotations. Every entry Ruby reports counts: definitions, copies
|
|
64
|
+
# (aliases, `module_function`) and visibility changes of inherited methods alike.
|
|
65
|
+
# The pending declarations are taken first, so a failed check leaves none behind and
|
|
66
|
+
# attaches nothing.
|
|
67
|
+
# @param owner [Module]
|
|
68
|
+
# @param name [Symbol]
|
|
69
|
+
# @param side [:instance, :singleton]
|
|
70
|
+
# @return [void]
|
|
71
|
+
# @raise [Adornment::InvalidTarget] for an annotation whose type does not target `side`
|
|
72
|
+
# @raise [Adornment::DefinitionError] when a validator rejects the method
|
|
73
|
+
def added(owner, name, side)
|
|
74
|
+
pending = Pending.take(owner)
|
|
75
|
+
annotations = pending.map(&:annotation).freeze
|
|
76
|
+
unless annotations.all? { |a| a.class.targets.include?(side) }
|
|
77
|
+
wrong = annotations.reject { |a| a.class.targets.include?(side) }
|
|
78
|
+
raise InvalidTarget, "#{describe(owner, name, side)}: #{wrong.map(&:inspect).join(', ')} " \
|
|
79
|
+
"cannot annotate #{side} methods (targets: #{wrong.first.class.targets.inspect})"
|
|
80
|
+
end
|
|
81
|
+
site = Site.new(owner, name, side, annotations)
|
|
82
|
+
pending.each { |declaration| validate!(declaration, site) }
|
|
83
|
+
State.for(owner).set_entry(side, name, annotations)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Runs one declaration's validator; a StandardError becomes a DefinitionError (its cause)
|
|
87
|
+
# pointing at the declaration.
|
|
88
|
+
# @param declaration [Pending::PendingDeclaration]
|
|
89
|
+
# @param site [Site]
|
|
90
|
+
# @return [void]
|
|
91
|
+
# @raise [Adornment::DefinitionError] with the validator's error as `cause`
|
|
92
|
+
def validate!(declaration, site)
|
|
93
|
+
declaration.annotation.validate(site)
|
|
94
|
+
rescue StandardError => e
|
|
95
|
+
location = declaration.location
|
|
96
|
+
error = DefinitionError.new("#{site.label}: #{e.message}" \
|
|
97
|
+
"#{" (declared at #{location})" if location}")
|
|
98
|
+
error.set_backtrace(location ? [location, *caller] : caller)
|
|
99
|
+
raise error, cause: e
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# --- verification ----------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
# Takes the owner's pending declarations and raises if there were any.
|
|
105
|
+
# @param owner [Module]
|
|
106
|
+
# @return [nil]
|
|
107
|
+
# @raise [Adornment::LeftoverAnnotations]
|
|
108
|
+
def verify(owner)
|
|
109
|
+
left = Pending.take(owner)
|
|
110
|
+
raise LeftoverAnnotations.new({ owner => left }) unless left.empty?
|
|
111
|
+
|
|
112
|
+
nil
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# @param owner [Module]
|
|
116
|
+
# @param name [Symbol]
|
|
117
|
+
# @param side [:instance, :singleton]
|
|
118
|
+
# @return [String] `Owner#name` or `Owner.name`
|
|
119
|
+
def describe(owner, name, side) = "#{owner.inspect}#{side == :singleton ? '.' : '#'}#{name}"
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Adornment
|
|
4
|
+
module Core
|
|
5
|
+
# Pending declarations: one fiber-local Hash, `{owner => [PendingDeclaration, ...]}`, that
|
|
6
|
+
# compares owners by identity (no user `hash`/`eql?` is called).
|
|
7
|
+
#
|
|
8
|
+
# Declarations wait for the next method definition of the owner they were made on, so at
|
|
9
|
+
# most one owner has pending declarations at a time. These raise {LeftoverAnnotations}
|
|
10
|
+
# (and clear the pending declarations):
|
|
11
|
+
#
|
|
12
|
+
# - a declaration on another owner while declarations are still pending;
|
|
13
|
+
# - a constant added to the owner while it has pending declarations (a nested
|
|
14
|
+
# `class Inner`, `X = ...`), via `const_added`.
|
|
15
|
+
#
|
|
16
|
+
# Nothing traces body ends: declarations left at the end of a body stay pending until one of
|
|
17
|
+
# the above, the owner's next definition, or verification. Pending declarations are seen
|
|
18
|
+
# only by their own fiber.
|
|
19
|
+
# @api private
|
|
20
|
+
module Pending
|
|
21
|
+
# The fiber-local key of the table.
|
|
22
|
+
KEY = :__adornment_core_pending__
|
|
23
|
+
# The shared empty result.
|
|
24
|
+
NONE = [].freeze
|
|
25
|
+
|
|
26
|
+
# A declaration waiting for the next method definition.
|
|
27
|
+
#
|
|
28
|
+
# The caller location is kept as captured and formatted only when it is read (diagnostics).
|
|
29
|
+
class PendingDeclaration
|
|
30
|
+
# @return [Adornment::Annotation]
|
|
31
|
+
attr_reader :annotation
|
|
32
|
+
|
|
33
|
+
# @api private
|
|
34
|
+
# @param annotation [Adornment::Annotation]
|
|
35
|
+
# @param location [Thread::Backtrace::Location, nil] the declaration's caller location
|
|
36
|
+
def initialize(annotation, location)
|
|
37
|
+
@annotation = annotation
|
|
38
|
+
@location = location
|
|
39
|
+
freeze
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# @return [String, nil] `path:line` of the declaration
|
|
43
|
+
def location
|
|
44
|
+
loc = @location
|
|
45
|
+
loc && "#{loc.path}:#{loc.lineno}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @api private
|
|
49
|
+
# @return [Thread::Backtrace::Location, nil] the location as captured
|
|
50
|
+
def raw_location = @location
|
|
51
|
+
|
|
52
|
+
# @return [String] `#<Adornment::Core::Pending::PendingDeclaration #<Traced> at path:line>`
|
|
53
|
+
def inspect = "#<Adornment::Core::Pending::PendingDeclaration #{@annotation.inspect}#{" at #{location}" if @location}>"
|
|
54
|
+
alias to_s inspect
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
extend self
|
|
58
|
+
|
|
59
|
+
# @return [Hash{Module => Array<PendingDeclaration>}] this fiber's table (created on first
|
|
60
|
+
# use, compared by identity), at most one entry
|
|
61
|
+
def table
|
|
62
|
+
Thread.current[KEY] ||= {}.compare_by_identity
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# @return [Boolean] whether this fiber has no pending declarations at all
|
|
66
|
+
def empty? = table.empty?
|
|
67
|
+
|
|
68
|
+
# @param owner [Module]
|
|
69
|
+
# @return [Boolean] whether `owner` has pending declarations in this fiber
|
|
70
|
+
def for?(owner)
|
|
71
|
+
t = table
|
|
72
|
+
!t.empty? && t.key?(owner)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Queues `annotation`, declared at `location`, for `owner`.
|
|
76
|
+
# @param owner [Module]
|
|
77
|
+
# @param annotation [Adornment::Annotation]
|
|
78
|
+
# @param location [Thread::Backtrace::Location, nil]
|
|
79
|
+
# @return [nil]
|
|
80
|
+
# @raise [LeftoverAnnotations] when another owner still has pending declarations
|
|
81
|
+
# @raise [DuplicateAnnotation] for a second non-repeatable annotation of one type (the
|
|
82
|
+
# owner's pending declarations are cleared first)
|
|
83
|
+
def push!(owner, annotation, location)
|
|
84
|
+
t = table
|
|
85
|
+
unless t.empty? || t.key?(owner)
|
|
86
|
+
left = take_all
|
|
87
|
+
raise LeftoverAnnotations.new(left, "#{owner.inspect} got a declaration while other declarations were pending")
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
declarations = (t[owner] ||= [])
|
|
91
|
+
type = annotation.class
|
|
92
|
+
|
|
93
|
+
if !type.repeatable? && declarations.any? { |p| p.annotation.instance_of?(type) }
|
|
94
|
+
t.delete(owner)
|
|
95
|
+
|
|
96
|
+
raise DuplicateAnnotation, "#{owner.inspect}: #{annotation.class.short_name} declared twice " \
|
|
97
|
+
"for one method (it is not repeatable); pending declarations cleared"
|
|
98
|
+
end
|
|
99
|
+
declarations << PendingDeclaration.new(annotation, location)
|
|
100
|
+
nil
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# @param owner [Module]
|
|
104
|
+
# @return [Array<PendingDeclaration>] the owner's declarations (removed)
|
|
105
|
+
def take(owner)
|
|
106
|
+
t = table
|
|
107
|
+
return NONE if t.empty?
|
|
108
|
+
|
|
109
|
+
t.delete(owner) || NONE
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# @param owner [Module]
|
|
113
|
+
# @return [Array<PendingDeclaration>] the owner's declarations (not removed)
|
|
114
|
+
def peek(owner)
|
|
115
|
+
t = table
|
|
116
|
+
t[owner] || NONE
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# @return [Hash{Module => Array<PendingDeclaration>}] every pending declaration (removed)
|
|
120
|
+
def take_all
|
|
121
|
+
t = table
|
|
122
|
+
return {} if t.empty?
|
|
123
|
+
|
|
124
|
+
left = t.dup
|
|
125
|
+
t.clear
|
|
126
|
+
left
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# A constant was added to `owner`: its pending declarations can no longer attach.
|
|
130
|
+
# @param owner [Module]
|
|
131
|
+
# @param name [Symbol] the constant
|
|
132
|
+
# @return [nil]
|
|
133
|
+
# @raise [LeftoverAnnotations] when `owner` has pending declarations (they are cleared)
|
|
134
|
+
def const_added(owner, name)
|
|
135
|
+
return unless for?(owner)
|
|
136
|
+
|
|
137
|
+
raise LeftoverAnnotations.new({ owner => take(owner) }, "constant #{name} added to #{owner.inspect} " \
|
|
138
|
+
"while declarations were pending")
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Adornment
|
|
4
|
+
module Core
|
|
5
|
+
# Lookups behind {Reflection}. The resolved method is found through Ruby's own lookup
|
|
6
|
+
# (`instance_method(name).owner`): one UnboundMethod, no ancestors. The queries that read
|
|
7
|
+
# overridden methods walk the ancestors, nearest first, through each module with its own entry
|
|
8
|
+
# for the name, and stop at an undef barrier. A visibility change (`private :inherited`) is an
|
|
9
|
+
# own entry like any other.
|
|
10
|
+
# @api private
|
|
11
|
+
module Query
|
|
12
|
+
# The shared empty result.
|
|
13
|
+
NONE = State::NONE
|
|
14
|
+
|
|
15
|
+
extend self
|
|
16
|
+
|
|
17
|
+
# Yields `(holder, side, annotations)` for each module the walk visits. The first yield is
|
|
18
|
+
# the resolved method.
|
|
19
|
+
# @param owner [Module]
|
|
20
|
+
# @param name [Symbol]
|
|
21
|
+
# @param singleton [Boolean]
|
|
22
|
+
# @yieldparam holder [Module] the class or module that holds the entry
|
|
23
|
+
# @yieldparam side [:instance, :singleton]
|
|
24
|
+
# @yieldparam annotations [Array<Adornment::Annotation>] frozen, possibly empty
|
|
25
|
+
# @return [nil]
|
|
26
|
+
def each_entry(owner, name, singleton)
|
|
27
|
+
start = singleton ? owner.singleton_class : owner
|
|
28
|
+
return unless start.method_defined?(name) || start.private_method_defined?(name)
|
|
29
|
+
|
|
30
|
+
ancestors = start.ancestors
|
|
31
|
+
i = next_own(ancestors, 0, name)
|
|
32
|
+
while i
|
|
33
|
+
holder, side = holder_of(ancestors[i])
|
|
34
|
+
yield holder, side, State.of(holder)&.entry(side, name) || NONE
|
|
35
|
+
later = next_own(ancestors, i + 1, name)
|
|
36
|
+
break if later.nil? || undef_between?(ancestors, i, later, name)
|
|
37
|
+
|
|
38
|
+
i = later
|
|
39
|
+
end
|
|
40
|
+
nil
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# The resolved method's annotations: those of the module Ruby's lookup names
|
|
44
|
+
# (`instance_method(name).owner`).
|
|
45
|
+
# @param owner [Module]
|
|
46
|
+
# @param name [Symbol]
|
|
47
|
+
# @param singleton [Boolean]
|
|
48
|
+
# @return [Array<Adornment::Annotation>] frozen; empty when nothing is attached or the method
|
|
49
|
+
# is not defined
|
|
50
|
+
def resolved_entry(owner, name, singleton)
|
|
51
|
+
start = singleton ? owner.singleton_class : owner
|
|
52
|
+
return NONE unless start.method_defined?(name) || start.private_method_defined?(name)
|
|
53
|
+
|
|
54
|
+
mod = start.instance_method(name).owner
|
|
55
|
+
entry = if mod.singleton_class?
|
|
56
|
+
State.of(mod.attached_object)&.entry(:singleton,
|
|
57
|
+
name)
|
|
58
|
+
else
|
|
59
|
+
State.of(mod)&.entry(:instance, name)
|
|
60
|
+
end
|
|
61
|
+
entry || NONE
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Whether `mod` has its own entry for `name` (modules prepended to it do not count).
|
|
65
|
+
# @param mod [Module]
|
|
66
|
+
# @param name [Symbol]
|
|
67
|
+
# @return [Boolean]
|
|
68
|
+
def own?(mod, name)
|
|
69
|
+
mod.method_defined?(name, false) || mod.private_method_defined?(name, false)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# @param ancestors [Array<Module>]
|
|
73
|
+
# @param from [Integer]
|
|
74
|
+
# @param name [Symbol]
|
|
75
|
+
# @return [Integer, nil] the index of the first module at or after `from` with its own entry
|
|
76
|
+
def next_own(ancestors, from, name)
|
|
77
|
+
i = from
|
|
78
|
+
while i < ancestors.size
|
|
79
|
+
return i if own?(ancestors[i], name)
|
|
80
|
+
|
|
81
|
+
i += 1
|
|
82
|
+
end
|
|
83
|
+
nil
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Whether a module strictly between the two indexes undefines `name` (an undef entry is not
|
|
87
|
+
# an own entry, so it is never found by {next_own}).
|
|
88
|
+
# @param ancestors [Array<Module>]
|
|
89
|
+
# @param from [Integer]
|
|
90
|
+
# @param to [Integer]
|
|
91
|
+
# @param name [Symbol]
|
|
92
|
+
# @return [Boolean]
|
|
93
|
+
def undef_between?(ancestors, from, to, name)
|
|
94
|
+
i = from + 1
|
|
95
|
+
while i < to
|
|
96
|
+
return true if ancestors[i].undefined_instance_methods.include?(name)
|
|
97
|
+
|
|
98
|
+
i += 1
|
|
99
|
+
end
|
|
100
|
+
false
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# @param mod [Module] an ancestor, possibly a singleton class
|
|
104
|
+
# @return [Array(Module, Symbol)] the class or module holding `mod`'s entries, and the side
|
|
105
|
+
def holder_of(mod)
|
|
106
|
+
mod.singleton_class? ? [mod.attached_object, :singleton] : [mod, :instance]
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# The resolved method's annotations, filtered by `type`.
|
|
110
|
+
# @param owner [Module]
|
|
111
|
+
# @param name [Symbol]
|
|
112
|
+
# @param type [Class, nil]
|
|
113
|
+
# @param singleton [Boolean]
|
|
114
|
+
# @return [Array<Adornment::Annotation>] frozen
|
|
115
|
+
def annotations_for(owner, name, type, singleton)
|
|
116
|
+
annotations = resolved_entry(owner, name, singleton)
|
|
117
|
+
return annotations if type.nil? || annotations.all?(type)
|
|
118
|
+
|
|
119
|
+
annotations.select { |a| a.is_a?(type) }.freeze
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Names with an entry in some ancestor (activated ones), in ancestor order.
|
|
123
|
+
# @param owner [Module]
|
|
124
|
+
# @param singleton [Boolean]
|
|
125
|
+
# @return [Array<Symbol>]
|
|
126
|
+
def candidate_names(owner, singleton)
|
|
127
|
+
names = []
|
|
128
|
+
(singleton ? owner.singleton_class : owner).ancestors.each do |mod|
|
|
129
|
+
entries = mod.singleton_class? ? State.of(mod.attached_object)&.entries(:singleton) : State.of(mod)&.entries(:instance)
|
|
130
|
+
names.concat(entries.keys) if entries
|
|
131
|
+
end
|
|
132
|
+
names.uniq
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Adornment
|
|
4
|
+
module Core
|
|
5
|
+
# The method annotations are about to attach to, passed to {Annotation#validate}. One site per
|
|
6
|
+
# definition, shared by every annotation in `annotations`, the complete proposed set (frozen).
|
|
7
|
+
class Site
|
|
8
|
+
# @return [Module] the class or module defining the method
|
|
9
|
+
attr_reader :owner
|
|
10
|
+
# @return [Symbol]
|
|
11
|
+
attr_reader :name
|
|
12
|
+
# @return [Array<Adornment::Annotation>] the complete proposed set (frozen)
|
|
13
|
+
attr_reader :annotations
|
|
14
|
+
|
|
15
|
+
# @api private
|
|
16
|
+
# @param owner [Module]
|
|
17
|
+
# @param name [Symbol]
|
|
18
|
+
# @param side [:instance, :singleton]
|
|
19
|
+
# @param annotations [Array<Adornment::Annotation>] the complete proposed set (frozen)
|
|
20
|
+
def initialize(owner, name, side, annotations)
|
|
21
|
+
@owner = owner
|
|
22
|
+
@name = name
|
|
23
|
+
@side = side
|
|
24
|
+
@annotations = annotations
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @note Takes no arguments, unlike `Object#method`, which it replaces on sites.
|
|
28
|
+
# @return [UnboundMethod] the method as defined now
|
|
29
|
+
def method = (singleton? ? @owner.singleton_class : @owner).instance_method(@name)
|
|
30
|
+
|
|
31
|
+
# @return [Boolean] whether the method is an instance method of {#owner}
|
|
32
|
+
def instance? = @side == :instance
|
|
33
|
+
|
|
34
|
+
# @return [Boolean] whether the method is a singleton method of {#owner}
|
|
35
|
+
def singleton? = @side == :singleton
|
|
36
|
+
|
|
37
|
+
# @return [String] `Owner#name` or `Owner.name`
|
|
38
|
+
def label = "#{@owner.inspect}#{singleton? ? '.' : '#'}#{@name}"
|
|
39
|
+
|
|
40
|
+
# @return [String] `#<Adornment::Core::Site Owner#name>`
|
|
41
|
+
def inspect = "#<Adornment::Core::Site #{label}>"
|
|
42
|
+
alias to_s inspect
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|