Skip to content

Add support for Ruby Structs #939

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Jun 1, 2025

Conversation

lekemula
Copy link
Contributor

@lekemula lekemula commented May 18, 2025

Hi,

This PR adds support for Ruby Struct class definitions both via inheritance and const assignment:

# @param bar [String]
# @param baz [Integer]
class Foo < Struct.new(:bar, :baz, keyword_init: true)
  def foo
  end
end

# @param bar [String]
# @param baz [Integer]
Foo = Struct.new(:bar, :baz) do
  def foo
  end
end

It completes:

  • Struct attributes
  • Struct initialise method with appropriate args or kwargs based on keyword_init option
  • Struct instance variables
  • Utilises @param yard tags on class definition to infer attribute types.
    • While this is not a YARD specification, I think it avoids quite some boilerplate that comes from @!attribute usage, which kind of defeats the purpose of Struct usage to avoid defining them by hand in the first place.
       class Foo < Struct.new(:bar, :baz)
         # @!attribute [rw] bar
         #   @return [String]
         # @!attribute [rw] baz
         #   @return [Integer]
         def foo
         end
       end

Approach

So I thought for quite a while, where this code could live, and I went back and forth many times between:

  • Adding it to the lib/solargraph/parser/parser_gem/node_processors
    • However, Struct.new is not a language construct, ie, node per se.
  • Adding it to the lib/solargraph/convention (direction I went with)
    • However, the code here mainly needs to hook into the parsing stage, rather than implement the Solargraph::Convention interface.

So, as we see, each approach has its upside/downside. I ultimately went with the second approach because while working on the solargraph-rspec plugin, I always felt a need for Solargraph to provide a way to hook into the parsing phase. Something that would prevent plugins from parsing the same code twice!

While I attempted to allow supporting multiple node processers, I stumbled over some issues with having "duplicate pins", which I couldn't find a good solution to, so I ended up with this "hybrid solution", hoping that we get to solve that problem eventually (which is why I left the TODO comments are for)

I plan to add support similarly for Data classes too, before jumping to that I wanted to got some feedback from you on the proposed approach and whether this any sense at all to you?

Thanks in advance for your thoughts and guidance!

@@ -2630,4 +2630,117 @@ def foo
clip = api_map.clip_at('test.rb', [13, 12])
expect(clip.infer.to_s).to eq('Integer')
end

it 'completes Struct methods' do
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are clip_specs the right place for these specs?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Clip specs are pretty heavy, but I can understand why. They're fine here for now. I'm planning updates to Chain that make it less reliant on Clip features. Part of that change will probably include moving some tests from clip_spec to chain_spec or the various link specs.

@@ -8,6 +8,17 @@ class CasgnNode < Parser::NodeProcessor::Base
include ParserGem::NodeMethods

def process
if Convention::StructDefinition::StructAssignmentNode.valid?(node)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like the fact that I had to change the node processor classes - do you see a better way to avoid that?

My initial attempt was to allow multiple node processors, as I explained in the PR description.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suspect that any solution would have required some sort of change to node processors. At the very least, they would need a mechanism to map anonymous class pins for declarations like class Foo < Struct.new. I'm good with handling it like this for now. We can try exploring more general solutions when we look into adding support for Data.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good 🙌!

@lekemula lekemula changed the title Add support for ruby Struct classes Add support for Ruby Structs May 18, 2025
@castwide
Copy link
Owner

castwide commented Jun 1, 2025

👍 :shipit:

@castwide castwide merged commit 68dc1c4 into castwide:master Jun 1, 2025
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants