Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions lib/utopia/project/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
require_relative "releases_document"

require_relative "guides"
require_relative "inheritance"
require_relative "linkify"

module Utopia
Expand Down Expand Up @@ -203,6 +204,13 @@ def link_for(definition)
end
end

# Resolve inheritance information for the given definition.
# @parameter definition [Decode::Definition] The definition to inspect.
# @returns [Inheritance] The inheritance information.
def inheritance_for(definition)
Inheritance.new(@index, definition)
end

# Get the guides collection for this project.
# @returns [Guides]
#
Expand Down
186 changes: 186 additions & 0 deletions lib/utopia/project/inheritance.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2026, by Samuel Williams.

module Utopia
module Project
# Resolves relationships and inherited methods for a definition.
class Inheritance
# A relationship declared by a definition.
Relationship = Struct.new(:kind, :name, :definition)

# A method inherited from another definition.
Method = Struct.new(:name, :definition)

# A group of inherited methods with the same origin.
Group = Struct.new(:definition, :methods)

# Initialize inheritance resolution for the given definition.
# @parameter index [Decode::Index] The index used to resolve relationships.
# @parameter definition [Decode::Definition] The definition to inspect.
def initialize(index, definition)
@index = index
@definition = definition
end

# The definition being inspected.
# @attribute [Decode::Definition]
attr :definition

# Enumerate the directly declared relationships.
# @returns [Array(Relationship)] The relationships in declaration order.
def relationships
@relationships ||= begin
relationships = []

if @definition.respond_to?(:super_class) && (name = @definition.super_class)
relationships << relationship(:super_class, name)
end

{
prepends: :prepend,
includes: :include,
extends: :extend,
}.each do |attribute, kind|
if @definition.respond_to?(attribute)
@definition.public_send(attribute).each do |name|
relationships << relationship(kind, name)
end
end
end

relationships
end
end

# Enumerate documented public and protected methods inherited by the definition.
# @returns [Array(Group)] Methods grouped by the definition which provides them.
def inherited_methods
@groups = []
@groups_by_definition = {}

collect_instance_methods(@definition, false, {}, {})
collect_class_methods(@definition, false, {}, {})

return @groups
end

private

def relationship(kind, name)
Relationship.new(kind, name, resolve(name, @definition))
end

def resolve(name, relative_to)
reference = relative_to.language.reference_for(name)
@index.lookup(reference, relative_to: relative_to)
end

def relationship_definitions(definition, attribute)
return [] unless definition.respond_to?(attribute)

definition.public_send(attribute).filter_map do |name|
resolve(name, definition)
end
end

def super_class_for(definition)
if definition.respond_to?(:super_class) && (name = definition.super_class)
resolve(name, definition)
end
end

def methods_for(definition, prefix)
return [] unless node = @index.trie.lookup(definition.full_path)

methods = node.children.values.flat_map do |child|
child.values || []
end.select do |child|
child.nested_name.start_with?(prefix)
end

methods.group_by(&:nested_name).map do |name, definitions|
definitions.find(&:documented?) || definitions.first
end
end

def singleton_for(definition)
if node = @index.trie.lookup(definition.full_path)
if singleton = node.children[:self]
return singleton.values&.first
end
end
end

def collect_instance_methods(definition, include_own, seen, visited, display_prefix = "#")
key = [definition.qualified_name, display_prefix]
return if visited[key]
visited[key] = true

relationship_definitions(definition, :prepends).reverse_each do |relationship|
collect_instance_methods(relationship, true, seen, visited, display_prefix)
end

collect_methods(definition, "#", display_prefix, include_own, seen)

relationship_definitions(definition, :includes).reverse_each do |relationship|
collect_instance_methods(relationship, true, seen, visited, display_prefix)
end

if display_prefix == "#" && (super_class = super_class_for(definition))
collect_instance_methods(super_class, true, seen, visited, display_prefix)
end
end

def collect_class_methods(definition, include_own, seen, visited)
key = definition.qualified_name
return if visited[key]
visited[key] = true

collect_methods(definition, ".", ".", include_own, seen)
if singleton = singleton_for(definition)
collect_methods(singleton, "#", ".", include_own, seen, definition)
end

relationship_definitions(definition, :extends).reverse_each do |relationship|
collect_instance_methods(relationship, true, seen, {}, ".")
end

if super_class = super_class_for(definition)
collect_class_methods(super_class, true, seen, visited)
end
end

def collect_methods(definition, source_prefix, display_prefix, include_own, seen, origin = definition)
methods_for(definition, source_prefix).each do |method|
name = "#{display_prefix}#{method.name}"
next if seen[name]
seen[name] = true

# Constructors are invoked through `.new` and are not useful as
# inherited methods.
if source_prefix == "#" && method.name == :initialize
next
end

next unless include_own
next unless method.documented?
next if method.respond_to?(:private?) && method.private?

add_method(origin, name, method)
end
end

def add_method(definition, name, method)
group = @groups_by_definition[definition.qualified_name] ||= begin
group = Group.new(definition, [])
@groups << group
group
end

group.methods << Method.new(name, method)
end
end
end
end
45 changes: 45 additions & 0 deletions pages/reference/_relationships.xnode
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?r
base = self[:base]
symbol = self[:symbol]
inheritance = base.inheritance_for(symbol)

relationships = inheritance.relationships
inherited_methods = inheritance.inherited_methods

relationship_labels = {
super_class: "Inherits from",
prepend: "Prepends",
include: "Includes",
extend: "Extends",
}
?>
<?r if relationships.any? ?>
<dl class="relationships"><?r
relationships.group_by(&:kind).each do |kind, entries|
?><dt>#{relationship_labels.fetch(kind)}</dt>
<dd><?r
entries.each_with_index do |relationship, index|
if index > 0
?>, <?r
end

?>#{base.linkify(relationship.name, symbol)}<?r
end
?></dd><?r
end
?></dl>
<?r end ?>

<?r if inherited_methods.any? ?>
<section>
<h2>Inherited Methods</h2><?r
inherited_methods.each do |group|
?><p>From <a href="#{base.link_for(group.definition)}"><code>#{group.definition.qualified_name}</code></a>:</p>
<ul class="index"><?r
group.methods.each do |method|
?><li><a href="#{base.link_for(method.definition)}"><code>#{method.name}</code></a></li><?r
end
?></ul><?r
end
?></section>
<?r end ?>
1 change: 1 addition & 0 deletions pages/reference/show.xnode
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<h1>
#{partial 'content:declaration', symbol: symbol}
</h1>
#{partial 'content:relationships', symbol: symbol}

<?r
if document = base.document_for(symbol)
Expand Down
12 changes: 12 additions & 0 deletions public/_static/site.css
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,18 @@ details dt, details dd {
margin: 1rem 0;
}

dl.relationships {
display: grid;
grid-template-columns: max-content minmax(0, 1fr);
align-items: baseline;
gap: 0.25rem 1rem;
margin-top: 0.5rem;
}

dl.relationships dt, dl.relationships dd {
margin: 0;
}

ul.index {
padding: 0;
list-style: none;
Expand Down
70 changes: 70 additions & 0 deletions test/utopia/project/.fixtures/inheritance.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2026, by Samuel Williams.

module Example
module Included
# An included method.
def included_method
end

# An included method which is overridden by the child.
def overridden_method
end

private

# A private included method.
def private_included_method
end
end

module Prepended
# A prepended method.
def prepended_method
end
end

module Extended
# An extended method.
def extended_method
end
end

class Parent
# Initialize the parent.
def initialize
end

# An inherited instance method.
def parent_method
end

# An inherited class method.
def self.parent_class_method
end

class << self
# An inherited singleton class method.
def singleton_class_method
end
end

private

# A private parent method.
def private_parent_method
end
end

class Child < Parent
include Included
prepend Prepended
extend Extended

# Override the included method.
def overridden_method
end
end
end
41 changes: 41 additions & 0 deletions test/utopia/project/inheritance.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2026, by Samuel Williams.

require "utopia/project/inheritance"

require "decode"

describe Utopia::Project::Inheritance do
let(:path) {File.expand_path(".fixtures/inheritance.rb", __dir__)}
let(:index) {Decode::Index.for(path)}
let(:definition) {index.definitions.fetch("Example::Child")}
let(:inheritance) {subject.new(index, definition)}

it "resolves declared relationships" do
relationships = inheritance.relationships.to_h do |relationship|
[relationship.kind, relationship.definition.qualified_name]
end

expect(relationships).to be == {
super_class: "Example::Parent",
prepend: "Example::Prepended",
include: "Example::Included",
extend: "Example::Extended",
}
end

it "groups inherited methods by their origin" do
methods = inheritance.inherited_methods.to_h do |group|
[group.definition.qualified_name, group.methods.map(&:name)]
end

expect(methods).to be == {
"Example::Prepended" => ["#prepended_method"],
"Example::Included" => ["#included_method"],
"Example::Parent" => ["#parent_method", ".parent_class_method", ".singleton_class_method"],
"Example::Extended" => [".extended_method"],
}
end
end
Loading
Loading