diff --git a/lib/decode/language/ruby/parser.rb b/lib/decode/language/ruby/parser.rb index 869edcd..a556872 100644 --- a/lib/decode/language/ruby/parser.rb +++ b/lib/decode/language/ruby/parser.rb @@ -163,15 +163,36 @@ def walk_definitions(node, parent = nil, source = nil, &block) yield definition when :constant_write_node - definition = Constant.new(node.name, - comments: comments_for(node), - parent: parent, - node: node, - language: @language, - ) - - store_definition(parent, node.name, definition) - yield definition + if super_class = struct_super_class_for(node.value) + definition = Class.new([node.name], + super_class: super_class, + visibility: :public, + comments: comments_for(node), + parent: parent, + node: node, + language: @language, + source: source, + ) + + store_definition(parent, node.name, definition) + yield definition + + if body = node.value.block&.body + with_visibility do + walk_definitions(body, definition, source, &block) + end + end + else + definition = Constant.new(node.name, + comments: comments_for(node), + parent: parent, + node: node, + language: @language, + ) + + store_definition(parent, node.name, definition) + yield definition + end when :call_node name = node.name @@ -506,6 +527,24 @@ def receiver_for(node) end end + def struct_super_class_for(node) + return unless node&.type == :call_node + return unless node.block + + case node.receiver&.type + when :constant_read_node + receiver_name = node.receiver.name.to_s + when :constant_path_node + receiver_name = nested_name_for(node.receiver) + end + + if receiver_name == "Struct" && node.name == :new + return "Struct" + elsif receiver_name == "Data" && node.name == :define + return "Data" + end + end + def singleton_name_for(node) case node.expression.type when :self_node diff --git a/releases.md b/releases.md index 137f8b3..cc22c3b 100644 --- a/releases.md +++ b/releases.md @@ -1,5 +1,9 @@ # Releases +## Unreleased + + - Add support for indexing methods inside `Struct.new` and `Data.define` assignment blocks. + ## v0.27.0 - Add `decode:documentation:markdown` bake task for generating LLM-optimized Markdown documentation. diff --git a/test/decode/language/ruby/.fixtures/data_struct.rb b/test/decode/language/ruby/.fixtures/data_struct.rb new file mode 100644 index 0000000..c1ffb4e --- /dev/null +++ b/test/decode/language/ruby/.fixtures/data_struct.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +# The context object. +Context = Struct.new(:arguments, keyword_init: true) do + # Build a context. + def self.for(arguments) + end + + # The completed words. + def words + end +end + +module Types + # The request object. + Request = Data.define(:arguments) do + # Build a request. + def self.for(arguments) + end + + # The completed words. + def words + end + end +end diff --git a/test/decode/language/ruby/parser.rb b/test/decode/language/ruby/parser.rb index 8feeb3b..2cdf7cd 100644 --- a/test/decode/language/ruby/parser.rb +++ b/test/decode/language/ruby/parser.rb @@ -196,6 +196,60 @@ end end + with "Struct.new and Data.define assignments" do + let(:path) {File.expand_path(".fixtures/data_struct.rb", __dir__)} + + it "treats Struct.new assignments as class-like containers" do + context = definitions.find{|definition| definition.full_path == [:Context]} + + expect(context).to be_a(Decode::Language::Ruby::Class) + expect(context).to have_attributes( + short_form: be == "class Context", + long_form: be == "class Context < Struct", + comments: be == ["The context object."], + ) + expect(context).to be(:container?) + end + + it "extracts methods from Struct.new assignment blocks" do + methods = definitions.select{|definition| definition.parent&.name == :Context} + + expect(methods.collect(&:short_form)).to be == [ + "def self.for", + "def words", + ] + expect(methods.collect(&:full_path)).to be == [ + [:Context, :for], + [:Context, :words], + ] + end + + it "treats Data.define assignments as class-like containers" do + request = definitions.find{|definition| definition.full_path == [:Types, :Request]} + + expect(request).to be_a(Decode::Language::Ruby::Class) + expect(request).to have_attributes( + short_form: be == "class Request", + long_form: be == "class Types::Request < Data", + comments: be == ["The request object."], + ) + expect(request).to be(:container?) + end + + it "extracts methods from Data.define assignment blocks" do + methods = definitions.select{|definition| definition.parent&.name == :Request} + + expect(methods.collect(&:short_form)).to be == [ + "def self.for", + "def words", + ] + expect(methods.collect(&:full_path)).to be == [ + [:Types, :Request, :for], + [:Types, :Request, :words], + ] + end + end + with "attributes" do let(:path) {File.expand_path(".fixtures/attributes.rb", __dir__)}