solargraph 0.60.3 → 0.61.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 (76) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/plugins.yml +1 -5
  3. data/.github/workflows/rspec.yml +3 -1
  4. data/CHANGELOG.md +26 -0
  5. data/Gemfile +0 -3
  6. data/lib/solargraph/api_map/index.rb +15 -2
  7. data/lib/solargraph/api_map/store.rb +13 -0
  8. data/lib/solargraph/api_map.rb +60 -81
  9. data/lib/solargraph/cache_dir.rb +58 -0
  10. data/lib/solargraph/collection/base.rb +50 -0
  11. data/lib/solargraph/collection/core.rb +17 -0
  12. data/lib/solargraph/collection/gem.rb +57 -0
  13. data/lib/solargraph/collection/rbs.rb +49 -0
  14. data/lib/solargraph/collection/stdlib.rb +25 -0
  15. data/lib/solargraph/collection.rb +31 -0
  16. data/lib/solargraph/complex_type.rb +18 -5
  17. data/lib/solargraph/convention.rb +5 -3
  18. data/lib/solargraph/diagnostics/require_not_found.rb +0 -4
  19. data/lib/solargraph/external.rb +159 -0
  20. data/lib/solargraph/language_server/message/text_document/definition.rb +4 -1
  21. data/lib/solargraph/language_server/message/text_document/hover.rb +1 -2
  22. data/lib/solargraph/library.rb +11 -24
  23. data/lib/solargraph/metagem.rb +80 -0
  24. data/lib/solargraph/parser/parser_gem/node_chainer.rb +22 -3
  25. data/lib/solargraph/parser/parser_gem/node_processors/namespace_node.rb +14 -5
  26. data/lib/solargraph/pin/closure.rb +17 -0
  27. data/lib/solargraph/pin/factory_parameter.rb +84 -0
  28. data/lib/solargraph/pin/method.rb +7 -4
  29. data/lib/solargraph/pin.rb +1 -0
  30. data/lib/solargraph/rbs_map/base.rb +31 -0
  31. data/lib/solargraph/rbs_map/conversions.rb +3 -1
  32. data/lib/solargraph/rbs_map/{core_map.rb → core.rb} +18 -39
  33. data/lib/solargraph/rbs_map/core_fills.rb +1 -1
  34. data/lib/solargraph/rbs_map/gem.rb +13 -0
  35. data/lib/solargraph/rbs_map/helpers.rb +60 -0
  36. data/lib/solargraph/rbs_map/path.rb +19 -0
  37. data/lib/solargraph/rbs_map/stdlib.rb +24 -0
  38. data/lib/solargraph/rbs_map.rb +7 -198
  39. data/lib/solargraph/repo.rb +177 -0
  40. data/lib/solargraph/shell.rb +49 -99
  41. data/lib/solargraph/source/chain/call.rb +190 -67
  42. data/lib/solargraph/source/chain/literal.rb +17 -17
  43. data/lib/solargraph/source/chain/parameter.rb +79 -0
  44. data/lib/solargraph/source/chain.rb +44 -5
  45. data/lib/solargraph/source/source_chainer.rb +40 -44
  46. data/lib/solargraph/source_map/clip.rb +5 -3
  47. data/lib/solargraph/source_map/mapper.rb +6 -2
  48. data/lib/solargraph/source_map.rb +10 -5
  49. data/lib/solargraph/type_checker/rules.rb +2 -1
  50. data/lib/solargraph/version.rb +1 -1
  51. data/lib/solargraph/workspace.rb +0 -1
  52. data/lib/solargraph/yard_map/cache.rb +1 -1
  53. data/lib/solargraph/yard_map/directives/attribute_directive.rb +1 -1
  54. data/lib/solargraph/yard_map/directives/domain_directive.rb +1 -1
  55. data/lib/solargraph/yard_map/directives/method_directive.rb +1 -1
  56. data/lib/solargraph/yard_map/directives/override_directive.rb +1 -1
  57. data/lib/solargraph/yard_map/directives/parse_directive.rb +1 -1
  58. data/lib/solargraph/yard_map/directives/scope_directive.rb +178 -0
  59. data/lib/solargraph/yard_map/directives/visibility_directive.rb +1 -1
  60. data/lib/solargraph/yard_map/directives.rb +5 -2
  61. data/lib/solargraph/yard_map/helpers.rb +1 -1
  62. data/lib/solargraph/yard_map/macro.rb +34 -5
  63. data/lib/solargraph/yard_map/mapper/to_constant.rb +1 -1
  64. data/lib/solargraph/yard_map/mapper/to_method.rb +1 -1
  65. data/lib/solargraph/yard_map/mapper/to_namespace.rb +1 -1
  66. data/lib/solargraph/yard_map/mapper.rb +37 -4
  67. data/lib/solargraph/yard_map.rb +1 -1
  68. data/lib/solargraph/yardoc.rb +43 -60
  69. data/lib/solargraph.rb +6 -3
  70. data/solargraph.gemspec +1 -1
  71. metadata +22 -9
  72. data/lib/solargraph/doc_map.rb +0 -439
  73. data/lib/solargraph/gem_pins.rb +0 -87
  74. data/lib/solargraph/pin_cache.rb +0 -248
  75. data/lib/solargraph/rbs_map/stdlib_map.rb +0 -71
  76. data/lib/solargraph/workspace/gemspecs.rb +0 -373
@@ -34,6 +34,7 @@ module Solargraph
34
34
  autoload :ZSuper, 'solargraph/source/chain/z_super'
35
35
  autoload :Hash, 'solargraph/source/chain/hash'
36
36
  autoload :Array, 'solargraph/source/chain/array'
37
+ autoload :Parameter, 'solargraph/source/chain/parameter'
37
38
 
38
39
  @@inference_stack = []
39
40
  @@inference_depth = 0
@@ -168,7 +169,7 @@ module Solargraph
168
169
  return ComplexType::UNDEFINED
169
170
  end
170
171
  type = infer_from_definitions(pins, links.last.last_context, api_map, locals)
171
- out = maybe_nil(type)
172
+ out = maybe_nil(type, api_map)
172
173
  logger.debug do
173
174
  "Chain#infer_uncached(links=#{links.map(&:desc)}, locals=#{locals.map(&:desc)}, " \
174
175
  "name_pin=#{name_pin}, name_pin.closure=#{name_pin&.closure&.inspect}, " \
@@ -182,6 +183,13 @@ module Solargraph
182
183
  links.last.is_a?(Chain::Literal)
183
184
  end
184
185
 
186
+ # @return [Boolean]
187
+ # @sg-ignore false "return type could not be inferred"
188
+ def require_parameter?
189
+ # @sg-ignore false "Unresolved call to require_parameter? on Solargraph::Source::Chain::Link"
190
+ links.last.is_a?(Chain::Parameter) && links.last.require_parameter?
191
+ end
192
+
185
193
  def undefined?
186
194
  links.any?(&:undefined?)
187
195
  end
@@ -199,8 +207,38 @@ module Solargraph
199
207
  @splat
200
208
  end
201
209
 
202
- def nullable?
203
- links.any?(&:nullable?)
210
+ # Whether this chain's inferred type should have nil added to it
211
+ # because a `&.` earlier in the chain might have short-circuited
212
+ # evaluation to nil.
213
+ #
214
+ # A `&.` only makes the *immediate* call nil-or-normal-result; a
215
+ # later, non-safe-navigated call in the same chain is still
216
+ # invoked on whatever that produced. If receiver is nil at that
217
+ # point, Ruby calls the method on nil itself instead of skipping
218
+ # it - so nil only continues to flow through subsequent links
219
+ # that NilClass defines as returning `self` (e.g., #tap,
220
+ # #itself). Any other subsequent call resolves to a concrete,
221
+ # non-nil-including type (e.g. NilClass#== returns Boolean), and
222
+ # nil no longer needs to be tracked past that point (unless a
223
+ # later `&.` reintroduces it).
224
+ #
225
+ # @param api_map [ApiMap]
226
+ # @return [Boolean]
227
+ def nullable? api_map
228
+ currently_nullable = false
229
+ links.each do |link|
230
+ if link.nullable?
231
+ currently_nullable = true
232
+ next
233
+ end
234
+ next unless currently_nullable
235
+ next unless link.is_a?(Chain::Call)
236
+
237
+ pin = api_map.get_method_stack('NilClass', link.word, scope: :instance).first
238
+ # @sg-ignore Need to add nil check here
239
+ currently_nullable = pin.nil? || pin.return_type.tag == 'self'
240
+ end
241
+ currently_nullable
204
242
  end
205
243
 
206
244
  include Logging
@@ -286,10 +324,11 @@ module Solargraph
286
324
  end
287
325
 
288
326
  # @param type [ComplexType, ComplexType::UniqueType]
327
+ # @param api_map [ApiMap]
289
328
  # @return [ComplexType, ComplexType::UniqueType]
290
- def maybe_nil type
329
+ def maybe_nil type, api_map
291
330
  return type if type.undefined? || type.void? || type.nullable?
292
- return type unless nullable?
331
+ return type unless nullable?(api_map)
293
332
  ComplexType.new(type.items + [ComplexType::NIL])
294
333
  end
295
334
 
@@ -31,55 +31,18 @@ module Solargraph
31
31
 
32
32
  # @return [Source::Chain]
33
33
  def chain
34
- # Special handling for files that end with an integer and a period
35
- # if phrase =~ /^[0-9]+\.$/
36
- # return Chain.new([Chain::Literal.new('Integer', Integer(phrase[0..-2])),
37
- # Chain::UNDEFINED_CALL])
38
- # end
39
- if phrase.start_with?(':') && !phrase.start_with?('::')
40
- return Chain.new([Chain::Literal.new('Symbol',
41
- # @sg-ignore Need to add nil check here
42
- phrase[1..].to_sym)])
43
- end
44
- if end_of_phrase.strip == '::' && source.code[Position.to_offset(
45
- source.code, position
46
- )].to_s.match?(/[a-z]/i)
47
- return SourceChainer.chain(source,
48
- Position.new(position.line,
49
- position.character + 1))
50
- end
34
+ return SourceChainer.chain(source, Position.new(position.line, position.character + 1)) if end_of_phrase.strip == '::' && source.code[Position.to_offset(source.code, position)].to_s.match?(/[a-z]/i)
35
+
51
36
  begin
52
- return Chain.new([]) if phrase.end_with?('..')
53
- # @type [::Parser::AST::Node, nil]
54
- node = nil
55
- # @type [::Parser::AST::Node, nil]
56
- parent = nil
57
- if !source.repaired? && source.parsed? && source.synchronized?
58
- tree = source.tree_at(position.line, position.column)
59
- node, parent = tree[0..2]
60
- elsif source.parsed? && source.repaired? && end_of_phrase == '.'
61
- node, parent = source.tree_at(fixed_position.line, fixed_position.column)[0..2]
62
- # provide filename and line so that we can look up local variables there later
63
- node = Parser.parse(fixed_phrase, source.filename, fixed_position.line) if node.nil?
64
- elsif source.repaired?
65
- node = Parser.parse(fixed_phrase, source.filename, fixed_position.line)
66
- else
67
- unless source.error_ranges.any? do |r|
68
- r.nil? || r.include?(fixed_position)
69
- end
70
- node, parent = source.tree_at(fixed_position.line,
71
- fixed_position.column)[0..2]
72
- end
73
- # Exception for positions that chain literal nodes in unsynchronized sources
74
- node = nil unless source.synchronized? || !Parser.infer_literal_node_type(node).nil?
75
- node = Parser.parse(fixed_phrase, source.filename, fixed_position.line) if node.nil?
76
- end
37
+ node, parent, tree = node_from_position
77
38
  rescue Parser::SyntaxError
78
39
  return Chain.new([Chain::UNDEFINED_CALL])
79
40
  end
41
+
80
42
  return Chain.new([Chain::UNDEFINED_CALL]) if node.nil? || (node.type == :sym && !phrase.start_with?(':'))
81
- # chain = NodeChainer.chain(node, source.filename, parent && parent.type == :block)
82
- chain = Parser.chain(node, source.filename, parent)
43
+
44
+ chain = Parser.chain(node, source.filename, parent, tree)
45
+
83
46
  if source.repaired? || !source.parsed? || !source.synchronized?
84
47
  if end_of_phrase.strip == '.'
85
48
  chain.links.push Chain::UNDEFINED_CALL
@@ -94,6 +57,39 @@ module Solargraph
94
57
 
95
58
  private
96
59
 
60
+ # @return [Array(Parser::AST::Node, Parser::AST::Node, Array<Parser::AST::Node>)]
61
+ # parent node, child node, and the tree at the position
62
+ def node_from_position
63
+ node = nil
64
+ parent = nil
65
+ tree = nil
66
+
67
+ if source.parsed? && !source.repaired? && source.synchronized?
68
+ tree = source.tree_at(position.line, position.column)
69
+ node, parent = tree[0..2]
70
+ elsif source.parsed? && source.repaired? && end_of_phrase == '.'
71
+ tree = source.tree_at(fixed_position.line, fixed_position.column)
72
+ node, parent = tree[0..2]
73
+ # provide filename and line so that we can look up local variables there later
74
+ node = Parser.parse(fixed_phrase, source.filename, fixed_position.line) if node.nil?
75
+ tree = source.tree_at(fixed_position.line, fixed_position.column) if node
76
+ elsif source.repaired?
77
+ node = Parser.parse(fixed_phrase, source.filename, fixed_position.line)
78
+ tree = source.tree_at(fixed_position.line, fixed_position.column) if node
79
+ else
80
+ unless source.error_ranges.any? { |r| r.nil? || r.include?(fixed_position) }
81
+ tree = source.tree_at(fixed_position.line, fixed_position.column)
82
+ node, parent = tree[0..2]
83
+ end
84
+ # Exception for positions that chain literal nodes in unsynchronized sources
85
+ node = nil unless source.synchronized? || !Parser.infer_literal_node_type(node).nil?
86
+ node = Parser.parse(fixed_phrase, source.filename, fixed_position.line) if node.nil?
87
+ tree = source.tree_at(fixed_position.line, fixed_position.column) if node
88
+ end
89
+
90
+ [node, parent, tree]
91
+ end
92
+
97
93
  # @return [Position]
98
94
  attr_reader :position
99
95
 
@@ -13,14 +13,14 @@ module Solargraph
13
13
  @cursor = cursor
14
14
  closure_pin = closure
15
15
  # @sg-ignore Need to add nil check here
16
- if closure_pin.is_a?(Pin::Block) && !Solargraph::Range.from_node(closure_pin.receiver).contain?(cursor.range.start)
16
+ if closure_pin.is_a?(Pin::Block) && !Solargraph::Range.from_node(closure_pin.receiver)&.contain?(cursor.range.start)
17
17
  closure_pin.rebind(api_map)
18
18
  end
19
19
  end
20
20
 
21
21
  # @return [Array<Pin::Base>] Relevant pins for infering the type of the Cursor's position
22
22
  def define
23
- return [] if cursor.comment? || cursor.chain.literal?
23
+ return [] if cursor.comment? || cursor.chain.literal? || cursor.chain.require_parameter?
24
24
  result = cursor.chain.define(api_map, closure, locals)
25
25
  result.concat file_global_methods
26
26
  if result.empty?
@@ -40,7 +40,8 @@ module Solargraph
40
40
  # @return [Completion]
41
41
  def complete
42
42
  return package_completions([]) if !source_map.source.parsed? || cursor.string?
43
- if cursor.chain.literal? && cursor.chain.links.last.word == '<Symbol>'
43
+ # TODO: Improve magic word comparsion == '<::Symbol>', too fragile
44
+ if cursor.chain.literal? && cursor.chain.links.last.word == '<::Symbol>'
44
45
  return package_completions(api_map.get_symbols)
45
46
  end
46
47
  return Completion.new([], cursor.range) if cursor.chain.literal?
@@ -211,6 +212,7 @@ module Solargraph
211
212
  result.concat api_map.get_constants('', cursor.start_of_constant? ? '' : context_pin.full_context.namespace, *gates) # .select { |pin| pin.name.start_with?(full) }
212
213
  end
213
214
  else
215
+ type = type.qualify(api_map, *gates)
214
216
  result.concat api_map.get_constants(type.namespace,
215
217
  cursor.start_of_constant? ? '' : context_pin.full_context.namespace, *gates)
216
218
  end
@@ -12,7 +12,7 @@ module Solargraph
12
12
 
13
13
  private_class_method :new
14
14
 
15
- DIRECTIVE_REGEXP = /(@!method|@!attribute|@!visibility|@!domain|@!macro|@!parse|@!override)/
15
+ DIRECTIVE_REGEXP = /(@!method|@!attribute|@!visibility|@!scope|@!domain|@!macro|@!parse|@!override)/
16
16
 
17
17
  # Generate the data.
18
18
  #
@@ -104,9 +104,13 @@ module Solargraph
104
104
  directive_processor = YardMap::Directives.for(directive)
105
105
  return unless directive_processor
106
106
 
107
- @pins += directive_processor.process_directive(
107
+ new_pins = directive_processor.process_directive(
108
108
  @source, @pins, source_position, comment_position, directive
109
109
  )
110
+ # A @!scope directive in the indented text of another directive applies
111
+ # only to the object that directive defines.
112
+ YardMap::Directives::ScopeDirective.apply_nested directive, new_pins
113
+ @pins += new_pins
110
114
  end
111
115
 
112
116
  # @param comment [String]
@@ -190,10 +190,12 @@ module Solargraph
190
190
  end
191
191
  end
192
192
 
193
- private
194
-
195
193
  # @return [Array<Pin::Base>]
196
- attr_writer :convention_pins
194
+ def convention_pins
195
+ @convention_pins || []
196
+ end
197
+
198
+ private
197
199
 
198
200
  # @return [Hash{Class<Pin::Base> => Array<Pin::Base>}]
199
201
  def pin_class_hash
@@ -206,9 +208,12 @@ module Solargraph
206
208
  @data ||= Data.new(source)
207
209
  end
208
210
 
211
+ # @param pins [Array<Pin::Base>]
209
212
  # @return [Array<Pin::Base>]
210
- def convention_pins
211
- @convention_pins || []
213
+ def convention_pins= pins
214
+ # unmemoizing the document_symbols in case it was called from any of conventions
215
+ @document_symbols = nil
216
+ @convention_pins = pins
212
217
  end
213
218
 
214
219
  # @generic T
@@ -75,7 +75,7 @@ module Solargraph
75
75
  # @todo 22: Translate to something flow sensitive typing understands
76
76
  # @todo 3: Need a downcast here
77
77
  #
78
- # flow sensitive typing could handle (96):
78
+ # flow sensitive typing could handle (97):
79
79
  #
80
80
  # @todo 36: flow sensitive typing needs to handle attrs
81
81
  # @todo 29: flow sensitive typing should be able to handle redefinition
@@ -113,6 +113,7 @@ module Solargraph
113
113
  # @todo 1: flow sensitive typing needs to create separate ranges for postfix if
114
114
  # @todo 1: flow sensitive typing needs to handle constants
115
115
  # @todo 1: flow sensitive typing needs to eliminate literal from union with return if foo == :bar
116
+ # @todo 1: Array#flatten returns a bare Array
116
117
  def require_all_unique_types_match_expected?
117
118
  report?(:require_all_unique_types_match_expected, :strong)
118
119
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Solargraph
4
- VERSION = ENV.fetch('SOLARGRAPH_FORCE_VERSION', '0.60.3')
4
+ VERSION = ENV.fetch('SOLARGRAPH_FORCE_VERSION', '0.61.0')
5
5
  end
@@ -10,7 +10,6 @@ module Solargraph
10
10
  #
11
11
  class Workspace
12
12
  autoload :Config, 'solargraph/workspace/config'
13
- autoload :Gemspecs, 'solargraph/workspace/gemspecs'
14
13
  autoload :RequirePaths, 'solargraph/workspace/require_paths'
15
14
 
16
15
  # @return [String]
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Solargraph
4
- class YardMap
4
+ module YardMap
5
5
  class Cache
6
6
  def initialize
7
7
  # @type [Hash{String => Array<Solargraph::Pin::Base>}]
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Solargraph
4
- class YardMap
4
+ module YardMap
5
5
  module Directives
6
6
  module AttributeDirective
7
7
  module_function
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Solargraph
4
- class YardMap
4
+ module YardMap
5
5
  module Directives
6
6
  module DomainDirective
7
7
  module_function
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Solargraph
4
- class YardMap
4
+ module YardMap
5
5
  module Directives
6
6
  module MethodDirective
7
7
  module_function
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Solargraph
4
- class YardMap
4
+ module YardMap
5
5
  module Directives
6
6
  module OverrideDirective
7
7
  module_function
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Solargraph
4
- class YardMap
4
+ module YardMap
5
5
  module Directives
6
6
  module ParseDirective
7
7
  module_function
@@ -0,0 +1,178 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Solargraph
4
+ module YardMap
5
+ module Directives
6
+ # Implements YARD's @!scope directive.
7
+ #
8
+ # As described in YARD::Tags::ScopeDirective, a @!scope directive that
9
+ # appears in a docstring attached to a method definition applies only to
10
+ # that method. Anywhere else it applies to all of the methods that follow
11
+ # it within the namespace it was defined in.
12
+ #
13
+ # class Example
14
+ # # @!scope class
15
+ # def foo; end # => Example.foo
16
+ #
17
+ # def bar; end # => Example#bar
18
+ # end
19
+ #
20
+ # class Other
21
+ # # @!scope class
22
+ #
23
+ # def foo; end # => Other.foo
24
+ #
25
+ # def bar; end # => Other.bar
26
+ # end
27
+ #
28
+ module ScopeDirective
29
+ module_function
30
+
31
+ VALID_SCOPES = %i[class instance].freeze
32
+
33
+ # Directives that define an object whose scope is inherited from a
34
+ # @!scope directive. @!parse is not one of them because the code it
35
+ # contains already declares the scope of everything it defines.
36
+ SCOPED_DIRECTIVES = %w[method attribute].freeze
37
+
38
+ # @param source [Solargraph::Source]
39
+ # @param pins [Array<Solargraph::Pin::Base>]
40
+ # @param source_position [Position]
41
+ # @param comment_position [Position]
42
+ # @param directive [YARD::Tags::Directive]
43
+ # @return [Array<Solargraph::Pin::Base>]
44
+ def process_directive source, pins, source_position, comment_position, directive
45
+ kind = parse_scope directive
46
+ return [] if kind.nil?
47
+
48
+ attached = attached_method pins, source_position
49
+ if attached && no_empty_lines?(source.code, comment_position.line, source_position.line)
50
+ attached.scope = kind
51
+ else
52
+ apply_to_namespace pins, kind, source_position
53
+ end
54
+
55
+ []
56
+ end
57
+
58
+ # @param directive [YARD::Tags::Directive]
59
+ # @return [::Symbol, nil] :class, :instance, or nil if the directive is malformed
60
+ def parse_scope directive
61
+ kind = directive.tag.text.to_s.strip.downcase.to_sym
62
+ VALID_SCOPES.include?(kind) ? kind : nil
63
+ end
64
+
65
+ # True if the scope of the object defined by this directive can be set
66
+ # by a @!scope directive.
67
+ #
68
+ # @param directive [YARD::Tags::Directive]
69
+ # @return [Boolean]
70
+ def scopes_directive? directive
71
+ SCOPED_DIRECTIVES.include? directive.tag.tag_name
72
+ end
73
+
74
+ # The scope set by a @!scope directive that appears in the indented
75
+ # text of another directive:
76
+ #
77
+ # @!method foo
78
+ # @!scope class
79
+ #
80
+ # YARD parses the indented text of a directive with a state of its own,
81
+ # so a directive nested this way applies only to the object the
82
+ # enclosing directive defines. It never affects the objects that
83
+ # follow.
84
+ #
85
+ # @param directive [YARD::Tags::Directive]
86
+ # @return [::Symbol, nil] :class, :instance, or nil if there is none
87
+ def nested_scope directive
88
+ text = directive.tag.text.to_s
89
+ return nil unless text.include? '@!scope'
90
+
91
+ kind = nil
92
+ Solargraph::Source.parse_docstring(text).directives.each do |nested|
93
+ next unless nested.tag.tag_name == 'scope'
94
+
95
+ # A later directive overrides an earlier one, but an invalid value
96
+ # leaves the current one in place, as it does in YARD.
97
+ kind = parse_scope(nested) || kind
98
+ end
99
+ kind
100
+ end
101
+
102
+ # Set the scope of pins generated by a directive.
103
+ #
104
+ # @param pins [Array<Solargraph::Pin::Base>]
105
+ # @param kind [::Symbol, nil] :class or :instance; nothing happens if nil
106
+ # @return [void]
107
+ def apply_scope pins, kind
108
+ return if kind.nil?
109
+
110
+ # Only methods have a scope. Applying one to any other kind of pin,
111
+ # such as a namespace generated by @!parse, would corrupt its path.
112
+ pins.each { |pin| pin.scope = kind if pin.is_a?(Pin::Method) }
113
+ end
114
+
115
+ # Apply a @!scope directive from the indented text of another directive
116
+ # to the pins that directive generated.
117
+ #
118
+ # @param directive [YARD::Tags::Directive]
119
+ # @param pins [Array<Solargraph::Pin::Base>] the pins generated by the directive
120
+ # @return [void]
121
+ def apply_nested directive, pins
122
+ return unless scopes_directive? directive
123
+
124
+ apply_scope pins, nested_scope(directive)
125
+ end
126
+
127
+ # Applies the scope to each method that follows the directive within
128
+ # the namespace the directive appears in. Methods in nested namespaces
129
+ # are excluded, since the directive only affects its own namespace.
130
+ #
131
+ # @param pins [Array<Solargraph::Pin::Base>]
132
+ # @param kind [::Symbol] :class or :instance
133
+ # @param source_position [Position]
134
+ # @return [void]
135
+ def apply_to_namespace pins, kind, source_position
136
+ namespace = namespace_at pins, source_position
137
+ return if namespace.nil?
138
+
139
+ pins.each do |pin|
140
+ next unless pin.is_a?(Pin::Method)
141
+ next unless pin.namespace == namespace.path
142
+ # @sg-ignore flow sensitive typing should narrow location after the nil check
143
+ next if pin.location.nil? || pin.location.range.start.line < source_position.line
144
+
145
+ pin.scope = kind
146
+ end
147
+ end
148
+
149
+ # The method defined on the line the comment is attached to, if any.
150
+ #
151
+ # @param pins [Array<Solargraph::Pin::Base>]
152
+ # @param source_position [Position]
153
+ # @return [Pin::Method, nil]
154
+ def attached_method pins, source_position
155
+ pins.select { |pin| pin.is_a?(Pin::Method) && pin.location&.range&.contain?(source_position) }.last
156
+ end
157
+
158
+ # @param pins [Array<Solargraph::Pin::Base>]
159
+ # @param position [Position]
160
+ # @return [Pin::Namespace, nil]
161
+ def namespace_at pins, position
162
+ pins.select { |pin| pin.is_a?(Pin::Namespace) && pin.location&.range&.contain?(position) }.last
163
+ end
164
+
165
+ # @param code [String]
166
+ # @param line1 [Integer]
167
+ # @param line2 [Integer]
168
+ # @return [Boolean]
169
+ def no_empty_lines? code, line1, line2
170
+ lines = code.lines[line1..line2]
171
+ return false if lines.nil?
172
+
173
+ lines.none? { |line| line.strip.empty? }
174
+ end
175
+ end
176
+ end
177
+ end
178
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Solargraph
4
- class YardMap
4
+ module YardMap
5
5
  module Directives
6
6
  module VisibilityDirective
7
7
  module_function
@@ -1,17 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Solargraph
4
- class YardMap
4
+ module YardMap
5
5
  module Directives
6
6
  autoload :AttributeDirective, 'solargraph/yard_map/directives/attribute_directive'
7
7
  autoload :MethodDirective, 'solargraph/yard_map/directives/method_directive'
8
8
  autoload :DomainDirective, 'solargraph/yard_map/directives/domain_directive'
9
9
  autoload :OverrideDirective, 'solargraph/yard_map/directives/override_directive'
10
10
  autoload :ParseDirective, 'solargraph/yard_map/directives/parse_directive'
11
+ autoload :ScopeDirective, 'solargraph/yard_map/directives/scope_directive'
11
12
  autoload :VisibilityDirective, 'solargraph/yard_map/directives/visibility_directive'
12
13
 
13
14
  # @param directive [YARD::Tags::Directive]
14
- # @return [Class<AttributeDirective>, Class<MethodDirective>, Class<DomainDirective>, Class<OverrideDirective>, Class<ParseDirective>, Class<VisibilityDirective>, nil]
15
+ # @return [Class<AttributeDirective>, Class<MethodDirective>, Class<DomainDirective>, Class<OverrideDirective>, Class<ParseDirective>, Class<ScopeDirective>, Class<VisibilityDirective>, nil]
15
16
  def self.for directive
16
17
  case directive.tag.tag_name
17
18
  when 'attribute'
@@ -24,6 +25,8 @@ module Solargraph
24
25
  OverrideDirective
25
26
  when 'parse'
26
27
  ParseDirective
28
+ when 'scope'
29
+ ScopeDirective
27
30
  when 'visibility'
28
31
  VisibilityDirective
29
32
  else # rubocop:disable Style/EmptyElse
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Solargraph
4
- class YardMap
4
+ module YardMap
5
5
  module Helpers
6
6
  module_function
7
7
 
@@ -1,9 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Solargraph
4
- class YardMap
4
+ module YardMap
5
5
  class Macro
6
- PROCESSABLE_DIRECTIVES = %w[method attribute parse].freeze
6
+ PROCESSABLE_DIRECTIVES = %w[method attribute parse scope].freeze
7
+
8
+ # Directives whose text is a value rather than a definition. The comments
9
+ # attached to the macro's call site belong to the objects the macro
10
+ # generates, but adding them to one of these directives would invalidate
11
+ # its content.
12
+ VALUE_DIRECTIVES = %w[parse scope].freeze
7
13
 
8
14
  class << self
9
15
  # @param directive [YARD::Tags::Directive]
@@ -73,13 +79,34 @@ module Solargraph
73
79
  # @return [Array<Pin::Base>]
74
80
  def generate_pins_from chain, pin, source_map
75
81
  call_location = Solargraph::Location.from_node(chain.node)
82
+ return [] unless call_location
83
+
84
+ # A @!scope directive in a macro applies to the objects the macro
85
+ # generates after it, the same way it applies to the objects that
86
+ # follow it in a namespace. Its effect ends with the macro.
87
+ #
88
+ # @type [::Symbol, nil]
89
+ inherited_scope = nil
90
+
76
91
  # @param generated_pins [Array<Pin::Base>]
77
92
  generate_yardoc_from(chain, source_map).reduce([]) do |generated_pins, directive|
93
+ # @sg-ignore flow sensitive typing confuses this block variable with the #directive attr reader
94
+ if directive.tag.tag_name == 'scope'
95
+ inherited_scope = Directives::ScopeDirective.parse_scope(directive) || inherited_scope
96
+ next generated_pins
97
+ end
78
98
  directive_processor = YardMap::Directives.for(directive)
79
- next generated_pins unless directive_processor && call_location
80
- generated_pins + directive_processor.process_directive(
99
+ next generated_pins unless directive_processor
100
+ new_pins = directive_processor.process_directive(
81
101
  source_map.source, source_map.pins, call_location.range.start, call_location.range.start, directive
82
102
  )
103
+ if Directives::ScopeDirective.scopes_directive?(directive)
104
+ # A @!scope directive nested in another directive's text applies
105
+ # only to the object that directive defines.
106
+ scope = Directives::ScopeDirective.nested_scope(directive) || inherited_scope
107
+ Directives::ScopeDirective.apply_scope new_pins, scope
108
+ end
109
+ generated_pins + new_pins
83
110
  end
84
111
  end
85
112
 
@@ -100,9 +127,11 @@ module Solargraph
100
127
  PROCESSABLE_DIRECTIVES.include?(directive.tag.tag_name)
101
128
  end
102
129
  directives.each do |directive|
130
+ next if VALUE_DIRECTIVES.include? directive.tag.tag_name
131
+
103
132
  # @sg-ignore chain.node is assumed to exist
104
133
  comments = source_map.source.comments_for(chain.node)
105
- if comments&.length&.positive? && directive.tag.tag_name != 'parse'
134
+ if comments&.length&.positive?
106
135
  directive.tag.text += "\n#{comments}"
107
136
  end
108
137
  end