solargraph 0.54.5 → 0.55.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +13 -0
  3. data/lib/solargraph/api_map/store.rb +3 -1
  4. data/lib/solargraph/api_map.rb +18 -8
  5. data/lib/solargraph/complex_type/unique_type.rb +88 -7
  6. data/lib/solargraph/complex_type.rb +35 -6
  7. data/lib/solargraph/convention/struct_definition/struct_assignment_node.rb +51 -0
  8. data/lib/solargraph/convention/struct_definition/struct_definition_node.rb +100 -0
  9. data/lib/solargraph/convention/struct_definition.rb +101 -0
  10. data/lib/solargraph/convention.rb +1 -0
  11. data/lib/solargraph/doc_map.rb +42 -18
  12. data/lib/solargraph/language_server/host/message_worker.rb +10 -7
  13. data/lib/solargraph/language_server/host.rb +1 -0
  14. data/lib/solargraph/location.rb +8 -0
  15. data/lib/solargraph/parser/comment_ripper.rb +11 -6
  16. data/lib/solargraph/parser/flow_sensitive_typing.rb +226 -0
  17. data/lib/solargraph/parser/node_methods.rb +14 -0
  18. data/lib/solargraph/parser/node_processor.rb +0 -1
  19. data/lib/solargraph/parser/parser_gem/class_methods.rb +9 -0
  20. data/lib/solargraph/parser/parser_gem/node_chainer.rb +10 -10
  21. data/lib/solargraph/parser/parser_gem/node_methods.rb +3 -1
  22. data/lib/solargraph/parser/parser_gem/node_processors/and_node.rb +21 -0
  23. data/lib/solargraph/parser/parser_gem/node_processors/casgn_node.rb +21 -1
  24. data/lib/solargraph/parser/parser_gem/node_processors/if_node.rb +21 -0
  25. data/lib/solargraph/parser/parser_gem/node_processors/namespace_node.rb +26 -5
  26. data/lib/solargraph/parser/parser_gem/node_processors/opasgn_node.rb +41 -0
  27. data/lib/solargraph/parser/parser_gem/node_processors/until_node.rb +28 -0
  28. data/lib/solargraph/parser/parser_gem/node_processors/while_node.rb +28 -0
  29. data/lib/solargraph/parser/parser_gem/node_processors.rb +10 -0
  30. data/lib/solargraph/parser.rb +1 -0
  31. data/lib/solargraph/pin/base.rb +9 -3
  32. data/lib/solargraph/pin/base_variable.rb +7 -1
  33. data/lib/solargraph/pin/block.rb +2 -0
  34. data/lib/solargraph/pin/breakable.rb +9 -0
  35. data/lib/solargraph/pin/local_variable.rb +7 -1
  36. data/lib/solargraph/pin/method.rb +20 -18
  37. data/lib/solargraph/pin/namespace.rb +10 -7
  38. data/lib/solargraph/pin/parameter.rb +13 -5
  39. data/lib/solargraph/pin/proxy_type.rb +12 -6
  40. data/lib/solargraph/pin/until.rb +18 -0
  41. data/lib/solargraph/pin/while.rb +18 -0
  42. data/lib/solargraph/pin.rb +3 -0
  43. data/lib/solargraph/rbs_map/conversions.rb +8 -8
  44. data/lib/solargraph/rbs_map/core_fills.rb +10 -3
  45. data/lib/solargraph/source/chain/array.rb +4 -3
  46. data/lib/solargraph/source/chain/call.rb +46 -17
  47. data/lib/solargraph/source/chain/constant.rb +1 -1
  48. data/lib/solargraph/source/chain/hash.rb +3 -2
  49. data/lib/solargraph/source/chain/link.rb +2 -0
  50. data/lib/solargraph/source/chain/literal.rb +22 -2
  51. data/lib/solargraph/source/chain/z_super.rb +1 -1
  52. data/lib/solargraph/source/chain.rb +77 -47
  53. data/lib/solargraph/source/source_chainer.rb +2 -2
  54. data/lib/solargraph/source_map/clip.rb +3 -1
  55. data/lib/solargraph/type_checker/checks.rb +4 -0
  56. data/lib/solargraph/type_checker.rb +35 -8
  57. data/lib/solargraph/version.rb +1 -1
  58. data/lib/solargraph/yard_map/mapper/to_method.rb +42 -15
  59. metadata +14 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 155e6f0e4fc44166c0d4b3c1ba6691ff1d1629d229adf8d37f6d3fe4b5fc0c77
4
- data.tar.gz: c33c4dab114c6a70b3c0dfbee6a5ccdc379cf284af3d34dae8a30473dbcdc517
3
+ metadata.gz: c8e98755e559a9524b7d057c860e1ebb6d8a46eeb73dd567744f7562a4d49a64
4
+ data.tar.gz: 33943305fcf0c4c534fd4abf5eb24a937f6390021e681017296aac7bb9546592
5
5
  SHA512:
6
- metadata.gz: 267350435fa9592fc49dc2c5b05f9f9157b576025ea4ed9abe3ebc58e4344d7412805ace58920585105fbb81eaa2bad246416c5456ce66656f22c8639054002d
7
- data.tar.gz: 7a3860337657dc869e7a96f8b95509867ece49db13e89523e09c1567490b427ed41f5fc57c87e5d8477bfe3857cbe97512bd811598adbb53f4a2f617e724c44c
6
+ metadata.gz: 37c0b331d67d299a865e4201c030dec6c1fd4ca14955012d702a44849bece0497c70449a3594ce8cdfe56669b93fbc80d1b291331a540453409a990344464ac1
7
+ data.tar.gz: c91397eeb5d2f10c551e97a741acb9a1e8fbae371b810bf375cc6024fc5985d6e2e581fb1d39c62ba9dd7f71b2cc6fa0ca801fcb74c89b5b8717beeaf8af10f0
data/CHANGELOG.md CHANGED
@@ -1,3 +1,16 @@
1
+ ## 0.55.0 - June 3, 2025
2
+ - Flow-sensitive typing - automatically downcast from is_a? calls (#856)
3
+ - Tuple enabler: infer literal types and use them for signature selection (#836)
4
+ - Signature selection improvements (#907)
5
+ - Add support for Ruby Structs (#939)
6
+ - [regression] Fix interface change breaking solargraph-rails (#940)
7
+ - [regression] Add back bundler/require support for solargraph-rails (#941)
8
+ - Add specs for initialize capabilities (#955)
9
+ - Create MethodAlias pins from YARD (#945)
10
+ - MessageWorker prioritizes synchronization (#956)
11
+ - initialize/new method pin cleanups (#949)
12
+ - Clip rebinds blocks when cursor is not part of receiver (#958)
13
+
1
14
  ## 0.54.5 - May 17, 2025
2
15
  - Repair unknown encoding errors (#936, #935)
3
16
  - Index arbitrary pinsets (#937)
@@ -73,6 +73,8 @@ module Solargraph
73
73
  return superclass_references[fqns].first if superclass_references.key?(fqns)
74
74
  return 'Object' if fqns != 'BasicObject' && namespace_exists?(fqns)
75
75
  return 'Object' if fqns == 'Boolean'
76
+ simplified_literal_name = ComplexType.parse("#{fqns}").simplify_literals.name
77
+ return simplified_literal_name if simplified_literal_name != fqns
76
78
  nil
77
79
  end
78
80
 
@@ -112,7 +114,7 @@ module Solargraph
112
114
  # @param fqns [String]
113
115
  # @return [Enumerable<Solargraph::Pin::Base>]
114
116
  def get_class_variables(fqns)
115
- namespace_children(fqns).select{|pin| pin.is_a?(Pin::ClassVariable)}
117
+ namespace_children(fqns).select { |pin| pin.is_a?(Pin::ClassVariable)}
116
118
  end
117
119
 
118
120
  # @return [Enumerable<Solargraph::Pin::Base>]
@@ -264,15 +264,19 @@ module Solargraph
264
264
  # Should not be prefixed with '::'.
265
265
  # @return [String, nil] fully qualified tag
266
266
  def qualify tag, context_tag = ''
267
- return tag if ['self', nil].include?(tag)
267
+ return tag if ['Boolean', 'self', nil].include?(tag)
268
268
 
269
- context_type = ComplexType.parse(context_tag).force_rooted
269
+ context_type = ComplexType.try_parse(context_tag).force_rooted
270
270
  return unless context_type
271
271
 
272
272
  type = ComplexType.try_parse(tag)
273
273
  return unless type
274
+ return tag if type.literal?
274
275
 
275
- fqns = qualify_namespace(type.rooted_namespace, context_type.namespace)
276
+ context_type = ComplexType.try_parse(context_tag)
277
+ return unless context_type
278
+
279
+ fqns = qualify_namespace(type.rooted_namespace, context_type.rooted_namespace)
276
280
  return unless fqns
277
281
 
278
282
  fqns + type.substring
@@ -319,6 +323,11 @@ module Solargraph
319
323
  result
320
324
  end
321
325
 
326
+ # @see Solargraph::Parser::FlowSensitiveTyping#visible_pins
327
+ def visible_pins(*args, **kwargs, &blk)
328
+ Solargraph::Parser::FlowSensitiveTyping.visible_pins(*args, **kwargs, &blk)
329
+ end
330
+
322
331
  # Get an array of class variable pins for a namespace.
323
332
  #
324
333
  # @param namespace [String] A fully qualified namespace
@@ -376,7 +385,7 @@ module Solargraph
376
385
  init_pin = get_method_stack(rooted_tag, 'initialize').first
377
386
  next pin unless init_pin
378
387
 
379
- type = ComplexType.try_parse(ComplexType.try_parse(rooted_tag).namespace)
388
+ type = ComplexType::SELF
380
389
  Pin::Method.new(
381
390
  name: 'new',
382
391
  scope: :class,
@@ -385,9 +394,10 @@ module Solargraph
385
394
  signatures: init_pin.signatures.map { |sig| sig.proxy(type) },
386
395
  return_type: type,
387
396
  comments: init_pin.comments,
388
- closure: init_pin.closure
389
- # @todo Hack to force TypeChecker#internal_or_core?
390
- ).tap { |pin| pin.source = :rbs }
397
+ closure: init_pin.closure,
398
+ source: init_pin.source,
399
+ type_location: init_pin.type_location,
400
+ )
391
401
  end
392
402
  end
393
403
  result.concat inner_get_methods('Kernel', :instance, [:public], deep, skip) if visibility.include?(:private)
@@ -814,7 +824,7 @@ module Solargraph
814
824
  return pin unless pin.is_a?(Pin::MethodAlias)
815
825
  return nil if @method_alias_stack.include?(pin.path)
816
826
  @method_alias_stack.push pin.path
817
- origin = get_method_stack(pin.full_context.tag, pin.original, scope: pin.scope).first
827
+ origin = get_method_stack(pin.full_context.tag, pin.original, scope: pin.scope, preserve_generics: true).first
818
828
  @method_alias_stack.pop
819
829
  return nil if origin.nil?
820
830
  args = {
@@ -27,7 +27,7 @@ module Solargraph
27
27
  # @return [UniqueType]
28
28
  def self.parse name, substring = '', make_rooted: nil
29
29
  if name.start_with?(':::')
30
- raise "Illegal prefix: #{name}"
30
+ raise ComplexTypeError, "Illegal prefix: #{name}"
31
31
  end
32
32
  if name.start_with?('::')
33
33
  name = name[2..-1]
@@ -48,7 +48,7 @@ module Solargraph
48
48
  subs = ComplexType.parse(substring[1..-2], partial: true)
49
49
  parameters_type = PARAMETERS_TYPE_BY_STARTING_TAG.fetch(substring[0])
50
50
  if parameters_type == :hash
51
- raise ComplexTypeError, "Bad hash type" unless !subs.is_a?(ComplexType) and subs.length == 2 and !subs[0].is_a?(UniqueType) and !subs[1].is_a?(UniqueType)
51
+ raise ComplexTypeError, "Bad hash type: name=#{name}, substring=#{substring}" unless !subs.is_a?(ComplexType) and subs.length == 2 and !subs[0].is_a?(UniqueType) and !subs[1].is_a?(UniqueType)
52
52
  # @todo should be able to resolve map; both types have it
53
53
  # with same return type
54
54
  # @sg-ignore
@@ -73,19 +73,63 @@ module Solargraph
73
73
  end
74
74
  raise "Please remove leading :: and set rooted instead - #{name.inspect}" if name.start_with?('::')
75
75
  @name = name
76
- @key_types = key_types
77
- @subtypes = subtypes
76
+ @parameters_type = parameters_type
77
+ if implicit_union?
78
+ @key_types = key_types.uniq
79
+ @subtypes = subtypes.uniq
80
+ else
81
+ @key_types = key_types
82
+ @subtypes = subtypes
83
+ end
78
84
  @rooted = rooted
79
85
  @all_params = []
80
- @all_params.concat key_types
81
- @all_params.concat subtypes
82
- @parameters_type = parameters_type
86
+ @all_params.concat @key_types
87
+ @all_params.concat @subtypes
88
+ end
89
+
90
+ def implicit_union?
91
+ # @todo use api_map to establish number of generics in type;
92
+ # if only one is allowed but multiple are passed in, treat
93
+ # those as implicit unions
94
+ ['Hash', 'Array', 'Set', '_ToAry', 'Enumerable', '_Each'].include?(name) && parameters_type != :fixed
83
95
  end
84
96
 
85
97
  def to_s
86
98
  tag
87
99
  end
88
100
 
101
+ def simplify_literals
102
+ transform do |t|
103
+ next t unless t.literal?
104
+ t.recreate(new_name: t.non_literal_name)
105
+ end
106
+ end
107
+
108
+ def literal?
109
+ non_literal_name != name
110
+ end
111
+
112
+ def non_literal_name
113
+ @non_literal_name ||= determine_non_literal_name
114
+ end
115
+
116
+ def determine_non_literal_name
117
+ # https://github.com/ruby/rbs/blob/master/docs/syntax.md
118
+ #
119
+ # _literal_ ::= _string-literal_
120
+ # | _symbol-literal_
121
+ # | _integer-literal_
122
+ # | `true`
123
+ # | `false`
124
+ return name if name.empty?
125
+ return 'NilClass' if name == 'nil'
126
+ return 'Boolean' if ['true', 'false'].include?(name)
127
+ return 'Symbol' if name[0] == ':'
128
+ return 'String' if ['"', "'"].include?(name[0])
129
+ return 'Integer' if name.match?(/^-?\d+$/)
130
+ name
131
+ end
132
+
89
133
  def eql?(other)
90
134
  self.class == other.class &&
91
135
  @name == other.name &&
@@ -113,6 +157,8 @@ module Solargraph
113
157
  def rbs_name
114
158
  if name == 'undefined'
115
159
  'untyped'
160
+ elsif literal?
161
+ name
116
162
  else
117
163
  rooted_name
118
164
  end
@@ -183,6 +229,23 @@ module Solargraph
183
229
  name == GENERIC_TAG_NAME || all_params.any?(&:generic?)
184
230
  end
185
231
 
232
+ # @param api_map [ApiMap] The ApiMap that performs qualification
233
+ # @param atype [ComplexType] type which may be assigned to this type
234
+ def can_assign?(api_map, atype)
235
+ logger.debug { "UniqueType#can_assign?(self=#{rooted_tags.inspect}, atype=#{atype.rooted_tags.inspect})" }
236
+ downcasted_atype = atype.downcast_to_literal_if_possible
237
+ out = downcasted_atype.all? do |autype|
238
+ autype.name == name || api_map.super_and_sub?(name, autype.name)
239
+ end
240
+ logger.debug { "UniqueType#can_assign?(self=#{rooted_tags.inspect}, atype=#{atype.rooted_tags.inspect}) => #{out}" }
241
+ out
242
+ end
243
+
244
+ # @return [UniqueType]
245
+ def downcast_to_literal_if_possible
246
+ SINGLE_SUBTYPE.fetch(rooted_tag, self)
247
+ end
248
+
186
249
  # @param generics_to_resolve [Enumerable<String>]
187
250
  # @param context_type [UniqueType, nil]
188
251
  # @param resolved_generic_values [Hash{String => ComplexType}] Added to as types are encountered or resolved
@@ -253,6 +316,12 @@ module Solargraph
253
316
  else
254
317
  next ComplexType::UNDEFINED
255
318
  end
319
+ elsif context_type.all?(&:implicit_union?)
320
+ if idx == 0 && !context_type.all_params.empty?
321
+ ComplexType.new(context_type.all_params)
322
+ else
323
+ ComplexType::UNDEFINED
324
+ end
256
325
  else
257
326
  context_type.all_params[idx] || ComplexType::UNDEFINED
258
327
  end
@@ -381,6 +450,18 @@ module Solargraph
381
450
 
382
451
  UNDEFINED = UniqueType.new('undefined', rooted: false)
383
452
  BOOLEAN = UniqueType.new('Boolean', rooted: true)
453
+ TRUE = UniqueType.new('true', rooted: true)
454
+ FALSE = UniqueType.new('false', rooted: true)
455
+ NIL = UniqueType.new('nil', rooted: true)
456
+ # @type [Hash{String => UniqueType}]
457
+ SINGLE_SUBTYPE = {
458
+ '::TrueClass' => UniqueType::TRUE,
459
+ '::FalseClass' => UniqueType::FALSE,
460
+ '::NilClass' => UniqueType::NIL
461
+ }.freeze
462
+
463
+
464
+ include Logging
384
465
  end
385
466
  end
386
467
  end
@@ -16,7 +16,13 @@ module Solargraph
16
16
  def initialize types = [UniqueType::UNDEFINED]
17
17
  # @todo @items here should not need an annotation
18
18
  # @type [Array<UniqueType>]
19
- @items = types.flat_map(&:items).uniq(&:to_s)
19
+ items = types.flat_map(&:items).uniq(&:to_s)
20
+ if items.any? { |i| i.name == 'false' } && items.any? { |i| i.name == 'true' }
21
+ items.delete_if { |i| i.name == 'false' || i.name == 'true' }
22
+ items.unshift(ComplexType::BOOLEAN)
23
+ end
24
+ items = [UniqueType::UNDEFINED] if items.any?(&:undefined?)
25
+ @items = items
20
26
  end
21
27
 
22
28
  # @sg-ignore Fix "Not enough arguments to Module#protected"
@@ -70,7 +76,7 @@ module Solargraph
70
76
  end
71
77
 
72
78
  # @yieldparam [UniqueType]
73
- # @return [Array]
79
+ # @return [Array<UniqueType>]
74
80
  def map &block
75
81
  @items.map &block
76
82
  end
@@ -93,6 +99,12 @@ module Solargraph
93
99
  end
94
100
  end
95
101
 
102
+ # @param atype [ComplexType] type which may be assigned to this type
103
+ # @param api_map [ApiMap] The ApiMap that performs qualification
104
+ def can_assign?(api_map, atype)
105
+ any? { |ut| ut.can_assign?(api_map, atype) }
106
+ end
107
+
96
108
  # @return [Integer]
97
109
  def length
98
110
  @items.length
@@ -103,10 +115,6 @@ module Solargraph
103
115
  @items
104
116
  end
105
117
 
106
- def tags
107
- @items.map(&:tag).join(', ')
108
- end
109
-
110
118
  # @param index [Integer]
111
119
  # @return [UniqueType]
112
120
  def [](index)
@@ -147,6 +155,23 @@ module Solargraph
147
155
  map(&:tag).join(', ')
148
156
  end
149
157
 
158
+ def tags
159
+ map(&:tag).join(', ')
160
+ end
161
+
162
+ def simple_tags
163
+ simplify_literals.tags
164
+ end
165
+
166
+ def literal?
167
+ @items.any?(&:literal?)
168
+ end
169
+
170
+ # @return [ComplexType]
171
+ def downcast_to_literal_if_possible
172
+ ComplexType.new(items.map(&:downcast_to_literal_if_possible))
173
+ end
174
+
150
175
  def desc
151
176
  rooted_tags
152
177
  end
@@ -175,6 +200,10 @@ module Solargraph
175
200
  any?(&:generic?)
176
201
  end
177
202
 
203
+ def simplify_literals
204
+ ComplexType.new(map(&:simplify_literals))
205
+ end
206
+
178
207
  # @param new_name [String, nil]
179
208
  # @yieldparam t [UniqueType]
180
209
  # @yieldreturn [UniqueType]
@@ -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
@@ -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
@@ -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
@@ -10,6 +10,7 @@ module Solargraph
10
10
  autoload :Gemfile, 'solargraph/convention/gemfile'
11
11
  autoload :Gemspec, 'solargraph/convention/gemspec'
12
12
  autoload :Rakefile, 'solargraph/convention/rakefile'
13
+ autoload :StructDefinition, 'solargraph/convention/struct_definition'
13
14
 
14
15
  # @type [Set<Convention::Base>]
15
16
  @@conventions = Set.new