From 5b6dfbbecb404e8e84816805ba436ad2d4d9c481 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Sun, 16 Aug 2026 16:36:43 +1200 Subject: [PATCH 1/5] List inherited methods and relationships Assisted-By: devx/08af623d-3e23-42c2-ada9-1ce2bcb7e46f --- lib/utopia/project/base.rb | 8 + lib/utopia/project/inheritance.rb | 186 +++++++++++++++++++ pages/reference/_relationships.xnode | 52 ++++++ pages/reference/show.xnode | 1 + test/utopia/project/.fixtures/inheritance.rb | 70 +++++++ test/utopia/project/inheritance.rb | 41 ++++ test/utopia/project/serve.rb | 3 + 7 files changed, 361 insertions(+) create mode 100644 lib/utopia/project/inheritance.rb create mode 100644 pages/reference/_relationships.xnode create mode 100644 test/utopia/project/.fixtures/inheritance.rb create mode 100644 test/utopia/project/inheritance.rb diff --git a/lib/utopia/project/base.rb b/lib/utopia/project/base.rb index 67aae4b..e1e3a16 100644 --- a/lib/utopia/project/base.rb +++ b/lib/utopia/project/base.rb @@ -16,6 +16,7 @@ require_relative "releases_document" require_relative "guides" +require_relative "inheritance" require_relative "linkify" module Utopia @@ -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] # diff --git a/lib/utopia/project/inheritance.rb b/lib/utopia/project/inheritance.rb new file mode 100644 index 0000000..915dc3f --- /dev/null +++ b/lib/utopia/project/inheritance.rb @@ -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 diff --git a/pages/reference/_relationships.xnode b/pages/reference/_relationships.xnode new file mode 100644 index 0000000..c5ae418 --- /dev/null +++ b/pages/reference/_relationships.xnode @@ -0,0 +1,52 @@ + + +
+

Relationships

+
#{relationship_labels.fetch(kind)}
+
0 + ?>, #{relationship.name}#{relationship.name}
+
+ + + +
+

Inherited Methods

From #{group.definition.qualified_name}:

+
+ diff --git a/pages/reference/show.xnode b/pages/reference/show.xnode index d72fcb9..9ba8a29 100644 --- a/pages/reference/show.xnode +++ b/pages/reference/show.xnode @@ -19,6 +19,7 @@ ?> #{partial 'content:examples', symbol: symbol} #{partial 'content:signature', symbol: symbol} + #{partial 'content:relationships', symbol: symbol} ["#prepended_method"], + "Example::Included" => ["#included_method"], + "Example::Parent" => ["#parent_method", ".parent_class_method", ".singleton_class_method"], + "Example::Extended" => [".extended_method"], + } + end +end diff --git a/test/utopia/project/serve.rb b/test/utopia/project/serve.rb index ebf5cd0..58106cd 100644 --- a/test/utopia/project/serve.rb +++ b/test/utopia/project/serve.rb @@ -80,5 +80,8 @@ expect(body).to be(:include?, "def release_names") expect(body).to be(:include?, 'class ReleasesDocument < Document') + expect(body).to be(:include?, "

Relationships

") + expect(body).to be(:include?, "
Inherits from
") + expect(body).to be(:include?, "/reference/Utopia/Project/Document/index#Utopia%3A%3AProject%3A%3ADocument%23root") end end From d9c2847ac8deae00d516aece6c01ea23b7afe46d Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Sun, 16 Aug 2026 16:55:09 +1200 Subject: [PATCH 2/5] Require Decode 0.30 Assisted-By: devx/08af623d-3e23-42c2-ada9-1ce2bcb7e46f --- utopia-project.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utopia-project.gemspec b/utopia-project.gemspec index 912f915..1bd340b 100644 --- a/utopia-project.gemspec +++ b/utopia-project.gemspec @@ -25,7 +25,7 @@ Gem::Specification.new do |spec| spec.required_ruby_version = ">= 3.3" - spec.add_dependency "decode", "~> 0.29" + spec.add_dependency "decode", "~> 0.30" spec.add_dependency "falcon" spec.add_dependency "markly", "~> 0.15" spec.add_dependency "thread-local" From 9cc93fb334e6ee0a086f54d25c72ab7b0d76d114 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Sun, 16 Aug 2026 16:59:11 +1200 Subject: [PATCH 3/5] Compact relationship metadata Assisted-By: devx/08af623d-3e23-42c2-ada9-1ce2bcb7e46f --- pages/reference/_relationships.xnode | 37 +++++++++++++--------------- public/_static/site.css | 12 +++++++++ test/utopia/project/serve.rb | 2 +- 3 files changed, 30 insertions(+), 21 deletions(-) diff --git a/pages/reference/_relationships.xnode b/pages/reference/_relationships.xnode index c5ae418..6348848 100644 --- a/pages/reference/_relationships.xnode +++ b/pages/reference/_relationships.xnode @@ -14,27 +14,24 @@ relationship_labels = { } ?> -
-

Relationships

-
#{relationship_labels.fetch(kind)}
-
0 - ?>, #{relationship.name}#{relationship.name}
#{relationship_labels.fetch(kind)}
+
0 + ?>, #{relationship.name}#{relationship.name}
-
+ end + ?> diff --git a/public/_static/site.css b/public/_static/site.css index 2396427..cc4c5cc 100644 --- a/public/_static/site.css +++ b/public/_static/site.css @@ -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; diff --git a/test/utopia/project/serve.rb b/test/utopia/project/serve.rb index 58106cd..7b70bb6 100644 --- a/test/utopia/project/serve.rb +++ b/test/utopia/project/serve.rb @@ -80,7 +80,7 @@ expect(body).to be(:include?, "def release_names") expect(body).to be(:include?, 'class ReleasesDocument < Document') - expect(body).to be(:include?, "

Relationships

") + expect(body).to be(:include?, '
') expect(body).to be(:include?, "
Inherits from
") expect(body).to be(:include?, "/reference/Utopia/Project/Document/index#Utopia%3A%3AProject%3A%3ADocument%23root") end From 65e48d8f8e6cf6417e84298136122393c7c11610 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Sun, 16 Aug 2026 17:56:22 +1200 Subject: [PATCH 4/5] Place relationships below reference title Assisted-By: devx/08af623d-3e23-42c2-ada9-1ce2bcb7e46f --- pages/reference/show.xnode | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/reference/show.xnode b/pages/reference/show.xnode index 9ba8a29..ec71309 100644 --- a/pages/reference/show.xnode +++ b/pages/reference/show.xnode @@ -9,6 +9,7 @@

#{partial 'content:declaration', symbol: symbol}

+ #{partial 'content:relationships', symbol: symbol} #{partial 'content:examples', symbol: symbol} #{partial 'content:signature', symbol: symbol} - #{partial 'content:relationships', symbol: symbol} Date: Sun, 16 Aug 2026 18:38:18 +1200 Subject: [PATCH 5/5] Syntax highlight relationship references Assisted-By: devx/08af623d-3e23-42c2-ada9-1ce2bcb7e46f --- pages/reference/_relationships.xnode | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/pages/reference/_relationships.xnode b/pages/reference/_relationships.xnode index 6348848..685e656 100644 --- a/pages/reference/_relationships.xnode +++ b/pages/reference/_relationships.xnode @@ -23,11 +23,7 @@ relationship_labels = { ?>, #{relationship.name}#{relationship.name}#{base.linkify(relationship.name, symbol)}