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.
@@ -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