Skip to content

Commit 68dc1c4

Browse files
authored
Add support for Ruby Structs (#939)
* Allow multiple node processors * Initial implementation of Struct via inheritance definition * Revert "Allow multiple node processors" This reverts commit 2432024218132f7d06a3b1600ebc17a2e87df260. * Process struct definition inside NamespaceNode * Support keyword_init * Support Struct definition via const assignment * Add struct @Ivars * Improve Pin inspection - include #path * Refactor code - Move classes into it's own file - Fix nil errors - Add TODO comments
1 parent 60036bc commit 68dc1c4

File tree

11 files changed

+417
-11
lines changed

11 files changed

+417
-11
lines changed

lib/solargraph/convention.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ module Convention
1010
autoload :Gemfile, 'solargraph/convention/gemfile'
1111
autoload :Gemspec, 'solargraph/convention/gemspec'
1212
autoload :Rakefile, 'solargraph/convention/rakefile'
13+
autoload :StructDefinition, 'solargraph/convention/struct_definition'
1314

1415
# @type [Set<Convention::Base>]
1516
@@conventions = Set.new
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# frozen_string_literal: true
2+
3+
module Solargraph
4+
module Convention
5+
module StructDefinition
6+
autoload :StructDefintionNode, 'solargraph/convention/struct_definition/struct_definition_node'
7+
autoload :StructAssignmentNode, 'solargraph/convention/struct_definition/struct_assignment_node'
8+
9+
module NodeProcessors
10+
class StructNode < Parser::NodeProcessor::Base
11+
def process
12+
return if struct_definition_node.nil?
13+
14+
loc = get_node_location(node)
15+
nspin = Solargraph::Pin::Namespace.new(
16+
type: :class,
17+
location: loc,
18+
closure: region.closure,
19+
name: struct_definition_node.class_name,
20+
comments: comments_for(node),
21+
visibility: :public,
22+
gates: region.closure.gates.freeze
23+
)
24+
pins.push nspin
25+
26+
# define initialize method
27+
initialize_method_pin = Pin::Method.new(
28+
name: 'initialize',
29+
parameters: [],
30+
scope: :instance,
31+
location: get_node_location(node),
32+
closure: nspin,
33+
visibility: :private,
34+
comments: comments_for(node)
35+
)
36+
37+
pins.push initialize_method_pin
38+
39+
struct_definition_node.attributes.map do |attribute_node, attribute_name|
40+
initialize_method_pin.parameters.push(
41+
Pin::Parameter.new(
42+
name: attribute_name,
43+
decl: struct_definition_node.keyword_init? ? :kwarg : :arg,
44+
location: get_node_location(attribute_node),
45+
closure: initialize_method_pin
46+
)
47+
)
48+
end
49+
50+
# define attribute accessors and instance variables
51+
struct_definition_node.attributes.each do |attribute_node, attribute_name|
52+
[attribute_name, "#{attribute_name}="].each do |name|
53+
method_pin = Pin::Method.new(
54+
name: name,
55+
parameters: [],
56+
scope: :instance,
57+
location: get_node_location(attribute_node),
58+
closure: nspin,
59+
comments: attribute_comments(attribute_node, attribute_name),
60+
visibility: :public
61+
)
62+
63+
pins.push method_pin
64+
65+
next unless name.include?('=') # setter
66+
pins.push Pin::InstanceVariable.new(name: "@#{attribute_name}",
67+
closure: method_pin,
68+
location: get_node_location(attribute_node),
69+
comments: attribute_comments(attribute_node, attribute_name))
70+
end
71+
end
72+
73+
process_children region.update(closure: nspin, visibility: :public)
74+
end
75+
76+
private
77+
78+
# @return [StructDefintionNode, nil]
79+
def struct_definition_node
80+
@struct_definition_node ||= if StructDefintionNode.valid?(node)
81+
StructDefintionNode.new(node)
82+
elsif StructAssignmentNode.valid?(node)
83+
StructAssignmentNode.new(node)
84+
end
85+
end
86+
87+
# @param attribute_node [Parser::AST::Node]
88+
# @return [String, nil]
89+
def attribute_comments(attribute_node, attribute_name)
90+
struct_comments = comments_for(attribute_node)
91+
return if struct_comments.nil? || struct_comments.empty?
92+
93+
struct_comments.split("\n").find do |row|
94+
row.include?(attribute_name)
95+
end&.gsub('@param', '@return')&.gsub(attribute_name, '')
96+
end
97+
end
98+
end
99+
end
100+
end
101+
end
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# frozen_string_literal: true
2+
3+
module Solargraph
4+
module Convention
5+
module StructDefinition
6+
# A node wrapper for a Struct definition via const assignment.
7+
# @example
8+
# MyStruct = Struct.new(:bar, :baz) do
9+
# def foo
10+
# end
11+
# end
12+
class StructAssignmentNode < StructDefintionNode
13+
class << self
14+
# @example
15+
# s(:casgn, nil, :Foo,
16+
# s(:block,
17+
# s(:send,
18+
# s(:const, nil, :Struct), :new,
19+
# s(:sym, :bar),
20+
# s(:sym, :baz)),
21+
# s(:args),
22+
# s(:def, :foo,
23+
# s(:args),
24+
# s(:send, nil, :bar))))
25+
def valid?(node)
26+
return false unless node&.type == :casgn
27+
return false if node.children[2].nil?
28+
struct_node = node.children[2].children[0]
29+
30+
struct_definition_node?(struct_node)
31+
end
32+
end
33+
34+
def class_name
35+
if node.children[0]
36+
Parser::NodeMethods.unpack_name(node.children[0]) + "::#{node.children[1]}"
37+
else
38+
node.children[1].to_s
39+
end
40+
end
41+
42+
private
43+
44+
# @return [Parser::AST::Node]
45+
def struct_node
46+
node.children[2].children[0]
47+
end
48+
end
49+
end
50+
end
51+
end
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# frozen_string_literal: true
2+
3+
module Solargraph
4+
module Convention
5+
module StructDefinition
6+
# A node wrapper for a Struct definition via inheritance.
7+
# @example
8+
# class MyStruct < Struct.new(:bar, :baz)
9+
# def foo
10+
# end
11+
# end
12+
class StructDefintionNode
13+
class << self
14+
# @example
15+
# s(:class,
16+
# s(:const, nil, :Foo),
17+
# s(:send,
18+
# s(:const, nil, :Struct), :new,
19+
# s(:sym, :bar),
20+
# s(:sym, :baz)),
21+
# s(:hash,
22+
# s(:pair,
23+
# s(:sym, :keyword_init),
24+
# s(:true)))),
25+
# s(:def, :foo,
26+
# s(:args),
27+
# s(:send, nil, :bar)))
28+
def valid?(node)
29+
return false unless node&.type == :class
30+
31+
struct_definition_node?(node.children[1])
32+
end
33+
34+
private
35+
36+
# @param struct_node [Parser::AST::Node]
37+
# @return [Boolean]
38+
def struct_definition_node?(struct_node)
39+
return false unless struct_node.is_a?(::Parser::AST::Node)
40+
return false unless struct_node&.type == :send
41+
return false unless struct_node.children[0]&.type == :const
42+
return false unless struct_node.children[0].children[1] == :Struct
43+
return false unless struct_node.children[1] == :new
44+
45+
true
46+
end
47+
end
48+
49+
# @return [Parser::AST::Node]
50+
def initialize(node)
51+
@node = node
52+
end
53+
54+
# @return [String]
55+
def class_name
56+
Parser::NodeMethods.unpack_name(node)
57+
end
58+
59+
# @return [Array<Array(Parser::AST::Node, String)>]
60+
def attributes
61+
struct_attribute_nodes.map do |struct_def_param|
62+
next unless struct_def_param.type == :sym
63+
[struct_def_param, struct_def_param.children[0].to_s]
64+
end.compact
65+
end
66+
67+
def keyword_init?
68+
keyword_init_param = struct_attribute_nodes.find do |struct_def_param|
69+
struct_def_param.type == :hash && struct_def_param.children[0].type == :pair &&
70+
struct_def_param.children[0].children[0].children[0] == :keyword_init
71+
end
72+
73+
return false if keyword_init_param.nil?
74+
75+
keyword_init_param.children[0].children[1].type == :true
76+
end
77+
78+
# @return [Parser::AST::Node]
79+
def body_node
80+
node.children[2]
81+
end
82+
83+
private
84+
85+
# @return [Parser::AST::Node]
86+
attr_reader :node
87+
88+
# @return [Parser::AST::Node]
89+
def struct_node
90+
node.children[1]
91+
end
92+
93+
# @return [Array<Parser::AST::Node>]
94+
def struct_attribute_nodes
95+
struct_node.children[2..-1]
96+
end
97+
end
98+
end
99+
end
100+
end

lib/solargraph/parser/node_processor.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ module NodeProcessor
99
autoload :Base, 'solargraph/parser/node_processor/base'
1010

1111
class << self
12-
# @type [Hash<Symbol, Class<NodeProcessor::Base>>]
1312
@@processors ||= {}
1413

1514
# Register a processor for a node type.

lib/solargraph/parser/parser_gem/node_methods.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ def pack_name(node)
4040
if n.is_a?(AST::Node)
4141
if n.type == :cbase
4242
parts = [''] + pack_name(n)
43-
else
43+
elsif n.type == :const
4444
parts += pack_name(n)
4545
end
4646
else

lib/solargraph/parser/parser_gem/node_processors/casgn_node.rb

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,17 @@ class CasgnNode < Parser::NodeProcessor::Base
88
include ParserGem::NodeMethods
99

1010
def process
11+
if Convention::StructDefinition::StructAssignmentNode.valid?(node)
12+
process_struct_assignment
13+
else
14+
process_constant_assignment
15+
end
16+
end
17+
18+
private
19+
20+
# @return [void]
21+
def process_constant_assignment
1122
pins.push Solargraph::Pin::Constant.new(
1223
location: get_node_location(node),
1324
closure: region.closure,
@@ -18,7 +29,16 @@ def process
1829
process_children
1930
end
2031

21-
private
32+
# TODO: Move this out of [CasgnNode] once [Solargraph::Parser::NodeProcessor] supports
33+
# multiple processors.
34+
def process_struct_assignment
35+
processor_klass = Convention::StructDefinition::NodeProcessors::StructNode
36+
processor = processor_klass.new(node, region, pins, locals)
37+
processor.process
38+
39+
@pins = processor.pins
40+
@locals = processor.locals
41+
end
2242

2343
# @return [String]
2444
def const_name

lib/solargraph/parser/parser_gem/node_processors/namespace_node.rb

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,20 @@ class NamespaceNode < Parser::NodeProcessor::Base
88
include ParserGem::NodeMethods
99

1010
def process
11-
sc = nil
12-
if node.type == :class and !node.children[1].nil?
13-
sc = unpack_name(node.children[1])
11+
superclass_name = nil
12+
superclass_name = unpack_name(node.children[1]) if node.type == :class && node.children[1]&.type == :const
13+
14+
if Convention::StructDefinition::StructDefintionNode.valid?(node)
15+
process_struct_definition
16+
else
17+
process_namespace(superclass_name)
1418
end
19+
end
20+
21+
private
22+
23+
# @param superclass_name [String, nil]
24+
def process_namespace(superclass_name)
1525
loc = get_node_location(node)
1626
nspin = Solargraph::Pin::Namespace.new(
1727
type: node.type,
@@ -23,15 +33,26 @@ def process
2333
gates: region.closure.gates.freeze
2434
)
2535
pins.push nspin
26-
unless sc.nil?
36+
unless superclass_name.nil?
2737
pins.push Pin::Reference::Superclass.new(
2838
location: loc,
2939
closure: pins.last,
30-
name: sc
40+
name: superclass_name
3141
)
3242
end
3343
process_children region.update(closure: nspin, visibility: :public)
3444
end
45+
46+
# TODO: Move this out of [NamespaceNode] once [Solargraph::Parser::NodeProcessor] supports
47+
# multiple processors.
48+
def process_struct_definition
49+
processor_klass = Convention::StructDefinition::NodeProcessors::StructNode
50+
processor = processor_klass.new(node, region, pins, locals)
51+
processor.process
52+
53+
@pins = processor.pins
54+
@locals = processor.locals
55+
end
3556
end
3657
end
3758
end

lib/solargraph/pin/method.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,9 +166,9 @@ def signature_help
166166
def desc
167167
# ensure the signatures line up when logged
168168
if signatures.length > 1
169-
"\n#{to_rbs}\n"
169+
path + " \n#{to_rbs}\n"
170170
else
171-
to_rbs
171+
super
172172
end
173173
end
174174

lib/solargraph/pin/namespace.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ def desc
4848
if name.nil? || name.empty?
4949
'(top-level)'
5050
else
51-
return_type.rooted_tags
51+
super
5252
end
5353
end
5454

0 commit comments

Comments
 (0)